Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add additional claims in Api Project when using IdentityServer 4

Sorry for my english.

I have three projects: IdentityServer, Ensino.Mvc, Ensino.Api. The IdentityServer Project provides the main identity information and claims - claim Profile, claim Address, claim Sid... etc, from the IdentityServer4 library. The Ensino.Mvc Project gets this information in a token and sends it to the API, so that the MVC is grranted access to the resources. The token contains all the claims provided by IdentityServer. But in the API, I need to generate other claims that are API specific, like: claim EnrollmentId that corresponds to claim Sid from the token. And also I want to add this claim in HttpContext for future purposes. Can somebody tell me how to achieve this?

I have this code in Startup.ConfigureServices:

// Add identity services
        services.AddAuthentication("Bearer")
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = "http://localhost:5100";
                options.RequireHttpsMetadata = false;
                options.ApiName = "beehouse.scope.ensino-api";
            });

        // Add mvc services
        services.AddMvc();

In other Project, without API, just mvc, I have inherited UserClaimsPrincipalFactory and overridden CreateAsync to add additional claims. I like to do something like this but in the API project. Is it possible?

What the best approach to do this?

EDIT: After some research, what I want to do is: Authentication by IdentityServer and set authorization in api, based on claims and specific api database data.

like image 545
Raul Medeiros Avatar asked Mar 15 '18 14:03

Raul Medeiros


2 Answers

In your API project you can add your own event handler to options.JwtBearerEvents.OnTokenValidated. This is the point where the ClaimsPrincipal has been set and you can add claims to the identity or add a new identity to the principal.

services.AddAuthentication("Bearer")
   .AddIdentityServerAuthentication(options =>
   {
       options.Authority = "http://localhost:5100";
       options.RequireHttpsMetadata = false;
       options.ApiName = "beehouse.scope.ensino-api";

       options.JwtBearerEvents.OnTokenValidated = async (context) => 
       {
           var identity = context.Principal.Identity as ClaimsIdentity;

           // load user specific data from database
           ...

           // add claims to the identity
           identity.AddClaim(new Claim("Type", "Value"));
       };
   });

Note that this will run on every request to the API so it's best to cache the claims if you're loading info from database.

Also, Identity Server should only be responsible for identifying users, not what they do. What they do is application specific (roles, permissions etc.) so you're correct in recognising this and avoiding the logic crossover with Identity Server.

like image 192
Brad Avatar answered Nov 14 '22 02:11

Brad


Making your own AuthenticationHandler that uses the IdentityServerAuthenticationHandler would be the best option. This would allow you to use DI, reject authentication, and skip the custom authentication handler when it is not needed.

Example AuthenticationHandler that first authenticates the token and then adds more claims:

public class MyApiAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // Pass authentication to IdentityServerAuthenticationHandler
        var authenticateResult = await Context.AuthenticateAsync("Bearer");

        // If token authentication fails, return immediately
        if (!authenticateResult.Succeeded)
        {
            return authenticateResult;
        }

        // Get user ID from token
        var userId = authenticateResult.Principal.Claims
            .FirstOrDefault(c => c.Type == JwtClaimTypes.Subject)?.Value;

        // Do additional checks for authentication
        // e.g. lookup user ID in database
        if (userId == null)
        {
            return AuthenticateResult.NoResult();
        }

        // Add additional claims
        var identity = authenticateResult.Principal.Identity as ClaimsIdentity;
        identity.AddClaim(new Claim("MyClaim", "MyValue"));

        return authenticateResult;
    }
}

Add handler to services:

services.AddAuthentication()
    .AddIdentityServerAuthentication(options =>
    {
        // ...
    })
    .AddScheme<AuthenticationSchemeOptions, MyApiAuthenticationHandler>("MyApiScheme", null);

Now you can use either scheme:

// Authenticate token and get extra API claims
[Authorize(AuthenticationSchemes = "MyApiScheme")]

// Authenticate just the token
[Authorize(AuthenticationSchemes = "Bearer")]

Note that IdentityServerAuthenticationHandler does the same thing, using the dotnet JWT handler:

public class IdentityServerAuthenticationHandler : AuthenticationHandler<IdentityServerAuthenticationOptions>
{
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        ...
        return await Context.AuthenticateAsync(jwtScheme);
        ...
    }
}
like image 41
makman99 Avatar answered Nov 14 '22 01:11

makman99