Reputation: 14954
I am building a React SPA with Identity Server 4 and dotnetcore 3.1. The Identity Server client is defined in appsettings.json
using the IdentityServerSPA
profile. Using oidc-client
on the frontend, I can login successfully. Inspecting the oidc
object, I can confirm that:
Bearer
openid profile OpenWorkShopAPI
If I subsequently try to call the API (either with Authorization: Bearer
as the id_token
or the access_token
), there is no sub
claim present. Therefore, _userManager.GetUserAsync(User);
fails.
Enumerating and printing the claims, I see:
Claim: "System.Security.Claims.ClaimsIdentity" "nbf" "1601597732"
Claim: "System.Security.Claims.ClaimsIdentity" "exp" "1601598032"
Claim: "System.Security.Claims.ClaimsIdentity" "iss" "http://dev.openwork.shop:5000"
Claim: "System.Security.Claims.ClaimsIdentity" "aud" "OpenWorkShopAPI"
Claim: "System.Security.Claims.ClaimsIdentity" "iat" "1601597732"
Claim: "System.Security.Claims.ClaimsIdentity" "at_hash" "MoAqNfND0ct1mUFKpUtgcg"
Claim: "System.Security.Claims.ClaimsIdentity" "s_hash" "D81HZF_ii2r0i5-4_ZxnLA"
Claim: "System.Security.Claims.ClaimsIdentity" "sid" "cdMH0nFKdikldL1Gy3S3Eg"
Claim: "System.Security.Claims.ClaimsIdentity" "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" "fdf0023d-7ae9-4aaf-87fe-0f320b869171"
Claim: "System.Security.Claims.ClaimsIdentity" "auth_time" "1601582136"
Claim: "System.Security.Claims.ClaimsIdentity" "http://schemas.microsoft.com/identity/claims/identityprovider" "local"
Claim: "System.Security.Claims.ClaimsIdentity" "http://schemas.microsoft.com/claims/authnmethodsreferences" "pwd"
Based upon this, I can work around this by accessing the name claim to find the ID directly:
string CurrentUserId => User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
But that seems neither correct nor ideal.
I have noticed that all of my MySQL tables (except AspNetUsers
) remain empty. There are no values in AspNetUserClaims
, for example. Yet, as I showed above, the oidc-client
is able to successfully see the sub
claim for the user ID.
Since I am using the DefaultIdentity
and AddApiAuthorization
, I do not expect to need to add any special logic to implement such sub-claims, and am not sure why it is missing on the API calls.
Configuration:
services.AddDefaultIdentity<UserProfile>(options => {
options.SignIn.RequireConfirmedAccount = true;
options.SignIn.RequireConfirmedEmail = true;
options.Stores.MaxLengthForKeys = 64;
}).AddEntityFrameworkStores<OWSData>();
WebHostOptions webHost = configuration.GetSection("WebHost").Get<WebHostOptions>();
string root = webHost.Url;
services.AddIdentityServer((options) => {
options.PublicOrigin = root;
options.IssuerUri = root;
options.UserInteraction.LoginUrl = $"{root}/account/login";
options.UserInteraction.LogoutUrl = $"{root}/account/logout";
options.UserInteraction.ErrorUrl = $"{root}/account/error";
options.UserInteraction.ConsentUrl = $"{root}/account/terms-of-service";
options.UserInteraction.DeviceVerificationUrl = $"{root}/account/device-verification";
}).AddApiAuthorization<UserProfile, OWSData>(options => {
// Serilog.Log.Information("Clients: {@clients}", options.Clients);
});
services.AddScoped<ICurrentUser, CurrentUser>();
services.AddTransient<IReturnUrlParser, AuthReturnUrl>();
// Auth (JWT)
services.AddAuthentication(options => {
// options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie((options) => {
webHost.ConfigureCookie(CookieName, options.Cookie);
options.LoginPath = "/account/login";
options.AccessDeniedPath = "/account/denied";
options.LogoutPath = "/account/logout";
options.SlidingExpiration = true;
})
.AddIdentityServerJwt()
.AddGoogle(options => {
ConfigureOAuth(options, "Google", configuration, webHost);
})
.AddGitHub(options => {
ConfigureOAuth(options, "GitHub", configuration, webHost);
options.Scope.Add("user:email");
options.EnterpriseDomain = configuration["GitHub:EnterpriseDomain"];
});
services.AddAuthorization(options => {
// options.DefaultPolicy = new AuthorizationPolicy();
});
// Auth (Password)
services.Configure<IdentityOptions>(options => {
//Password settings
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
//Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
//User settings
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@";
options.User.RequireUniqueEmail = false;
});
// Cookies
services.ConfigureApplicationCookie(options => {
webHost.ConfigureCookie(CookieName, options.Cookie);
});
Upvotes: 1
Views: 1484
Reputation: 2394
The JwtSecurityTokenHandler
on JwtBearerAuthentication
middleware, is mapping some claims by default. Along these mappings, sub
claim is mapped to the ClaimTypes.NameIdentifier
. Mappings are listed here.
You can change the code on API to set the name identifier claim as NameClaimType
. NameClaimType
is used to set Identity.Name
.
Here is the code change required on API:
services.AddAuthentication("Bearer").AddJwtBearer("Bearer",
options =>
{
options.Authority = "http://localhost:5000";
options.Audience = "api1";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"//To set Identity.Name
};
});
Here is a sample working code: https://github.com/nahidf/IdentityServer4-adventures/blob/ids4-4/src/CoreApi/Startup.cs#L36
Edit: If you are using IdentityServerSPA
template to create the project, its just a template which has some extension methods and internally using same methods we use in manual setup
For-example per Docs IdentityServerJwt
is to do this:
Represents an API that is hosted alongside with IdentityServer. The app is configured to have a single scope that defaults to the app name.
To achieve above purpose IdentityServerJwt is calling AddJwtBearer
internally.
And also it uses IdentityServerJwtDescriptor to to add the api resource.
So if we go with manual setup or just using IDS4 templates, we can see calls in our code.
In this case cause you have access to AddJwtBearer as its called inside the extension method, you can change the JwtBearerOptions
afterwards like this:
services.Configure<JwtBearerOptions>("Bearer",
options =>
{
new TokenValidationParameters()
{
NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"//To set Identity.Name
};
});
You have another option too which will remove all the mappings I mentioned above, its to add this code on StartUp at the first line.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
Upvotes: 3