In an ASP.NET Core application we are building we are using OpenID Connect Hybrid flow.
From the Identity Server documentation:
Access tokens are a bit more sensitive than identity tokens, and we don’t want to expose them to the “outside” world if not needed. OpenID Connect includes a flow called “Hybrid Flow” which gives us the best of both worlds, the identity token is transmitted via the browser channel, so the client can validate it before doing any more work. And if validation is successful, the client opens a back-channel to the token service to retrieve the access token.
On our ASP.NET Core backend we use the acquired access token to call another API. However as an access token typically has a short lifetime, this only works until the access token is expired. To refresh our access token, we can use a refresh token to acquire a new access token from our Security Token Service.
After trying multiple possible solutions, I ended up with an implementation where I use an ASP.NET Core MVC filter to handle this:
public class RefreshTokenFilter : ActionFilterAttribute | |
{ | |
private readonly ApplicationOptions _applicationOptions; | |
public RefreshTokenFilter(ApplicationOptions applicationOptions) | |
{ | |
_applicationOptions = applicationOptions; | |
} | |
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) | |
{ | |
var expiresAt = await context.HttpContext.GetTokenAsync("expires_at"); | |
var expire = DateTime.Parse(expiresAt); | |
if (expire < DateTime.Now.AddMinutes(5)) //Check if the access token will expire in 5 minutes | |
{ | |
await RefreshTokens(context.HttpContext); | |
} | |
await base.OnActionExecutionAsync(context, next); | |
} | |
private async Task RefreshTokens(HttpContext httpContext) | |
{ | |
var discoveryClient = new DiscoveryClient(_applicationOptions.Authentication.Authority); | |
var disco = await discoveryClient.GetAsync(); | |
if (disco.IsError) throw new Exception(disco.Error); | |
var tokenClient = new TokenClient(disco.TokenEndpoint, _applicationOptions.Authentication.ClientId, _applicationOptions.Authentication.ClientSecret); | |
var rt = await httpContext.GetTokenAsync("refresh_token"); | |
var tokenResult = await tokenClient.RequestRefreshTokenAsync(rt); | |
if (!tokenResult.IsError) | |
{ | |
var old_id_token = await httpContext.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.AuthenticateAsync("Cookies"); | |
info.Properties.StoreTokens(tokens); | |
await httpContext.SignInAsync("Cookies", info.Principal, info.Properties); | |
} | |
} | |
} |