在 ASP.NET Core 中處理過期的刷新令牌
有關解決此問題的程式碼,請參見下文
我正在嘗試找到處理在 ASP.NET Core 2.1 中過期的刷新令牌的最佳和最有效的方法。
讓我再解釋一下。
我正在使用 OAUTH2 和 OIDC 請求授權碼授權流(或帶有 OIDC 的混合流)。這種流/授權類型使我可以訪問 AccessToken 和 RefreshToken(授權碼也是如此,但這不適用於這個問題)。
訪問令牌和刷新令牌由 ASP.NET Core 儲存,可以分別使用
HttpContext.GetTokenAsync("access_token");和檢索HttpContext.GetTokenAsync("refresh_token");。我可以刷新
access_token沒有任何問題。當refresh_token以某種方式過期、撤銷或無效時,問題就會出現。正確的流程是讓使用者登錄並再次返回整個身份驗證流程。然後應用程序會返回一組新的令牌。
我的問題是如何以最好和最正確的方法實現這一目標。我決定編寫一個自定義中間件來嘗試更新
access_token它是否已過期。然後中間件將新令牌設置到AuthenticationPropertiesHttpContext 中,以便以後管道中的任何呼叫都可以使用它。如果由於某種原因刷新令牌失敗,我需要再次呼叫 ChallengeAsync。我從中間件呼叫 ChallengeAsync。
這是我遇到一些有趣行為的地方。但是,大多數情況下,這可行,但有時我會收到 500 個錯誤,而沒有關於失敗原因的有用資訊。似乎中間件在嘗試從中間件呼叫 ChallengeAsync 時遇到問題,並且可能另一個中間件也在嘗試訪問上下文。
我不太確定發生了什麼。我不太確定這是否是放置此邏輯的正確位置。也許我不應該在中間件中使用它,也許在其他地方。也許 HttpClient 的 Polly 是最好的地方。
我願意接受任何想法。
感謝您的任何幫助,您可以提供。
對我有用的程式碼解決方案
感謝Mickaël Derriey的幫助和指導(請務必查看他的答案以獲取有關此解決方案的更多資訊)。這是我想出的解決方案,它對我有用:
options.Events = new CookieAuthenticationEvents { OnValidatePrincipal = context => { //check to see if user is authenticated first if (context.Principal.Identity.IsAuthenticated) { //get the user's tokens var tokens = context.Properties.GetTokens(); var refreshToken = tokens.FirstOrDefault(t => t.Name == "refresh_token"); var accessToken = tokens.FirstOrDefault(t => t.Name == "access_token"); var exp = tokens.FirstOrDefault(t => t.Name == "expires_at"); var expires = DateTime.Parse(exp.Value); //check to see if the token has expired if (expires < DateTime.Now) { //token is expired, let's attempt to renew var tokenEndpoint = "https://token.endpoint.server"; var tokenClient = new TokenClient(tokenEndpoint, clientId, clientSecret); var tokenResponse = tokenClient.RequestRefreshTokenAsync(refreshToken.Value).Result; //check for error while renewing - any error will trigger a new login. if (tokenResponse.IsError) { //reject Principal context.RejectPrincipal(); return Task.CompletedTask; } //set new token values refreshToken.Value = tokenResponse.RefreshToken; accessToken.Value = tokenResponse.AccessToken; //set new expiration date var newExpires = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn); exp.Value = newExpires.ToString("o", CultureInfo.InvariantCulture); //set tokens in auth properties context.Properties.StoreTokens(tokens); //trigger context to renew cookie with new token values context.ShouldRenew = true; return Task.CompletedTask; } } return Task.CompletedTask; } };
訪問令牌和刷新令牌由 ASP.NET 核心儲存
我認為重要的是要注意令牌儲存在向您的應用程序標識使用者的 cookie 中。
現在這是我的觀點,但我認為自定義中間件不是刷新令牌的正確位置。這樣做的原因是,如果您成功刷新令牌,則需要替換現有令牌並將其發送回瀏覽器,以新 cookie 的形式替換現有令牌。
這就是為什麼我認為最相關的地方是當 ASP.NET Core 讀取 cookie 時。每個身份驗證機制都會暴露幾個事件;對於 cookie,
ValidatePrincipal在讀取 cookie 並成功反序列化身份後,每個請求都會呼叫一個呼叫。public void ConfigureServices(ServiceCollection services) { services .AddAuthentication() .AddCookies(new CookieAuthenticationOptions { Events = new CookieAuthenticationEvents { OnValidatePrincipal = context => { // context.Principal gives you access to the logged-in user // context.Properties.GetTokens() gives you access to all the tokens return Task.CompletedTask; } } }); }這種方法的好處是,如果您設法更新令牌並將其儲存在 中
AuthenticationProperties,context則類型為 的變數CookieValidatePrincipalContext具有名為 的屬性ShouldRenew。將該屬性設置為true指示中間件發出新的 cookie。如果您無法更新令牌,或者您發現刷新令牌已過期並且您想阻止使用者繼續前進,那麼同一類有一個
RejectPrincipal方法可以指示 cookie 中間件將請求視為匿名請求。這樣做的好處是,如果您的 MVC 應用程序只允許經過身份驗證的使用者訪問它,MVC 將負責發出
HTTP 401響應,身份驗證系統將擷取並轉換為挑戰,並且使用者將被重定向回身份提供者。我有一些程式碼顯示了這將如何
mderriey/TokenRenewal在 GitHub 上的儲存庫中執行。雖然意圖不同,但它顯示瞭如何使用這些事件的機制。