简体   繁体   English

身份服务器:在 MVC 客户端的混合流中添加对访问令牌的声明

[英]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/

  1. portal is not an identity resource: you should remove门户不是身份资源:您应该删除

new IdentityResource("portal", new List{ "tenantId", "userId", "user", "role", "name"}) new IdentityResource("portal", new List{ "tenantId", "userId", "user", "role", "name"})

  1. 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") } },

    }; }; } }
    1. Try setting GrantTypes.Implicit in the client.尝试在客户端中设置 GrantTypes.Implicit。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 带有客户端机密的Identity Server 4混合流错误 - Identity Server 4 Hybrid Flow error with Client Secret 身份服务器 4:向访问令牌添加声明 - Identity Server 4: adding claims to access token IdentityServer4声明不属于混合流令牌的一部分 - IdentityServer4 claims are not part of the token on hybrid flow IdentityServer调用API,其令牌已传递到MVC客户端,并带有混合流 - IdentityServer call API with token delivered to MVC client wit hybrid flow 密码流的 IdentityServer4 客户端不包括访问令牌中的 TestUser 声明 - IdentityServer4 client for Password Flow not including TestUser claims in access token Identity Server 4 - MVC 客户端的 OpenIdConnect 处理程序中缺少角色声明 - Identity Server 4 - Role claims missing in OpenIdConnect handler in MVC client Identity Server 4 向用户添加自定义声明 - Identity Server 4 add custom claims to User 如何在身份服务器 4 的客户端获得额外的声明? - How to get additional claims on client side in identity server 4? 当授予类型为 ClientCredentialsFlow 时,访问令牌中不包含 MockIdentityServer 客户端声明 - MockIdentityServer Client Claims not included in the access token when Grant type was ClientCredentialsFlow 具有混合身份验证流程的客户端证书 - Client certificate with Hybrid Authentication Flow
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM