-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Implemented the JWT Token refresh logic through the OpenIdConnectHandler #61861
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
|
@@ -66,6 +68,16 @@ | |
/// </summary> | ||
public Func<PushedAuthorizationContext, Task> OnPushAuthorization { get; set; } = context => Task.CompletedTask; | ||
|
||
/// <summary> | ||
/// Invoked when the the token needs to be refreshed. | ||
/// </summary> | ||
public Func<TokenRefreshContext, Task> OnTokenRefreshing { get; set; } = context => Task.CompletedTask; | ||
Check failure on line 74 in src/Security/Authentication/OpenIdConnect/src/Events/OpenIdConnectEvents.cs
|
||
|
||
/// <summary> | ||
/// Invoked immedaitely after the ticket has been refreshed. | ||
/// </summary> | ||
public Func<TokenRefreshContext, Task> OnTokenRefreshed { get; set; } = context => Task.CompletedTask; | ||
Check failure on line 79 in src/Security/Authentication/OpenIdConnect/src/Events/OpenIdConnectEvents.cs
|
||
|
||
/// <summary> | ||
/// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed. | ||
/// </summary> | ||
|
@@ -125,4 +137,8 @@ | |
/// <param name="context"></param> | ||
/// <returns></returns> | ||
public virtual Task PushAuthorization(PushedAuthorizationContext context) => OnPushAuthorization(context); | ||
|
||
public virtual Task TokenRefreshing(TokenRefreshContext context) => OnTokenRefreshing(context); | ||
|
||
public virtual Task TokenRefreshed(TokenRefreshContext context) => OnTokenRefreshed(context); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
/// <summary> | ||
/// Represents a context for the TokenRefresh and TokenRefreshing events. | ||
/// </summary> | ||
public class TokenRefreshContext : RemoteAuthenticationContext<OpenIdConnectOptions> | ||
Check failure on line 12 in src/Security/Authentication/OpenIdConnect/src/Events/TokenRefreshContext.cs
|
||
{ | ||
/// <summary> | ||
/// Gets or sets a value indicating whether the token should be refreshed by the OpenIdConnectHandler or not. | ||
/// </summary> | ||
/// <remarks> | ||
/// 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 <see cref="OpenIdConnectEvents.OnTokenRefreshing"/> event, | ||
/// which may take the responsibility for updating the token. In that case, | ||
/// the handler should set the <see cref="ShouldRefresh"/> to `false` to indicate that the token has already | ||
/// been refreshed and the <see cref="OpenIdConnectHandler"/> shouldn't try to refresh it. | ||
/// </remarks> | ||
public bool ShouldRefresh { get; set; } = true; | ||
Check failure on line 25 in src/Security/Authentication/OpenIdConnect/src/Events/TokenRefreshContext.cs
|
||
|
||
/// <summary> | ||
/// Creates a <see cref="TokenValidatedContext"/> | ||
/// </summary> | ||
/// <inheritdoc /> | ||
public TokenRefreshContext(HttpContext context, AuthenticationScheme scheme, OpenIdConnectOptions options, ClaimsPrincipal principal, AuthenticationProperties properties) | ||
Check failure on line 31 in src/Security/Authentication/OpenIdConnect/src/Events/TokenRefreshContext.cs
|
||
: base(context, scheme, options, properties) | ||
=> Principal = principal; | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
/// <param name="principal">The <see cref="ClaimsPrincipal"/> used as the replacement</param> | ||
public void ReplacePrincipal(ClaimsPrincipal principal) => Principal = principal; | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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 @@ | |||||||||||
ex.Data["error_uri"] = errorUri; | ||||||||||||
return ex; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Handles the `ValidatePrincipal event fired from the underlying CookieAuthenticationHandler. | ||||||||||||
/// This is used for refreshing OIDC auth. token if needed. | ||||||||||||
/// </summary> | ||||||||||||
/// <param name="context">The CookieValidatePrincipalContext passed as part of the event.</param> | ||||||||||||
internal static async Task ValidatePrincipal(CookieValidatePrincipalContext context) | ||||||||||||
{ | ||||||||||||
var authHandlerProvider = context.HttpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>(); | ||||||||||||
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) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the
|
||||||||||||
{ | ||||||||||||
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, | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the |
||||||||||||
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; | ||||||||||||
Comment on lines
+1600
to
+1601
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the other parameters need to be added like here? aspnetcore/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs Lines 1394 to 1398 in 4141e0e
|
||||||||||||
} | ||||||||||||
|
||||||||||||
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); | ||||||||||||
Check failure on line 1639 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs
|
||||||||||||
} | ||||||||||||
|
||||||||||||
validateContext.ShouldRenew = true; | ||||||||||||
await Options.Events.TokenRefreshed(tokenRefreshContext); | ||||||||||||
} | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 @@ | |
public class OpenIdConnectPostConfigureOptions : IPostConfigureOptions<OpenIdConnectOptions> | ||
{ | ||
private readonly IDataProtectionProvider _dp; | ||
private readonly CookieAuthenticationOptions _cookieAuthenticationOptions; | ||
private readonly IAuthenticationHandlerProvider _handlerProvider; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of <see cref="OpenIdConnectPostConfigureOptions"/>. | ||
/// </summary> | ||
/// <param name="dataProtection">The <see cref="IDataProtectionProvider"/>.</param> | ||
public OpenIdConnectPostConfigureOptions(IDataProtectionProvider dataProtection) | ||
public OpenIdConnectPostConfigureOptions(IDataProtectionProvider dataProtection, CookieAuthenticationOptions cookieAuthenticationOptions, IAuthenticationHandlerProvider handlerProvider) | ||
Check failure on line 27 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectPostConfigureOptions.cs
|
||
{ | ||
_dp = dataProtection; | ||
_cookieAuthenticationOptions = cookieAuthenticationOptions; | ||
_handlerProvider = handlerProvider; | ||
} | ||
|
||
/// <summary> | ||
|
@@ -105,6 +110,8 @@ | |
}; | ||
} | ||
} | ||
|
||
_cookieAuthenticationOptions?.Events.OnValidatePrincipal = OpenIdConnectHandler.ValidatePrincipal; | ||
} | ||
|
||
private sealed class StringSerializer : IDataSerializer<string> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?