Home > Software engineering >  How can I map 'scope' values to Identity Claims?
How can I map 'scope' values to Identity Claims?

Time:10-22

I've specified an Authorization Policy that requires the scope my_custom_value, e.g.

services.AddAuthorization(AuthConfig.GetAuthorizationOptions);

// ...

public static void GetAuthorizationOptions(AuthorizationOptions options)
{
    options.AddPolicy("MyPolicy", policy =>
    {
        policy.RequireScope("my_custom_value");
    });

Requests for endpoints that are protected by MyPolicy are failing because the Principal doesn't contain any scopes

I can see that my auth token has the following scopes:

"scope": [
    "openid",
    "profile",
    "my_custom_value",
    "offline_access"
],

It appears these are not being mapped to the Principal's claims. When I inspect the Claims later when the user attempts to access a protected endpoint, there are no scopes.

policy.RequireAssertion(context =>
{
    if (context.User.HasClaim(c => c.Type == "scope")) // <-- always false
    {
        if (context.User.HasClaim(c => c.Value == "my_custom_value"))
        {
            return true;
        }
    }

Why are the scopes not being mapped? What do I need to do to map them?

For reference, I've tried it with

options.ClaimActions.MapUniqueJsonKey(JwtClaimTypes.Scope, "scope");
options.Scope.Add("my_custom_value");

Am I supposed to implement a custom IProfileService to include the scopes in the OnUserInformationReceived event?

CodePudding user response:

When doing oidc auth using MVC only the IdentityToken claims are mapped to the ClaimsPrincipal. I couldn't figure out a way to map or include access tokens claims to the ClaimsPrincipal.

I ended up writing an authorization handler that validates the access token and performs required claim checks. I assume you read about authorization policies in asp.net 5.0.

public class AccessTokenAuthorizationHandler : AuthorizationHandler<AccessTokenRequirement> {

readonly IOptionsMonitor<OpenIdConnectOptions> _openIdConnectOptions;
readonly ILogger<AccessTokenAuthorizationHandler> _logger;
readonly IOptions<OpenIdOptions> _openIdOptions;

public AccessTokenAuthorizationHandler(
  ILogger<AccessTokenAuthorizationHandler> logger, 
  IOptionsMonitor<OpenIdConnectOptions> openIdConnectOptions, 
  IOptions<OpenIdOptions> openIdOptions) {
  _logger = logger;
  _openIdConnectOptions = openIdConnectOptions;
  _openIdOptions = openIdOptions;
}

protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AccessTokenRequirement requirement) {
  if (context == null) {
    throw new ArgumentNullException(nameof(context));
  }
  if (requirement == null) {
    throw new ArgumentNullException(nameof(requirement));
  }
  if (context.Resource is Microsoft.AspNetCore.Mvc.ActionContext actionContext) {
    ClaimsPrincipal principal = await GetAccessTokenPrincipal(actionContext.HttpContext).ConfigureAwait(false);
   
    // verify your requirement
    if (condition met) {
      context.Succeed(requirement);
    }
  }
}

private async Task<ClaimsPrincipal> GetAccessTokenPrincipal(HttpContext httpContext) {
  if (httpContext == null) {
    return null;
  }
  String accessToken = await httpContext.GetUserAccessTokenAsync().ConfigureAwait(false);
  if (!String.IsNullOrWhiteSpace(accessToken)) {
    try {
      TokenValidationParameters validationParameters = await BuildValidationParameters();
      return new JwtSecurityTokenHandler().ValidateToken(accessToken, validationParameters, out var rawValidatedToken);
      
    }
    catch (SecurityTokenValidationException validationException) {
      _logger.LogWarning(validationException, "Access token not valid.");
    }
    catch (Exception ex) {
      _logger.LogError(ex, "Access token could not be validated.");
    }
  }
  return null;
}

private async Task<TokenValidationParameters> BuildValidationParameters() {
  var options = _openIdConnectOptions.Get(OpenIdConnectDefaults.AuthenticationScheme);
  var discoveryDocument = await options.ConfigurationManager.GetConfigurationAsync(CancellationToken.None);
  var signingKeys = discoveryDocument.SigningKeys;
  var validationParameters = new TokenValidationParameters {
    RequireExpirationTime = true,
    RequireSignedTokens = true,
    ValidateIssuer = true,
    ValidIssuer = options.Authority,
    ValidateIssuerSigningKey = true,
    IssuerSigningKeys = signingKeys,
    ValidateLifetime = true,
    ValidateAudience = true,
    ValidAudience = "your audience",
    ValidateActor = false,
    ValidTypes = new String[] { "at jwt" },
    ClockSkew = TimeSpan.FromMinutes(2),
  };
  return validationParameters;
}

}

I am not happy i had to do it this way, though i think it is done properly. To retrieve the access token i am using nuget package IdentityModel.AspNetCore, Version=3.0.0.0

I don't understand why not more people have this problem. Of course if your app consumes data from an api you pass on the access token, and there the access token becomes the claims principal. But if your mvc app performs direct database access (and might be later extracted to an api) you need to somehow be able to check claims of the access token. Maybe we have some conceptual misunderstanding...

Regarding the profile service. I think trying to include access token claims into the identity token would not be the correct approach. I think it wouldn't even be possible because you have no information about requested scopes when the service is called for the identity token.

  • Related