9

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.

Raul Medeiros
  • 177
  • 2
  • 9
  • What do you mean by generating a claim **in** the API? The claims are generated on IdentityServer, and are based on the user/client that is being authenticated. For me it seems that you need a scope, that is required by your API, and this scope should contain your additional claims. Am I correct? – m3n7alsnak3 Mar 15 '18 at 14:52
  • @m3n7alsnak3, I think yes. My Ensino.Api create a "School", and I should call my user SchoolPrincipal with specific Id. IdentityServer only knows about profile, not school. So, in Ensino.Api i have to add the claim SchoolPrincipal to identity information. – Raul Medeiros Mar 15 '18 at 14:59
  • 1
    I had basically the same need of you, and by extending IdentityServer4 API, like to create a new endpoint there you could do what you need in IdentityServer itself, by doing a post request from your API. For that the solution is on: [Custom endpoint for authorized clients on Identity Server 4](https://stackoverflow.com/questions/46302843/custom-endpoint-for-authorized-clients-on-identity-server-4) For complement, check this link too: [IdentityServer4 Adding more API Endpoints doc](http://docs.identityserver.io/en/release/topics/add_apis.html) – Flaviano Flauber Oct 15 '18 at 14:35

3 Answers3

8

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.

Brad
  • 4,493
  • 2
  • 17
  • 24
2

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);
        ...
    }
}
makman99
  • 1,045
  • 14
  • 18
0

OK so step by step:

  1. You need to create an API Resource(beehouse.scope.ensino-api in your case, but I'll recommend you to hide such info when posting code here) in Identity Server. It should be with the same name as your options.ApiName
  2. You need to add this scope to the allowed scopes of your MVC client.

Both steps are described here, but the main thing is when adding the resource you can do something like:

new ApiResource("beehouse.scope.ensino-api", "My test resource", new List<string>() { "claim1", "claim2" });

and then in your client configuration:

new Client
    {
        ClientId = "client",
        .
        .
        // scopes that client has access to
        AllowedScopes = { "beehouse.scope.ensino-api" }
        .
        .
    }

This will add the claims that are associated with this resource to the token. Of course you will have to set this claims on Identity Server level, but from what you said, you already know how to do this.

m3n7alsnak3
  • 3,026
  • 1
  • 15
  • 24
  • 1
    This way I must add claims in Identity Server. That's just what i'm trying to avoid. I don't know if my approach is bad architeture, but I think to resolve in this way: Api receives token. Api gets sid from token. Api go to database and try get School of this sid. If have school Id, Api Adds "SchoolPrincipal" Claim. Then Api go to database and try get Enrollments. If have Enrollments, add "Student" claim. I'm trying to prevent IdentityServer from knowing this. Identity Server don't have access to School database. And Api don't access Identity server database. – Raul Medeiros Mar 15 '18 at 15:55