Skip to content

Instantly share code, notes, and snippets.

@halter73
Last active November 9, 2023 17:29
Show Gist options
  • Save halter73/f970ab51812acbac818f678e76d9ecbd to your computer and use it in GitHub Desktop.
Save halter73/f970ab51812acbac818f678e76d9ecbd to your computer and use it in GitHub Desktop.
ASP.NET Core OIDC Cookie Refresh
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
namespace Microsoft.Extensions.DependencyInjection;
// https://github.com/dotnet/aspnetcore/issues/8175
internal sealed class CookieOidcRefresher(IOptionsMonitor<OpenIdConnectOptions> oidcOptionsMonitor) : IDisposable
{
private readonly HttpClient refreshClient = new();
private readonly OpenIdConnectProtocolValidator oidcTokenValidator = new()
{
// Refresh requests do not use the nonce parameter. Otherwise, we'd use oidcOptions.ProtocolValidator.
RequireNonce = false,
};
public async Task ValidateOrRefreshCookieAsync(CookieValidatePrincipalContext validateContext, string oidcScheme)
{
var accessTokenExpirationText = validateContext.Properties.GetTokenValue("expires_at");
if (!DateTimeOffset.TryParse(accessTokenExpirationText, out var accessTokenExpiration))
{
return;
}
var oidcOptions = oidcOptionsMonitor.Get(oidcScheme);
var now = oidcOptions.TimeProvider!.GetUtcNow();
if (now + TimeSpan.FromMinutes(5) < accessTokenExpiration)
{
return;
}
var oidcConfiguration = await oidcOptions.ConfigurationManager!.GetConfigurationAsync(validateContext.HttpContext.RequestAborted);
var tokenEndpoint = oidcConfiguration.TokenEndpoint ?? throw new InvalidOperationException("Cannot refresh cookie. TokenEndpoint missing!");
using var refreshResponse = await refreshClient.PostAsync(tokenEndpoint,
new FormUrlEncodedContent(new Dictionary<string, string?>()
{
["grant_type"] = "refresh_token",
["client_id"] = oidcOptions.ClientId,
["client_secret"] = oidcOptions.ClientSecret,
["scope"] = string.Join(" ", oidcOptions.Scope),
["refresh_token"] = validateContext.Properties.GetTokenValue("refresh_token"),
}));
if (!refreshResponse.IsSuccessStatusCode)
{
validateContext.RejectPrincipal();
return;
}
var refreshJson = await refreshResponse.Content.ReadAsStringAsync();
var message = new OpenIdConnectMessage(refreshJson);
var validationParameters = oidcOptions.TokenValidationParameters.Clone();
if (oidcOptions.ConfigurationManager is BaseConfigurationManager baseConfigurationManager)
{
validationParameters.ConfigurationManager = baseConfigurationManager;
}
else
{
validationParameters.ValidIssuer = oidcConfiguration.Issuer;
validationParameters.IssuerSigningKeys = oidcConfiguration.SigningKeys;
}
var validationResult = await oidcOptions.TokenHandler.ValidateTokenAsync(message.IdToken, validationParameters);
if (!validationResult.IsValid)
{
validateContext.RejectPrincipal();
return;
}
oidcTokenValidator.ValidateTokenResponse(new()
{
ProtocolMessage = message,
ClientId = oidcOptions.ClientId,
ValidatedIdToken = JwtSecurityTokenConverter.Convert(validationResult.SecurityToken as JsonWebToken),
});
validateContext.ShouldRenew = true;
validateContext.ReplacePrincipal(new ClaimsPrincipal(validationResult.ClaimsIdentity));
var expiresIn = int.Parse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture);
var expiresAt = now + TimeSpan.FromSeconds(expiresIn);
validateContext.Properties.StoreTokens([
new() { Name = "access_token", Value = message.AccessToken },
new() { Name = "id_token", Value = message.IdToken },
new() { Name = "refresh_token", Value = message.RefreshToken },
new() { Name = "token_type", Value = message.TokenType },
new() { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) },
]);
}
public void Dispose() => refreshClient.Dispose();
}
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
namespace Microsoft.Extensions.DependencyInjection;
internal static partial class CookieOidcServiceCollectionExtensions
{
public static IServiceCollection ConfigureCookieOidcRefresh(this IServiceCollection services, string cookieScheme, string oidcScheme)
{
services.AddSingleton<CookieOidcRefresher>();
services.AddOptions<CookieAuthenticationOptions>(cookieScheme).Configure<CookieOidcRefresher>((cookieOptions, refresher) =>
{
cookieOptions.Events.OnValidatePrincipal = context => refresher.ValidateOrRefreshCookieAsync(context, oidcScheme);
});
services.AddOptions<OpenIdConnectOptions>(oidcScheme).Configure(oidcOptions =>
{
// Request a refresh_token.
oidcOptions.Scope.Add("offline_access");
// Store the refresh_token.
oidcOptions.SaveTokens = true;
});
return services;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment