While keeping in mind that there are no good or bad answers, just different design decisions with pros and cons, let me start with a simple way of managing this scenario.
You have two roles, the customer and the admin. If neither application will have open registration we can say that users will need to have one of these possible roles assigned before being able to log in.
We said that we want to store role information in Auth0 (which, just as a reminder, it’s a design choice, but not the only one). We can use the user’s app_metadata property, a multi-purpose storage bag in the user profile that’s useful to put this type of information.
So a user will either have:
"app_metadata" : {
"roles" : ["admin"]
}
or
"app_metadata" : {
"roles" : ["customer"]
}
I’m modeling roles
as an array just in case you need to accomodate for different roles in the future, with a user having more than one role (a common use case). If you know each user will have only one role, you could simplify this and use a simple role
string instead.
How will you update the user profile for this information is a whole different subject. But you can do so through the dashboard directly or use the Management API, from a backend application that handles user provisioning. But let’s simplify this for now and work under the assumption that each user will have a roles
property set in their app_metadata
.
When an application requests a token for the backend API, you can outright deny the authorization if the user doesn’t have the proper role using a rule:
function(user, context, callback) {
user.app_metadata = user.app_metadata || {};
user.app_metadata.roles = user.app_metadata.roles || [];
const customerClientID = "<the_client_id_for_the_customer_app>";
if (context.clientID === customerClientID &&
user.app_metadata.roles.indexOf("customer") < 0) {
return callback(new Error("The user doesn't have a customer role"));
}
[...] // repeat a similar check for admins and the admin app
[...]
callback(null, user, context);
}
The above will guarantee that Auth0 won’t issue an access token for the wrong user. If Auth0 issues the access token, it will look something like this:
{
"iss": "<your_auth0_domain>",
"sub": "<the_user_id>",
"aud": [
"<your_api_identifier>",
"https://<your_auth0_domain>/userinfo"
],
"iat": 1551011018,
"exp": 1551018218,
"azp": "<the_client_id_for_the_app_that_requested_the_token>",
"scope": "openid"
}
In this particular case, and based on the rule that we wrote above, you could use the azp
claim (“Authorized party”, i.e. the app that requested the token) as an indication of what the bearer of the token can do. If it is the admin app, it can do admin-related operations. If it is the customers app, then it can do only customer-related operations. But checking authorization based on the application might not be flexible. So you could:
- Go fetch the user authorization information on each API request, to check for the user’s actual role each time the API is accessed.
- Put the role information in the token at the moment of issuance.
Option 1 might be required on some scenarios where roles are changing often and the API needs to react to these changes immediately. And this might work well if the authorization data lives next to your backend data. But remember that in this case we decided to store the role information on Auth0, and loading the user profile from Auth0 on each API request will not work well from a performance standpoint.
Option 2 will work better in this case. To put the role information in the access token, you can write another rule, like this one, to add a custom claim:
function(user, context, callback) {
user.app_metadata = user.app_metadata || {};
user.app_metadata.roles = user.app_metadata.roles || [];
const namespace = "https://yourapp.com/claims/";
context.accessToken[namespace+"roles"] = user.app_metadata.roles;
callback(null, user, context);
}
You can read more about custom claims here: JSON Web Token Claims
With this in place, the access token will now look like this:
{
"iss": "<your_auth0_domain>",
"sub": "<the_user_id>",
"aud": [
"<your_api_identifier>",
"https://<your_auth0_domain>/userinfo"
],
"iat": 1551011018,
"exp": 1551018218,
"azp": "<the_client_id_for_the_app_that_requested_the_token>",
"scope": "openid",
"https://yourapp.com/claims/roles":["admin"]
}
So now your backend API can check for the https://yourapp.com/claims/roles
claim and allow or deny access to certain operations based on the role information.
Remember that this is just one answer. If your authorization schemes were more complex, you might want to store the information on your backend, or maybe a combination of the two (e.g. roles on Auth0, and finer-grained authorization information on the backend side).
You will also need to think how to provision users. Whether someone will assign the roles after the user logs in, whether you’ll come up with an invitation-like system (like the Auth0 dashboard does), or other approaches.
Hope this helps a bit.