IdentityServer4 - 在遵循混合 MVC 快速入门后使用刷新令牌
IdentityServer4 - Using Refresh Tokens after following the Quickstart for Hybrid MVC
我已经按照文档页面中的快速入门进行了操作,并使用 IdentityServer 进行身份验证,对三种服务(IdentityServer、一种 Api 服务、一种 ASPNET MVC 应用程序)进行了工作配置。
一切正常(登录、登录、授权等),直到 1 小时后 access_token 过期。此时,MVC 应用程序开始(正确地)从 API 服务接收 401(因为令牌已过期)。那时,我知道我应该使用 refresh_token 来获得一个新的 access_token。
我正在寻找一种自动刷新 access_token 的机制并偶然发现了这个:https://github.com/mderriey/TokenRenewal/blob/master/src/MvcClient/Startup.cs (from )。我尝试使用它,但它没有用(即使身份验证成功,TokenEndpointResponse
仍为 null)。
我知道如何使用 refresh_token
来获取新的 access_token
,但是在我拥有它之后,我该如何将它重新插入到 cookie 中以便将来的请求可以访问新代币?
McvHybrid 样本有一个很好的例子,可以让新的 access_token
和 refresh_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");
}
作为来自 MVC 客户端 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
{}
首先,请务必使用 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);
}
}
}
}
用法与 Rafael 展示的相同。
你提供给https://github.com/mderriey/TokenRenewal/blob/master/src/MvcClient/Startup.cs的link对我帮助很大!
问题出在 AddOpenIdConnect 部分。您想要的事件不是 OnTokenValidated 事件。您应该使用 OnTokenResponseReceived 事件。届时您将有一个适当的 access_token 和 refresh_token 添加到 cookie。
我制作了一个中间件,当访问令牌的生命周期超过一半时,它会自动完成这项工作。所以你不需要调用任何方法或应用任何过滤器。只需将其插入 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
之后,使其仅在 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);
}
}
注意:我没有设置 cookie 过期时间,因为在我们的例子中它取决于刷新令牌的生命周期 身份服务器不提供。如果我将 cookie 的到期与访问令牌的到期对齐,我将无法在其到期后刷新访问令牌。
哦,还有一件事。 UseAccessTokenLifetime
清除 cookie 但不注销用户。重新加载页面后会注销。没有找到修复它的方法。
IdentityServer4-Github 有另一个(新的?)MvcAutomaticTokenManagement example。
StartUp.cs
调用 extension-method AddAutomaticTokenManagement()
,后者又调用许多其他内容。因为其他一些答案中的链接变得无效,我很想包括所有内容,但是要引用的代码(和文件太多)太多了 - 去看看吧。
最相关(?)部分:
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
// [removed about 20 lines of code to get and check tokens here...]
if (dtRefresh < _clock.UtcNow)
{
var shouldRefresh = _pendingRefreshTokenRequests.TryAdd(refreshToken.Value, true);
if (shouldRefresh)
{
try
{
var response = await _service.RefreshTokenAsync(refreshToken.Value);
if (response.IsError)
{
_logger.LogWarning("Error refreshing token: {error}", response.Error);
return;
}
context.Properties.UpdateTokenValue("access_token", response.AccessToken);
context.Properties.UpdateTokenValue("refresh_token", response.RefreshToken);
var newExpiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn);
context.Properties.UpdateTokenValue("expires_at", newExpiresAt.ToString("o", CultureInfo.InvariantCulture));
await context.HttpContext.SignInAsync(context.Principal, context.Properties);
}
finally
{
_pendingRefreshTokenRequests.TryRemove(refreshToken.Value, out _);
}
}
}
}
public override async Task SigningOut(CookieSigningOutContext context)
{
// [removed about 15 lines of code to get and check tokens here...]
var response = await _service.RevokeTokenAsync(refreshToken.Value);
if (response.IsError)
{
_logger.LogWarning("Error revoking token: {error}", response.Error);
return;
}
}
我已经按照文档页面中的快速入门进行了操作,并使用 IdentityServer 进行身份验证,对三种服务(IdentityServer、一种 Api 服务、一种 ASPNET MVC 应用程序)进行了工作配置。
一切正常(登录、登录、授权等),直到 1 小时后 access_token 过期。此时,MVC 应用程序开始(正确地)从 API 服务接收 401(因为令牌已过期)。那时,我知道我应该使用 refresh_token 来获得一个新的 access_token。
我正在寻找一种自动刷新 access_token 的机制并偶然发现了这个:https://github.com/mderriey/TokenRenewal/blob/master/src/MvcClient/Startup.cs (from TokenEndpointResponse
仍为 null)。
我知道如何使用 refresh_token
来获取新的 access_token
,但是在我拥有它之后,我该如何将它重新插入到 cookie 中以便将来的请求可以访问新代币?
McvHybrid 样本有一个很好的例子,可以让新的 access_token
和 refresh_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");
}
作为来自 MVC 客户端 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
{}
首先,请务必使用 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);
}
}
}
}
用法与 Rafael 展示的相同。
你提供给https://github.com/mderriey/TokenRenewal/blob/master/src/MvcClient/Startup.cs的link对我帮助很大!
问题出在 AddOpenIdConnect 部分。您想要的事件不是 OnTokenValidated 事件。您应该使用 OnTokenResponseReceived 事件。届时您将有一个适当的 access_token 和 refresh_token 添加到 cookie。
我制作了一个中间件,当访问令牌的生命周期超过一半时,它会自动完成这项工作。所以你不需要调用任何方法或应用任何过滤器。只需将其插入 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
之后,使其仅在 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);
}
}
注意:我没有设置 cookie 过期时间,因为在我们的例子中它取决于刷新令牌的生命周期 身份服务器不提供。如果我将 cookie 的到期与访问令牌的到期对齐,我将无法在其到期后刷新访问令牌。
哦,还有一件事。 UseAccessTokenLifetime
清除 cookie 但不注销用户。重新加载页面后会注销。没有找到修复它的方法。
IdentityServer4-Github 有另一个(新的?)MvcAutomaticTokenManagement example。
StartUp.cs
调用 extension-method AddAutomaticTokenManagement()
,后者又调用许多其他内容。因为其他一些答案中的链接变得无效,我很想包括所有内容,但是要引用的代码(和文件太多)太多了 - 去看看吧。
最相关(?)部分:
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
// [removed about 20 lines of code to get and check tokens here...]
if (dtRefresh < _clock.UtcNow)
{
var shouldRefresh = _pendingRefreshTokenRequests.TryAdd(refreshToken.Value, true);
if (shouldRefresh)
{
try
{
var response = await _service.RefreshTokenAsync(refreshToken.Value);
if (response.IsError)
{
_logger.LogWarning("Error refreshing token: {error}", response.Error);
return;
}
context.Properties.UpdateTokenValue("access_token", response.AccessToken);
context.Properties.UpdateTokenValue("refresh_token", response.RefreshToken);
var newExpiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn);
context.Properties.UpdateTokenValue("expires_at", newExpiresAt.ToString("o", CultureInfo.InvariantCulture));
await context.HttpContext.SignInAsync(context.Principal, context.Properties);
}
finally
{
_pendingRefreshTokenRequests.TryRemove(refreshToken.Value, out _);
}
}
}
}
public override async Task SigningOut(CookieSigningOutContext context)
{
// [removed about 15 lines of code to get and check tokens here...]
var response = await _service.RevokeTokenAsync(refreshToken.Value);
if (response.IsError)
{
_logger.LogWarning("Error revoking token: {error}", response.Error);
return;
}
}