有了新的 access_token,如何更新我的 cookie?

How do I update my cookie, having got a new access_token?

使用刷新令牌获取新的访问令牌后,我想用该访问令牌更新我的客户端 cookie。

我的客户可以使用 ajax 登录并调用我的 REST API,但是当第一个授权到期时,自然 API 调用将不再有效。

我有一个 .NET Web 应用程序,它使用自己的 REST API。 API 是同一项目的一部分。它没有自己的启动配置。

由于 cookie 是在每个请求的 header 中发送的,因此它需要有新的未过期的访问令牌,这样我就不会得到请求的 'User unauthorized'。

现在我可以使用我的刷新令牌获得一个新令牌,但 cookie 的值没有改变,所以我认为我需要在客户端发送任何请求之前更新我的 cookie 以反映新的访问令牌.

下面是我的混合客户端:

using IdentityModel.Client;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;

namespace Cts.HomeService.Web.App_Start
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var identityServerSection = (IdentityServerSectionHandler)System.Configuration.ConfigurationManager.GetSection("identityserversection");

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "Cookies",
                CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
            });


            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                ClientId = "localTestClient",
                Authority = "http://localhost:5000",
                RedirectUri = identityServerSection.Identity.RedirectUri,
                Scope = "openid profile offline_access",
                ResponseType = "code id_token",
                RequireHttpsMetadata = false,
                PostLogoutRedirectUri = identityServerSection.Identity.RedirectUri,

                TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role",
                },
                SignInAsAuthenticationType = "Cookies",
                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthorizationCodeReceived = async n =>
                    {
                        var tokenClient = new TokenClient(
                            "http://localhost:5000/connect/token",
                            "localTestClient",
                            "");

                        var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
                            n.Code, n.RedirectUri);

                        if (tokenResponse.IsError)
                        {
                            throw new Exception(tokenResponse.Error);
                        }

                        // use the access token to retrieve claims from userinfo
                        var userInfoClient = new UserInfoClient(
                            "http://localhost:5000/connect/userinfo");

                        var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);

                        // create new identity
                        var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
                        id.AddClaims(userInfoResponse.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", tokenResponse.IdentityToken));
                        id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));

                        n.AuthenticationTicket = new AuthenticationTicket(
                            new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
                            n.AuthenticationTicket.Properties);
                    },

                    RedirectToIdentityProvider = n =>
                    {
                        {
                            // so here I'll grab the access token
                            if (isAccessTokenExpired()) {
                                var cancellationToken = new CancellationToken();
                                var newAccessToken = context.GetNewAccessTokenAsync(refresh_token, null, cancellationToken);
                               // now what?
                            }

                            // if signing out, add the id_token_hint
                            if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                            {
                                var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");

                                if (idTokenHint != null)
                                {
                                    n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                                }
                            }
                            return Task.FromResult(0);
                        }
                    }
                }
            });
        }
    }
}

我调查了很多事情,但我的 cookie 的价值始终保持不变。我考虑过删除旧的 cookie 并手动构建新的 cookie,但这需要以正确的方式加密它,而且它闻起来很有趣,肯定不是惯用的方式。

我觉得我一定缺少一些简单的东西。我希望有一种简单的 "UpdateCookie(newToken)" 方法,我已经尝试过 SignIn() 和 SignOut() 但这些对我来说都没有用,实际上似乎根本没有与 cookie 交互。

这就是我的工作方式,添加以下行:

SecurityTokenValidated = context =>
                        {
                            context.AuthenticationTicket.Properties.AllowRefresh = true;
                            context.AuthenticationTicket.Properties.IsPersistent = true;
                        }

然后在 AuthorizationCodeReceived 的末尾添加:

HttpContext.Current.GetOwinContext().Authentication.SignIn(new AuthenticationProperties
                                    {
                                        ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn),
                                        AllowRefresh = true,
                                        IssuedUtc = DateTime.UtcNow,
                                        IsPersistent = true
                                    }, newIdentity);

其中 newIdentity 是您的声明身份,希望对您有所帮助。

我最近遇到了同样的问题,解决方案是:

  1. OpenIdConnectAuthenticationOptions中设置UseTokenLifetime = false用于配置OAuth中间件(否则session cookie生命周期将设置为access token生命周期,通常为一小时)
  2. 创建您自己的 CookieAuthenticationProvider 来验证访问令牌过期
  3. 令牌过期(或接近过期)时:
    1. 使用刷新令牌获取新的访问令牌(如果 MSAL 用于 OAuth - 这是一个简单的 IConfidentialClientApplication.AcquireTokenSilent() 方法调用)
    2. 使用 ISecurityTokenValidator.ValidateToken() 方法
    3. 使用获取的访问令牌构建一个新的 IIdentity 对象
    4. 用新建的标识替换请求上下文标识
    5. 调用IAuthenticationManager.SignIn(properties, freshIdentity)更新会话cookie

这是使刷新令牌与 OWIN cookie 中间件一起工作的完整解决方案:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using EPiServer.Logging;
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Host.SystemWeb;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;

namespace MyApp
{
    public class OwinStartup
    {
        public void Configuration(IAppBuilder app)
        {
            var openIdConnectOptions = new OpenIdConnectAuthenticationOptions
            {
                UseTokenLifetime = false,
                // ...
            };

            var msalAppBuilder = new MsalAppBuilder();
            var refreshTokenHandler = new RefreshTokenHandler(msalAppBuilder, openIdConnectOptions);

            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                CookieManager = new SystemWebChunkingCookieManager(),
                Provider = new RefreshTokenCookieAuthenticationProvider(refreshTokenHandler)
            });
        }
    }

    public class RefreshTokenCookieAuthenticationProvider : CookieAuthenticationProvider
    {
        private readonly RefreshTokenHandler _refreshTokenHandler;

        private static readonly ILogger _log = LogManager.GetLogger();

        public RefreshTokenCookieAuthenticationProvider(RefreshTokenHandler refreshTokenHandler)
        {
            _refreshTokenHandler = refreshTokenHandler;
        }

        public override async Task ValidateIdentity(CookieValidateIdentityContext context)
        {
            var exp = context.Identity?.FindFirst("exp")?.Value;

            if (string.IsNullOrEmpty(exp))
            {
                return;
            }

            var utcNow = DateTimeOffset.UtcNow;
            var expiresUtc = DateTimeOffset.FromUnixTimeSeconds(long.Parse(exp));
            var maxMinsBeforeExpires = TimeSpan.FromMinutes(2);

            if (expiresUtc - utcNow >= maxMinsBeforeExpires)
            {
                return;
            }

            try
            {
                var freshIdentity = await _refreshTokenHandler.TryRefreshAccessTokenAsync(context.Identity);

                if (freshIdentity != null)
                {
                    context.ReplaceIdentity(freshIdentity);
                    context.OwinContext.Authentication.SignIn(context.Properties, (ClaimsIdentity) freshIdentity);
                }
                else
                {
                    context.RejectIdentity();
                }
            }
            catch (Exception ex)
            {
                _log.Error("Can't refresh user token", ex);
                context.RejectIdentity();
            }
        }
    }

    public class RefreshTokenHandler
    {
        private readonly MsalAppBuilder _msalAppBuilder;
        private readonly OpenIdConnectAuthenticationOptions _openIdConnectOptions;

        public RefreshTokenHandler(
            MsalAppBuilder msalAppBuilder,
            OpenIdConnectAuthenticationOptions openIdConnectOptions)
        {
            _msalAppBuilder = msalAppBuilder;
            _openIdConnectOptions = openIdConnectOptions;
        }

        public async Task<IIdentity> TryRefreshAccessTokenAsync(IIdentity identity, CancellationToken ct = default)
        {
            try
            {
                var idToken = await GetFreshIdTokenAsync(identity, ct);
                var freshIdentity = await GetFreshIdentityAsync(idToken, ct);

                return freshIdentity;
            }
            catch (MsalUiRequiredException)
            {
                return null;
            }
        }

        private async Task<string> GetFreshIdTokenAsync(IIdentity identity, CancellationToken ct)
        {
            var principal = new ClaimsPrincipal(identity);
            var app = _msalAppBuilder.BuildConfidentialClientApplication(principal);

            var accounts = await app.GetAccountsAsync();
            var result = await app.AcquireTokenSilent(new[] {"openid"}, accounts.FirstOrDefault()).ExecuteAsync(ct);

            return result.IdToken;
        }

        private async Task<IIdentity> GetFreshIdentityAsync(string idToken, CancellationToken ct)
        {
            var validationParameters = await CreateTokenValidationParametersAsync(ct);
            var principal = _openIdConnectOptions.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out _);
            var identity = (ClaimsIdentity) principal.Identity;

            return identity;
        }

        // This is additional code for cases with multiple issuers - can be skipped if this configuration is static
        private async Task<TokenValidationParameters> CreateTokenValidationParametersAsync(CancellationToken ct)
        {
            var validationParameters = _openIdConnectOptions.TokenValidationParameters.Clone();
            var configuration = await _openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(ct);

            validationParameters.ValidIssuers = (validationParameters.ValidIssuers ?? new string[0])
                .Union(new[] {configuration.Issuer})
                .ToList();
            validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys ?? new SecurityKey[0])
                .Union(configuration.SigningKeys)
                .ToList();

            return validationParameters;
        }
    }

    // From official samples: https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/blob/master/TaskWebApp/Utils/MsalAppBuilder.cs
    public class MsalAppBuilder
    {
        public IConfidentialClientApplication BuildConfidentialClientApplication(ClaimsPrincipal currentUser)
        {
            // ...
        }
    }
}