diff --git a/src/Security/Authentication/OpenIdConnect/src/Events/OpenIdConnectEvents.cs b/src/Security/Authentication/OpenIdConnect/src/Events/OpenIdConnectEvents.cs
index 2a66006bd2dd..5b51c579262b 100644
--- a/src/Security/Authentication/OpenIdConnect/src/Events/OpenIdConnectEvents.cs
+++ b/src/Security/Authentication/OpenIdConnect/src/Events/OpenIdConnectEvents.cs
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.AspNetCore.Authentication.OpenIdConnect.Events;
+
namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;
///
@@ -66,6 +68,16 @@ public class OpenIdConnectEvents : RemoteAuthenticationEvents
///
public Func OnPushAuthorization { get; set; } = context => Task.CompletedTask;
+ ///
+ /// Invoked when the the token needs to be refreshed.
+ ///
+ public Func OnTokenRefreshing { get; set; } = context => Task.CompletedTask;
+
+ ///
+ /// Invoked immedaitely after the ticket has been refreshed.
+ ///
+ public Func OnTokenRefreshed { get; set; } = context => Task.CompletedTask;
+
///
/// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
///
@@ -125,4 +137,8 @@ public class OpenIdConnectEvents : RemoteAuthenticationEvents
///
///
public virtual Task PushAuthorization(PushedAuthorizationContext context) => OnPushAuthorization(context);
+
+ public virtual Task TokenRefreshing(TokenRefreshContext context) => OnTokenRefreshing(context);
+
+ public virtual Task TokenRefreshed(TokenRefreshContext context) => OnTokenRefreshed(context);
}
diff --git a/src/Security/Authentication/OpenIdConnect/src/Events/TokenRefreshContext.cs b/src/Security/Authentication/OpenIdConnect/src/Events/TokenRefreshContext.cs
new file mode 100644
index 000000000000..ed19e63d68b2
--- /dev/null
+++ b/src/Security/Authentication/OpenIdConnect/src/Events/TokenRefreshContext.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Security.Claims;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Authentication.OpenIdConnect.Events;
+
+///
+/// Represents a context for the TokenRefresh and TokenRefreshing events.
+///
+public class TokenRefreshContext : RemoteAuthenticationContext
+{
+ ///
+ /// Gets or sets a value indicating whether the token should be refreshed by the OpenIdConnectHandler or not.
+ ///
+ ///
+ /// The default value of this property is `true`, which indicates,
+ /// that the OpenIdConnectHandler should be responsible for refreshing the token.
+ /// However, custom handler can be registered for the event,
+ /// which may take the responsibility for updating the token. In that case,
+ /// the handler should set the to `false` to indicate that the token has already
+ /// been refreshed and the shouldn't try to refresh it.
+ ///
+ public bool ShouldRefresh { get; set; } = true;
+
+ ///
+ /// Creates a
+ ///
+ ///
+ public TokenRefreshContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal principal, AuthenticationProperties properties)
+ : base(context, scheme, options, properties)
+ => Principal = principal;
+
+ ///
+ /// Called to replace the claims principal. The supplied principal will replace the value of the
+ /// Principal property, which determines the identity of the authenticated request.
+ ///
+ /// The used as the replacement
+ public void ReplacePrincipal(ClaimsPrincipal principal) => Principal = principal;
+}
diff --git a/src/Security/Authentication/OpenIdConnect/src/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj b/src/Security/Authentication/OpenIdConnect/src/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj
index 68b81587a9a0..930cadaf8b1f 100644
--- a/src/Security/Authentication/OpenIdConnect/src/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj
+++ b/src/Security/Authentication/OpenIdConnect/src/Microsoft.AspNetCore.Authentication.OpenIdConnect.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
index 716126163e04..73a0475cf663 100644
--- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
+++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
@@ -11,9 +11,11 @@
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
+using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
@@ -1531,4 +1533,113 @@ private OpenIdConnectProtocolException CreateOpenIdConnectProtocolException(Open
ex.Data["error_uri"] = errorUri;
return ex;
}
+
+ ///
+ /// Handles the `ValidatePrincipal event fired from the underlying CookieAuthenticationHandler.
+ /// This is used for refreshing OIDC auth. token if needed.
+ ///
+ /// The CookieValidatePrincipalContext passed as part of the event.
+ internal static async Task ValidatePrincipal(CookieValidatePrincipalContext context)
+ {
+ var authHandlerProvider = context.HttpContext.RequestServices.GetRequiredService();
+ var handler = await authHandlerProvider.GetHandlerAsync(context.HttpContext, context.Scheme.Name);
+ if (handler is OpenIdConnectHandler oidcHandler)
+ {
+ await oidcHandler.HandleValidatePrincipalAsync(context);
+ }
+ }
+
+ private async Task HandleValidatePrincipalAsync(CookieValidatePrincipalContext validateContext)
+ {
+ var accessTokenExpirationText = validateContext.Properties.GetTokenValue("expires_at");
+ if (!DateTimeOffset.TryParse(accessTokenExpirationText, out var accessTokenExpiration))
+ {
+ return;
+ }
+
+ var oidcOptions = this.OptionsMonitor.Get(validateContext.Scheme.Name);
+ var now = oidcOptions.TimeProvider!.GetUtcNow();
+ if (now + TimeSpan.FromMinutes(5) < accessTokenExpiration)
+ {
+ return;
+ }
+
+ var tokenRefreshContext = new Events.TokenRefreshContext(Context, Scheme, oidcOptions, validateContext.Principal!, validateContext.Properties);
+ await Options.Events.TokenRefreshing(tokenRefreshContext);
+ if (tokenRefreshContext.ShouldRefresh)
+ {
+ 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 oidcOptions.Backchannel.PostAsync(tokenEndpoint,
+ new FormUrlEncodedContent(new Dictionary()
+ {
+ ["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;
+ }
+
+ var validatedIdToken = JwtSecurityTokenConverter.Convert(validationResult.SecurityToken as JsonWebToken);
+ validatedIdToken.Payload["nonce"] = null;
+ Options.ProtocolValidator.ValidateTokenResponse(new()
+ {
+ ProtocolMessage = message,
+ ClientId = oidcOptions.ClientId,
+ ValidatedIdToken = validatedIdToken,
+ });
+
+ var principal = new ClaimsPrincipal(validationResult.ClaimsIdentity);
+ validateContext.ReplacePrincipal(principal);
+ tokenRefreshContext.ReplacePrincipal(principal);
+
+ 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) },
+ ]);
+ }
+ else
+ {
+ // a handler for the `OpenIdConnectOptions.Events.TokenRefreshing` event has updated the principal,
+ // so we need to pass that down through the validateContext.
+ validateContext.ReplacePrincipal(tokenRefreshContext.Principal);
+ }
+
+ validateContext.ShouldRenew = true;
+ await Options.Events.TokenRefreshed(tokenRefreshContext);
+ }
}
diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectPostConfigureOptions.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectPostConfigureOptions.cs
index 4a75bd64a565..1738e4cecb3b 100644
--- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectPostConfigureOptions.cs
+++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectPostConfigureOptions.cs
@@ -3,6 +3,7 @@
using System.Net.Http;
using System.Text;
+using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols;
@@ -16,14 +17,18 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;
public class OpenIdConnectPostConfigureOptions : IPostConfigureOptions
{
private readonly IDataProtectionProvider _dp;
+ private readonly CookieAuthenticationOptions _cookieAuthenticationOptions;
+ private readonly IAuthenticationHandlerProvider _handlerProvider;
///
/// Initializes a new instance of .
///
/// The .
- public OpenIdConnectPostConfigureOptions(IDataProtectionProvider dataProtection)
+ public OpenIdConnectPostConfigureOptions(IDataProtectionProvider dataProtection, CookieAuthenticationOptions cookieAuthenticationOptions, IAuthenticationHandlerProvider handlerProvider)
{
_dp = dataProtection;
+ _cookieAuthenticationOptions = cookieAuthenticationOptions;
+ _handlerProvider = handlerProvider;
}
///
@@ -105,6 +110,8 @@ public void PostConfigure(string? name, OpenIdConnectOptions options)
};
}
}
+
+ _cookieAuthenticationOptions?.Events.OnValidatePrincipal = OpenIdConnectHandler.ValidatePrincipal;
}
private sealed class StringSerializer : IDataSerializer