Filter scopes with an action when using grant_type password

When using grant_type=password to retrieve a token, all of the application permissions are returned in the access token scopes, even though they are not in the request. This is apparently intentional behaviour, however I need to filter out some scopes based on user permissions. Previously, this could be done via a rule, however, with rules being deprecated, I am trying to move this to an action, but there doesn’t seem to be any way to see all the scopes that will be returned, so I cannot remove them with api.accessToken.removeScope(), and there is no way for me to completely clear the scopes either.

Is there any way to either configure auth0 to not include all the application scopes when using grant_type=password, or to remove them with an action, without hardcoding the list of possible scopes inside the action?

Hi @morgan.zolob,

Welcome to the Auth0 Community!

Generally, only the scopes defined during the authentication request and for the user are returned in the access token scopes.

For example:

User permissions granted for a specific API:

  • read:reports
  • update:reports

Login request:

curl --request POST \
  --url 'https://onboarding-tiow.us.auth0.com/oauth/token' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data grant_type=password \
  --data 'username={username}' \
  --data 'password={password}' \
  --data 'audience={Test-API}' \
  --data scope=read:sample openid profile email  \
  --data 'client_id={yourClientId|}' \
  --data 'client_secret={yourClientSecret}'

Based on this information, the returned scopes are the following:

  • "scopes": "read:sample openid profile email"
  • "permissions": [ "read:reports", "update:reports" ]

Secondly, when using the api.accessToken.removeScope() method, I successfully removed any scopes in the authorization request.

If you are encountering issues with this, could you please share your existing Rule and what you have drafted so far for your post-login Action script?

Thanks,
Rueben

@rueben.tiow Sorry, I should have been more clear that this issue is only when not including api scopes in the request. If the request has no scopes, or if it only has OIDC scopes, like openid, then auth0 will return all the API scopes in the response. In this case, I cannot remove them with api.accessToken.removeScope() because I do not know the scope names, because they are nowhere in the event.

I came up with a hacky workaround for this issue. If you request a non-standard claim (so one not on this page), then auth0 will filter the returned API scopes to only the requested ones. So you can request a scope that doesn’t exist, and Auth0 will filter out all of the API scopes.

So, in my action I first check if the request is a password grant request, and if it is, I check to make sure that it includes a “password_grant” scope. This isn’t a real scope, but including it makes auth0 only include the requested scopes, and not every scope. Since I can read the requested scopes in the event, I can remove them with api.accessToken.removeScope(). Here is all the code for the action:

const resourceOwnerProtocols = [
  "oauth2-password",
  "oauth2-resource-owner",
  "oauth2-resource-owner-jwt-bearer",
];

const resourceOwnerRequiredScope = "password_grant";

const oidcStandardClaims = new Set([
  "openid",
  "sub",
  "name",
  "given_name",
  "family_name",
  "middle_name",
  "nickname",
  "preferred_username",
  "profile",
  "picture",
  "website",
  "email",
  "email_verified",
  "gender",
  "birthdate",
  "zoneinfo",
  "locale",
  "phone_number",
  "phone_number_verified",
  "address",
  "updated_at",
]);

/**
 * Handler that will be called during the execution of a PostLogin flow.
 *
 * @param {Event} event - Details about the user and the context in which they are logging in.
 * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
 */
exports.onExecutePostLogin = async (event, api) => {
  let scopes;
  if (resourceOwnerProtocols.includes(event.transaction?.protocol ?? "")) {
    // Requested scopes will be in the body
    scopes = (event.request.body.scope ?? "").replace(/\s+/g, " ").split(" ");
    if (!scopes.includes(resourceOwnerRequiredScope)) {
      api.access.deny(
        `Password grant requests must include the '${resourceOwnerRequiredScope}' scope.`
      );
      return;
    }
  } else if (Array.isArray(event.transaction?.requested_scopes)) {
    scopes = event.transaction?.requested_scopes;
  } else {
    api.access.deny("Unable to read requested scopes");
    return;
  }

  // Remove any scopes which are not OIDC standard claims
  for (const scope of scopes) {
    if (!oidcStandardClaims.has(scope)) {
      api.accessToken.removeScope(scope);
    }
  }

  // Add scopes based on user permissions or whatever else
  api.accessToken.addScope("example:scope");
};

Obviously it’s a bit of a hack, but it does work.

1 Like

Hey @morgan.zolob,

Thank you for sharing your solution!

Let us know if you have any further questions.

Cheers,
Rueben

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.