Two apps, one api

Hi guys, Im trying to find the best approach to solve my problem.

What I have:

  • Api(graphql & nestjs)
  • An admin app(where administrators can create reports)
  • Customers app(where customers can read reports)
  • Both apps are consuming api

What I need:

  • I want Auth0 to login administrators accounts in admin web app
  • I want Auth0 to login customers accounts in customers web app
  • I want to use a middleware in my api to check jwt tokens

What is not possible:

  • Administrators cannot log in customers app
  • Customers cannot log in administrators app

So, the question is: What is the best approach? Should I create 2 separated auth0.applications? Should users have different roles?

Sorry for my bad english. Big thanks

Hi @Isilva

Are you planning to have separate applications for the admin and the customer audiences, or it will be the same app with a different experience based on who logs in?

Both scenarios can be accomodated. If you plan on having two different applications, you would have separate applications in Auth0 (each with the corresponding configuration including callback URLs).

A separate decision is how you manage authorization so that you can check, in the API, whether the access token issued by Auth0 is good to access certain endpoints. E.g. if this is an admin-only endpoint? Is the user that authorized this token for the application an administrator?

Auth0 will, by default, include the sub claim containing the user ID in the access token. With that information alone you might be able to check, in the backend database, whether the user is an administrator and decide if the API endpoint can be accessed with that token.

If the role information will be kept on the Auth0 side (it could be, for instance, an entry in the user’s app_metadata property, or using the Authorization Extension), then you could put the role information into the token as a form of optimization, so that the JWT access token includes the role and the backend API can simply look for an “admin” role in the token.

Sorry for a very generic response, but there are many aspects that were not mentioned by you and there’s no one generic good answer for this scenario.

Hi Nicolas. First of all, big thanks for your awesome response :slight_smile:

Answer: Two different apps

Answer: This part I understood

Answer: I have a general api(graphql), so, admin and customers can use query operations, but only administrators can write in mutations. Admins can only log in in admin app and customers only in customer app.

Answer: Yes, I want to store in Auth0 side.

Did you understood me? One api, two apps, one app for customers, another one for administrators. Auth0 to manage users and authentications.

An administrator cannot be a client and a client cannot be an administrator.

Big thanks again and if possible, show me the simple & right way :slight_smile:

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:

  1. Go fetch the user authorization information on each API request, to check for the user’s actual role each time the API is accessed.
  2. 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.

1 Like

Hi Nicholas, big thanks again. I have no words to thank you for the great explanation you gave me :slight_smile:.

Re: True story

Re: I will try this way

Re: A string is enough

Re: I will store the role & userID inside of JWT. I need performance. I don’t want to recheck in each request.

Re: What you mean here? I thought it was the auth0 that generates the tokens.

Re: I will use this way.

One more question:

How can I deny customers trying to login admin app or admins trying to customer app?

Ty :slight_smile:

Yes, sorry I wasn’t clear on that. The application requests a token to Auth0. The user gives consent (by authenticating and, depending on the situation, by a consent prompt) and then Auth0 issues the token to the application.

The rule I wrote before will do exactly that, with this part:

  if (context.clientID === customerClientID &&
    user.app_metadata.roles.indexOf("customer") < 0) {
    return callback(new Error("The user doesn't have a customer role"));
  }