Dylan Snel
Dylan Snel

Reputation: 700

Identity: Propagating claims to downstream services

I am trying to set up a distributed system with Duende-IdentityServer. In my architecture I am using a BFF (Backend For Front) as an API-GateWay for my client.

When my user is logged in using the BFF I want requests to propagate from the BFF to downstream services. (I am using GraphQL with stitching and schema federation, but I feel that might be irrelevant to the question.) Because I feel that it is important for the downstream services to be in control of the authorization of their data I would like claims received by the BFF to be forwarded to the downstream services. I figured something like attaching a JWT Bearer with the claims would work and was hoping that that way my downstream services wouldn't have to contact the identity server to validate the claims.

I tried a few things, but it is quite easy to get lost in the world that is OAuth2 and OIDC. I can't imagine my use case being that

Here is what I tried so far:

In the BFF:

//program.cs

 builder.Services.AddHttpClient(GraphQLSchemas.Identity, c => c.BaseAddress = new Uri("https://localhost:7500/graphql")).AddUserAccessTokenHandler();
 builder.Services.AddGraphQLServer()
            .AddRemoteSchemasFromRedis("GraphQL", sp => sp.GetRequiredService<ConnectionMultiplexer>())
            .ModifyOptions(x => x.RemoveUnreachableTypes = true);
services.AddBff();
   
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Bff-Cookie";
        options.DefaultChallengeScheme = "oidc";
        options.DefaultSignOutScheme = "oidc";
    })
    .AddCookie("Bff-Cookie", options =>
    {
        // set session lifetime
        options.ExpireTimeSpan = TimeSpan.FromHours(8);

        // sliding or absolute
        options.SlidingExpiration = true;

        // host prefixed cookie name
        options.Cookie.Name = bffOptions.Cookie.Name ;
        options.Cookie.Domain = bffOptions.Cookie.Domain;

        // strict SameSite handling
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    })
    .AddOpenIdConnect("oidc", options =>
    {
        options.Authority = bffOptions.IdentityServer.Host;

        // confidential client using code flow + PKCE
        options.ClientId = bffOptions.IdentityServer.ClientId;
      
        options.ResponseType = OpenIdConnectResponseType.Code;
        options.ResponseMode = "query";

        options.MapInboundClaims = false;
        options.GetClaimsFromUserInfoEndpoint = false;
        options.SaveTokens = true;
        //options.
        // request scopes +refresh tokens
        options.Scope.Clear();
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("email");
        //options.Scope.Add("Administrator");
        options.Scope.Add("roles");
        options.Scope.Add("offline_access");
        options.ClaimActions.MapJsonKey("role", "role", "role");
        options.TokenValidationParameters.RoleClaimType = JwtClaimTypes.Role;
    });
    /// code omitted for brevity
     app.UseBff();

If I log in on the bff these are the claims I get: Claims on bff However the access_token doesnt reflect this: Claims on access_token

So when my HttpClient uses .AddUserAccessTokenHandler(); Only the access_token is passed to my downstream service:

//program.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
         .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
         {
             options.Authority = "https://localhost:7500";
             options.MapInboundClaims = false;

             options.TokenValidationParameters = new TokenValidationParameters()
             {
                 ValidateAudience = false,
                 ValidTypes = new[] { "at+jwt" },

                 NameClaimType = "name",
                 RoleClaimType = "role"
             };
         });
  //code omitted for brevity
app.MapGraphQL().RequireAuthorization(new AuthorizeAttribute
{
    AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme
}).AllowAnonymous();

Claims inside the downstream service But as you can see the Role claim etc is not passed.

How can receive the claims in in my downstream service? Preferably without reaching out to the identityserver. (Though it would be kinda nice if the downstream service could validate the jwt sent to it.

P.S. I also tried to follow 2 tutorials that create a ProfileService implementation, but for some reason when i register another profile service with the DI container the login through the bff fails and I havent been able to figure out why yet. A breakpoint in profile service would not be hit.

Upvotes: 3

Views: 839

Answers (2)

Tore Nestenius
Tore Nestenius

Reputation: 19971

The claims you see in .NET is the claims from the id_token or from the userinfo endpoint. They are separate from the ones found in the access token. You configure this in IdentityServer.

See my answer at ApiResource vs ApiScope vs IdentityResource for more details about this.

So, the IdentityResource and ApiResource defines what claims can be returned for a given user. Then as this picture shows, those requested claims, are then looked up against the user database and the claims that is found in the database are then returned and used in the ID and access token. as the picture from one of my training classes shows:

enter image description here

To complement this answer, I wrote a blog post that goes into more detail about this topic: IdentityServer – IdentityResource vs. ApiResource vs. ApiScope

Upvotes: 1

Amir
Amir

Reputation: 1274

As you may know, Authentication is the concern of the gateway and authorization is Domain-specific and is related to downstream services (each one). JWT token should be validated and verified in the gateway and verification/validation process can be ignored in downstream services.

You can pass JWT headers (Authorization: Bearer bla) to the downstream services and they should ignore the validation like this:

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = false;
    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        RequireExpirationTime = false,
        ValidateLifetime = false,
        SignatureValidator = (jwt, tokenValidationParameters) =>
        {
            return new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(jwt);
        }
    };
}); 

this code makes all of your routes and HTTP request flows to fullfil User object and its Claims and you can take action like this in your controllers:

//You will have claims in your User object even if you mark the action as anonymous.
//[AllowAnonymous]
public async Task Do() 
{
    this.User.HasClaim("bla")
}

This is because of the protocol and Dotnet supports JWT out of the box and I highly recommend you to forward JWT-formatted headers and do not change the token format/protocol for downstream services.

Upvotes: 0

Related Questions