I was able to create a rule that adds one of two roles to an access token if a particular api
scope is requested. Anyone with an email in the domain @example.org
is given admin access, and everyone else gets user access.
Specifically, I am requesting the scopes openid profile email api
from an SPA. This rule seems to work and I’m able to authenticate the resulting access token with a WebApi attribute like [Authorize(Roles="api:user")]
.
However, I’m unsure that my rule is entirely correct, because I’m a little confused about all the side-effects a rule is supposed to have. Does this look correct?
function (user, context, callback) {
user.app_metadata = user.app_metadata || {};
// the user's requested scope
var requested_scopes_string = context.request.query.scope || '';
var requested_scopes = requested_scopes_string.split(' ');
// if the user requested API access, then figure
// out which custom openid connect header to add
if (requested_scopes.indexOf("api") >= 0) {
if(context.accessToken) {
var roles = ];
if (user.email.indexOf('@example.org') > -1) {
roles.push("api:admin");
} else {
roles.push("api:user");
}
// this needs to be namespaced, as per
// https://auth0.com/docs/scopes/preview
context.accessToken"https://example.org/roles"] = roles.join(" ");
}
// I think the scope needs to be echoed back in the access_token?
context.accessToken.scope = requested_scopes;
}
// persist the app_metadata update.
auth0.users.updateAppMetadata(user.user_id, user.app_metadata)
.then(function(){
callback(null, user, context);
})
.catch(function(err){
callback(err);
});
}
I’m using WebApi core, so I set up the JWT middleware like this:
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
Audience = configuration"Auth0:ApiIdentifier"],
Authority = $"https://{configuration"Auth0:Domain"]}/",
TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "https://example.org/roles"
}
}
Using a rule to implement an access policy to control the information/roles/scopes contained in the access token issued for your API is an adequate solution; of course, it will always depend if the access policy is correctly implemented, but that would be the case either if it’s implemented by a rule, directly in your application or in any other place.
If you currently navigate to the scopes section of an API you configured you’ll also get a reinforcement that your approach is suitable, more specifically, there’s this message:
By default, any user on any client can ask for any scope defined here. You can implement access policies to limit this behavior via Rules.
(emphasis is mine)
With regards to your actual implementation, this may not apply to you depending on the circumstances, but the code you showed is based on emails so there is a need to ensure that the user email is verified, otherwise making security decisions around unverified email addresses will be an issue. In your case, you may be ensuring this in other place, but calling this out in case someone else considers reusing the code.
As final notes:
- your API seems to focus on roles instead of scopes for authorization so in your rule you may not need to make anything around
accessToken.scope
as the scopes are not being used by the API.
- you don’t seem to be changing the user metadata as part of the rule so the code could be simplified.
1 Like
Thanks for the feedback! I originally did this by mapping the scopes to policies as per your canonical example, but I wanted to be able to use .Net Roles instead so I rewrote it.
It’s not really clear to me how to correctly model this with scopes, claims and roles—I just want someone to request “api” access, and I want to tell them whether they have “admin” or “user” access (admin access being a superset of user). Is there any advantage to parsing the returned scope instead of my “roles” property?
Scopes are used per OAuth2 to communicate the permissions/intentions that the client application requires for communicating with the resource server (API) associated with the server. However, after that, nothing mandates that they need to be contained in the access token. The way to represent that information is an implementation detail. You could represent them with a direct relationship and just include the scopes directly in the token or go another route. You chose to represent the requested scopes as roles within your token; this is acceptable.