|
11 | 11 | using System.Text;
|
12 | 12 | using System.Text.Encodings.Web;
|
13 | 13 | using System.Text.Json;
|
| 14 | +using Microsoft.AspNetCore.Authentication.Cookies; |
14 | 15 | using Microsoft.AspNetCore.Authentication.OAuth;
|
15 | 16 | using Microsoft.AspNetCore.Http;
|
16 | 17 | using Microsoft.AspNetCore.WebUtilities;
|
| 18 | +using Microsoft.Extensions.DependencyInjection; |
17 | 19 | using Microsoft.Extensions.Logging;
|
18 | 20 | using Microsoft.Extensions.Options;
|
19 | 21 | using Microsoft.Extensions.Primitives;
|
@@ -1531,4 +1533,113 @@ private OpenIdConnectProtocolException CreateOpenIdConnectProtocolException(Open
|
1531 | 1533 | ex.Data["error_uri"] = errorUri;
|
1532 | 1534 | return ex;
|
1533 | 1535 | }
|
| 1536 | + |
| 1537 | + /// <summary> |
| 1538 | + /// Handles the `ValidatePrincipal event fired from the underlying CookieAuthenticationHandler. |
| 1539 | + /// This is used for refreshing OIDC auth. token if needed. |
| 1540 | + /// </summary> |
| 1541 | + /// <param name="context">The CookieValidatePrincipalContext passed as part of the event.</param> |
| 1542 | + internal static async Task ValidatePrincipal(CookieValidatePrincipalContext context) |
| 1543 | + { |
| 1544 | + var authHandlerProvider = context.HttpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>(); |
| 1545 | + var handler = await authHandlerProvider.GetHandlerAsync(context.HttpContext, context.Scheme.Name); |
| 1546 | + if (handler is OpenIdConnectHandler oidcHandler) |
| 1547 | + { |
| 1548 | + await oidcHandler.HandleValidatePrincipalAsync(context); |
| 1549 | + } |
| 1550 | + } |
| 1551 | + |
| 1552 | + private async Task HandleValidatePrincipalAsync(CookieValidatePrincipalContext validateContext) |
| 1553 | + { |
| 1554 | + var accessTokenExpirationText = validateContext.Properties.GetTokenValue("expires_at"); |
| 1555 | + if (!DateTimeOffset.TryParse(accessTokenExpirationText, out var accessTokenExpiration)) |
| 1556 | + { |
| 1557 | + return; |
| 1558 | + } |
| 1559 | + |
| 1560 | + var oidcOptions = this.OptionsMonitor.Get(validateContext.Scheme.Name); |
| 1561 | + var now = oidcOptions.TimeProvider!.GetUtcNow(); |
| 1562 | + if (now + TimeSpan.FromMinutes(5) < accessTokenExpiration) |
| 1563 | + { |
| 1564 | + return; |
| 1565 | + } |
| 1566 | + |
| 1567 | + var tokenRefreshContext = new Events.TokenRefreshContext(Context, Scheme, oidcOptions, validateContext.Principal!, validateContext.Properties); |
| 1568 | + await Options.Events.TokenRefreshing(tokenRefreshContext); |
| 1569 | + if (tokenRefreshContext.ShouldRefresh) |
| 1570 | + { |
| 1571 | + var oidcConfiguration = await oidcOptions.ConfigurationManager!.GetConfigurationAsync(validateContext.HttpContext.RequestAborted); |
| 1572 | + var tokenEndpoint = oidcConfiguration.TokenEndpoint ?? throw new InvalidOperationException("Cannot refresh cookie. TokenEndpoint missing!"); |
| 1573 | + |
| 1574 | + using var refreshResponse = await oidcOptions.Backchannel.PostAsync(tokenEndpoint, |
| 1575 | + new FormUrlEncodedContent(new Dictionary<string, string?>() |
| 1576 | + { |
| 1577 | + ["grant_type"] = "refresh_token", |
| 1578 | + ["client_id"] = oidcOptions.ClientId, |
| 1579 | + ["client_secret"] = oidcOptions.ClientSecret, |
| 1580 | + ["scope"] = string.Join(" ", oidcOptions.Scope), |
| 1581 | + ["refresh_token"] = validateContext.Properties.GetTokenValue("refresh_token"), |
| 1582 | + })); |
| 1583 | + |
| 1584 | + if (!refreshResponse.IsSuccessStatusCode) |
| 1585 | + { |
| 1586 | + validateContext.RejectPrincipal(); |
| 1587 | + return; |
| 1588 | + } |
| 1589 | + |
| 1590 | + var refreshJson = await refreshResponse.Content.ReadAsStringAsync(); |
| 1591 | + var message = new OpenIdConnectMessage(refreshJson); |
| 1592 | + |
| 1593 | + var validationParameters = oidcOptions.TokenValidationParameters.Clone(); |
| 1594 | + if (oidcOptions.ConfigurationManager is BaseConfigurationManager baseConfigurationManager) |
| 1595 | + { |
| 1596 | + validationParameters.ConfigurationManager = baseConfigurationManager; |
| 1597 | + } |
| 1598 | + else |
| 1599 | + { |
| 1600 | + validationParameters.ValidIssuer = oidcConfiguration.Issuer; |
| 1601 | + validationParameters.IssuerSigningKeys = oidcConfiguration.SigningKeys; |
| 1602 | + } |
| 1603 | + |
| 1604 | + var validationResult = await oidcOptions.TokenHandler.ValidateTokenAsync(message.IdToken, validationParameters); |
| 1605 | + |
| 1606 | + if (!validationResult.IsValid) |
| 1607 | + { |
| 1608 | + validateContext.RejectPrincipal(); |
| 1609 | + return; |
| 1610 | + } |
| 1611 | + |
| 1612 | + var validatedIdToken = JwtSecurityTokenConverter.Convert(validationResult.SecurityToken as JsonWebToken); |
| 1613 | + validatedIdToken.Payload["nonce"] = null; |
| 1614 | + Options.ProtocolValidator.ValidateTokenResponse(new() |
| 1615 | + { |
| 1616 | + ProtocolMessage = message, |
| 1617 | + ClientId = oidcOptions.ClientId, |
| 1618 | + ValidatedIdToken = validatedIdToken, |
| 1619 | + }); |
| 1620 | + |
| 1621 | + var principal = new ClaimsPrincipal(validationResult.ClaimsIdentity); |
| 1622 | + validateContext.ReplacePrincipal(principal); |
| 1623 | + tokenRefreshContext.ReplacePrincipal(principal); |
| 1624 | + |
| 1625 | + var expiresIn = int.Parse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture); |
| 1626 | + var expiresAt = now + TimeSpan.FromSeconds(expiresIn); |
| 1627 | + validateContext.Properties.StoreTokens([ |
| 1628 | + new() { Name = "access_token", Value = message.AccessToken }, |
| 1629 | + new() { Name = "id_token", Value = message.IdToken }, |
| 1630 | + new() { Name = "refresh_token", Value = message.RefreshToken }, |
| 1631 | + new() { Name = "token_type", Value = message.TokenType }, |
| 1632 | + new() { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) }, |
| 1633 | + ]); |
| 1634 | + } |
| 1635 | + else |
| 1636 | + { |
| 1637 | + // a handler for the `OpenIdConnectOptions.Events.TokenRefreshing` event has updated the principal, |
| 1638 | + // so we need to pass that down through the validateContext. |
| 1639 | + validateContext.ReplacePrincipal(tokenRefreshContext.Principal); |
| 1640 | + } |
| 1641 | + |
| 1642 | + validateContext.ShouldRenew = true; |
| 1643 | + await Options.Events.TokenRefreshed(tokenRefreshContext); |
| 1644 | + } |
1534 | 1645 | }
|
0 commit comments