繁体   English   中英

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

[英]Identity Server: Add claims to access token in hybrid flow in MVC client

我已阅读文档并按照示例进行操作,但我无法将用户声明放入访问令牌中。 我的客户端不是 ASP.NET Core,因此 MVC 客户端的配置与 v4 示例不同。

除非我误解了文档,否则 ApiResources 用于在创建访问令牌时填充配置文件服务中的 RequestedClaimTypes。 客户端应该将 api 资源添加到它的范围列表中以包含关联的用户声明。 就我而言,它们没有连接。

当使用“ClaimsProviderAccessToken”的调用方调用 ProfileService.GetProfileDataAsync 时,请求的声明类型为空。 即使我在此处设置了 context.IssuedClaims,当再次调用“AccessTokenValidation”时,也不会设置对上下文的声明。

在 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);
    }

身份服务器客户端:

    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;
    }

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),

                    },

                }
            },

        };
    }

身份资源:

    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"})
        };
    }

更新:

下面是 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

因为我没有看到你的await AssembleUserClaims(context);发生了什么我建议检查它是否正在执行以下操作:

基于您从context.ProtoclMessage.AccessToken或从对TokenEndpoint的调用中获得的访问令牌,您应该创建一个新的ClaimsIdentity 你这样做是因为你没有提到它吗?

像这样:

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);

还有一件事 - 阅读有关资源的内容。 在您的特定情况下,您关心 IdentityResources (但我看到您也在那里)。

那么 - 在调用UserInfoEndpoint时,您是否看到响应中的声明? 如果没有 - 那么问题是它们没有发行。

检查这些,我们可以深入挖掘。

祝你好运

编辑

我有一个您可能喜欢或不喜欢的解决方案,但我会建议您这样做。

在 IdentityServer 项目中, AccountController.cs中有一个方法public async Task<IActionResult> Login(LoginInputModel model, string button)

这是用户单击登录页面(或您在那里的任何自定义页面)上的登录按钮后的方法。

在此方法中有一个调用await HttpContext.SignInAsync 此调用接受参数用户主题、用户名、身份验证属性和声明列表 您可以在此处添加您的自定义声明,然后当您在AuthorizationCodeReceived中调用 userinfo 端点时它将出现。 我刚刚测试了这个并且它有效。

其实我发现这是添加自定义声明的方法。 否则 - IdentityServer 不知道您的自定义声明,并且无法用值填充它们。 尝试一下,看看它是否适合你。

您需要修改 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);
                    }
                }

(考虑是否有任何与身份服务器版本相关的更改,因为此代码是为身份服务器 3 构建的。)

为什么将“门户”列为身份资源和 Api 资源? 这可能会引起一些混乱。

此外,在我切换到 IdentityServer4 和 asp.net core 之前,我的 IdentityServer3 启动代码看起来与您使用 MVC 时的代码非常相似。 您可能想查看 IdentityServer3 的示例。

我可能会给出一些建议,在 MVC 的“ResponseType”字段中,您可以尝试“code id_token token”

此外,您在 AuthorizationCodeReceived 上设置声明,而不是使用 SecurityTokenValidated。

但是您不必像人们提到的那样做任何习俗。 IdentityServer4 像您尝试的那样处理自定义 ApiResources。

您可以尝试实现自己的 IProfileService 并按以下方式覆盖它:

services.AddIdentityServer()
    .//add clients, scopes,resources here
    .AddProfileService<YourOwnProfileProvider>();

欲了解更多信息,请查看此处:

https://damienbod.com/2016/10/01/identityserver4-webapi-and-angular2-in-a-single-asp-net-core-project/

  1. 门户不是身份资源:您应该删除

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

  1. api 资源的名称应该是一致的:

     public static IEnumerable GetApiResources() { return new List { new ApiResource { Name= "portal", UserClaims = { "tenantId","userId","user" }, Scopes = { new Scope("portal","portal") } },

    }; }
    1. 尝试在客户端中设置 GrantTypes.Implicit。

暂无
暂无

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

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM