1

The browser app

I have a browser app (CRA, TypeScript) which is issuing, after successfully authenticating to Azure AD, a request to my API:

public async acquireAccessToken(): Promise<string | undefined> {
    let res: AuthResponse | undefined = undefined;
    const params: AuthenticationParameters = {
        scopes: ["Users.Read"],
    };

    try {
        res = await this.msal.acquireTokenSilent(params);
    } catch (error) {
        res = await this.msal.acquireTokenPopup(params);
    }

    return !res || !res.accessToken ? undefined : res.accessToken;
}

The one before is a utility method to get the access token to contact the API, the actual call is here:

const token = await acquireAccessToken();
const res = await fetch("/controller/test", {
    method: "GET",
    headers: {
        "Authorization": `Bearer ${token}`
    },
});

console.log(res.text());

Where msal is the UserAgentApplication I am using as client to handle authentication and authorization in my browser app.

I have everything correctly set up in Azure where a registration app is used to represent the browser app, and another registration app is used to describe the API I need to contact.

The API

The API server is an ASP.NET Core 3.1 C# application whose Startup.cs is:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseAuthentication();
        app.UseAuthorization();
    }
}

I have removed all the extra code and left the parts that concern auth.

The controller I am contacting is:

[ApiController]
[Route("controller")]
public class MyController : ControllerBase
{
    [HttpGet("test/")]
    [Authorize(Roles = "Admin")]
    public async Task<string> Test()
    {
        return "Ok";
    }

    [HttpGet("test2/")]
    [Authorize]
    public async Task<string> Test2()
    {
        return "Ok";
    }

    [HttpGet("test3/")]
    public async Task<string> Test3()
    {
        return "Ok";
    }
}

Azure

The setup in Azure is simple: apart from the two app registrations for the browser app and the API, I have set in the browser app registration some custom roles and assigned them to some users.

I authenticate in the browser app using a user who has the Admin app role assigned to it.


The problem

When my client app tries to fetch data using these endpoints:

  • /controller/test3
  • /controller/test2

Everything is fine as one is unprotected and the other one uses a simple [Authorize].

However when trying to fetch from /controller/test, I get 403 (Forbidden).

Why can't I make the roles work?


More info

While debugging when fetching test2, I can see, in the controller, that this.User is present and there are several claims. Among those claims, I cannot see anything relating to the role. The access token I get has the following form:

{
  "aud": "api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "iss": "https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/",
  "iat": 1607034050,
  "nbf": 1607034050,
  "exp": 1607037950,
  "acr": "1",
  "aio": "AWQAm/8RAAAAH1j5tZzINJFi5fsMsgf99gcrnqQA+dOhWBpFmsgy3jsr0pFJ0AxvenqthiNLmRqKzqx6l+9SuLlRniAVCTOoqEE7MonnOetO3h7g1/Bm520rS0qiX/gpCCWYm/UwDlJ+",
  "amr": [
    "pwd"
  ],
  "appid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "appidacr": "0",
  "email": "xxx@xxx.xxx",
  "idp": "https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/",
  "ipaddr": "xxx.xxx.xxx.xxx",
  "name": "XXX",
  "oid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "rh": "0.AAAASupzDdEU8EyBI3R6nFeJQHVORvhJZ2hDjJoEO5yPUcZ0AEU.",
  "scp": "Users.Read",
  "sub": "frR45l2dTAIyXZ-3Yn2mGNbBcBX9CrGisgJ4L8zOCd4",
  "tid": "0d73ea4a-14d1-4cf0-8123-747a9c578940",
  "unique_name": "xxx@xxx.xxx",
  "uti": "39dk-rAAP0KiJN5dwhs4AA",
  "ver": "1.0"
}

As you can see, no claim relating to roles.

But note that I can successfully get the role in the user token I get when authenticating. I need that claim to flow in the access token too when I use it to contact the API. How?

Andry
  • 16,172
  • 27
  • 138
  • 246
  • Use https://jwt.ms/ to parse your access token and provide screenshots. – Carl Zhao Dec 04 '20 at 01:50
  • 1
    You could check [this thread](https://stackoverflow.com/questions/45956935/azure-ad-roles-claims-missing-in-access-token): it seems that roles claim will not issue into access_token. The roles only issued issued in the access token when we request the access token using the client credentials flow which contains the permission which require admin consent. You could also check [How to use Azure AD to generate tokens with role definition](https://stackoverflow.com/questions/42656708/), Besides, you could also try to use the ID token. – Zhi Lv Dec 04 '20 at 02:47
  • @CarlZhao: I have added the info on the access token. – Andry Dec 04 '20 at 06:37
  • 1
    @Andry The `delegated permissions` are only the `scp` claims, there is no `roles` claims. If you want the role claims to appear in the access token, you need to grant the `application permission` to the application, and then use the **client credential flow** to obtain the access token. I have answered similar questions before, see: https://stackoverflow.com/questions/63410297/how-to-make-azure-ad-access-token-compliant-for-its-signature-validation-outside/63449621#63449621 – Carl Zhao Dec 04 '20 at 06:48
  • @CarlZhao: So is it just sufficient to add my client app in the API under "Authorized Client Applications"? That will make the "Client Credential Flow" work? Sorry if this seems repeating, but I need to understand the main different from the scenario I have now (which seems to me "Scope Based") from the scenario you are suggesting. In the end I have a user authenticated into a client app and then the client app access an API on its behalf. Is using roles, as I am doing, even the right thing to do here considering it is kinda difficult to get the roles in the access token? – Andry Dec 04 '20 at 07:08
  • @CarlZhao I can see that my client app is issuing a request to get an access token to "/authorize" and not to "/token". Is this the main difference? – Andry Dec 04 '20 at 07:15
  • Are you using auth code flow? "/authorize" is the process by which the logged-in user obtains the authorization code. see: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code – Carl Zhao Dec 04 '20 at 07:27
  • 1
    In fact, the only difference between delegated permissions and application permissions is whether there are users logged in. Application permissions allow an application in Azure Active Directory to act as it's own entity, rather than on behalf of a specific user. Delegated permissions allow an application in Azure Active Directory to perform actions on behalf of a particular user. – Carl Zhao Dec 04 '20 at 07:32
  • @Andry If you need to log in as a user, then you can only use delegated permissions, but as I said in the comments, delegated permissions only have scp claims and no roles claims. If you need to authorize based on the role of the logged in user, you can only use id token . see: https://learn.microsoft.com/en-us/azure/active-directory/develop/id-tokens#payload-claims – Carl Zhao Dec 04 '20 at 07:39
  • @CarlZhao: I get the ID token when I sign in the user in the client app. You are saying I should use that token to contact my API instead of the access token? But that flow is not recommended. Btw I have tried the client credentials flow, using a secret to get a token, I still cannot see my roles in there. I did exactly as you reported in your post, specifying client_id, secret, grant_type=client_credentials and scope my application ID with default. I get a token, but that thing lacks roles :( – Andry Dec 04 '20 at 07:54
  • Have you granted `application permissions` to the client application? https://i.stack.imgur.com/xj5M5.png – Carl Zhao Dec 04 '20 at 07:56
  • @CarlZhao Yes that fixed it. But I do not want to go there. The secret has to be provided by the app and therefore it complicates stuff as a vault should be used. I am thinking I should use graph in the API, to add an authorization policy by checking the user roles. Since those roles cannot flow inside the access token I get through the authorize flow, I will get them using graph. Does it make sense? – Andry Dec 04 '20 at 08:02
  • You can accept answers that are helpful to you to end the thread. – Carl Zhao Dec 15 '20 at 14:34

2 Answers2

0

I found your problem:

You cannot set a custom role in the manifest of the browser application. You need to set a custom role in the manifest of the api application, and then assign the role to the user.

Then you need to use the auth code flow to get the access token, and the roles claims will be included in it.


In fact, the access token is created based on the intended recipient of your token (ie your api). If you want to access the api, you must have permissions or roles. In your question, this role is you Custom, when you grant the user a custom role of the api application, then the user has the role that can access the api application. (Note that this is not to assign the custom role of the client application to the user, because you need to access the api application, so the role of api application is required), then the user can log in to the client application and request a token from the api application. When you obtain the token and use the token to access the API, the API only needs to verify whether the user you log in has the role to access it.

Carl Zhao
  • 8,543
  • 2
  • 11
  • 19
  • It actually makes sense. Let me try this thing. And btw, thanks for spending time on this man! – Andry Dec 04 '20 at 08:47
  • But wait... my users sign-in in the browser client app, then the browser client app will issue requests to the API. If I need to do what you told me, then I need to define roles in the API app registration (now they are defined in the browser app registration in AZ) but then I also need to add users in the API enterprise app and assign them roles there too. But when i sign in users in the browser app I use the browser app registration client-id, when exactly does the API does the auth flow? Every time I issue a request to the API from my browser app? – Andry Dec 04 '20 at 08:56
  • @Andry Because the comment has a word limit, I replied to you in the answer. – Carl Zhao Dec 04 '20 at 09:36
  • The problem is that I am not understanding the flow at all. Plus I cannot see this documented :( Maybe I am missing it, which in case, can you please point me to the doc describing what you are saying. – Andry Dec 04 '20 at 09:48
  • This is the flow: 1. User in browser app signs in against its app reg. He gets an id token. 2. User now signed in requests an access token to an api. He gets it because in the browser app reg. the API app reg. is listed in the permissions with the scopes. Now, you are suggesting I move he registration of my users and roles to the API app reg. But how then can my users sign in? When does the signin happen? If I have moved my users to the API app reg, only the API app can issue a request to authenticate, but since no user interaction happen, how can a user type in UN and PW? – Andry Dec 04 '20 at 09:52
  • Yes, the user is logging in to the client application, but when logging in, its scope parameter is set to `api://api app client id/xxx`, so it is a token based on the api request, so the user must have an api application Custom role. Otherwise, when you use the token to request the API, it will prompt you 403 Forbidden! – Carl Zhao Dec 04 '20 at 10:30
0

This was a hard nut to crack and is not available in auth0 by default.

You can set roles in the Auth0 id token for OpenId, But you have to write a rule in auth0 dashboard:

Go To Rules section on your Auth0 dashboard and create a new rule: enter image description here

and then use this code to add the user role claims to the id token, that will be returned in the JWT token claims:

function addRolesToAccessToken(user, context, callback) {
  
  
  const namespace = 'http://schemas.microsoft.com/ws/2008/06/identity/claims';
  const assignedRoles = (context.authorization || {}).roles;

  let idTokenClaims = context.idToken || {};
  let accessTokenClaims = context.accessToken || {};

  idTokenClaims[`${namespace}/role`] = assignedRoles;
  accessTokenClaims[`${namespace}/role`] = assignedRoles;

  context.idToken = idTokenClaims;
  context.accessToken = accessTokenClaims;
  
  return callback(null, user, context);
}

Hope this helps! Cheers

Zeeshan Adil
  • 1,937
  • 5
  • 23
  • 42