Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Authorize Policy always return 403 (Forbidden) - MVC/API

I created an API (.Net Core 2 with EF Core) with an endpoint to retrieve certain roles. I integrated ASPNetIdentity into my project and I am using AspNetRoles and the AspNetRoleClaims.

When calling the API the user has a specific role (Admin) in my case. This role has a few role claims. In the startup.cs I added policies for this role:

   options.AddPolicy(
      "Create Roles", policy => policy.RequireClaim("Can create roles", "role.create"));
   options.AddPolicy(
      "View Roles", policy => policy.RequireClaim("Can read roles", "role.read"));
   options.AddPolicy(
      "Edit Roles", policy => policy.RequireClaim("Can update roles", "role.update"));
   options.AddPolicy(
      "Delete Roles", policy => policy.RequireClaim("Can delete roles", "role.delete"));

In my Frontend the users can login with their Microsoft (azure) account and their oidc claim (ID) matches the ID in the AspNetUser table, if their oidc is not found in the user table, they are automatically added (with their oidc id as the aspnetuser id) and they get a default role.

When calling the Role endpoint however, it always returns a 403 error (forbidden). When I check the tables, the user has the right role and role claims to access the endpoint. How is it possible that it keeps returning 403?

The endpoint looks as follow:

[HttpGet]
[Authorize(Policy = "View Roles")]
public IEnumerable<IdentityRole> GetRole()
{
     return _context.Roles;
}

After some research I found a post which tells that you need to include the role (claim) of the user in the token which is being send to the API, though this would mean I will need a GET endpoint that first returns the role(s) of the user, the frontend needs to pick it up and add it to the token, and then call all the other endpoints with the role included in the token? Or am I on the wrong track here?

----UPDATE----

I am 90% sure that the Policy/Authorization check needs the Role claim to be included in Token of the user. The process however is as following right now:

  1. The user goes to the frontend project (react frontend).
  2. The frontend uses adal.js to check if the user is authenticated, if he is not authenticated, then the user gets redirected to the Microsoft login page.
  3. After successful logging in, the API is being called.
  4. In the DI (AddJwtBearer) of the API, the oid claim is being compared with the ID of the ASpNetUsers table, if it is not present, the user is added to the AspNetUser table using the oid value for the ID of the AspNetUser.

Now that the user is also added in the AspNetUser table, I can use the Asp.Net Identity for doing Authorization using Roles and RoleClaims.

The problem however is that the initial received token by the API is the Azure token, which knows nothing about my Identity tables (correct me if I am wrong). I believe this is also the cause of my policy not working (again, correct me if I am wrong).

I found a post where the problem is more or less the same (https://joonasw.net/view/adding-custom-claims-aspnet-core-2), the trick is to extend the current token with my needed Identity claims such as ClaimTypes.Role.

My code to achieve this is as follow:

// Add authentication (Azure AD)
            services
                .AddAuthentication(sharedOptions =>
                {
                    sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                    sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 
                    sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; // Use JWT as ChallendgeSchema, if not, ASPNet Identity will be used by default and this will return a default non-existing endpoint (because it is not created): Account/Login; https://stackoverflow.com/questions/45878166/asp-net-core-2-0-disable-automatic-challenge
                })
                .AddJwtBearer(options =>
                {
                    options.Audience = this.Configuration["AzureAd:ClientId"];
                    options.Authority = $"{this.Configuration["AzureAd:Instance"]}{this.Configuration["AzureAd:TenantId"]}";

                    // Added events which checks if the user (token user) exists in our own database, if not then the user is being added with a 'User' role
                    options.Events = new JwtBearerEvents()
                    {
                        OnTokenValidated = context =>
                        {
                            // Check if roles are present
                            CheckRoles cr = new CheckRoles();
                            cr.CreateRoles(services.BuildServiceProvider());

                            // Check if the user has an OID claim(oid = object id = user id)
                            if (!context.Principal.HasClaim(c => c.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier")) 
                            {
                                context.Fail($"The claim 'oid' is not present in the token.");
                            }

                            ClaimsPrincipal userPrincipal = context.Principal;

                            CheckUser cu = new CheckUser();

                            cu.CreateUser(userPrincipal, services.BuildServiceProvider());

                            // Extend the current token with my (test) Role claim
                            var claims = new List<Claim>
                            {
                                new Claim(ClaimTypes.Role, "Admin")
                            };
                            var appIdentity = new ClaimsIdentity(claims);
                            context.Principal.AddIdentity(appIdentity);


                            return Task.CompletedTask;
                        }
                    };
                });

Sadly the above does not work, when calling the API from the frontend, the token remains unchanged and the RoleClaim is not being added.. Anyone has a clue how to add my RoleClaim to the token so that I can use my policies?

like image 427
Nicolas Avatar asked May 14 '18 11:05

Nicolas


1 Answers

When calling the API the user has a specific role (Admin) in my case. This role has a few role claims.

If a user has role.read as ClaimTypes.Role in principal object, them you can create policy like the following in Startup.cs -

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddAuthorization(options =>
    {
        options.AddPolicy("View Roles", policyBuilder =>
        {
            policyBuilder.RequireAuthenticatedUser()
                .RequireAssertion(context =>
                    context.User.HasClaim(ClaimTypes.Role, "role.read"))
                .Build();
        });
    });
    ...
}

Update

You need to add JwtBearerDefaults.AuthenticationScheme authentication type to claims identity, so that it matches with default scheme.

services
   .AddAuthentication(sharedOptions =>
   {
      ...
   })
   .AddJwtBearer(options =>
   {
      ...
      options.Events = new JwtBearerEvents()
      {         
         OnTokenValidated = context =>
         {
            ...
            var appIdentity = new ClaimsIdentity(claims, 
                   JwtBearerDefaults.AuthenticationScheme);
                               ^^^^^

            context.Principal.AddIdentity(appIdentity);

            return Task.CompletedTask;
         }
      };
   });
like image 181
Win Avatar answered Oct 17 '22 04:10

Win