Set custom claims for ClaimsIdentity using IdentityServer4 authentication

Viewed 3099

I have an ASP.NET Core 2.1 application authenticated using IdentityServer4.TokenValidation

authenticationBuilder.AddIdentityServerAuthentication(AuthorizationConstants.IpreoAccountAuthenticationScheme, options =>
    {
      options.RequireHttpsMetadata = false;
      options.ApiName = apiName;
      options.ApiSecret = apiSecret;
      options.Authority = authority;
      options.LegacyAudienceValidation = true;
    });

What is the best way how can I add custom claims to identity? Taking into account that we still need to have an opportunity to use Authorize attribute with Roles validation.

For bearer authentication for example we can use OnTokenValidated handler which is fired on each request. But for IdentityServerAuthenticationOptions Events property is of type of object and initializing it with a dummy object with OnTokenValidated property does not work.

We have to support JWT and reference tokens. Also we need to support multiple authentication schemes

Any ideas or suggestions?

2 Answers

Ruard van Elburg gave me a good idea about using a middleware. The only thing I had to update use this approach for multiple authentication schemes was overriding IAuthenticationSchemeProvider to keep using UseAuthentication middleware.

https://github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication/AuthenticationMiddleware.cs

So it returns default scheme based on a request content

What I had to do:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();
    app.UseMiddleware<ClaimsMiddleware>(); // to set claims for authenticated user
    app.UseMvc();
}

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);    
    services.AddTransient<IAuthenticationSchemeProvider, CustomAuthenticationSchemeProvider>(); 
    services.AddAuthorization();
    services.AddAuthentication // add authentication for multiple schemes
}

You will need middleware for that. As an example, I suggest you take a look at the PolicyServer. It has the same approach.

IdentityServer handles the authentication, while authorization is handled by the PolicyServer. The free OSS version adds claims in the middleware.

From the source code:

/// Add the policy server claims transformation middleware to the pipeline.
/// This middleware will turn application roles and permissions into claims
/// and add them to the current user
public static IApplicationBuilder UsePolicyServerClaims(this IApplicationBuilder app)
{
    return app.UseMiddleware<PolicyServerClaimsMiddleware>();
}

Where PolicyServerClaimsMiddleware is:

public class PolicyServerClaimsMiddleware
{
    private readonly RequestDelegate _next;

    /// <summary>
    /// Initializes a new instance of the <see cref="PolicyServerClaimsMiddleware"/> class.
    /// </summary>
    /// <param name="next">The next.</param>
    public PolicyServerClaimsMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    /// <summary>
    /// Invoke
    /// </summary>
    /// <param name="context">The context.</param>
    /// <param name="client">The client.</param>
    /// <returns></returns>
    public async Task Invoke(HttpContext context, IPolicyServerRuntimeClient client)
    {
        if (context.User.Identity.IsAuthenticated)
        {
            var policy = await client.EvaluateAsync(context.User);

            var roleClaims = policy.Roles.Select(x => new Claim("role", x));
            var permissionClaims = policy.Permissions.Select(x => new Claim("permission", x));

            var id = new ClaimsIdentity("PolicyServerMiddleware", "name", "role");
            id.AddClaims(roleClaims);
            id.AddClaims(permissionClaims);

            context.User.AddIdentity(id);
        }

        await _next(context);
    }
}

And from startup:

public void ConfigureServices(IServiceCollection services)
{

    services.AddMvcCore(options =>
    {
        // workaround: https://github.com/aspnet/Mvc/issues/7809
        options.AllowCombiningAuthorizeFilters = false;
    })
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
    .AddAuthorization();

    // This is not relevant for you, but just to show how policyserver is implemented.
    // The bottom line is that you can implement this anyway you like.

    // this sets up the PolicyServer client library and policy
    // provider - configuration is loaded from appsettings.json
    services.AddPolicyServerClient(Configuration.GetSection("Policy"))
            .AddAuthorizationPermissionPolicies();

}

public void Configure(IApplicationBuilder app)
{
    app.UseAuthentication();

    // add this middleware to make roles and permissions available as claims
    // this is mainly useful for using the classic [Authorize(Roles="foo")] and IsInRole functionality
    // this is not needed if you use the client library directly or the new policy-based authorization framework in ASP.NET Core
    app.UsePolicyServerClaims();

    app.UseMvc();
}
Related