[英]Identity Server: Add claims to access token in hybrid flow in MVC client
I've read the docs and followed the examples but I am unable to get user claims into the access token.我已阅读文档并按照示例进行操作,但我无法将用户声明放入访问令牌中。 My client is not ASP.NET core, so the configuration of the MVC client is not the same as the v4 samples.
我的客户端不是 ASP.NET Core,因此 MVC 客户端的配置与 v4 示例不同。
Unless I have misunderstood the docs, the ApiResources are used to populate the RequestedClaimTypes in the profile service when creating the access token.除非我误解了文档,否则 ApiResources 用于在创建访问令牌时填充配置文件服务中的 RequestedClaimTypes。 The client should add the api resource to it's list of scopes to include associated userclaims.
客户端应该将 api 资源添加到它的范围列表中以包含关联的用户声明。 In my case they are not being connected.
就我而言,它们没有连接。
When ProfileService.GetProfileDataAsync is called with a caller of "ClaimsProviderAccessToken", the requested claim types are empty.当使用“ClaimsProviderAccessToken”的调用方调用 ProfileService.GetProfileDataAsync 时,请求的声明类型为空。 Even if I set the context.IssuedClaims in here, when it is called again for "AccessTokenValidation" the claims on the context are not set.
即使我在此处设置了 context.IssuedClaims,当再次调用“AccessTokenValidation”时,也不会设置对上下文的声明。
In the MVC app:在 MVC 应用程序中:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
UseTokenLifetime = false,
ClientId = "portal",
ClientSecret = "secret",
Authority = authority,
RequireHttpsMetadata = false,
RedirectUri = redirectUri,
PostLogoutRedirectUri = postLogoutRedirectUri,
ResponseType = "code id_token",
Scope = "openid offline_access portal",
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
await AssembleUserClaims(n);
},
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
});
private static async Task AssembleUserClaims(AuthorizationCodeReceivedNotification notification)
{
string authCode = notification.ProtocolMessage.Code;
string redirectUri = "https://myuri.com";
var tokenClient = new TokenClient(tokenendpoint, "portal", "secret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(authCode, redirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(new Uri(userinfoendpoint), tokenResponse.AccessToken);
var userInfoResponse = await userInfoClient.GetAsync();
// create new identity
var id = new ClaimsIdentity(notification.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", notification.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", notification.AuthenticationTicket.Identity.FindFirst("sid").Value));
notification.AuthenticationTicket = new AuthenticationTicket(id, notification.AuthenticationTicket.Properties);
}
Identity Server Client:身份服务器客户端:
private Client CreatePortalClient(Guid tenantId)
{
Client portal = new Client();
portal.ClientName = "Portal MVC";
portal.ClientId = "portal";
portal.ClientSecrets = new List<Secret> { new Secret("secret".Sha256()) };
portal.AllowedGrantTypes = GrantTypes.HybridAndClientCredentials;
portal.RequireConsent = false;
portal.RedirectUris = new List<string> {
"https://myuri.com",
};
portal.AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"portal"
};
portal.Enabled = true;
portal.AllowOfflineAccess = true;
portal.AlwaysSendClientClaims = true;
portal.AllowAccessTokensViaBrowser = true;
return portal;
}
The API resource: API资源:
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource
{
Name= "portalresource",
UserClaims = { "tenantId","userId","user" },
Scopes =
{
new Scope()
{
Name = "portalscope",
UserClaims = { "tenantId","userId","user",ClaimTypes.Role, ClaimTypes.Name),
},
}
},
};
}
The Identity resource:身份资源:
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
// some standard scopes from the OIDC spec
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource("portal", new List<string>{ "tenantId", "userId", "user", "role", "name"})
};
}
UPDATE:更新:
Here is the interaction between the MVC app and the Identity Server (IS):下面是 MVC 应用程序和身份服务器 (IS) 之间的交互:
MVC:
Owin Authentication Challenge
IS:
AccountController.LoginAsync - assemble user claims and call HttpContext.SignInAsync with username and claims)
ProfileService.IsActiveAsync - Context = "AuthorizeEndpoint", context.Subject.Claims = all userclaims
ClaimsService.GetIdentityTokenClaimsAsync - Subject.Claims (all userclaims), resources = 1 IdentityResource (OpenId), GrantType = Hybrid
MVC:
SecurityTokenValidated (Notification Callback)
AuthorizationCodeReceived - Protocol.Message has Code and IdToken call to TokenClient.RequestAuthorizationCodeAsync()
IS:
ProfileService.IsActiveAsync - Context = "AuthorizationCodeValidation", context.Subject.Claims = all userclaims
ClaimsService.GetAccessTokenClaimsAsync - Subject.Claims (all userclaims), resources = 2 IdentityResource (openId,profile), GrantType = Hybrid
ProfileService.GetProfileDataAsync - Context = "ClaimsProviderAccessToken", context.Subject.Claims = all userclaims, context.RequestedClaimTypes = empty, context.IssuedClaims = name,role,user,userid,tenantid
ClaimsService.GetIdentityTokenClaimsAsync - Subject.Claims (all userclaims), resources = 2 IdentityResource (openId,profile), GrantType = authorization_code
MVC:
call to UserInfoClient with tokenResponse.AccessToken
IS:
ProfileService.IsActiveAsync - Context = "AccessTokenValidation", context.Subject.Claims = sub,client_id,aud,scope etc (expecting user and tenantId here)
ProfileService.IsActiveAsync - Context = "UserInfoRequestValidation", context.Subject.Claims = sub,auth_time,idp, amr
ProfileService.GetProfileDataAsync - Context = "UserInfoEndpoint", context.Subject.Claims = sub,auth_time,idp,amp, context.RequestedClaimTypes = sub
As I'm not seeing what happens in your await AssembleUserClaims(context);
因为我没有看到你的
await AssembleUserClaims(context);
发生了什么I would suggest to check if it is doing the following:我建议检查它是否正在执行以下操作:
Based on the the access token that you have from either the context.ProtoclMessage.AccessToken
or from the call to the TokenEndpoint
you should create a new ClaimsIdentity
.基于您从
context.ProtoclMessage.AccessToken
或从对TokenEndpoint
的调用中获得的访问令牌,您应该创建一个新的ClaimsIdentity
。 Are you doing this, because you are not mentioning it?你这样做是因为你没有提到它吗?
Something like this:像这样:
var tokenClient = new TokenClient(
IdentityServerTokenEndpoint,
"clientId",
"clientSecret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaims(n.AuthenticationTicket.Identity.Claims);
// get user info claims and add them to the identity
var userInfoClient = new UserInfoClient(IdentityServerUserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
var userInfoEndpointClaims = userInfoResponse.Claims;
// this line prevents claims duplication and also depends on the IdentityModel library version. It is a bit different for >v2.0
id.AddClaims(userInfoEndpointClaims.Where(c => id.Claims.Any(idc => idc.Type == c.Type && idc.Value == c.Value) == false));
// create the authentication ticket
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
And one more thing - read this regarding the resources.还有一件事 - 阅读有关资源的内容。 In your particular case, you care about IdentityResources (but I see that you also have it there).
在您的特定情况下,您关心 IdentityResources (但我看到您也在那里)。
So - when calling the UserInfoEndpoint
do you see the claims in the response?那么 - 在调用
UserInfoEndpoint
时,您是否看到响应中的声明? If no - then the problem is that they are not issued.如果没有 - 那么问题是它们没有发行。
Check these, and we can dig in more.检查这些,我们可以深入挖掘。
Good luck祝你好运
EDIT编辑
I have a solution that you may, or may not like, but I'll suggest it.我有一个您可能喜欢或不喜欢的解决方案,但我会建议您这样做。
In the IdentityServer project, in the AccountController.cs
there is a method public async Task<IActionResult> Login(LoginInputModel model, string button)
.在 IdentityServer 项目中,
AccountController.cs
中有一个方法public async Task<IActionResult> Login(LoginInputModel model, string button)
。
This is the method after the user has clicked the login button on the login page (or whatever custom page you have there).这是用户单击登录页面(或您在那里的任何自定义页面)上的登录按钮后的方法。
In this method there is a call await HttpContext.SignInAsync
.在此方法中有一个调用
await HttpContext.SignInAsync
。 This call accept parameters the user subject, username, authentication properties and list of claims .此调用接受参数用户主题、用户名、身份验证属性和声明列表。 Here you can add your custom claim, and then it will appear when you call the userinfo endpoint in the
AuthorizationCodeReceived
.您可以在此处添加您的自定义声明,然后当您在
AuthorizationCodeReceived
中调用 userinfo 端点时它将出现。 I just tested this and it works.我刚刚测试了这个并且它有效。
Actually I figured out that this is the way to add custom claims.其实我发现这是添加自定义声明的方法。 Otherwise - IdentityServer doesn't know about your custom claims, and is not able to populate them with values.
否则 - IdentityServer 不知道您的自定义声明,并且无法用值填充它们。 Try it out and see if it works for you.
尝试一下,看看它是否适合你。
You need to modify the code of "Notifications" block in MVC App like mentioned below:您需要修改 MVC App 中“通知”块的代码,如下所示:
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n => {
var userInfoClient = new UserInfoClient(UserInfoEndpoint);
var userInfoResponse = await userInfoClient.GetAsync(n.ProtocolMessage.AccessToken);
var identity = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
identity.AddClaims(userInfoResponse.Claims);
var tokenClient = new TokenClient(TokenEndpoint, "portal", "secret");
var response = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
identity.AddClaim(new Claim("access_token", response.AccessToken));
identity.AddClaim(new Claim("expires_at", DateTime.UtcNow.AddSeconds(response.ExpiresIn).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
identity.AddClaim(new Claim("refresh_token", response.RefreshToken));
identity.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(identity, n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token").Value;
n.ProtocolMessage.IdTokenHint = idTokenHint;
}
return Task.FromResult(0);
}
}
(consider if any changes related to the version of identity server as this code was built for identity server 3.) (考虑是否有任何与身份服务器版本相关的更改,因为此代码是为身份服务器 3 构建的。)
Why do you have "portal" listed as an identity resource and Api resource?为什么将“门户”列为身份资源和 Api 资源? That could be causing some confusion.
这可能会引起一些混乱。
Also, before I switched to IdentityServer4 and asp.net core, my IdentityServer3 startup code looked very similar to what you have with MVC.此外,在我切换到 IdentityServer4 和 asp.net core 之前,我的 IdentityServer3 启动代码看起来与您使用 MVC 时的代码非常相似。 You may want to look at the examples for IdentityServer3.
您可能想查看 IdentityServer3 的示例。
Some suggestions I may give, in your "ResponseType" field for MVC, you could try "code id_token token"我可能会给出一些建议,在 MVC 的“ResponseType”字段中,您可以尝试“code id_token token”
Also, you are setting your claims on AuthorizationCodeReceived, instead use SecurityTokenValidated.此外,您在 AuthorizationCodeReceived 上设置声明,而不是使用 SecurityTokenValidated。
But you shouldn't have to do anything custom like people are mentioning.但是您不必像人们提到的那样做任何习俗。 IdentityServer4 handles custom ApiResources like you are attempting to do.
IdentityServer4 像您尝试的那样处理自定义 ApiResources。
You can try to implement your own IProfileService and override it following way:您可以尝试实现自己的 IProfileService 并按以下方式覆盖它:
services.AddIdentityServer()
.//add clients, scopes,resources here
.AddProfileService<YourOwnProfileProvider>();
For more information look up here:欲了解更多信息,请查看此处:
https://damienbod.com/2016/10/01/identityserver4-webapi-and-angular2-in-a-single-asp-net-core-project/ https://damienbod.com/2016/10/01/identityserver4-webapi-and-angular2-in-a-single-asp-net-core-project/
new IdentityResource("portal", new List{ "tenantId", "userId", "user", "role", "name"}) new IdentityResource("portal", new List{ "tenantId", "userId", "user", "role", "name"})
Names for the api resources should be consistent: api 资源的名称应该是一致的:
public static IEnumerable GetApiResources() { return new List { new ApiResource { Name= "portal", UserClaims = { "tenantId","userId","user" }, Scopes = { new Scope("portal","portal") } },
};}; }
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.