I\'ve followed the Quickstart in the documentation page and have a working configuration of three services (IdentityServer, one Api service, one ASPNET MVC application) usin
First, be sure to use IdentityModel library (nuget it). Second, since Auth 2.0 is out there are some breaking changes and HttpContext.Authentication used in Rafaels solution is now obsolete. Here are the changes which should be made to get it up and running as a filter again
var expat = filterContext.HttpContext.Authentication.GetTokenAsync("expires_at").Result;
should become:
var expat = filterContext.HttpContext.GetTokenAsync("expires_at").Result;
var rt = filterContext.HttpContext.Authentication.GetTokenAsync("refresh_token").Result;
should become:
var rt = filterContext.HttpContext.GetTokenAsync("refresh_token").Result;
var oldIdToken = filterContext.HttpContext.Authentication.GetTokenAsync("id_token").Result;
should become
var oldIdToken = filterContext.HttpContext.GetTokenAsync("id_token").Result;
var info = filterContext.HttpContext.Authentication.GetAuthenticateInfoAsync("Cookies").Result;
should become
var info = filterContext.HttpContext.AuthenticateAsync("Cookies").Result;
filterContext.HttpContext.Authentication.SignInAsync("Cookies", info.Principal, info.Properties);
should become
filterContext.HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
And this is the a whole code:
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);
}
}
}
}
Usаge is the same as Rafael showed.
As an option to RenewTokens method from MVC Client example, I made one filter that makes the job automatically, when the token is about 10 minutes or less to expire.
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);
}
}
}
}
Usage:
[Authorize]
[TokenFilter]
public class HomeController : Controller
{}
The link you provided to https://github.com/mderriey/TokenRenewal/blob/master/src/MvcClient/Startup.cs really helped me!
The gotcha was in the AddOpenIdConnect section. The event you want is not the OnTokenValidated event. You should use the OnTokenResponseReceived event. It's at that point you'll have a proper access_token and refresh_token to add to the cookie.
I made middleware that makes the job automatically, when more than half of the life of the access token has passed. So you don't need to call any method or apply any filter. Just insert this into Startup.cs and whole application is covered:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Other code here
app.UseAutomaticSilentRenew("http://localhost:5000/", "clientId", "clientSecret")
app.UseAccessTokenLifetime();
// And here
}
UseAutomaticSilentRenew - Renews access and refresh tokens
UseAccessTokenLifetime - Signs user out if access token is expired. Put this after UseAutomaticSilentRenew
to make it work only if the UseAutomaticSilentRenew failed to obtain new access token earlier.
Implementation:
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: I didn't set cookie expiration time because in our case it depends on refresh token lifetime witch is not provided by identity server. If I'd aligned expiration of the cookie with the expiration of the access token I would't be able to refresh access token after its expiration.
Oh, and another thing. UseAccessTokenLifetime
clears the cookie but doesn't signs out the user. Sign out occurs after you reload the page. Didn't find a way to fix it.
The IdentityServer4-Github has another (new?) MvcHybridAutomaticRefresh example.
StartUp.cs
calls the extension-method AddAutomaticTokenManagement()
, which in turn calls lots of other stuff. Because the links in some other answers turned invalid, I would love to include all, but it is way too much code (and too many files) to quote - go check it out.
Most relevant(?) part:
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;
}
}
The McvHybrid sample has a good example for getting the new access_token
and refresh_token
back into the principal. Here's a link to the github file with the code, which is located in RenewTokens()
as shown below.
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");
}