openid-connectduende-identity-server

How can I provide sub claim within access token for ClientCredentials flow?


I've run into a problem on how to add sub claim to access token for machine to machine communication (ClientCredentials flow). The reason for doing this is because AWS/GCP is adding this claim always. When assuming AWS role it fails because of missing sub claim. Its value will match the client_id. I've found two ways to provide sub claim:

  1. Inherit and implement ICustomTokenRequestValidator

     if (context.Result.ValidatedRequest.GrantType == "client_credentials")
     {
         // Add the "sub" claim to the client's claims.
         var clientId = context.Result.ValidatedRequest.Client.ClientId;
         var subClaim = new Claim("sub", clientId);
         context.Result.ValidatedRequest.ClientClaims.Add(subClaim);
     }
    
  2. Override DefaultTokenService

    var result = await base.CreateAccessTokenAsync(request);
    
    if (result != null && result.SubjectId == null)
    {
        result.Claims.Add(new Claim(JwtClaimTypes.Subject, request.ValidatedRequest?.ClientId));
    }
    

After the change I've stack with problem that now access token can't be accessed by IdentityServer. It's triggering ProfileService, while this service should be triggered only for user-interactive flows and not for client credential flow. UserManager from ASP.NET Identity will fail always when trying to convert string to int using FindByIdAsync. As sub claim now is string.

Has anyone encountered the same problem when trying to add sub claim to all requests?


Solution

  • I've end-up with 2nd option and checking if client is with specific scope. Since the purpose of token is to be used outside of ID and we don't want to break existing functionality sub token is added only for specific scope.

    if (result != null
        && result.SubjectId == null
        && request.ValidatedRequest.Client.AllowedScopes.Any(c => ScopesWithSubClaim.Contains(c, StringComparer.OrdinalIgnoreCase))
        && request.ValidatedRequest.Client.AllowedGrantTypes.Contains(GrantType.ClientCredentials, StringComparer.OrdinalIgnoreCase))
    {
        result.Claims.Add(new Claim(JwtClaimTypes.Subject, request.ValidatedRequest.ClientId));
    }
    

    Also according to docs sub claim for ClientCredentails flow is non-standard practice. Still GCP/AWS are doing this.

    The inclusion of a "sub" claim in tokens issued for the Client Credentials flow is not standard practice and is typically not needed for this flow's intended purpose.

    Since token with issued sub claim won't be used inside ID I've created requirement to forbid it.

    public class ForbiddenScopesHandler : AuthorizationHandler<ForbiddenScopesRequirment>
    {
        override protected Task HandleRequirementAsync(AuthorizationHandlerContext context, ForbiddenScopesRequirment requirement)
        {
            foreach (var scope in context.User.Claims
                .Where(c => string.Equals(c.Type, JwtClaimTypes.Scope, StringComparison.OrdinalIgnoreCase)))
            {
                if (requirement.ForbiddenScopes.Contains(scope.Value))
                {
                    context.Fail(new AuthorizationFailureReason(this, $"Requesting with scope {scope.Value} is forbidden for this client"));
                    return Task.CompletedTask;
                }
            }
    
            // if there is no scope claim or it presents but without forbidden values we succeed. For example for Cookie scheme you won't have scope claim.
            context.Succeed(requirement);
    
            return Task.CompletedTask;
        }
    }
    

    Then this requirement should be attached to your policy .AddRequirements(new ForbiddenScopesRequirment("scope"))). Solution is hacky and leads to custom behavior for clients with specific scopes. Asp.Net will refuse authentication which avoids triggering ProfileService.