Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Including Core Identity role claims in Identity Server 4 id_token

I am having an issue including role claim after a successful login into Identity Server 4 (IS4) with integration with AspNet Core Identity. This prevents me from using the "Authorize(Roles=xxx)" attribute to secure the access to the api.

I was following samples provided in the Identity Server 4/AspNet Identity integration documentations. Suprisingly the documentation don't have an example to include a role claim which I think is a very common scenario.

I setup 3 projects as per the IS4 documentation with HybridClientCredential grant type specified in the host, created the AspNet Identity DB, and add the role ("Admin") manually into the database generated by EF Core. Provided I've setup things correctly, I am expecting the role to be automatically included into the user claims after a successful login.

  • AspNet Core (Individual Authentication) / Identity Server (host)
  • MVC App (client)
  • MVC Web Api (api)

This is the code I am using:

Host:

public class Config
{
    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new[]
        {
            // expanded version if more control is needed
            new ApiResource
            {
                Name = "api1",

                Description = "My API",

                // secret for using introspection endpoint
                ApiSecrets =
                {
                    new Secret("secret".Sha256())
                },

                // include the following using claims in access token (in addition to subject id)
                UserClaims = { "role" },

                // this API defines two scopes
                Scopes =
                {
                    new Scope()
                    {
                        Name = "api1",
                        DisplayName = "Full access to API 1",
                        UserClaims = new [] { "role" }
                    }
                }
            }
        };
    }

    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile()
        };
    }

    public static IEnumerable<Client> GetClients()
    {
        return new List<Client>()
        {
            new Client
            {
                ClientId = "mvc",
                ClientName = "MVC Client",
                AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,

                RequireConsent = false,

                ClientSecrets =
                {
                    new Secret("secret".Sha256())
                },

                RedirectUris           = { "http://localhost:5002/signin-oidc" },
                PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
                AlwaysIncludeUserClaimsInIdToken = true,
                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "api1"
                },
                AllowOfflineAccess = true
            }
        };
    }

}

Client:

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services.
        services.AddMvc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationScheme = "Cookies"
        });

        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

        app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
        {
            AuthenticationScheme = "oidc",
            SignInScheme = "Cookies",

            Authority = "http://localhost:5000",
            RequireHttpsMetadata = false,

            ClientId = "mvc",
            ClientSecret = "secret",

            ResponseType = "code id_token",
            Scope = { "api1", "offline_access" },

            GetClaimsFromUserInfoEndpoint = true,
            SaveTokens = true,

            TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = JwtClaimTypes.Name,
                RoleClaimType = JwtClaimTypes.Role,
            }
        });

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

Api:

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

    // GET api/values/5
    [HttpGet("{id}")]
    public 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)
    {
    }
}

I got the setup working except that the host not including the role claim after a successful login.

I was wondering if anyone could help to let me know how to resolve this issue? Thanks.

like image 981
Adrian Avatar asked Aug 26 '17 22:08

Adrian


1 Answers

Hi this is the way I create custom Policy role-based,

1)

in Config -> Client section add:

 Claims = new Claim[]
 {
     new Claim("Role", "admin")
 }

2)

Then in the Api -> startup.cs -> ConfigureServices add:

    services.AddAuthorization(options =>
    {
        options.AddPolicy("admin", policyAdmin =>
        {
            policyAdmin.RequireClaim("client_Role", "Admin");
        });

        //otherwise you already have "api1" as scope

        options.AddPolicy("admin", builder =>
        {
            builder.RequireScope("api1");
        });
    });

3)

Then use it this way:

    [Route("api/[controller]")]
    [Authorize("admin")]
    public class ValuesController : Controller

If you analize the token you'll have something like this:

{
  "alg": "RS256",
  "kid": "2f2fcd9bc8c2e54a1f29acf77b2f1d32",
  "typ": "JWT"
}

{
  "nbf": 1513935820,
  "exp": 1513937620,
  "iss": "http://localhost/identityserver",
  "aud": [
    "http://localhost/identityserver/resources",
    "MySecuredApi"
  ],
  "client_id": "adminClient",
  "client_Role": "admin",       <---------------
  "scope": [
    "api.full_access",
    "api.read_only"
  ]
}

PS:

you cannot use "RequireRole" because Identity Server 4 when you use:

Claims = new Claim[]
{
    new Claim("Role", "admin")
},

will create:

client_Role: admin

but "RequireRole" uses:

Role: admin

so it won't match.

As you can test:

using System.Security.Claims;
MessageBox.Show("" + new Claim("Role", "admin"));

UPDATE WITH RequireRole : Clear "ClientClaimsPrefix"

in Config -> Client section add:

ClientClaimsPrefix = "",
Claims = new Claim[]
{
 new Claim(ClaimTypes.Role, "Admin")
}

Then in the Api -> startup.cs -> ConfigureServices add:

services.AddAuthorization(options =>
{
    options.AddPolicy("admin", builder =>
    {
        builder.RequireRole(new[] { "Admin" });
    });
});

Then use it this way:

    [Route("api/[controller]")]
    [Authorize("admin")]
    public class ValuesController : Controller

Otherwise without the "Policy" use like this:

[Authorize(Roles = "Admin")]
like image 69
AngelBlueSky Avatar answered Oct 22 '22 00:10

AngelBlueSky