将外部登录令牌从 Identity Server 流向客户端应用程序

Posted

技术标签:

【中文标题】将外部登录令牌从 Identity Server 流向客户端应用程序【英文标题】:Flow external login tokens from the Identity Server to the client app 【发布时间】:2020-04-20 11:05:45 【问题描述】:

我已经基于 .net core 2.2 设置了 IdentityServer4,并使用 OpenIdConnect 中间件将 Xero 配置为外部登录。我有一个为身份验证配置 IdentityServer 的客户端应用程序。我喜欢在客户端应用程序中访问的不仅是来自 IdentityServer 的身份验证令牌,还有来自外部登录的令牌。有一个MS documentation 建议在OnGetCallbackAsync 中包含外部登录令牌:

var props = new AuthenticationProperties();
props.StoreTokens(info.AuthenticationTokens);
props.IsPersistent = true;
await _signInManager.SignInAsync(user, props);

由于我的 IdentityServer 模板没有OnGetCallbackAsync 方法,我假设在ExternalLoginControllerExternalLoginCallback 操作中实现上述操作将完成这项工作(我可能错了):

public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
    
        var context = await _interactionService.GetAuthorizationContextAsync(returnUrl);

        if (remoteError != null)
        
            ModelState.AddModelError(string.Empty, $"Error from external provider: remoteError");

            return View($"~/Login/nameof(LoginController.Login)");
        

        var info = await _signInManager.GetExternalLoginInfoAsync();

        if (info == null)
        
            return RedirectToAction(nameof(LoginController.Login), "Login");
        

        // Sign in the user with this external login provider if the user already has a login.
        var result = await _signInManager.ExternalLoginSignInAsync(
            info.LoginProvider,
            info.ProviderKey,
            Constants.AuthenticationProps.Defaults.IsPersistent);

        var emailClaim = ClaimTypes.Email;

        if (result.Succeeded)
                        
            var user = await _userManager.FindByNameAsync(info.Principal.FindFirstValue(emailClaim));

            var props = new AuthenticationProperties();
            props.StoreTokens(info.AuthenticationTokens);
            props.IsPersistent = true;
            await _signInManager.SignInAsync(user, props, info.LoginProvider);

            await _events.RaiseAsync(new UserLoginSuccessEvent(user.Email, user.Id.ToString(), $"user.GivenName user.FamilyName"));
            await _events.RaiseAsync(new UserLoginEvent(user.Id, context?.ClientId));
            _logger.LogInformation(5, "User logged in with Name provider.", info.LoginProvider);
            return RedirectToLocal(returnUrl);
         ...

所以登录工作正常,我在客户端应用程序(asp.net 核心)中通过HttpContext.GetTokenAsync("access_token") 获得身份服务器令牌,但是仍然无法弄清楚如何在客户端应用程序中访问外部登录令牌。我不确定我是否遗漏了什么,或者这是否是将外部登录令牌流向我的客户端应用程序的正确方法,如果是,如何在客户端应用程序中访问这些 AuthenticationTokens

这是我的客户端应用程序 openidconnect 配置供参考:

        services.AddAuthentication(options =>
        
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = "oidc";
        )
        .AddCookie(
            CookieAuthenticationDefaults.AuthenticationScheme,
            options =>
        
            options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
            options.Cookie.Name = "xero";
            options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        )
        .AddOpenIdConnect("oidc", options =>
        
            options.Authority = "https://localhost:44333";//IdentityServer address
            options.RequireHttpsMetadata = true;
            options.ClientId = "MyAppClientId";
            options.ClientSecret = "MyAppClientSecret";
            options.ResponseType = "code id_token";
            options.SaveTokens = true;
            options.GetClaimsFromUserInfoEndpoint = true;
            options.Scope.Clear();
            options.Scope.Add("openid");
            options.Scope.Add("profile");
            options.Scope.Add("email");
            options.ClaimActions.MapAllExcept("iss", "nbf", "exp", "aud", "nonce", "iat", "c_hash");

            options.TokenValidationParameters = new TokenValidationParameters
            
                NameClaimType = JwtClaimTypes.Name,
                RoleClaimType = JwtClaimTypes.Role
            ;

            options.TokenValidationParameters = new TokenValidationParameters
            
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true
            ;

            options.Events = new OpenIdConnectEvents
            
                OnRedirectToIdentityProvider = ctx =>
                
                    if (ctx.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Authentication)
                    
                        ctx.ProtocolMessage.AcrValues = "idp:xero";
                    
                    return Task.CompletedTask;
                
            ;
        );

【问题讨论】:

【参考方案1】:

如果在 Identity Server 中实现外部登录,Identity server 从外部 provider 接收到 id token/access token 后,将解码 token 并获取用户的声明,登录用户,然后创建身份服务器自己的 token,最后返回到您的客户端应用程序。 Identity Server 不会处理来自外部提供者的令牌,但您可以在ExternalControllerCallback 方法中获取令牌:

var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);

//get the tokens
var tokens = result.Properties.GetTokens();
var idToken = tokens.Where(x => x.Name.Equals("id_token")).FirstOrDefault().Value;

然后你可以保存你想要的任何数据,缓存令牌,并返回到客户端,例如,在像this sample这样的令牌响应中。当然,您的客户端应用程序也可以在身份验证后发出另一个请求以获取令牌(在 ExternalController 中保留令牌)。

【讨论】:

我在 Idsrv @NanYu 中访问外部令牌没有问题。我可以通过info.AuthenticationTokens 在我的回调方法中获取它们,并按照我在代码中的说明保存它们。由于令牌非常重,我不想将它们包含在 Idsrv 令牌中。您能否详细说明如何发出第二次请求以获取外部令牌?有参考链接吗? 您可以在会话、数据库中缓存任何您想要缓存的地方,然后在ICustomTokenRequestValidator 中添加响应,如上面的代码示例。另一个选项是设置一个受保护的 api,它可以从缓存中获取令牌,您的客户端应用程序将在验证 IDS4 时获取访问该 api 的访问令牌:github.com/IdentityServer/IdentityServer4/tree/master/samples/… 附带说明,您的外部提供程序选项应将SaveTokens 设置为true,以便填充GetTokens 结果。【参考方案2】:

我通过在ExternalLoginCallback 中添加一个缺失的部分来使其工作。我替换了以下代码:

var props = new AuthenticationProperties();
props.StoreTokens(info.AuthenticationTokens);
props.IsPersistent = true;
await _signInManager.SignInAsync(user, props, info.LoginProvider);

用这一行:

await _signInManager.UpdateExternalAuthenticationTokensAsync(info);

它将外部登录凭据(access_tokenexpires_atid_tokenrefresh_tokentoken_type)保存在 [dbo].[UserTokens] 的 Identity SQL DB 中。 接下来我只需要以某种方式从表中获取数据。

在身份令牌中包含这些凭据不是一个好主意,并且可能会使响应标头太大并且客户端请求可能会失败,因此我实现了一个 API,以便通过发送用户 ID 和接收外部令牌来由我的客户端应用程序调用。 感谢@NanYu 的提示。

【讨论】:

以上是关于将外部登录令牌从 Identity Server 流向客户端应用程序的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Identity(使用 IdentityServer4)获取外部资源 oauth 访问令牌

Identity Server 4 - 如何解决客户端注销后访问令牌仍然有效?

移动和 Web 应用程序的 Identity Server3 身份验证

如何撤销存储在 Identity Server 数据库中的 asp net core 中的刷新令牌

带有 docker-compose 的 Identity Server 4 在 /connect/authorize/callback 之后重定向到登录页面

有没有办法撤销另一个用户的访问令牌并结束他们在 Identity Server 4 中的会话?