How does the consent flow determine which resources to ask for? - javascript

I have a multi-tenant service principal that exposes a custom API. Using MSAL.js' UserAgentApplication I'm able to ask for consent for the resources I need on first-time use with loginPopup. However, I'm confused as to which resources to specify in the request. For instance, let's say I use the following popup (note the lack of scope):
await this.userAgentApplication.loginPopup({
prompt: 'consent',
authority: "https://login.microsoftonline.com/organizations"
})
The application will simply request the user's profile. Fair enough.
However, let's say I configure the popup as follows:
await this.userAgentApplication.loginPopup({
scopes: ["api://xyz/Some.Scope"]
prompt: "consent"
authority: "https://login.microsoftonline.com/organizations"
})
This causes an exception:
The user or administrator has not consented to use the application with ID 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx' named 'XYZ'. Send an interactive authorization request for this user and resource.
Why do I get this error even when I'm logging in using a Global Administrator account?
Lastly, in addition to our own API data we needed to be able to read Graph Groups in application context, so I requested these using the .default endpoint (permissions are specified in the Service Principal registration). I did this using the following popup:
await this.userAgentApplication.loginPopup({
scopes: ["https://graph.microsoft.com/.default"],
prompt: 'consent',
authority: "https://login.microsoftonline.com/organizations"
})
The result of the last attempt was ... all the permissions I was hoping for!
Sign in and read user profile
Read and write all groups
My Custom API scope (Application name)
But why does our Graph consent request automatically include requests for other custom scopes?

It is the expected behaviour if you use admin consent for the resources.
When you set ***/.default as the scope, it is equivalent to executing "Grant admin consent for {your tenant}" in Azure portal.
So it will asks admin consent for all the required permissions no matter whether they are from the required resource or not.
But if you set https://graph.microsoft.com/user.read, it will ask you to do consent only for user.read permission.
So in this case, once you use the last one to do the admin consent, api://xyz/Some.Scope will also take effect.
I also have a test with my custom API api://***/.default and api://***/user.write and both work as expected.
You can also try to use the following request to do the admin consent:
https://login.microsoftonline.com/jmaster.onmicrosoft.com/oauth2/v2.0/authorize?
client_id={client id}
&response_type=code
&redirect_uri={redirect url}
&response_mode=query
&scope=api://xyz/Some.Scope
&state=12345
&prompt=consent
Please have a retry with api://***/.default. Don't worry if it doesn't work, because api://xyz/Some.Scope will also take effect using the last code snippet.

Related

How to keep my node js endpoints safe and to be accessible only from my website?

I have endpoints coded in Nodejs... I use the following codes to keep them safe...
const corsOption = {
origin: ['https://www.mywebsite.com'],
};
app.use(cors(corsOption));
if (host !== "myendpoint.com") {
return res.status(403).json({ message: "forbidden access" });
}
will these keep my endpoints safe... or do I have to do anything more for my endpoints to keep them safe... I don't want bots or anyone else to use it... I know that they are public but I want to restrict access... pls, any help or suggestion ???
thank you
To be sure you can control who can access your endpoint, you can setup a token authentication.
When you send a request to your endpoint, the header should include:
Authorization: Token {your token}
And in your endpoint, you can check if the token is authorized or not (by storing authorized token in a database). If the token is not recognized, you can send back a 403 error.
If your website accesses your endpoints, this means that any browser that can display your website must also be able to access your endpoints. Requests are not made by your website, they are made by browsers visiting your website.
You must first ask how much you want to restrict access:
Restrict to individual known users to whom you send a password via mail, which they must then type into your website ("log on") before they can make any requests to your endpoints.
Restrict to users who have self-registered. Can anyone in the world then self-register, or do you demand confirmation via an email address?
Restrict to users who can log on with their Google (or Facebook, or ...) account.
Zain_Ul_Din's answer shows details of a possible implementation for the "self-registration" case. See also What's the best way to add social login (Sign in with Google) to existing email/password app and database?
you can implement user authentication and authorization in your Node js app to restrict access.
for this you can use the jsonwebtoken npm package.
Look up John Smilga's node and express projects on google for a 10hr video including 4 projects. One of the projects introduces JSON web tokens and how to use them.I highly recommend that.
You can also use the express-rate-limit package. With this you should be able to 'limit' how many requests a user can make to your API endpoints within a set amount of time. If the requests exceed that limit then this middleware steps in and stops further access (Haven't tested it in production myself but looks good)

Single flow: sign user in via Google oAuth AND grant offline/server access?

I'm trying to implement Google sign-in and API access for a web app with a Node.js back end. Google's docs provide two options using a combo of platform.js client-side and google-auth-library server-side:
Google Sign-In with back-end auth, via which users can log into my app using their Google account. (auth2.signIn() on the client and verifyIdToken() on the server.)
Google Sign-in for server-side apps, via which I can authorize the server to connect to Google directly on behalf of my users. (auth2.grantOfflineAccess() on the client, which returns a code I can pass to getToken() on the server.)
I need both: I want to authenticate users via Google sign-in; and, I want to set up server auth so it can also work on behalf of the user.
I can't figure out how to do this with a single authentication flow. The closest I can get is to do the two in sequence: authenticate the user first with signIn(), and then (as needed), do a second pass via grantOfflineAccess(). This is problematic:
The user now has to go through two authentications back to back, which is awkward and makes it look like there's something broken with my app.
In order to avoid running afoul of popup blockers, I can't give them those two flows on top of each other; I have to do the first authentication, then supply a button to start the second authentication. This is super-awkward because now I have to explain why the first one wasn't enough.
Ideally there's some variant of signIn() that adds the offline access into the initial authentication flow and returns the code along with the usual tokens, but I'm not seeing anything. Help?
(Edit: Some advice I received elsewhere is to implement only flow #2, then use a secure cookie store some sort of user identifier that I check against the user account with each request. I can see that this would work functionally, but it basically means I'm rolling my own login system, which would seem to increase the chance I introduce bugs in a critical system.)
To add an API to an existing Google Sign-In integration the best option is to implement incremental authorization. For this, you need to use both google-auth-library and googleapis, so that users can have this workflow:
Authenticate with Google Sign-In.
Authorize your application to use their information to integrate it with a Google API. For instance, Google Calendar. 
For this, your client-side JavaScript for authentication might require some changes to request
offline access:
$('#signinButton').click(function() {
auth2.grantOfflineAccess().then(signInCallback);
});
In the response, you will have a JSON object with an authorization code:
{"code":"4/yU4cQZTMnnMtetyFcIWNItG32eKxxxgXXX-Z4yyJJJo.4qHskT-UtugceFc0ZRONyF4z7U4UmAI"}
After this, you can use the one-time code to exchange it for an access token and refresh token.
Here are some workflow details:
The code is your one-time code that your server can exchange for its own access token and refresh token. You can only obtain a refresh token after the user has been presented an authorization dialog requesting offline access. If you've specified the select-account prompt in the OfflineAccessOptions [...], you must store the refresh token that you retrieve for later use because subsequent exchanges will return null for the refresh token
Therefore, you should use google-auth-library to complete this workflow in the back-end. For this,
you'll use the authentication code to get a refresh token. However, as this is an offline workflow,
you also need to verify the integrity of the provided code as the documentation explains:
If you use Google Sign-In with an app or site that communicates with a backend server, you might need to identify the currently signed-in user on the server. To do so securely, after a user successfully signs in, send the user's ID token to your server using HTTPS. Then, on the server, verify the integrity of the ID token and use the user information contained in the token
The final function to get the refresh token that you should persist in your database might look like
this:
const { OAuth2Client } = require('google-auth-library');
/**
* Create a new OAuth2Client, and go through the OAuth2 content
* workflow. Return the refresh token.
*/
function getRefreshToken(code, scope) {
return new Promise((resolve, reject) => {
// Create an oAuth client to authorize the API call. Secrets should be
// downloaded from the Google Developers Console.
const oAuth2Client = new OAuth2Client(
YOUR_CLIENT_ID,
YOUR_CLIENT_SECRET,
YOUR_REDIRECT_URL
);
// Generate the url that will be used for the consent dialog.
await oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope,
});
// Verify the integrity of the idToken through the authentication
// code and use the user information contained in the token
const { tokens } = await client.getToken(code);
const ticket = await client.verifyIdToken({
idToken: tokens.id_token!,
audience: keys.web.client_secret,
});
idInfo = ticket.getPayload();
return tokens.refresh_token;
})
}
At this point, we've refactored the authentication workflow to support Google APIs. However, you haven't asked the user to authorize it yet. Since you also need to grant offline access, you should request additional permissions through your client-side application. Keep in mind that you already need an active session.
const googleOauth = gapi.auth2.getAuthInstance();
const newScope = "https://www.googleapis.com/auth/calendar"
googleOauth = auth2.currentUser.get();
googleOauth.grantOfflineAccess({ scope: newScope }).then(
function(success){
console.log(JSON.stringify({ message: "success", value: success }));
},
function(fail){
alert(JSON.stringify({message: "fail", value: fail}));
});
You're done with the front-end changes and you're only missing one step. To create a Google API's client in the back-end with the googleapis library, you need to use the refresh token from the previous step.
For a complete workflow with a Node.js back-end, you might find my gist helpful.
While authentication (sign in), you need to add "offline" access type (by default online) , so you will get a refresh token which you can use to get access token later without further user consent/authentication. You don't need to grant offline later, but only during signing in by adding the offline access_type. I don't know about platform.js but used "passport" npm module . I have also used "googleapis" npm module/library, this is official by Google.
https://developers.google.com/identity/protocols/oauth2/web-server
https://github.com/googleapis/google-api-nodejs-client
Check this:
https://github.com/googleapis/google-api-nodejs-client#generating-an-authentication-url
EDIT: You have a server side & you need to work on behalf of the user. You also want to use Google for signing in. You just need #2 Google Sign-in for server-side apps , why are you considering both #1 & #2 options.
I can think of #2 as the proper way based on your requirements. If you just want to signin, use basic scope such as email & profile (openid connect) to identify the user. And if you want user delegated permission (such as you want to automatically create an event in users calendar), just add the offline access_type during sign in. You can use only signing in for registered users & offline_access for new users.
Above is a single authentication flow.

Problems authenticating Service Account for google reseller api using the nodejs library

I am trying to access the google reseller api using the nodejs library, which has very shi..., I mean spotty documentation. I tried following the example here, but I fail at step 3 with this error:
code: 403,
errors: [
{
domain: 'global',
reason: 'insufficientPermissions',
message: 'Authenticated user is not authorized to perform this action.'
}
]
My configuration looks like this:
const OAUTH2_SCOPES = [
"https://www.googleapis.com/auth/admin.directory.user",
"https://www.googleapis.com/auth/apps.order",
"https://www.googleapis.com/auth/siteverification",
"https://www.googleapis.com/auth/cloud-platform",
];
const authJWT = new google.auth.JWT({
keyFile: JSON_PRIVATE_KEY_FILE,
scopes: OAUTH2_SCOPES,
subject: RESELLER_ADMIN_USER,
email: "gsuite-reseller#some-cool-name-because-why-not.iam.gserviceaccount.com",
});
Using basic google fu, I found this thread, which suggested that my problem has to do with impersonation. So I exchanged the email in the subject property, with my account email, which has the owner rights specified. I also gave the service account owner rights, because at that point I am pretty clueless. This unfortunately only changed the error message to:
status: 401,
statusText: 'Unauthorized'
Does anybody have an idea what goes wrong? The 401 suggests that there are credentials missing. Would I have to also specify my private emails credentials in addition to the ones of the service account? If yes, then where? I did not find any property on the google.auth.JWT.options object which sounded promising.
Why do you need domain-wide delegation?
When you use a service account and enable domain-wide delegation, it means that you allow the service account to impesonate the user and act on his behalf
If you use a service account without impersonation - the service account can only perform operations to which it is autherized - e.g. it can access files on your Drive or access your Calendar - but only if you explicitly shared those with the service account!
To perform requests for which the service account is not authorized, you need to make the service account impersonate a domain user that has the necessary authorization - that is you need to impersonate the user
However to impersonate the user, you need to explicitly give the service account the permission to act on behalf of a user - this is called domain-wide delegation
Enabling domain-wide delegation will not make "every created user to have to go through manual authorization" or affect any other non-service account related behavior
the only thing domain-wide delegation does is to allow a service account to represent a user
Without enabling domain-wide delegation the impersonaiton of a user will not be authorized and setting a subject will throw you an error
References:
https://support.google.com/a/answer/162106?hl=en
https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority
https://developers.google.com/admin-sdk/directory/v1/guides/delegation

Client has not been granted scopes: read roles

Using the node-auth0 package, had connections working properly, but am now attempting to do some work with roles and have added the read:roles scope, which now results in an error.
Minimal code below:
const ManagementClient = require('auth0').ManagementClient;
const auth0 = new ManagementClient(
{
domain: process.env.AUTH0_DOMAIN,
clientId: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
scope: 'read:users update:users read:roles'
});
// both these calls fail
const auth0_roles = auth0.roles.getAll();
const auth0_tester = auth0.users.create(data);
Now the calls to EITHER roles or users fail with this error:
{"error":"access_denied","error_description":"Client has not been granted scopes: read:roles"}
If I remove read:roles from the scope, the create user call succeeds (but obviously the roles.getAll fails)
I've checked my API permissions in the dashboard (APIs => Auth0 Management API => Permissions), and it appears to include everything:
(NOTE: there's a message at the top of the permissions that indicates Important: Permission Management is not available for APIs representing Auth0 Resources. - and I am NOT able to change permissions )
Per my comment to Shayan's excellent answer, I'd actually been in that area of the Auth0 dashboard many times and had missed the magic link.
The image below shows where you have to click in order to set the client grants you want the application to have.
You cannot change add/edit/remove permissions for your Management API resource because as the dashboard message says: It is an Auth0 resource server with the identifier/audience as https://YOUR_DOMAIN.REGION.auth0.com/api/v2/. The scopes are managed by Auth0. You can however modify what scopes are granted to your Server-side Clients i.e Machine-to-Machine or Web Application client types, and limit what they can request when using Client Credentials grant to request an Access Token for an API.
In the same section under Management API resource settings, besides the "Permissions" tab, you can open "Machine to Machine Applications" tab, find your clientID and make sure it is authorized to request the scopes it requires for that API resource (eg. the read:roles scope). These are called Client Grants and you can manage them from Dashboard or Management API: https://auth0.com/docs/api/management/v2#!/Client_Grants/get_client_grants
Auth0 has a Client Credential Hook you can utilize for more specific logic in your flow, read more here: https://auth0.com/docs/api-auth/tutorials/client-credentials/customize-with-hooks

Is it possible to post to chat.postMessage as any user in a Slack team?

I'm building a Slack integration that is intended to modify some text and then post it to a Slack channel as though the user who triggered the command had said it.
e.g. /makeFace disapproval
#Ben 3:45pm
ಠ_ಠ
I ask for the client permission scope, which adds the chat:write:user permission. But when I hit the chat.postMessage endpoint, it only seems to allow you to post as the user who added the integration because the token it returns seems to be individuated for that user.
I know that giphy, for instance, sends its gif messages as though you are the originator, but I can't find out how they manage it. Is there any documentation for sending messages as other members of the team?
There are 2 ways to achieve this:
A. Overwriting username and icon
When you send a message with chat.postMessage it is possible to set a user name with the property username. The message will then appear as being send by that user (same for icon with icon_url).
However, this is not meant to impersonate real users, so even if you use the same username and icon as the real user the message will have the app tag, so that they can be distinguished from a real user.
Here is an example how it looks like (from a gamer Slack about flying and killing space ships):
But depending on what your requirements are that might work for you.
If you want to use it make sure to also set the as_user property to false (yes, really) and it will not work with a bot token, only with a user token.
See here for more details on how it works.
This also works for the legacy version of Incoming Webhooks, not with the current version of incoming webhooks though. (You can still get the legacy version, see this answer)
B. Having the user's token
Another approach is to always use the token from the respective user for sending the message. In combination with as_user = true messages sent by your app will look exactly as if they would come from the respective user (no APP tag).
To make that happen your app would need to collect tokens from all users on your workspace and store them for later use. This can be done by asking every user to install your app (called adding a "configuration") through the Oauth process (same you use to install your app to a workspace), which allows your app to collect and store those tokens for later use.
Update: This doesn't work. It impersonates the user who installed the app, so it merely seems to work... until another user tries to use it (and they end up impersonating you).
Go to your App's management page. Select "OAuth & Permissions".
Add the chat.write OAuth Scope to your app as a User Token Scope, not a Bot Token scope.
Take note of your User OAuth Token at the top of this page (not your But User OAuth Token).
Call chat.postMessage with
username = user id of the user you'd like to post on behalf of
token = the token from step 3. above
The resulting post will be 100% impersonated. Not just the name and icon as mentioned in other answers, but it'll 100% function as if it came from the user.
I hope this will help those who are still facing this issue.
First give the chat:write and chat:write.customize scope to your bot. The scope chat:write.customize Send messages as #your_slack_app with a customized username and avatar
From "OAuth & Permissions" settings get the bot OAuth token or even bot access token (both will work).
Then set the arguments like the following.
username to specify the username for the published message.
icon_url to specify a URL to an image to use as the profile photo alongside the message.
icon_emoji to specify an emoji (using colon shortcodes, eg. :white_check_mark:) to use as the profile photo alongside the message.
You can visit the docs from here

Categories

Resources