首页 文章

IdentityServer4 - 在遵循混合MVC的快速入门之后使用刷新令牌

提问于
浏览
9

我已经跟踪了文档页面中的Quickstart,并使用IdentityServer进行了三种服务(IdentityServer,一种Api服务,一种ASPNET MVC应用程序)的工作配置进行身份验证 .

一切正常(登录,登录,授权等),直到access_token到期后1小时 . 此时,MVC应用程序开始(正确地)从API服务接收401(因为令牌已过期) . 那时,我知道我应该使用refresh_token来获取新的access_token .

我一直在寻找一种自动刷新access_token的机制,并偶然发现:https://github.com/mderriey/TokenRenewal/blob/master/src/MvcClient/Startup.cs(来自this answer) . 我尝试使用它,但它不起作用(即使身份验证成功, TokenEndpointResponse 也为null) .

我知道如何使用 refresh_token 来获取新的 access_token ,但是在我拥有它之后,我将如何将其插回到cookie中以便将来的请求可以访问新的令牌?

5 回答

  • 0

    McvHybrid样本有一个很好的例子,可以让新的 access_tokenrefresh_token 重新进入校长 . 这是带有代码的github文件的link,该文件位于 RenewTokens() ,如下所示 .

    public async Task<IActionResult> RenewTokens()
        {
            var disco = await DiscoveryClient.GetAsync(Constants.Authority);
            if (disco.IsError) throw new Exception(disco.Error);
    
            var tokenClient = new TokenClient(disco.TokenEndpoint, "mvc.hybrid", "secret");
            var rt = await     HttpContext.Authentication.GetTokenAsync("refresh_token");
            var tokenResult = await tokenClient.RequestRefreshTokenAsync(rt);
    
            if (!tokenResult.IsError)
            {
                var old_id_token = await HttpContext.Authentication.GetTokenAsync("id_token");
                var new_access_token = tokenResult.AccessToken;
                var new_refresh_token = tokenResult.RefreshToken;
    
                var tokens = new List<AuthenticationToken>();
                tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = old_id_token });
                tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = new_access_token });
                tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = new_refresh_token });
    
                var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
                tokens.Add(new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) });
    
                var info = await HttpContext.Authentication.GetAuthenticateInfoAsync("Cookies");
                info.Properties.StoreTokens(tokens);
                await HttpContext.Authentication.SignInAsync("Cookies", info.Principal, info.Properties);
    
                return Redirect("~/Home/Secure");
            }
    
            ViewData["Error"] = tokenResult.Error;
            return View("Error");
        }
    
  • 1

    作为来自MVC Client example的RenewTokens方法的一个选项,我做了一个过滤器,当令牌大约10分钟或更短时间到期时,该过滤器自动生成作业 .

    public class TokenFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var expat = filterContext.HttpContext.Authentication.GetTokenAsync("expires_at").Result;
    
            var dataExp = DateTime.Parse(expat, null, DateTimeStyles.RoundtripKind);
    
            if ((dataExp - DateTime.Now).TotalMinutes < 10)
            {
                var disco = DiscoveryClient.GetAsync("http://localhost:5000/").Result;
                if (disco.IsError) throw new Exception(disco.Error);
    
                var tokenClient = new TokenClient(disco.TokenEndpoint, "clientId",
                    "clientSecret");
    
                var rt = filterContext.HttpContext.Authentication.GetTokenAsync("refresh_token").Result;
                var tokenResult = tokenClient.RequestRefreshTokenAsync(rt).Result;
    
                if (!tokenResult.IsError)
                {
                    var oldIdToken = filterContext.HttpContext.Authentication.GetTokenAsync("id_token").Result;
                    var newAccessToken = tokenResult.AccessToken;
                    var newRefreshToken = tokenResult.RefreshToken;
    
                    var tokens = new List<AuthenticationToken>
                    {
                        new AuthenticationToken {Name = OpenIdConnectParameterNames.IdToken, Value = oldIdToken},
                        new AuthenticationToken
                        {
                            Name = OpenIdConnectParameterNames.AccessToken,
                            Value = newAccessToken
                        },
                        new AuthenticationToken
                        {
                            Name = OpenIdConnectParameterNames.RefreshToken,
                            Value = newRefreshToken
                        }
                    };
    
                    var expiresAt = DateTime.Now + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
                    tokens.Add(new AuthenticationToken
                    {
                        Name = "expires_at",
                        Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                    });
    
                    var info = filterContext.HttpContext.Authentication.GetAuthenticateInfoAsync("Cookies").Result;
                    info.Properties.StoreTokens(tokens);
                    filterContext.HttpContext.Authentication.SignInAsync("Cookies", info.Principal, info.Properties);
                }
            }
        }
    }
    

    用法:

    [Authorize]
    [TokenFilter]
    public class HomeController : Controller
    {}
    
  • 1

    首先,一定要使用IdentityModel库(nuget it) . 其次,由于Auth 2.0已经出现,因此Rafaels解决方案中使用的HttpContext.Authentication现在已经过时 . 以下是应该进行的更改,以便再次将其作为过滤器运行

    var expat = filterContext.HttpContext.Authentication.GetTokenAsync("expires_at").Result;
    

    应成为:

    var expat = filterContext.HttpContext.GetTokenAsync("expires_at").Result;
    

    var rt = filterContext.HttpContext.Authentication.GetTokenAsync("refresh_token").Result;
    

    应成为:

    var rt = filterContext.HttpContext.GetTokenAsync("refresh_token").Result;
    

    var oldIdToken = filterContext.HttpContext.Authentication.GetTokenAsync("id_token").Result;
    

    应该成为

    var oldIdToken = filterContext.HttpContext.GetTokenAsync("id_token").Result;
    

    var info = filterContext.HttpContext.Authentication.GetAuthenticateInfoAsync("Cookies").Result;
    

    应该成为

    var info = filterContext.HttpContext.AuthenticateAsync("Cookies").Result;
    

    filterContext.HttpContext.Authentication.SignInAsync("Cookies", info.Principal, info.Properties);
    

    应该成为

    filterContext.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
    

    这是一个完整的代码:

    public class TokenFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var expat = filterContext.HttpContext.GetTokenAsync("expires_at").Result;
    
            var dataExp = DateTime.Parse(expat, null, DateTimeStyles.RoundtripKind);
    
            if ((dataExp - DateTime.Now).TotalMinutes < 10)
            {
                var disco = DiscoveryClient.GetAsync("http://localhost:5000/").Result;
                if (disco.IsError) throw new Exception(disco.Error);
    
                var tokenClient = new TokenClient(disco.TokenEndpoint, "clientId",
                "clientSecret");
    
                var rt = filterContext.HttpContext.GetTokenAsync("refresh_token").Result;
                var tokenResult = tokenClient.RequestRefreshTokenAsync(rt).Result;
    
                if (!tokenResult.IsError)
                {
                    var oldIdToken = filterContext.HttpContext.GetTokenAsync("id_token").Result;
                    var newAccessToken = tokenResult.AccessToken;
                    var newRefreshToken = tokenResult.RefreshToken;
    
                    var tokens = new List<AuthenticationToken>
                    {
                        new AuthenticationToken {Name = OpenIdConnectParameterNames.IdToken, Value = oldIdToken},
                        new AuthenticationToken
                        {
                            Name = OpenIdConnectParameterNames.AccessToken,
                            Value = newAccessToken
                        },
                        new AuthenticationToken
                        { 
                            Name = OpenIdConnectParameterNames.RefreshToken,
                            Value = newRefreshToken
                        }
                    };
    
                    var expiresAt = DateTime.Now + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
                    tokens.Add(new AuthenticationToken
                    {
                        Name = "expires_at",
                        Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                    });
    
                    var info = filterContext.HttpContext.AuthenticateAsync("Cookies").Result;
                    info.Properties.StoreTokens(tokens);  
                    filterContext.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
                }
            }
        }
    }
    

    Usage和Rafael所说的相同 .

  • 5

    当令牌的一半以上生命通过时,我制作了自动完成工作的中间件 . 因此,您无需调用任何方法或应用任何过滤器 . 只需将其插入Startup.cs并涵盖整个应用程序:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // Other code here
    
        app.UseAutomaticSilentRenew("http://localhost:5000/", "clientId", "clientSecret")
        app.UseAccessTokenLifetime();
    
        // And here
    }
    

    UseAutomaticSilentRenew - 续订访问权限并刷新令牌
    UseAccessTokenLifetime - 如果访问令牌已过期,则退出用户 . 只需在UseAutomaticSilentRenew之前无法获取新的访问令牌时,将其放在第二位 .

    执行:

    public static class OidcExtensions
    {
        public static IApplicationBuilder UseAutomaticSilentRenew(this IApplicationBuilder builder, string authority, string clientId, string clientSecret, string cookieSchemeName = CookieAuthenticationDefaults.AuthenticationScheme)
        {
            return builder.UseMiddleware<AutomaticSilentRenewMiddleware>(authority, clientId, clientSecret, cookieSchemeName);
        }
    
        public static IApplicationBuilder UseAccessTokenLifetime(this IApplicationBuilder builder, string cookieSchemeName = CookieAuthenticationDefaults.AuthenticationScheme)
        {
            return builder.UseMiddleware<TokenLifetimeMiddleware>(OpenIdConnectParameterNames.AccessToken, cookieSchemeName);
        }
    
        public static IApplicationBuilder UseIdTokenLifetime(this IApplicationBuilder builder, string cookieSchemeName = CookieAuthenticationDefaults.AuthenticationScheme)
        {
            return builder.UseMiddleware<TokenLifetimeMiddleware>(OpenIdConnectParameterNames.IdToken, cookieSchemeName);
        }
    }
    
    public class AutomaticSilentRenewMiddleware
    {
        private readonly RequestDelegate next;
        private readonly string authority;
        private readonly string clientId;
        private readonly string clientSecret;
        private readonly string cookieSchemeName;
    
        public AutomaticSilentRenewMiddleware(RequestDelegate next, string authority, string clientId, string clientSecret, string cookieSchemeName)
        {
            this.next = next;
            this.authority = authority;
            this.clientId = clientId;
            this.clientSecret = clientSecret;
            this.cookieSchemeName = cookieSchemeName;
        }
    
        public async Task InvokeAsync(HttpContext context)
        {
            string oldAccessToken = await context.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            if (!string.IsNullOrEmpty(oldAccessToken))
            {
                JwtSecurityToken tokenInfo = new JwtSecurityToken(oldAccessToken);
    
                // Renew access token if pass halfway of its lifetime
                if (tokenInfo.ValidFrom + (tokenInfo.ValidTo - tokenInfo.ValidFrom) / 2 < DateTime.UtcNow)
                {
                    string tokenEndpoint;
                    var disco = await DiscoveryClient.GetAsync(authority);
                    if (!disco.IsError)
                    {
                        tokenEndpoint = disco.TokenEndpoint;
                    }
                    else
                    {
                        // If failed to get discovery document use default URI
                        tokenEndpoint = authority + "/connect/token";
                    }
                    TokenClient tokenClient = new TokenClient(tokenEndpoint, clientId, clientSecret);
                    string oldRefreshToken = await context.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
                    TokenResponse tokenResult = await tokenClient.RequestRefreshTokenAsync(oldRefreshToken);
    
                    if (!tokenResult.IsError)
                    {
                        string idToken = await context.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
                        string newAccessToken = tokenResult.AccessToken;
                        string newRefreshToken = tokenResult.RefreshToken;
    
                        var tokens = new List<AuthenticationToken>
                        {
                            new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = idToken },
                            new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = newAccessToken },
                            new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = newRefreshToken }
                        };
    
                        AuthenticateResult info = await context.AuthenticateAsync(cookieSchemeName);
                        info.Properties.StoreTokens(tokens);
                        await context.SignInAsync(cookieSchemeName, info.Principal, info.Properties);
                    }
                }
            }
    
            await next.Invoke(context);
        }
    }
    
    public class TokenLifetimeMiddleware
    {
        private readonly RequestDelegate next;
        private readonly string tokenName;
        private readonly string cookieSchemeName;
    
        public TokenLifetimeMiddleware(RequestDelegate next, string tokenName, string cookieSchemeName)
        {
            this.next = next;
            this.tokenName = tokenName;
            this.cookieSchemeName = cookieSchemeName;
        }
    
        public async Task InvokeAsync(HttpContext context)
        {
            string token = await context.GetTokenAsync(tokenName);
            if (!string.IsNullOrEmpty(token))
            {
                DateTime validTo = new JwtSecurityToken(token).ValidTo;
                if (validTo < DateTime.UtcNow)
                {
                    // Sign out if token is no longer valid
                    await context.SignOutAsync(cookieSchemeName);
                }
            }
    
            await next.Invoke(context);
        }
    }
    

    Note :我没有't set cookie expiration time because in our case it depends on refresh token lifetime witch is not provided by identity server. If I aligned the expiration of the cookie with the expiration of the access token I would'能够在到期后刷新访问令牌 .

    哦,还有一件事 . UseAccessTokenLifetime清除cookie但不会注销用户 . 重新加载页面后退出 . 没有找到解决方法 .

  • 11

    你提供给https://github.com/mderriey/TokenRenewal/blob/master/src/MvcClient/Startup.cs的链接对我有帮助!

    问题出在AddOpenIdConnect部分 . 您想要的事件不是OnTokenValidated事件 . 您应该使用OnTokenResponseReceived事件 . 就在这时,您将有一个适当的access_token和refresh_token来添加到cookie .

相关问题