User claims in ASP.NET Core using OpenID Connect Authentication

This article shows two possible ways of getting user claims in an ASP.NET Core application which uses an IdentityServer4 service. Both ways have advantages and require setting different code configurations in both applications.

Code: https://github.com/damienbod/AspNetCoreHybridFlowWithApi

To use OpenID Connect in an ASP.NET Core application, the Microsoft.AspNetCore.Authentication.OpenIdConnect package can be used. This needs to be added as a reference in the project.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.0.0" />
  </ItemGroup>

</Project>

Option 1: Returning the claims in the id_token

The profile claims can be returned in the id_token which is returned after a successful authentication. The ASP.NET Core client application just needs to request the profile scope.

public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(options =>
	{
		options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
	})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
	   options.SignInScheme = "Cookies";
	   options.Authority = "https://localhost:44352";
	   options.RequireHttpsMetadata = true;
	   options.ClientId = "codeflowpkceclient";
	   options.ClientSecret = "codeflow_pkce_client_secret";
	   options.ResponseType = "code";
	   options.UsePkce = true;
	   options.Scope.Add("profile");
	   options.Scope.Add("offline_access");
	   options.SaveTokens = true;
   });

   services.AddAuthorization();
   services.AddRazorPages();
}

In IdentityServer4, the corresponding client configuration uses the AlwaysIncludeUserClaimsInIdToken property to include the user profile claims in the id_token. By implementing the IProfileService, any claims can be added.

With this, all claims will be returned in the id_token and can then be used in the client application. This increases the size of the token, which might be important if you add to many claims. All values will be included and available in the User.Identity context in the client application.

new Client
{
	ClientName = "codeflowpkceclient",
	ClientId = "codeflowpkceclient",
	ClientSecrets = {new Secret("codeflow_pkce_client_secret".Sha256()) },
	AllowedGrantTypes = GrantTypes.Code,
	RequirePkce = true,
	RequireClientSecret = true,
	AllowOfflineAccess = true,
	AlwaysSendClientClaims = true,
	UpdateAccessTokenClaimsOnRefresh = true,
	AlwaysIncludeUserClaimsInIdToken = true,
	RedirectUris = {
		"https://localhost:44330/signin-oidc",
		$"{codeFlowClientUrl}/signin-oidc"
	},
	PostLogoutRedirectUris = {
		"https://localhost:44330/signout-callback-oidc",
		$"{codeFlowClientUrl}/signout-callback-oidc"
	},
	AllowedScopes = new List<string>
	{
		IdentityServerConstants.StandardScopes.OpenId,
		IdentityServerConstants.StandardScopes.Profile,
		IdentityServerConstants.StandardScopes.OfflineAccess,
		"role"
	}
}

Option 2: Returning the claims using the UserInfo API

A second way to get the user claims is to use the OpenID Connect User Info API. The ASP.NET Core client application uses the GetClaimsFromUserInfoEndpoint property to configure this. One important difference to option 1, is that you MUST specify the claims you require using the MapUniqueJsonKey method, otherwise only the name, given_name and email standard claims will be available in the client application. The claims included in the id_token are mapped per default. This is the major difference to the first option. You must explicit define some of the standard claims you require.

public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(options =>
	{
		options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
	})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
	   options.SignInScheme = "Cookies";
	   options.Authority = "https://localhost:44352";
	   options.RequireHttpsMetadata = true;
	   options.ClientId = "codeflowpkceclient";
	   options.ClientSecret = "codeflow_pkce_client_secret";
	   options.ResponseType = "code";
	   options.UsePkce = true;
	   options.Scope.Add("profile");
	   options.Scope.Add("offline_access");
	   options.SaveTokens = true;
	   options.GetClaimsFromUserInfoEndpoint = true;
	   options.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username");
	   options.ClaimActions.MapUniqueJsonKey("gender", "gender");
   });

   services.AddAuthorization();
   services.AddRazorPages();
}

The IdentityServer4 can be configured without the AlwaysIncludeUserClaimsInIdToken set.

new Client
{
	ClientName = "codeflowpkceclient",
	ClientId = "codeflowpkceclient",
	ClientSecrets = {new Secret("codeflow_pkce_client_secret".Sha256()) },
	AllowedGrantTypes = GrantTypes.Code,
	RequirePkce = true,
	RequireClientSecret = true,
	AllowOfflineAccess = true,
	AlwaysSendClientClaims = true,
	UpdateAccessTokenClaimsOnRefresh = true,
	//AlwaysIncludeUserClaimsInIdToken = true,
	RedirectUris = {
		"https://localhost:44330/signin-oidc",
		$"{codeFlowClientUrl}/signin-oidc"
	},
	PostLogoutRedirectUris = {
		"https://localhost:44330/signout-callback-oidc",
		$"{codeFlowClientUrl}/signout-callback-oidc"
	},
	AllowedScopes = new List<string>
	{
		IdentityServerConstants.StandardScopes.OpenId,
		IdentityServerConstants.StandardScopes.Profile,
		IdentityServerConstants.StandardScopes.OfflineAccess,
		"role"
	}
}

Mapping the Name property for the http user context.

The User.Identity.Name property can be matched from any claim using the TokenValidationParameters. If the default value is not returned, then you need to map this explicitly.

options.TokenValidationParameters = new TokenValidationParameters
{
  NameClaimType = "email", 
  // RoleClaimType = "role"
};

ASP.NET Core also does some magic mapping as a default. Some claims are removed, and some are added. If you want to take control of this, you can turn this off as follows:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

Links:

https://openid.net/specs/openid-connect-core-1_0.html

https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims

https://docs.microsoft.com/en-us/aspnet/core/security/?view=aspnetcore-3.0

https://tools.ietf.org/html/rfc7636

https://docs.microsoft.com/en-us/aspnet/core/razor-pages

Missing Claims in the ASP.NET Core 2 OpenID Connect Handler?

4 comments

  1. […] User claims in ASP.NET Core using OpenID Connect Authentication (Damien Bowden) […]

  2. […] User claims in ASP.NET Core using OpenID Connect Authentication – Damien Bowden […]

  3. […] User claims in ASP.NET Core using OpenID Connect Authentication Implementing authorization in Blazor ASP.NET Core applications using Azure AD security groups Implementing User Management with ASP.NET Core Identity and custom claims […]

  4. Gaurav Puri · · Reply

    This is an excellent explanation!!! I was struggling to understand why some claims are not available to my logged in user with cookies authentication.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: