First of all, thank you very much for an amazing piece of software. IdentityServer4 is truly an awesome product and I am so happy you guys are dedicating so much of your time and energy to the project.
While everything about IdentityServer so far has been working great, I have run into an issue that is close to driving me crazy. I have an ASP.NET Core (v2.0.5) application with IdentityServer4 and IdentityServer4.AspNetIdentity v2.1.0 and I have followed your quickstart to make it work with the latest version of ASP.NET Core Identity.
I also have a Python client written in Flask, which is using flask-oauthlib v0.9.4, to connect to my ASP.NET app via OAuth2. I have managed to establish the initial connection and I am able to authorize as a user from my Python app.
The issue is that _flask-oauthlib_ expects either a username or an email field in the access token response and, no matter what I do, adding either of these claims to the response doesn't seem to be working.
Here is what my Config.cs file looks like:
public class Config
{
public static IEnumerable<IdentityResource> GetIdentityResources()
{
var customProfile = new IdentityResource(
name: "custom.profile",
displayName: "Custom Profile",
claimTypes: new[] { JwtClaimTypes.Email });
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Email(),
new IdentityResources.Profile(),
customProfile
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("supersetapi", "Superset API", new[] { JwtClaimTypes.Email })
};
}
public static IEnumerable<Client> GetClients(IConfiguration config)
{
var clients = new List<Client>
{
new Client
{
ClientId = "supersetclient",
AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
ClientSecrets =
{
new Secret(config["SupersetClientSecret"].Sha256())
},
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Profile,
"supersetapi"
},
RedirectUris =
{
"http://localhost:8880/oauth-authorized/sensomind",
"https://insights.sensomind.com/oauth-authorized/sensomind"
},
RequireConsent = false,
AlwaysIncludeUserClaimsInIdToken = true
}
};
return clients;
}
}
As you can see from the file, I have tried adding the claims to the access token response in different ways but nothing works. I have also created my own instance of IProfileService, which can be seen here:
public class ProfileService : IProfileService
{
protected UserManager<ApplicationUser> _userManager;
public ProfileService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = _userManager.GetUserAsync(context.Subject).Result;
var claims = new List<Claim>
{
new Claim(IdentityModel.JwtClaimTypes.Email, user.Email)
};
context.IssuedClaims.AddRange(claims);
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context)
{
var user = _userManager.GetUserAsync(context.Subject).Result;
context.IsActive = (user != null);
return Task.FromResult(0);
}
}
The GetProfileDataAsync is being invoked correctly whenever I hit the /connect/token endpoint and I can verify that my email claim indeed does get added to the context.IssuedClaims collection. See the screenshot below:

I have also tried adding my claims using the context.AddRequestedClaims(claims) method but that didn't work either. I subsequently tried setting AlwaysIncludeUserClaimsInIdToken to false, thinking that it was possibly overriding my email claim, but that didn't change anything. Despite all these efforts I continue to get the following response in my Python client:
2018-01-22 17:19:40,964:DEBUG:flask_appbuilder.security.views:Provider: sensomind
2018-01-22 17:19:40,965:DEBUG:flask_appbuilder.security.views:Going to call authorize for: sensomind
2018-01-22 17:19:41,844:DEBUG:flask_appbuilder.security.views:Authorized init
2018-01-22 17:19:41,845:DEBUG:flask_oauthlib:Prepare oauth2 remote args {'client_secret': 'NhpBmicjwzr3X04NFndVqTtwlWKnd6TEC8K1Znd55eYu4xCu4d', 'code': u'969368ac7d09411c2595752c8c5b8c3ae45aa11ae653b6df6050e4886cbeb8ba', 'redirect_uri': 'http://localhost:8880/oauth-authorized/sensomind'}
2018-01-22 17:19:41,845:DEBUG:flask_oauthlib:Request 'http://b19d8520.ngrok.io/connect/token' with 'POST' method2018-01-22 17:19:45,000:DEBUG:flask_appbuilder.security.views:OAUTH Authorized resp: {u'access_token': u'eyJhbGciOiJSUzI1NiIsImtpZCI6IjZlNWE2Nzk0NDUyMGQ0YTliYTkzYjk1ZTE1NmJhMjZjIiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MTY2NDE1ODQsImV4cCI6MTUxNjY0NTE4NCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDozOTk3NyIsImF1ZCI6WyJodHRwOi8vbG9jYWxob3N0OjM5OTc3L3Jlc291cmNlcyIsInN1cGVyc2V0YXBpIl0sImNsaWVudF9pZCI6InN1cGVyc2V0Y2xpZW50Iiwic3ViIjoiNGMyZjk1OWEtNzEwYy00NTU3LThmY2MtMTg0YWZmMGQ5Njg3IiwiYXV0aF90aW1lIjoxNTE2NjM4NDY0LCJpZHAiOiJsb2NhbCIsImVtYWlsIjoic2ViYXN0aWFuLmJyYW5kZXNAdnJpbm5vLmNvbSIsInNjb3BlIjpbIm9wZW5pZCIsImVtYWlsIiwicHJvZmlsZSIsInN1cGVyc2V0YXBpIl0sImFtciI6WyJwd2QiXX0.Yn-eZbE5i0T8kTuMwChN837wpygw0a_R6_wG2oP-5yqp3uAaFhNT0Nx5gpwCAv8Dk-8aq8Iry7EBI1l3blJlAPVR1R67zhm_AXu22q5n808F6mmeWZNlR3nFZaI6RXHZrXD89yztkXMSFXgyYglJWXIrF8nHqJk1UBmcW7GwLSWiNIhBbOjEJnYQgX41su3BA0gU8IHAlrYEaWjf0hKuvmUULV5Ffmt-XHL-iebCOqsGz3B6PgDdfYk9RZkjydOezvTqfJHBY1D_510y49x8ChO3GbvwDdsHCc16EUXswXW2TNtJbHY5VMxkAmlUbljTLmsjbxFbZDjw3_J7HYFWyw', u'id_token': u'eyJhbGciOiJSUzI1NiIsImtpZCI6IjZlNWE2Nzk0NDUyMGQ0YTliYTkzYjk1ZTE1NmJhMjZjIiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MTY2NDE1ODQsImV4cCI6MTUxNjY0MTg4NCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDozOTk3NyIsImF1ZCI6InN1cGVyc2V0Y2xpZW50IiwiaWF0IjoxNTE2NjQxNTg0LCJhdF9oYXNoIjoiNzlubVp1NDV3b1R6N1dQaE5reEtfQSIsInNpZCI6IjhiOGEwNTc4MGU4YTYwYTc0ZWFiYTczMmFiYTRlOWNlIiwic3ViIjoiNGMyZjk1OWEtNzEwYy00NTU3LThmY2MtMTg0YWZmMGQ5Njg3IiwiYXV0aF90aW1lIjoxNTE2NjM4NDY0LCJpZHAiOiJsb2NhbCIsImFtciI6WyJwd2QiXX0.E-JTZQ_8Y6YuyBuch8xFbO5FKSSKACzahJBBiGAkQsICHW2ivh5Er-1YKp2Hx1ClqFVlqI8EomBKLWjm-8nAVGjgqnRkBfOOP5DAhtkIkpkOwVpF67auuwcKgRNLylhFAbM-M3Vd_2p2NWflO20MQeCtcPYxjArVzQE6frhbfa9S7EWdHHiI8N8UGDhEQMDX5cqCgIsOWg7qY39Qdczex-qtKL5kxD0de2wM6hqHFyw1VvkZvpNOIfXha5sIHQNDhJ6sMtUYGjr9Gp_ORPi94F2-9XKo9Ffw1Bj-5_TnUVeuBTnMVR2d4ffdU0iy_V2N1OCbmiAUJAK-kt8HbJiEFg', u'expires_in': 3600, u'token_type': u'Bearer'}
2018-01-22 17:19:45,000:DEBUG:flask_appbuilder.security.views:User info retrieved from sensomind: {}
2018-01-22 17:19:45,001:DEBUG:flask_appbuilder.security.views:No whitelist for OAuth provider
2018-01-22 17:19:45,001:ERROR:flask_appbuilder.security.manager:User info does not have username or email {}
Here is what I am seeing in my debug console whenever I tried to issue a request to /connect/token from my client:
[...]
dbug: IdentityServer4.Validation.TokenRequestValidator[0]
Validation of authorization code token request success
info: IdentityServer4.Validation.TokenRequestValidator[0]
Token request validation success
{
"ClientId": "supersetclient",
"GrantType": "authorization_code",
"AuthorizationCode": "82cb2395740ce463cfee701b94d3b115ffbf32915d0278112b0dd40ad4095027",
"Raw": {
"grant_type": "authorization_code",
"scope": "openid email profile supersetapi",
"client_secret": "***REDACTED***",
"code": "82cb2395740ce463cfee701b94d3b115ffbf32915d0278112b0dd40ad4095027",
"client_id": "supersetclient",
"redirect_uri": "http://localhost:8880/oauth-authorized/sensomind"
}
}
dbug: IdentityServer4.Services.DefaultClaimsService[0]
Getting claims for access token for client: supersetclient
dbug: IdentityServer4.Services.DefaultClaimsService[0]
Getting claims for access token for subject: 4c2f959a-710c-4557-8fcc-184aff0d9687
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (60ms) [Parameters=[@__user_Id_0='?' (Size = 450)], CommandType='Text', CommandTimeout='30']
SELECT [uc].[Id], [uc].[ClaimType], [uc].[ClaimValue], [uc].[UserId]
FROM [AspNetUserClaims] AS [uc]
WHERE [uc].[UserId] = @__user_Id_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (49ms) [Parameters=[@__userId_0='?' (Size = 450)], CommandType='Text', CommandTimeout='30']
SELECT [role].[Name]
FROM [AspNetUserRoles] AS [userRole]
INNER JOIN [AspNetRoles] AS [role] ON [userRole].[RoleId] = [role].[Id]
WHERE [userRole].[UserId] = @__userId_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (44ms) [Parameters=[@__normalizedName_0='?' (Size = 256)], CommandType='Text', CommandTimeout='30']
SELECT TOP(1) [r].[Id], [r].[ConcurrencyStamp], [r].[Name], [r].[NormalizedName]
FROM [AspNetRoles] AS [r]
WHERE [r].[NormalizedName] = @__normalizedName_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (38ms) [Parameters=[@__role_Id_0='?' (Size = 450)], CommandType='Text', CommandTimeout='30']
SELECT [rc].[ClaimType], [rc].[ClaimValue]
FROM [AspNetRoleClaims] AS [rc]
WHERE [rc].[RoleId] = @__role_Id_0
dbug: IdentityServer4.Services.DefaultClaimsService[0]
Getting claims for identity token for subject: 4c2f959a-710c-4557-8fcc-184aff0d9687 and client: supersetclient
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (52ms) [Parameters=[@__user_Id_0='?' (Size = 450)], CommandType='Text', CommandTimeout='30']
SELECT [uc].[Id], [uc].[ClaimType], [uc].[ClaimValue], [uc].[UserId]
FROM [AspNetUserClaims] AS [uc]
WHERE [uc].[UserId] = @__user_Id_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (43ms) [Parameters=[@__userId_0='?' (Size = 450)], CommandType='Text', CommandTimeout='30']
SELECT [role].[Name]
FROM [AspNetUserRoles] AS [userRole]
INNER JOIN [AspNetRoles] AS [role] ON [userRole].[RoleId] = [role].[Id]
WHERE [userRole].[UserId] = @__userId_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (51ms) [Parameters=[@__normalizedName_0='?' (Size = 256)], CommandType='Text', CommandTimeout='30']
SELECT TOP(1) [r].[Id], [r].[ConcurrencyStamp], [r].[Name], [r].[NormalizedName]
FROM [AspNetRoles] AS [r]
WHERE [r].[NormalizedName] = @__normalizedName_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (54ms) [Parameters=[@__role_Id_0='?' (Size = 450)], CommandType='Text', CommandTimeout='30']
SELECT [rc].[ClaimType], [rc].[ClaimValue]
FROM [AspNetRoleClaims] AS [rc]
WHERE [rc].[RoleId] = @__role_Id_0
dbug: IdentityServer4.Endpoints.TokenEndpoint[0]
Token request success.
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 837.4842ms 200 application/json; charset=UTF-8
The output seems to be as expected. So I really do not understand why the email claim is not being added.
When calling the /connect/userinfo endpoint (e.g., using Postman) with the access token I received in my Python client, I correctly receive the claims I am interested in:

I have spent hours trying solutions of other people on both GitHub and StackOverflow but nothing seems to work, which makes me think that this could be due to a bug.
I really hope you will be able to help me out, as I have come to rely very much upon IdentityServer4 and have been extremely excited about it for the all the other work I have been doing with it. Thank you!
+1
The issue is that flask-oauthlib expects either a username or an email field in the access token response
Well, that's silly. Clients (either your code or some library you're using to communicate with the token server) should not be parsing and reading the contents of access tokens -- they're not for the client to consume. They're for the API to consume.
As for how claims are configured and delivered based on config in IdentityServer:
Claims are either sent to the client, and/or the API and that's based on the scopes requested (Identity vs. API). Only the claims associated with identity resources go to the client, and only claims associated with API resources go to the API.
When a client only requests response_type=id_token (which means no API is being used) then all the claims for the identity scopes requested go into the id_token. When the client requests response_type=id_token token, then (by default) only the sub claim goes into the id_token and the rest of the identity claims are returned via the userinfo endpoint. If you don't mind a larger id_token and don't want the extra round-trip to userinfo, then you can configure the AlwaysIncludeUserClaimsInIdToken flag.
Is your flask-oauthlib library OIDC compliant and does it know how to connect to userinfo?
While it is true that claims can be targeted to any relying party as shown in the OpenID Connect spec, it should be clear that the expectation of the spec is that the claims are sent to the client, who can then pass them to any other endpoint it desires. Many implementations of OpenID Connect have the client process the claims. Here is the selection of the spec:
Hi @brockallen. Thanks a lot for your quick response. My library flask-oauthlib doesn't support OpenID Connect nor does the wrapping library Flask-AppBuilder, which I am primarily interacting with. However, you suggestions got me thinking and I went on to do a little more research. It turns out that somebody has come across the same issue in the libraries I am working with and has suggested some sort of hack:
https://github.com/dpgaspar/Flask-AppBuilder/pull/618/files
They are proposing a small extension to the way flask-oauthlib parses the user info required. This pull request allows for making a subsequent requests to an endpoint such as /connect/userinfo, which lets me get the claims I need. Now everything works. 馃槂
I appreciate you getting back to me and being so open to the community in general. Please continue the awesome work! Meanwhile, I guess I have a little more reading up to with regards to the OIDC spec. This auth stuff isn't so trivial...
This auth stuff isn't so trivial...
true dat
Hi. I'm not able to get this working. Since the pull request: https://github.com/dpgaspar/Flask-AppBuilder/pull/618/files
has been closed, I assume that it has been merged into the library, yet it does not seem to work.
The system I'm trying to get it to work with is Apache Superset 0.24.0, from this image:
amancevice/superset:0.24.0
What I'm actually trying to achieve is to do OpenIDConnect authentication, but using oAuth really. Everything works great besides the username/email discovery from the OpenIDConnect provider. Any help or suggestions would be much appreciated!
I understand that OpenIDConnect is not supported, but this discussion above seems to indicate that it is possible to "hack" oAuth to do OpenIDConnect. I am aware of an actual OIDC lib, but it seems far more effort to implement this for Superset, plus the instructions is not very clear here on where to actually find the files to change: https://stackoverflow.com/questions/47678321/using-openid-keycloak-with-superset
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
true dat