簡體   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