sc1234
sc1234

Reputation: 157

Added Custom Claim, showing in ID token missing in Access token

I have a .NET Core Identity Provider (which also uses IdentityServer4) which authenticates SPA applications with Azure AD. I am adding an "oid" claim with the object identifier value received from Azure. The problem is that from the SPA application I can see the "oid" claim in the ID token but cannot see it in the access token. I need the oid in the access token as well. Here is the relevant code:

Startup.cs

services.AddAuthentication()
    .AddCookie("Cookies", options =>
    {
        options.ExpireTimeSpan = TimeSpan.FromMinutes(10);

    })
    .AddOpenIdConnect(ActiveDirectoryTenants.TenantA, ActiveDirectoryTenants.TenantA, options => Configuration.Bind("TenantAAzureAd", options))
    .AddOpenIdConnect(ActiveDirectoryTenants.TenantB, ActiveDirectoryTenants.TenantB, options => Configuration.Bind("TenantBAzureAd", options));
    
AddActiveDirectoryOpenIdConnectOptions(services, ActiveDirectoryTenants.TenantA);
AddActiveDirectoryOpenIdConnectOptions(services, ActiveDirectoryTenants.TenantB);

I have a common function to add other options to these configurations. I tried to add the oid claim in OnTokenValidated but didn't receive the oid claim in the access token.

protected virtual void AddActiveDirectoryOpenIdConnectOptions(IServiceCollection services, string tenant)
{
    services.Configure<OpenIdConnectOptions>(tenant, options =>
    {
        options.Authority = options.Authority + "/v2.0/"; 
        options.TokenValidationParameters.ValidateIssuer = false; 

        options.Events = new OpenIdConnectEvents
        {
            OnRedirectToIdentityProvider = ctx =>
            {
                ctx.ProtocolMessage.LoginHint = ctx.Properties.GetString("username");
                return Task.CompletedTask;
            },
            OnTokenValidated = ctx =>
            {
                //Maybe need to add oid here??? 
            }
        };
    });
}

The oid claim is being added after successfully logging in to Azure AD.

AccountController.cs

public async Task<IActionResult> ExternalLoginCallback(string returnUrl, string remoteError = null, string openIdScheme = null)
{
    var authResult = await HttpContext.AuthenticateAsync(openIdScheme ?? ActiveDirectoryTenants.TenantA);
    var externalUser = authResult.Principal;
    var claims = externalUser.Claims.ToList();
    var applicationUser = //gets the user based on the email found in claims, omitted for brevity
    
    await userManager.AddClaimAsync(applicationUser, new Claim("oid", claims.First(x => x.Type == http://schemas.microsoft.com/identity/claims/objectidentifier).Value));
    
    await signInManager.SignInAsync(applicationUser, false, "AzureAD");

    return Redirect("~/");
}

The ID token received in the SPA application (note the oid claim):

{
  "nbf": xxx,
  "exp": xxx,
  "iss": "https://localhost:3000", 
  "aud": "xxx-spa-test",
  "iat": xxx,
  "at_hash": "",
  "s_hash": "",
  "sid": "",
  "sub": "guid",
  "auth_time": 1620026953,
  "idp": "AzureAD",
  "display_name": "Test User",
  "oid": "guid",
  "role": [
    "Staff",
  ],
  "name": "test@azureaddomain",
  "amr": [
    "external"
  ]
}

The access token received in the SPA application (note the missing oid claim):

{
  "nbf": xxx,
  "exp": xxx,
  "iss": "https://localhost:3000",
  "aud": [
    "https://localhost:3000/resources",
    "xxx-api-test-scope"
  ],
  "client_id": "xxx-spa-test",
  "sub": "guid",
  "auth_time": 1620026953,
  "idp": "AzureAD",
  "role": [
    "Staff",
  ],
  "name": "test@azureaddomain",
  "scope": [
    "openid",
    "profile",
    "xxx-api-test-scope"
  ],
  "amr": [
    "external"
  ]
}

Upvotes: 0

Views: 1051

Answers (1)

Tore Nestenius
Tore Nestenius

Reputation: 19971

For the claim to end up in the access token, you need to add a ApiScope and add the Userclaim name to it. Alternatively, add an ApiScope and an ApiResource that contains the UserClaim.

Like

var apiresource1 = new ApiResource()
{
    Name = "apiresource1",   //This is the name of the API
    ApiSecrets = new List<Secret>
    {
        new Secret("myapisecret".Sha256())
    },
    Description = "This is the order Api-resource description",
    Enabled = true,
    DisplayName = "Orders API Service",
    Scopes = new List<string> { "apiscope1"},
    UserClaims = new List<string>
    {
        //Custom user claims that should be provided when requesting access to this API.
        //These claims will be added to the access token, not the ID-token!
        "apiresource1-userclaim3",
    }
};

See my answer here for more details

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

Upvotes: 1

Related Questions