请参阅下面的代码,以解决此问题
我正在尝试找到最佳和最有效的方法来处理ASP.NET Core 2.1中已过期的刷新令牌。
让我解释一下。
我正在使用OAUTH2和OIDC来请求授权码授予流(或与OIDC的混合流)。这种流/授予类型使我可以访问AccessToken和RefreshToken(也包括授权码,但这不是针对此问题)。
访问令牌和刷新令牌由ASP.NET核心存储,可以分别使用HttpContext.GetTokenAsync("access_token");
和HttpContext.GetTokenAsync("refresh_token");
进行检索。
我可以刷新access_token
,没有任何问题。当refresh_token
过期,被撤销或以某种方式无效时,该问题就起作用了。
正确的流程是让用户再次登录,然后再次返回整个身份验证流程。然后,应用程序将返回一组新的令牌。
我的问题是如何以最佳和最正确的方法实现这一目标。我决定编写一个自定义中间件,尝试在access_token
过期时进行续订。然后,中间件将新令牌设置为HttpContext的AuthenticationProperties
中,以便以后在管道中进行的任何调用都可以使用它。
如果由于任何原因刷新令牌失败,我需要再次调用ChallengeAsync。我正在从中间件调用ChallengeAsync。
这是我遇到一些有趣行为的地方。但是,在大多数情况下,这是可行的,但有时会出现500个错误,而对于失败的原因却没有任何有用的信息。似乎中间件在尝试从中间件调用ChallengeAsync时遇到问题,也许另一个中间件也在尝试访问上下文。
我不太确定发生了什么。我不确定这是否是放置此逻辑的正确位置。也许我不应该在中间件中使用它,也许在其他地方。也许Polly是HttpClient的最佳选择。
我愿意接受任何想法。
感谢您提供的任何帮助。
最适合我的代码解决方案
感谢Mickaël Derriey的帮助和指导(一定要查看他的答案以获取有关此解决方案的更多信息)这是我想出的解决方案,它对我有用: / p>
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
//check to see if user is authenticated first
if (context.Principal.Identity.IsAuthenticated)
{
//get the users 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;
}
};
答案 0 :(得分:9)
访问令牌和刷新令牌由ASP.NET核心存储
我认为必须注意,令牌存储在cookie中,该cookie标识应用程序的用户。
现在这是我的看法,但是我认为自定义中间件不是刷新令牌的正确位置。 这样做的原因是,如果成功刷新令牌,则需要替换现有的令牌,并将其以新的Cookie的形式发送回浏览器,以替换现有的令牌。
这就是为什么我认为最相关的地方是ASP.NET Core正在读取cookie的原因。每种身份验证机制都会公开多个事件。对于Cookie,有一个名为ValidatePrincipal
的Cookie,它在读取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
的{{1}}变量具有一个称为ShouldRenew
。将该属性设置为CookieValidatePrincipalContext
会指示中间件发出新的cookie。
如果您无法续签令牌,或者您发现刷新令牌已过期并且想要阻止用户继续前进,则同一类具有RejectPrincipal
方法,该方法指示Cookie中间件处理请求好像是匿名的。
这样做的好处是,如果您的MVC应用仅允许经过身份验证的用户访问,则MVC将负责发出true
响应,身份验证系统将捕获该响应并将其转换为“挑战”,而用户将被重定向回身份提供者。
我有一些代码说明了如何在GitHub上的mderriey/TokenRenewal
存储库中进行处理。尽管目的不同,但它说明了如何使用这些事件的机制。
答案 1 :(得分:5)
我创建了一个替代实现,它具有一些其他优点:
AddOpenIdConnect
方法的 OpenID配置选项。这样可以简化客户端配置。这是更新的OnValidatePrincipal
方法:
private async Task OnValidatePrincipal(CookieValidatePrincipalContext context)
{
const string accessTokenName = "access_token";
const string refreshTokenName = "refresh_token";
const string expirationTokenName = "expires_at";
if (context.Principal.Identity.IsAuthenticated)
{
var exp = context.Properties.GetTokenValue(expirationTokenName);
if (exp != null)
{
var expires = DateTime.Parse(exp, CultureInfo.InvariantCulture).ToUniversalTime();
if (expires < DateTime.UtcNow)
{
// If we don't have the refresh token, then check if this client has set the
// "AllowOfflineAccess" property set in Identity Server and if we have requested
// the "OpenIdConnectScope.OfflineAccess" scope when requesting an access token.
var refreshToken = context.Properties.GetTokenValue(refreshTokenName);
if (refreshToken == null)
{
context.RejectPrincipal();
return;
}
var cancellationToken = context.HttpContext.RequestAborted;
// Obtain the OpenIdConnect options that have been registered with the
// "AddOpenIdConnect" call. Make sure we get the same scheme that has
// been passed to the "AddOpenIdConnect" call.
//
// TODO: Cache the token client options
// The OpenId Connect configuration will not change, unless there has
// been a change to the client's settings. In that case, it is a good
// idea not to refresh and make sure the user does re-authenticate.
var serviceProvider = context.HttpContext.RequestServices;
var openIdConnectOptions = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenIdConnectOptions>>().Get(OpenIdConnectScheme);
var configuration = openIdConnectOptions.Configuration ?? await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(cancellationToken).ConfigureAwait(false);
// Set the proper token client options
var tokenClientOptions = new TokenClientOptions
{
Address = configuration.TokenEndpoint,
ClientId = openIdConnectOptions.ClientId,
ClientSecret = openIdConnectOptions.ClientSecret
};
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
using var httpClient = httpClientFactory.CreateClient();
var tokenClient = new TokenClient(httpClient, tokenClientOptions);
var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken, cancellationToken: cancellationToken).ConfigureAwait(false);
if (tokenResponse.IsError)
{
context.RejectPrincipal();
return;
}
// Update the tokens
var expirationValue = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn).ToString("o", CultureInfo.InvariantCulture);
context.Properties.StoreTokens(new []
{
new AuthenticationToken { Name = refreshTokenName, Value = tokenResponse.RefreshToken },
new AuthenticationToken { Name = accessTokenName, Value = tokenResponse.AccessToken },
new AuthenticationToken { Name = expirationTokenName, Value = expirationValue }
});
// Update the cookie with the new tokens
context.ShouldRenew = true;
}
}
}
}