16
votes

I am using IdentityServer4 with .Net Core 2.1 and Asp.Net Core Identity. I have two projects in my Solution.

  • IdentityServer
  • Web API

I want to Protect my Web APIs, I use postman for requesting new tokens, It works and tokens are generated successfully. When I use [Authorize] on my controllers without Roles it works perfectly but when I use [Authorize(Roles="Student")] (even with [Authorize(Policy="Student")]) it always return 403 forbidden

Whats wrong with my code

Web API startup.cs

  public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvcCore()
            .AddAuthorization(options => options.AddPolicy("Student", policy => policy.RequireClaim("Role", "Student")))
            .AddAuthorization(options => options.AddPolicy("Teacher", policy => policy.RequireClaim("Role", "Teacher")))
            .AddAuthorization(options => options.AddPolicy("Admin", policy => policy.RequireClaim("Role", "Admin")))
            .AddJsonFormatters();

            services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    options.Authority = "http://localhost:5000";
                    options.RequireHttpsMetadata = false;
                    options.ApiName = "api1";
                });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseAuthentication();
            app.UseMvc();
        }
    }

Test API :

  [Route("api/[controller]")]
    [ApiController]
    [Authorize(Roles="Student")]
    public class ValuesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public ActionResult<string> Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody] string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }

IdentityServer startup.cs

  public class Startup
    {

        public IConfiguration Configuration { get; }
        public IHostingEnvironment Environment { get; }

        public Startup(IConfiguration configuration, IHostingEnvironment environment)
        {
            Configuration = configuration;
            Environment = environment;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            string connectionString = Configuration.GetConnectionString("DefaultConnection");

            string migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;


            services.AddDbContext<ApplicationDbContext>(options =>
                             options.UseSqlServer(connectionString));

            services.AddIdentity<ApplicationUser, ApplicationRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);


            services.Configure<IISOptions>(iis =>
            {
                iis.AuthenticationDisplayName = "Windows";
                iis.AutomaticAuthentication = false;
            });

            IIdentityServerBuilder builder = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;
            })
                .AddAspNetIdentity<ApplicationUser>()

                // this adds the config data from DB (clients, resources)
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = b =>
                        b.UseSqlServer(connectionString,
                            sql => sql.MigrationsAssembly(migrationsAssembly));
                })
                // this adds the operational data from DB (codes, tokens, consents)
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = b =>
                        b.UseSqlServer(connectionString,
                            sql => sql.MigrationsAssembly(migrationsAssembly));

                    // this enables automatic token cleanup. this is optional.
                    options.EnableTokenCleanup = true;
                    // options.TokenCleanupInterval = 15; // frequency in seconds to cleanup stale grants. 15 is useful during debugging
                })
                .AddProfileService<ProfileService>();

            if (Environment.IsDevelopment())
            {
                builder.AddDeveloperSigningCredential();
            }
            else
            {
                throw new Exception("need to configure key material");
            }

            services.AddAuthentication();

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {


          //  InitializeDatabase(app);



            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseIdentityServer();

            //app.Run(async (context) =>
            //{
            //    await context.Response.WriteAsync("Hello World!");
            //});
        }



        }
    }

IdentityServer4 config.cs

   public class Config
    {
        // scopes define the resources in your system
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
            };
        }

        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("api1", "My API"),
                 new ApiResource("roles", "My Roles"),
                 new IdentityResource("roles", new[] { "role" })
            };
        }

        // clients want to access resources (aka scopes)
        public static IEnumerable<Client> GetClients()
        {
            // client credentials client
            return new List<Client>
            {
                // resource owner password grant client
                new Client
                {
                    ClientId = "ro.client",
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowedScopes = { "api1","roles" }
                }
            };
        }

    }

Token Sample

eyJhbGciOiJSUzI1NiIsImtpZCI6ImU0ZjczZDU5MjQ2YjVjMmFjOWVkNDI2ZGU4YzlhNGM2IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1NDYyNTk0NTYsImV4cCI6MTU0NjI2MzA1NiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJhcGkxIl0sImNsaWVudF9pZCI6InJvLmNsaWVudCIsInN1YiI6IjIiLCJhdXRoX3RpbWUiOjE1NDYyNTk0NTYsImlkcCI6ImxvY2FsIiwic2NvcGUiOlsicm9sZXMiLCJhcGkxIl0sImFtciI6WyJwd2QiXX0.D6OvbrGx2LwrYSySne59VJ_-_kZ-WriNUbDiETiHO4pknYJzBxKr307DxvBImlvP8w35Cxj3rKxwyWDqVxyhdFhFvFFuHmxqIAv_g2r37lYj3ExcGYAn23Q1i4PuXXBWQe2AHuwFsN2cfPcG39f-N-q7pfLFhoHacXe8vSWyvKxSD0Vj3qVz15cj5VMV1R8qhodXMO-5sZfY1wNfkcJmqmXnbpPnUK_KKUY1Pi6YJkU1nYRXGRoW7YLXc7Y2SFSfa9c1ubU3DDVJV0JqVxSBpfGnvydHEpk-gBx11yQgW5nsJdu6Bi2-DVGA5AdZ_-7pz0AVI-eZPwk2lNtlivmoeA

APS.NET_USERS Table

enter image description here

APS.NET_USERS_Claims Table

enter image description here

Postman enter image description here

ApiRequest enter image description here

Claims While Using [Authorize]

enter image description here

3

3 Answers

8
votes

In your Api, somewhere before services.AddAuthentication("Bearer") add a line for JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();.

More info at this post.

EDIT: Additionally, try to update your identity resources configuration with roles identity resource.

    // scopes define the resources in your system
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
            new IdentityResource("roles", new[] { "role" })
        };
    }

And your client AllowedScopes needs adding roles as well then:

    AllowedScopes = { "api1", "roles" }

Lastly, your postman request should then ask for the roles scope to be included scope: api1 roles.

EDIT 2: Also, update your profile to include roles in the issued claims:

    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        context.IssuedClaims.AddRange(context.Subject.Claims);

        var user = await _userManager.GetUserAsync(context.Subject);

        var roles = await _userManager.GetRolesAsync(user);

        foreach (var role in roles)
        {
            context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, role));
        }
    }

The above should probably be updated to only add roles claim when it is requested.

Make sure your newly issued JWT tokens now include roles claim like the one in below:

eyJhbGciOiJSUzI1NiIsImtpZCI6ImU0ZjczZDU5MjQ2YjVjMmFjOWVkNDI2ZGU4YzlhNGM2IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1NDY0Mzk0MTIsImV4cCI6MTU0NjQ0MzAxMiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJhcGkxIiwicm9sZXMiXSwiY2xpZW50X2lkIjoicm8uY2xpZW50Iiwic3ViIjoiMiIsImF1dGhfdGltZSI6MTU0NjQzOTQxMSwiaWRwIjoibG9jYWwiLCJyb2xlIjpbIkFkbWluIiwiU3R1ZGVudCJdLCJzY29wZSI6WyJvcGVuaWQiLCJhcGkxIiwicm9sZXMiXSwiYW1yIjpbInB3ZCJdfQ.irLmhkyCTQB77hm3XczL4krGMUqAH8izllG7FmQhZIQaYRqI7smLIfrqd6UBDFWTDpD9q0Xx0oefUzjBrwq2XnhGSm83vxlZXaKfb0RdLbYKtC4BlypgTEj8OC-G0ktPqoN1C0lh2_Y2PfKyQYieSRlEXkOHeK6VWfpYKURx6bl33EVDcwe_bxPO1K4axdudtORpZ_4OOkx9b_HvreYaCkuUqzUzrNhYUMl028fPFwjRjMmZTmlDJDPu3Wz-jTaSZ9CHxELG5qIzmpbujCVknh3I0QxRU8bSti2bk7Q139zaiPP2vT5RWAqwnhIeuY9xZb_PnUsjBaxyRVQZ0vTPjQ

11
votes

The problem is that the claims are not added to the access token.

There are two tokens, the access token and the identity token.

When you want to add claims to the identity token, then you'll have to configure the IdentityResource. If you want to add claims to the access token, then you'll have to configure the ApiResource (or scope).

This should fix it for you:

public static IEnumerable<ApiResource> GetApiResources()
{
    return new List<ApiResource>
    {
        new ApiResource("api1", "My API"),
        new ApiResource("roles", "My Roles", new[] { "role" })
    };
}

Make sure the client (postman) requests the roles scope.

I did test it with the sample code from IdentityServer. In my setup I've added the role 'TestUser' to alice:

new TestUser
{
    SubjectId = "1",
    Username = "alice",
    Password = "password",
    Claims = new List<Claim> { new Claim("role", "TestUser") } 
},

The Postman call, please note the requested scope:

enter image description here

The access token including the role claim:

enter image description here

1
votes

I have solved this by adding 'role' in Type column in ApiClaims table see the image below.

ApiResourceId column name found in ApiClaims table is primary key of ApiResources table with Id column name.

enter image description here