I've finally managed to setup a project using IdentityServer4 to allow users to sign in with a single account into multiple apps. However, I feel like it's not completely as it should be.
This is my OAuthOptions
class
public class CentralOptions : OAuthOptions
{
public CentralOptions()
{
ClaimsIssuer = "https://localhost:44359";
CallbackPath = new Microsoft.AspNetCore.Http.PathString("/signin-central");
AuthorizationEndpoint = "https://localhost:44359/connect/authorize";
TokenEndpoint = "https://localhost:44359/connect/token";
UserInformationEndpoint = "https://localhost:44359/connect/userinfo";
Scope.Add("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier");
Scope.Add("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name");
Scope.Add("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
Scope.Add("openid");
Scope.Add("profile");
Scope.Add("email");
Scope.Add("phone");
Scope.Add("role");
Scope.Add("weatherforecasts.read");
Scope.Add("weatherforecasts.write");
UsePkce = true;
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "sub");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "name");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "email");
//ClaimActions.MapJsonKey("sub", "sub");
//ClaimActions.MapJsonKey("name", "name");
//ClaimActions.MapJsonKey("email", "email");
}
}
As you can see, right now I have to duplicate the RequestedClaims
, once for the actual claim type, once for some shortname. I've been tweaking the contents of the database for some time, but can't figure out what I have to change to only have the claims once (I suppose that http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
should not be present in the bearer token, and email
should. But if I change the claims in the database and the application, the signin fails because it relies on the ClaimTypes.NameIdentifier
claim to be present in the bearer token.
After I signed in, IS gives me an access token, for example:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjVCOTBDN0JBNkExMjI2RjEyMEU0QzJGOEQzMjIwMzAxIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2MzcxNzkxNDAsImV4cCI6MTYzNzI2NTU0MCwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzNTkiLCJhdWQiOiJ3ZWF0aGVyZm9yZWNhc3RzIiwiY2xpZW50X2lkIjoiU3NvQXBwbGljYXRpb25DbGllbnQiLCJjZW50cmFsLXRoZWNsaWVudCI6IlRoZSBTU08gY2xpZW50Iiwic3ViIjoiOTU5YzliZmEtZWQzMC00NjM4LTk5ODYtNjNjZjE1ODllZmY4IiwiYXV0aF90aW1lIjoxNjM3MTc5MTM3LCJpZHAiOiJsb2NhbCIsImVtYWlsIjoicGlldGVyamFuQGV4YW1wbGUuY29tIiwibmFtZSI6IlBpZXRlcmphbiIsImlkIjoiOTU5YzliZmEtZWQzMC00NjM4LTk5ODYtNjNjZjE1ODllZmY4IiwicGhvbmUiOiIrMzIxMjMvNDUuNjcuODkiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiUGlldGVyamFuIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvZW1haWxhZGRyZXNzIjoicGlldGVyamFuQGV4YW1wbGUuY29tIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbW9iaWxlcGhvbmUiOiIrMzIxMjMvNDUuNjcuODkiLCJqdGkiOiI3RjA0QTA5MDM3MUNEMjQ2MENCQzg3OUY3MDEwOTU1MyIsInNpZCI6IjA0NDYzRDlBRDNENDRCNUExQTNCQTRFOTczRUE5OTI4IiwiaWF0IjoxNjM3MTc5MTQwLCJzY29wZSI6WyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiLCJvcGVuaWQiLCJwcm9maWxlIiwiZW1haWwiLCJwaG9uZSIsInJvbGUiLCJ3ZWF0aGVyZm9yZWNhc3RzLnJlYWQiLCJ3ZWF0aGVyZm9yZWNhc3RzLndyaXRlIl0sImFtciI6WyJwd2QiXX0.KBKLezXnUs6s-bU9hme7Ab7ADZN8DEewqfUncDwR0c2_LFqAnyCw3IZ85VJC4t-NN6xJYu8ROk-cX9PDKIQzEAOWGkOrQuqeaspKfIpl_rCq4qbP7x7uflToqPO245iU6xlzxVnGuaG1o_sSILNQA_YZJV8nsmXJkdB2QonuCZwvrBh5URFXV5cZpivlWznJls9eqfRM9MjlRpWe-NCI6I7FExfCaRgPZ4b1XwyrmmQWNlaKJOmIM3qag1pQshdXBSzg3w65htj89zOKKWSNl6Go6Q_0pZzbv0FLcMUMR_GTzuw56_CFobavD40T65wQQlXxf0cfkzbrdyAx7k8tyg
Decoded it looks like this
{
"alg": "RS256",
"kid": "5B90C7BA6A1226F120E4C2F8D3220301",
"typ": "at+jwt"
}
{
"nbf": 1637179140,
"exp": 1637265540,
"iss": "https://localhost:44359",
"aud": "weatherforecasts",
"client_id": "SsoApplicationClient",
"central-theclient": "The SSO client",
"sub": "959c9bfa-ed30-4638-9986-63cf1589eff8",
"auth_time": 1637179137,
"idp": "local",
"email": "pieterjan@example.com",
"name": "Pieterjan",
"id": "959c9bfa-ed30-4638-9986-63cf1589eff8",
"phone": "+32123/45.67.89",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Pieterjan",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "pieterjan@example.com",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone": "+32123/45.67.89",
"jti": "7F04A090371CD2460CBC879F70109553",
"sid": "04463D9AD3D44B5A1A3BA4E973EA9928",
"iat": 1637179140,
"scope": [
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
"openid",
"profile",
"email",
"phone",
"role",
"weatherforecasts.read",
"weatherforecasts.write"
],
"amr": [
"pwd"
]
}
With this token you can send a request to
https://localhost:44359/connect/userinfo
And this gives the following response
{
"email": "pieterjan@example.com",
"name": "Pieterjan",
"id": "959c9bfa-ed30-4638-9986-63cf1589eff8",
"phone": "+32123/45.67.89",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Pieterjan",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "pieterjan@example.com",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone": "+32123/45.67.89",
"sub": "959c9bfa-ed30-4638-9986-63cf1589eff8"
}
It seems to me that you're supposed to have only shortname qualifiers in the response (email
, name
, sub
, phone
), is that correct? But if I rearrange this, the response from /connect/userinfo
won't contain a http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
claim, and the signin will fail at following line which looks for this exact claim in the response from this very UserInfo endpoint, and thus will fail.
I'm guessing I have to tweak the OAuthOptions.ClaimActions
which currently read this:
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "sub");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "name");
ClaimActions.MapJsonKey("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "email");
I've been playing around with them already, but the slightest modification results in all claims no longer being returned from IdentityServer.
= client.allowedGrantTypes
= client.ClientSecrets
= client.AllowedScopes
So here you see that right now I have scopes which probably shouldn't be in the database, since they're actually claim types, but if I remove them the NameIdentifier
claim type will not be present in the Identity.
= client.RedirectUris
= client.Claims
= identityResource.UserClaims
Of which the markup reads:
List of associated user claims that should be included when this resource is requested.
So clearly here I had to introduce double lines for the external login to start working.
= user.Claims
How can I setup my code and database correctly so that I no longer need those duplicate claims for my application to work/my external login to succeed?
Also, should the claims be persisted at database level, or generated during login?
Thanks in advance.
I would only use the shorter claim names in IdentityServer and do the necessary claims transformation or mapping in the client.
I would look at doing the transform in the client or API using the MapUniqueJsonKey:
options.ClaimActions.MapUniqueJsonKey("website", "website");
options.ClaimActions.MapUniqueJsonKey("gender", "gender");
options.ClaimActions.MapUniqueJsonKey("birthdate", "birthdate");
I tink its important to understand what the ClaimsPrincipal user object contains after authentication (but before Authorization).
For more advanced transformation needs, then do take a look at using the IClaimsTransformation interface.
To complement this answer, I wrote a blog post that goes into more detail about this topic: Debugging OpenID Connect claim problems in ASP.NET Core
More info: