diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs index 6cbb1925..eee68c57 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs @@ -365,14 +365,6 @@ namespace OpenIddict.Abstractions /// ValueTask PruneAsync(CancellationToken cancellationToken = default); - /// - /// Revokes an authorization. - /// - /// The authorization to revoke. - /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - ValueTask RevokeAsync([NotNull] object authorization, CancellationToken cancellationToken = default); - /// /// Sets the application identifier associated with an authorization. /// @@ -384,6 +376,14 @@ namespace OpenIddict.Abstractions /// ValueTask SetApplicationIdAsync([NotNull] object authorization, [CanBeNull] string identifier, CancellationToken cancellationToken = default); + /// + /// Tries to revoke an authorization. + /// + /// The authorization to revoke. + /// The that can be used to abort the operation. + /// true if the authorization was successfully revoked, false otherwise. + ValueTask TryRevokeAsync([NotNull] object authorization, CancellationToken cancellationToken = default); + /// /// Updates an existing authorization. /// diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index f9e9868d..5978f846 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -71,17 +71,6 @@ namespace OpenIddict.Abstractions /// ValueTask DeleteAsync([NotNull] object token, CancellationToken cancellationToken = default); - /// - /// Extends the specified token by replacing its expiration date. - /// - /// The token. - /// The date on which the token will no longer be considered valid. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - ValueTask ExtendAsync([NotNull] object token, [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken = default); - /// /// Retrieves the tokens corresponding to the specified /// subject and associated with the application identifier. @@ -394,22 +383,6 @@ namespace OpenIddict.Abstractions /// ValueTask PruneAsync(CancellationToken cancellationToken = default); - /// - /// Redeems a token. - /// - /// The token to redeem. - /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - ValueTask RedeemAsync([NotNull] object token, CancellationToken cancellationToken = default); - - /// - /// Revokes a token. - /// - /// The token to revoke. - /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - ValueTask RevokeAsync([NotNull] object token, CancellationToken cancellationToken = default); - /// /// Sets the application identifier associated with a token. /// @@ -432,6 +405,31 @@ namespace OpenIddict.Abstractions /// ValueTask SetAuthorizationIdAsync([NotNull] object token, [CanBeNull] string identifier, CancellationToken cancellationToken = default); + /// + /// Tries to extend the specified token by replacing its expiration date. + /// + /// The token. + /// The date on which the token will no longer be considered valid. + /// The that can be used to abort the operation. + /// true if the token was successfully extended, false otherwise. + ValueTask TryExtendAsync([NotNull] object token, [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken = default); + + /// + /// Tries to redeem a token. + /// + /// The token to redeem. + /// The that can be used to abort the operation. + /// true if the token was successfully redemeed, false otherwise. + ValueTask TryRedeemAsync([NotNull] object token, CancellationToken cancellationToken = default); + + /// + /// Tries to revoke a token. + /// + /// The token to revoke. + /// The that can be used to abort the operation. + /// true if the token was successfully revoked, false otherwise. + ValueTask TryRevokeAsync([NotNull] object token, CancellationToken cancellationToken = default); + /// /// Updates an existing token. /// diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 36f48b59..9a07bf05 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -95,9 +95,11 @@ namespace OpenIddict.Abstractions public const string CodeChallengeMethod = "oi_cd_chlg_meth"; public const string IdentityTokenLifetime = "oi_idt_lft"; public const string Nonce = "oi_nce"; + public const string Presenters = "oi_prst"; public const string RedirectUri = "oi_reduri"; public const string RefreshTokenLifetime = "oi_reft_lft"; - public const string Resource = "oi_rsrc"; + public const string Resources = "oi_rsrc"; + public const string Scopes = "oi_scp"; public const string TokenId = "oi_tkn_id"; public const string TokenUsage = "oi_tkn_use"; } diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs index fa21789e..321bf763 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -1095,7 +1095,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.AuthorizedParty)); + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.Private.Presenters)); } /// @@ -1110,7 +1110,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.Private.Resource)); + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.Private.Resources)); } /// @@ -1125,16 +1125,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - // Note: scopes are deliberately formatted as a single space-separated - // string to respect the usual representation of the standard scope claim. - // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-02. - var value = principal.GetClaim(Claims.Scope); - if (string.IsNullOrEmpty(value)) - { - return ImmutableHashSet.Create(StringComparer.Ordinal); - } - - return ImmutableHashSet.CreateRange(StringComparer.Ordinal, GetValues(value, Separators.Space)); + return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.Private.Scopes)); } /// @@ -1275,21 +1266,6 @@ namespace OpenIddict.Abstractions return principal.GetClaim(Claims.Private.TokenId); } - /// - /// Gets the public token identifier associated with the claims principal. - /// - /// The claims principal. - /// The unique identifier or null if the claim cannot be found. - public static string GetPublicTokenId([NotNull] this ClaimsPrincipal principal) - { - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - - return principal.GetClaim(Claims.JwtId); - } - /// /// Gets the token usage associated with the claims principal. /// @@ -1417,7 +1393,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - return principal.FindAll(Claims.AuthorizedParty).Any(); + return principal.FindAll(Claims.Private.Presenters).Any(); } /// @@ -1453,7 +1429,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - return principal.FindAll(Claims.Private.Resource).Any(); + return principal.FindAll(Claims.Private.Resources).Any(); } /// @@ -1489,7 +1465,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - return principal.FindAll(Claims.Scope).Any(); + return principal.FindAll(Claims.Private.Scopes).Any(); } /// @@ -1530,7 +1506,8 @@ namespace OpenIddict.Abstractions if (date.HasValue) { - var claim = new Claim(Claims.IssuedAt, date?.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64); + var value = date?.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture); + var claim = new Claim(Claims.IssuedAt, value, ClaimValueTypes.Integer64); ((ClaimsIdentity) principal.Identity).AddClaim(claim); } @@ -1554,7 +1531,8 @@ namespace OpenIddict.Abstractions if (date.HasValue) { - var claim = new Claim(Claims.ExpiresAt, date?.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture), ClaimValueTypes.Integer64); + var value = date?.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture); + var claim = new Claim(Claims.ExpiresAt, value, ClaimValueTypes.Integer64); ((ClaimsIdentity) principal.Identity).AddClaim(claim); } @@ -1609,7 +1587,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - return principal.SetClaims(Claims.AuthorizedParty, presenters.Distinct(StringComparer.Ordinal)); + return principal.SetClaims(Claims.Private.Presenters, presenters.Distinct(StringComparer.Ordinal)); } /// @@ -1641,7 +1619,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - return principal.SetClaims(Claims.Private.Resource, resources.Distinct(StringComparer.Ordinal)); + return principal.SetClaims(Claims.Private.Resources, resources.Distinct(StringComparer.Ordinal)); } /// @@ -1672,15 +1650,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - if (scopes == null) - { - return principal.RemoveClaims(Claims.Scope); - } - - // Note: scopes are deliberately formatted as a single space-separated - // string to respect the usual representation of the standard scope claim. - // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-02. - return principal.SetClaim(Claims.Scope, string.Join(" ", scopes.Distinct(StringComparer.Ordinal))); + return principal.SetClaims(Claims.Private.Scopes, scopes.Distinct(StringComparer.Ordinal)); } /// @@ -1792,22 +1762,6 @@ namespace OpenIddict.Abstractions return principal.SetClaim(Claims.Private.TokenId, identifier); } - /// - /// Sets the public token identifier associated with the claims principal. - /// - /// The claims principal. - /// The unique identifier to store. - /// The claims principal. - public static ClaimsPrincipal SetPublicTokenId([NotNull] this ClaimsPrincipal principal, string identifier) - { - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - - return principal.SetClaim(Claims.JwtId, identifier); - } - private static IEnumerable GetValues(string source, char[] separators) { Debug.Assert(!string.IsNullOrEmpty(source), "The source string shouldn't be null or empty."); diff --git a/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj b/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj index d84c04d7..762352df 100644 --- a/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj +++ b/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj @@ -14,6 +14,7 @@ + diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 073d639e..1ce35c0b 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -18,6 +18,7 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictExceptions; namespace OpenIddict.Core { @@ -912,45 +913,72 @@ namespace OpenIddict.Core => Store.PruneAsync(cancellationToken); /// - /// Revokes an authorization. + /// Sets the application identifier associated with an authorization. /// - /// The authorization to revoke. + /// The authorization. + /// The unique identifier associated with the client application. /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - public virtual async ValueTask RevokeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async ValueTask SetApplicationIdAsync( + [NotNull] TAuthorization authorization, [CanBeNull] string identifier, CancellationToken cancellationToken = default) { if (authorization == null) { throw new ArgumentNullException(nameof(authorization)); } - var status = await Store.GetStatusAsync(authorization, cancellationToken); - if (!string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase)) - { - await Store.SetStatusAsync(authorization, OpenIddictConstants.Statuses.Revoked, cancellationToken); - await UpdateAsync(authorization, cancellationToken); - } + await Store.SetApplicationIdAsync(authorization, identifier, cancellationToken); + await UpdateAsync(authorization, cancellationToken); } /// - /// Sets the application identifier associated with an authorization. + /// Tries to revoke an authorization. /// - /// The authorization. - /// The unique identifier associated with the client application. + /// The authorization to revoke. /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual async ValueTask SetApplicationIdAsync( - [NotNull] TAuthorization authorization, [CanBeNull] string identifier, CancellationToken cancellationToken = default) + /// true if the authorization was successfully revoked, false otherwise. + public virtual async ValueTask TryRevokeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) { if (authorization == null) { throw new ArgumentNullException(nameof(authorization)); } - await Store.SetApplicationIdAsync(authorization, identifier, cancellationToken); - await UpdateAsync(authorization, cancellationToken); + var status = await Store.GetStatusAsync(authorization, cancellationToken); + if (string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + await Store.SetStatusAsync(authorization, OpenIddictConstants.Statuses.Revoked, cancellationToken); + + try + { + await UpdateAsync(authorization, cancellationToken); + + Logger.LogInformation("The authorization '{Identifier}' was successfully revoked.", + await Store.GetIdAsync(authorization, cancellationToken)); + + return true; + } + + catch (ConcurrencyException exception) + { + Logger.LogDebug(exception, "A concurrency exception occurred while trying to revoke the authorization '{Identifier}'.", + await Store.GetIdAsync(authorization, cancellationToken)); + + return false; + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An exception occurred while trying to revoke the authorization '{Identifier}'.", + await Store.GetIdAsync(authorization, cancellationToken)); + + return false; + } } /// @@ -1169,12 +1197,12 @@ namespace OpenIddict.Core ValueTask IOpenIddictAuthorizationManager.PruneAsync(CancellationToken cancellationToken) => PruneAsync(cancellationToken); - ValueTask IOpenIddictAuthorizationManager.RevokeAsync(object authorization, CancellationToken cancellationToken) - => RevokeAsync((TAuthorization) authorization, cancellationToken); - ValueTask IOpenIddictAuthorizationManager.SetApplicationIdAsync(object authorization, string identifier, CancellationToken cancellationToken) => SetApplicationIdAsync((TAuthorization) authorization, identifier, cancellationToken); + ValueTask IOpenIddictAuthorizationManager.TryRevokeAsync(object authorization, CancellationToken cancellationToken) + => TryRevokeAsync((TAuthorization) authorization, cancellationToken); + ValueTask IOpenIddictAuthorizationManager.UpdateAsync(object authorization, CancellationToken cancellationToken) => UpdateAsync((TAuthorization) authorization, cancellationToken); diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 02547677..74d5b331 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -18,6 +18,7 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictExceptions; namespace OpenIddict.Core { @@ -194,27 +195,6 @@ namespace OpenIddict.Core await Store.DeleteAsync(token, cancellationToken); } - /// - /// Extends the specified token by replacing its expiration date. - /// - /// The token. - /// The date on which the token will no longer be considered valid. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual async ValueTask ExtendAsync([NotNull] TToken token, - [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken = default) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - await Store.SetExpirationDateAsync(token, date, cancellationToken); - await UpdateAsync(token, cancellationToken); - } - /// /// Retrieves the tokens corresponding to the specified /// subject and associated with the application identifier. @@ -949,87 +929,201 @@ namespace OpenIddict.Core => Store.PruneAsync(cancellationToken); /// - /// Redeems a token. + /// Sets the application identifier associated with a token. /// - /// The token to redeem. + /// The token. + /// The unique identifier associated with the client application. /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - public virtual async ValueTask RedeemAsync([NotNull] TToken token, CancellationToken cancellationToken = default) + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async ValueTask SetApplicationIdAsync([NotNull] TToken token, + [CanBeNull] string identifier, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } - var status = await Store.GetStatusAsync(token, cancellationToken); - if (!string.Equals(status, OpenIddictConstants.Statuses.Redeemed, StringComparison.OrdinalIgnoreCase)) + await Store.SetApplicationIdAsync(token, identifier, cancellationToken); + await UpdateAsync(token, cancellationToken); + } + + /// + /// Sets the authorization identifier associated with a token. + /// + /// The token. + /// The unique identifier associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async ValueTask SetAuthorizationIdAsync([NotNull] TToken token, + [CanBeNull] string identifier, CancellationToken cancellationToken = default) + { + if (token == null) { - await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Redeemed, cancellationToken); - await UpdateAsync(token, cancellationToken); + throw new ArgumentNullException(nameof(token)); } + + await Store.SetAuthorizationIdAsync(token, identifier, cancellationToken); + await UpdateAsync(token, cancellationToken); } /// - /// Revokes a token. + /// Tries to extend the specified token by replacing its expiration date. /// - /// The token to revoke. + /// The token. + /// The date on which the token will no longer be considered valid. /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - public virtual async ValueTask RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken = default) + /// true if the token was successfully extended, false otherwise. + public virtual async ValueTask TryExtendAsync([NotNull] TToken token, + [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } - var status = await Store.GetStatusAsync(token, cancellationToken); - if (!string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase)) + if (date == await Store.GetExpirationDateAsync(token, cancellationToken)) + { + return true; + } + + await Store.SetExpirationDateAsync(token, date, cancellationToken); + + try { - await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Revoked, cancellationToken); await UpdateAsync(token, cancellationToken); + + if (date != null) + { + Logger.LogInformation("The expiration date of the refresh token '{Identifier}' was successfully updated: {Date}.", + await Store.GetIdAsync(token, cancellationToken), date); + } + + else + { + Logger.LogInformation("The expiration date of the refresh token '{Identifier}' was successfully removed.", + await Store.GetIdAsync(token, cancellationToken)); + } + + return true; + } + + catch (ConcurrencyException exception) + { + Logger.LogDebug(exception, "A concurrency exception occurred while trying to update the " + + "expiration date of the token '{Identifier}'.", + await Store.GetIdAsync(token, cancellationToken)); + + return false; + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An exception occurred while trying to update the " + + "expiration date of the token '{Identifier}'.", + await Store.GetIdAsync(token, cancellationToken)); + + return false; } } /// - /// Sets the application identifier associated with a token. + /// Tries to redeem a token. /// - /// The token. - /// The unique identifier associated with the client application. + /// The token to redeem. /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual async ValueTask SetApplicationIdAsync([NotNull] TToken token, - [CanBeNull] string identifier, CancellationToken cancellationToken = default) + /// true if the token was successfully redemeed, false otherwise. + public virtual async ValueTask TryRedeemAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } - await Store.SetApplicationIdAsync(token, identifier, cancellationToken); - await UpdateAsync(token, cancellationToken); + var status = await Store.GetStatusAsync(token, cancellationToken); + if (string.Equals(status, OpenIddictConstants.Statuses.Redeemed, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Redeemed, cancellationToken); + + try + { + await UpdateAsync(token, cancellationToken); + + Logger.LogInformation("The token '{Identifier}' was successfully marked as redeemed.", + await Store.GetIdAsync(token, cancellationToken)); + + return true; + } + + catch (ConcurrencyException exception) + { + Logger.LogDebug(exception, "A concurrency exception occurred while trying to redeem the token '{Identifier}'.", + await Store.GetIdAsync(token, cancellationToken)); + + return false; + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An exception occurred while trying to redeem the token '{Identifier}'.", + await Store.GetIdAsync(token, cancellationToken)); + + return false; + } } /// - /// Sets the authorization identifier associated with a token. + /// Tries to revoke a token. /// - /// The token. - /// The unique identifier associated with the authorization. + /// The token to revoke. /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual async ValueTask SetAuthorizationIdAsync([NotNull] TToken token, - [CanBeNull] string identifier, CancellationToken cancellationToken = default) + /// true if the token was successfully revoked, false otherwise. + public virtual async ValueTask TryRevokeAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } - await Store.SetAuthorizationIdAsync(token, identifier, cancellationToken); - await UpdateAsync(token, cancellationToken); + var status = await Store.GetStatusAsync(token, cancellationToken); + if (string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Revoked, cancellationToken); + + try + { + await UpdateAsync(token, cancellationToken); + + Logger.LogInformation("The token '{Identifier}' was successfully revoked.", + await Store.GetIdAsync(token, cancellationToken)); + + return true; + } + + catch (ConcurrencyException exception) + { + Logger.LogDebug(exception, "A concurrency exception occurred while trying to revoke the token '{Identifier}'.", + await Store.GetIdAsync(token, cancellationToken)); + + return false; + } + + catch (Exception exception) + { + Logger.LogWarning(exception, "An exception occurred while trying to revoke the token '{Identifier}'.", + await Store.GetIdAsync(token, cancellationToken)); + + return false; + } } /// @@ -1201,9 +1295,6 @@ namespace OpenIddict.Core ValueTask IOpenIddictTokenManager.DeleteAsync(object token, CancellationToken cancellationToken) => DeleteAsync((TToken) token, cancellationToken); - ValueTask IOpenIddictTokenManager.ExtendAsync(object token, DateTimeOffset? date, CancellationToken cancellationToken) - => ExtendAsync((TToken) token, date, cancellationToken); - IAsyncEnumerable IOpenIddictTokenManager.FindAsync(string subject, string client, CancellationToken cancellationToken) => FindAsync(subject, client, cancellationToken).OfType(); @@ -1291,18 +1382,21 @@ namespace OpenIddict.Core ValueTask IOpenIddictTokenManager.PruneAsync(CancellationToken cancellationToken) => PruneAsync(cancellationToken); - ValueTask IOpenIddictTokenManager.RedeemAsync(object token, CancellationToken cancellationToken) - => RedeemAsync((TToken) token, cancellationToken); - - ValueTask IOpenIddictTokenManager.RevokeAsync(object token, CancellationToken cancellationToken) - => RevokeAsync((TToken) token, cancellationToken); - ValueTask IOpenIddictTokenManager.SetApplicationIdAsync(object token, string identifier, CancellationToken cancellationToken) => SetApplicationIdAsync((TToken) token, identifier, cancellationToken); ValueTask IOpenIddictTokenManager.SetAuthorizationIdAsync(object token, string identifier, CancellationToken cancellationToken) => SetAuthorizationIdAsync((TToken) token, identifier, cancellationToken); + ValueTask IOpenIddictTokenManager.TryExtendAsync(object token, DateTimeOffset? date, CancellationToken cancellationToken) + => TryExtendAsync((TToken) token, date, cancellationToken); + + ValueTask IOpenIddictTokenManager.TryRedeemAsync(object token, CancellationToken cancellationToken) + => TryRedeemAsync((TToken) token, cancellationToken); + + ValueTask IOpenIddictTokenManager.TryRevokeAsync(object token, CancellationToken cancellationToken) + => TryRevokeAsync((TToken) token, cancellationToken); + ValueTask IOpenIddictTokenManager.UpdateAsync(object token, CancellationToken cancellationToken) => UpdateAsync((TToken) token, cancellationToken); diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs index 5ce77d1d..69588892 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs @@ -106,65 +106,22 @@ namespace OpenIddict.Server.AspNetCore return false; } - protected override async Task HandleAuthenticateAsync() + protected override Task HandleAuthenticateAsync() { var transaction = Context.Features.Get()?.Transaction; - if (transaction?.Request == null) + if (transaction == null) { throw new InvalidOperationException("An identity cannot be extracted from this request."); } - var context = new ProcessAuthenticationContext(transaction); - await _provider.DispatchAsync(context); - - if (context.Principal == null || context.IsRequestHandled || context.IsRequestSkipped) - { - return AuthenticateResult.NoResult(); - } - - else if (context.IsRejected) + if (transaction.Properties.TryGetValue(OpenIddictServerConstants.Properties.AmbientPrincipal, out var principal)) { - var builder = new StringBuilder(); - - if (!string.IsNullOrEmpty(context.Error)) - { - builder.AppendLine("An error occurred while authenticating the current request:"); - builder.AppendFormat("Error code: ", context.Error); - - if (!string.IsNullOrEmpty(context.ErrorDescription)) - { - builder.AppendLine(); - builder.AppendFormat("Error description: ", context.ErrorDescription); - } - - if (!string.IsNullOrEmpty(context.ErrorUri)) - { - builder.AppendLine(); - builder.AppendFormat("Error URI: ", context.ErrorUri); - } - } - - else - { - builder.Append("An unknown error occurred while authenticating the current request."); - } - - return AuthenticateResult.Fail(new Exception(builder.ToString()) - { - // Note: the error details are stored as additional exception properties, - // which is similar to what other ASP.NET Core security handlers do. - Data = - { - [Parameters.Error] = context.Error, - [Parameters.ErrorDescription] = context.ErrorDescription, - [Parameters.ErrorUri] = context.ErrorUri - } - }); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket( + (ClaimsPrincipal) principal, + OpenIddictServerAspNetCoreDefaults.AuthenticationScheme))); } - return AuthenticateResult.Success(new AuthenticationTicket( - context.Principal, - OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)); + return Task.FromResult(AuthenticateResult.NoResult()); } protected override async Task HandleChallengeAsync([CanBeNull] AuthenticationProperties properties) diff --git a/src/OpenIddict.Server.DataProtection/IOpenIddictServerDataProtectionFormatter.cs b/src/OpenIddict.Server.DataProtection/IOpenIddictServerDataProtectionFormatter.cs new file mode 100644 index 00000000..cc665e37 --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/IOpenIddictServerDataProtectionFormatter.cs @@ -0,0 +1,18 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.IO; +using System.Security.Claims; +using JetBrains.Annotations; + +namespace OpenIddict.Server.DataProtection +{ + public interface IOpenIddictServerDataProtectionFormatter + { + ClaimsPrincipal ReadToken([NotNull] BinaryReader reader); + void WriteToken([NotNull] BinaryWriter writer, [NotNull] ClaimsPrincipal principal); + } +} \ No newline at end of file diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs index 3948e277..c402bec9 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs @@ -65,6 +65,21 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.DataProtectionProvider = provider); } + /// + /// Configures OpenIddict to use a specific formatter instead of relying on the default instance. + /// + /// The formatter used to read and write tokens. + /// The . + public OpenIddictServerDataProtectionBuilder UseFormatter([NotNull] IOpenIddictServerDataProtectionFormatter formatter) + { + if (formatter == null) + { + throw new ArgumentNullException(nameof(formatter)); + } + + return Configure(options => options.Formatter = formatter); + } + /// /// Configures OpenIddict to use the Data Protection format when /// issuing new access tokens, refresh tokens and authorization codes. diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs new file mode 100644 index 00000000..fc149965 --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs @@ -0,0 +1,379 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Claims; +using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using Properties = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Properties; + +namespace OpenIddict.Server.DataProtection +{ + public class OpenIddictServerDataProtectionFormatter : IOpenIddictServerDataProtectionFormatter + { + public ClaimsPrincipal ReadToken(BinaryReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + var (principal, properties) = Read(reader, version: 5); + if (principal == null) + { + return null; + } + + // Tokens serialized using the ASP.NET Core Data Protection stack are compound + // of both claims and special authentication properties. To ensure existing tokens + // can be reused, well-known properties are manually mapped to their claims equivalents. + + return principal + .SetAudiences(GetArrayProperty(properties, Properties.Audiences)) + .SetCreationDate(GetDateProperty(properties, Properties.Issued)) + .SetExpirationDate(GetDateProperty(properties, Properties.Expires)) + .SetPresenters(GetArrayProperty(properties, Properties.Presenters)) + .SetScopes(GetArrayProperty(properties, Properties.Scopes)) + + .SetClaim(Claims.Private.AccessTokenLifetime, GetProperty(properties, Properties.AccessTokenLifetime)) + .SetClaim(Claims.Private.AuthorizationCodeLifetime, GetProperty(properties, Properties.AuthorizationCodeLifetime)) + .SetClaim(Claims.Private.AuthorizationId, GetProperty(properties, Properties.InternalAuthorizationId)) + .SetClaim(Claims.Private.CodeChallenge, GetProperty(properties, Properties.CodeChallenge)) + .SetClaim(Claims.Private.CodeChallengeMethod, GetProperty(properties, Properties.CodeChallengeMethod)) + .SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) + .SetClaim(Claims.Private.Nonce, GetProperty(properties, Properties.Nonce)) + .SetClaim(Claims.Private.RedirectUri, GetProperty(properties, Properties.OriginalRedirectUri)) + .SetClaim(Claims.Private.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)) + .SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)); + + static (ClaimsPrincipal principal, ImmutableDictionary properties) Read(BinaryReader reader, int version) + { + if (version != reader.ReadInt32()) + { + return (null, ImmutableDictionary.Create()); + } + + // Read the authentication scheme associated to the ticket. + _ = reader.ReadString(); + + // Read the number of identities stored in the serialized payload. + var count = reader.ReadInt32(); + if (count < 0) + { + return (null, ImmutableDictionary.Create()); + } + + var identities = new ClaimsIdentity[count]; + for (var index = 0; index != count; ++index) + { + identities[index] = ReadIdentity(reader); + } + + var properties = ReadProperties(reader, version); + + return (new ClaimsPrincipal(identities), properties); + } + + static ClaimsIdentity ReadIdentity(BinaryReader reader) + { + var identity = new ClaimsIdentity( + authenticationType: reader.ReadString(), + nameType: ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType), + roleType: ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType)); + + // Read the number of claims contained in the serialized identity. + var count = reader.ReadInt32(); + + for (int index = 0; index != count; ++index) + { + var claim = ReadClaim(reader, identity); + + identity.AddClaim(claim); + } + + // Determine whether the identity has a bootstrap context attached. + if (reader.ReadBoolean()) + { + identity.BootstrapContext = reader.ReadString(); + } + + // Determine whether the identity has an actor identity attached. + if (reader.ReadBoolean()) + { + identity.Actor = ReadIdentity(reader); + } + + return identity; + } + + static Claim ReadClaim(BinaryReader reader, ClaimsIdentity identity) + { + var type = ReadWithDefault(reader, identity.NameClaimType); + var value = reader.ReadString(); + var valueType = ReadWithDefault(reader, ClaimValueTypes.String); + var issuer = ReadWithDefault(reader, ClaimsIdentity.DefaultIssuer); + var originalIssuer = ReadWithDefault(reader, issuer); + + var claim = new Claim(type, value, valueType, issuer, originalIssuer, identity); + + // Read the number of properties stored in the claim. + var count = reader.ReadInt32(); + + for (var index = 0; index != count; ++index) + { + var key = reader.ReadString(); + var propertyValue = reader.ReadString(); + + claim.Properties.Add(key, propertyValue); + } + + return claim; + } + + static ImmutableDictionary ReadProperties(BinaryReader reader, int version) + { + if (version != reader.ReadInt32()) + { + return ImmutableDictionary.Create(); + } + + var properties = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal); + var count = reader.ReadInt32(); + for (var index = 0; index != count; ++index) + { + properties.Add(reader.ReadString(), reader.ReadString()); + } + + return properties.ToImmutable(); + } + + static string ReadWithDefault(BinaryReader reader, string defaultValue) + { + var value = reader.ReadString(); + + if (string.Equals(value, "\0", StringComparison.Ordinal)) + { + return defaultValue; + } + + return value; + } + + static string GetProperty(IReadOnlyDictionary properties, string name) + => properties.TryGetValue(name, out var value) ? value : null; + + static IEnumerable GetArrayProperty(IReadOnlyDictionary properties, string name) + => properties.TryGetValue(name, out var value) ? JArray.Parse(value).Values() : Enumerable.Empty(); + + static DateTimeOffset? GetDateProperty(IReadOnlyDictionary properties, string name) + => properties.TryGetValue(name, out var value) ? (DateTimeOffset?) + DateTimeOffset.ParseExact(value, "r", CultureInfo.InvariantCulture) : null; + } + + public void WriteToken([NotNull] BinaryWriter writer, [NotNull] ClaimsPrincipal principal) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + var properties = new Dictionary(); + + // Unlike ASP.NET Core Data Protection-based tokens, tokens serialized using the new format + // can't include authentication properties. To ensure tokens can be used with previous versions + // of OpenIddict are issued, well-known claims are manually mapped to their properties equivalents. + + SetProperty(properties, Properties.Issued, principal.GetCreationDate()?.ToString("r", CultureInfo.InvariantCulture)); + SetProperty(properties, Properties.Expires, principal.GetExpirationDate()?.ToString("r", CultureInfo.InvariantCulture)); + + SetProperty(properties, Properties.AccessTokenLifetime, principal.GetClaim(Claims.Private.AccessTokenLifetime)); + SetProperty(properties, Properties.AuthorizationCodeLifetime, principal.GetClaim(Claims.Private.AuthorizationCodeLifetime)); + SetProperty(properties, Properties.IdentityTokenLifetime, principal.GetClaim(Claims.Private.IdentityTokenLifetime)); + SetProperty(properties, Properties.RefreshTokenLifetime, principal.GetClaim(Claims.Private.RefreshTokenLifetime)); + + SetProperty(properties, Properties.CodeChallenge, principal.GetClaim(Claims.Private.CodeChallenge)); + SetProperty(properties, Properties.CodeChallengeMethod, principal.GetClaim(Claims.Private.CodeChallengeMethod)); + + SetProperty(properties, Properties.InternalAuthorizationId, principal.GetInternalAuthorizationId()); + SetProperty(properties, Properties.InternalTokenId, principal.GetInternalTokenId()); + + SetProperty(properties, Properties.Nonce, principal.GetClaim(Claims.Private.Nonce)); + SetProperty(properties, Properties.OriginalRedirectUri, principal.GetClaim(Claims.Private.RedirectUri)); + + SetArrayProperty(properties, Properties.Audiences, principal.GetAudiences()); + SetArrayProperty(properties, Properties.Presenters, principal.GetPresenters()); + SetArrayProperty(properties, Properties.Scopes, principal.GetScopes()); + + // Copy the principal and exclude the claim that were mapped to authentication properties. + principal = principal.Clone(claim => claim.Type switch + { + Claims.Audience => false, + Claims.ExpiresAt => false, + Claims.IssuedAt => false, + + Claims.Private.AccessTokenLifetime => false, + Claims.Private.AuthorizationCodeLifetime => false, + Claims.Private.AuthorizationId => false, + Claims.Private.CodeChallenge => false, + Claims.Private.CodeChallengeMethod => false, + Claims.Private.IdentityTokenLifetime => false, + Claims.Private.Nonce => false, + Claims.Private.Presenters => false, + Claims.Private.RedirectUri => false, + Claims.Private.RefreshTokenLifetime => false, + Claims.Private.Scopes => false, + Claims.Private.TokenId => false, + + _ => true + }); + + Write(writer, version: 5, principal.Identity.AuthenticationType, principal, properties); + writer.Flush(); + + // Note: the following local methods closely matches the logic used by ASP.NET Core's + // authentication stack and MUST NOT be modified to ensure tokens encrypted using + // the OpenID Connect server middleware can be read by OpenIddict (and vice-versa). + + static void Write(BinaryWriter writer, int version, string scheme, + ClaimsPrincipal principal, IReadOnlyDictionary properties) + { + writer.Write(version); + writer.Write(scheme); + + // Write the number of identities contained in the principal. + writer.Write(principal.Identities.Count()); + + foreach (var identity in principal.Identities) + { + WriteIdentity(writer, identity); + } + + WriteProperties(writer, version, properties); + } + + static void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity) + { + writer.Write(identity.AuthenticationType ?? string.Empty); + WriteWithDefault(writer, identity.NameClaimType, ClaimsIdentity.DefaultNameClaimType); + WriteWithDefault(writer, identity.RoleClaimType, ClaimsIdentity.DefaultRoleClaimType); + + // Write the number of claims contained in the identity. + writer.Write(identity.Claims.Count()); + + foreach (var claim in identity.Claims) + { + WriteClaim(writer, claim); + } + + var bootstrap = identity.BootstrapContext as string; + if (!string.IsNullOrEmpty(bootstrap)) + { + writer.Write(true); + writer.Write(bootstrap); + } + + else + { + writer.Write(false); + } + + if (identity.Actor != null) + { + writer.Write(true); + WriteIdentity(writer, identity.Actor); + } + + else + { + writer.Write(false); + } + } + + static void WriteClaim(BinaryWriter writer, Claim claim) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (claim == null) + { + throw new ArgumentNullException(nameof(claim)); + } + + WriteWithDefault(writer, claim.Type, claim.Subject?.NameClaimType ?? ClaimsIdentity.DefaultNameClaimType); + writer.Write(claim.Value); + WriteWithDefault(writer, claim.ValueType, ClaimValueTypes.String); + WriteWithDefault(writer, claim.Issuer, ClaimsIdentity.DefaultIssuer); + WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer); + + // Write the number of properties contained in the claim. + writer.Write(claim.Properties.Count); + + foreach (var property in claim.Properties) + { + writer.Write(property.Key ?? string.Empty); + writer.Write(property.Value ?? string.Empty); + } + } + + static void WriteProperties(BinaryWriter writer, int version, IReadOnlyDictionary properties) + { + writer.Write(version); + writer.Write(properties.Count); + + foreach (var property in properties) + { + writer.Write(property.Key ?? string.Empty); + writer.Write(property.Value ?? string.Empty); + } + } + + static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) + => writer.Write(string.Equals(value, defaultValue, StringComparison.Ordinal) ? "\0" : value); + + static void SetProperty(IDictionary properties, string name, string value) + { + if (string.IsNullOrEmpty(value)) + { + properties.Remove(name); + } + + else + { + properties[name] = value; + } + } + + static void SetArrayProperty(IDictionary properties, string name, IEnumerable values) + { + var array = new JArray(values); + if (array.Count == 0) + { + properties.Remove(name); + } + + else + { + properties[name] = array.ToString(Formatting.None); + } + } + } + } +} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs deleted file mode 100644 index 05866246..00000000 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs +++ /dev/null @@ -1,836 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; -using static OpenIddict.Abstractions.OpenIddictConstants; -using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; -using static OpenIddict.Server.OpenIddictServerEvents; -using static OpenIddict.Server.OpenIddictServerHandlers.Serialization; -using Properties = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Properties; - -namespace OpenIddict.Server.DataProtection -{ - public static partial class OpenIddictServerDataProtectionHandlers - { - public static class Serialization - { - public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( - /* - * Access token serialization: - */ - AttachAccessTokenSerializationProtector.Descriptor, - SerializeDataProtectionToken.Descriptor, - - /* - * Authorization code serialization: - */ - AttachAuthorizationCodeSerializationProtector.Descriptor, - SerializeDataProtectionToken.Descriptor, - - /* - * Refresh token serialization: - */ - AttachRefreshTokenSerializationProtector.Descriptor, - SerializeDataProtectionToken.Descriptor, - - /* - * Access token deserialization: - */ - AttachAccessTokenDeserializationProtector.Descriptor, - DeserializeDataProtectionToken.Descriptor, - - /* - * Authorization code deserialization: - */ - AttachAuthorizationCodeDeserializationProtector.Descriptor, - DeserializeDataProtectionToken.Descriptor, - - /* - * Refresh token deserialization: - */ - AttachRefreshTokenDeserializationProtector.Descriptor, - DeserializeDataProtectionToken.Descriptor); - - /// - /// Contains the logic responsible of generating a Data Protection token. - /// - public class SerializeDataProtectionToken : IOpenIddictServerHandler where TContext : BaseSerializingContext - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler>() - .SetOrder(SerializeJwtBearerToken.Descriptor.Order - 5000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] TContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!context.Properties.TryGetValue(Properties.DataProtector, out var property) || - !(property is IDataProtector protector)) - { - throw new InvalidOperationException(new StringBuilder() - .Append("No suitable data protector was found for the specified token type.") - .Append("This may indicate that the OpenIddict Data Protection services were not correctly registered.") - .ToString()); - } - - var properties = new Dictionary(); - - // Unlike ASP.NET Core Data Protection-based tokens, tokens serialized using the new format - // can't include authentication properties. To ensure tokens can be used with previous versions - // of OpenIddict are issued, well-known claims are manually mapped to their properties equivalents. - - SetProperty(properties, Properties.AccessTokenLifetime, - context.Principal.GetClaim(Claims.Private.AccessTokenLifetime)); - - SetProperty(properties, Properties.AuthorizationCodeLifetime, - context.Principal.GetClaim(Claims.Private.AuthorizationCodeLifetime)); - - SetProperty(properties, Properties.CodeChallenge, - context.Principal.GetClaim(Claims.Private.CodeChallenge)); - - SetProperty(properties, Properties.CodeChallengeMethod, - context.Principal.GetClaim(Claims.Private.CodeChallengeMethod)); - - SetProperty(properties, Properties.Expires, - context.Principal.GetExpirationDate()?.ToString("r", CultureInfo.InvariantCulture)); - - SetProperty(properties, Properties.IdentityTokenLifetime, - context.Principal.GetClaim(Claims.Private.IdentityTokenLifetime)); - - SetProperty(properties, Properties.InternalAuthorizationId, context.Principal.GetInternalAuthorizationId()); - SetProperty(properties, Properties.InternalTokenId, context.Principal.GetInternalTokenId()); - - SetProperty(properties, Properties.Issued, - context.Principal.GetCreationDate()?.ToString("r", CultureInfo.InvariantCulture)); - - SetProperty(properties, Properties.OriginalRedirectUri, - context.Principal.GetClaim(Claims.Private.RedirectUri)); - - SetProperty(properties, Properties.RefreshTokenLifetime, - context.Principal.GetClaim(Claims.Private.RefreshTokenLifetime)); - - SetArrayProperty(properties, Properties.Audiences, context.Principal.GetAudiences()); - SetArrayProperty(properties, Properties.Presenters, context.Principal.GetPresenters()); - SetArrayProperty(properties, Properties.Scopes, context.Principal.GetScopes()); - - using var buffer = new MemoryStream(); - using var writer = new BinaryWriter(buffer); - - Write(writer, version: 5, context.Principal.Identity.AuthenticationType, context.Principal, properties); - writer.Flush(); - - context.Token = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); - context.HandleSerialization(); - - return default; - - // Note: the following local methods closely matches the logic used by ASP.NET Core's - // authentication stack and MUST NOT be modified to ensure tokens encrypted using - // the OpenID Connect server middleware can be read by OpenIddict (and vice-versa). - - static void Write(BinaryWriter writer, int version, string scheme, - ClaimsPrincipal principal, IReadOnlyDictionary properties) - { - writer.Write(version); - writer.Write(scheme); - - // Write the number of identities contained in the principal. - writer.Write(principal.Identities.Count()); - - foreach (var identity in principal.Identities) - { - WriteIdentity(writer, identity); - } - - WriteProperties(writer, version, properties); - } - - static void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity) - { - writer.Write(identity.AuthenticationType ?? string.Empty); - WriteWithDefault(writer, identity.NameClaimType, ClaimsIdentity.DefaultNameClaimType); - WriteWithDefault(writer, identity.RoleClaimType, ClaimsIdentity.DefaultRoleClaimType); - - // Write the number of claims contained in the identity. - writer.Write(identity.Claims.Count()); - - foreach (var claim in identity.Claims) - { - WriteClaim(writer, claim); - } - - var bootstrap = identity.BootstrapContext as string; - if (!string.IsNullOrEmpty(bootstrap)) - { - writer.Write(true); - writer.Write(bootstrap); - } - - else - { - writer.Write(false); - } - - if (identity.Actor != null) - { - writer.Write(true); - WriteIdentity(writer, identity.Actor); - } - - else - { - writer.Write(false); - } - } - - static void WriteClaim(BinaryWriter writer, Claim claim) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (claim == null) - { - throw new ArgumentNullException(nameof(claim)); - } - - WriteWithDefault(writer, claim.Type, claim.Subject?.NameClaimType ?? ClaimsIdentity.DefaultNameClaimType); - writer.Write(claim.Value); - WriteWithDefault(writer, claim.ValueType, ClaimValueTypes.String); - WriteWithDefault(writer, claim.Issuer, ClaimsIdentity.DefaultIssuer); - WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer); - - // Write the number of properties contained in the claim. - writer.Write(claim.Properties.Count); - - foreach (var property in claim.Properties) - { - writer.Write(property.Key ?? string.Empty); - writer.Write(property.Value ?? string.Empty); - } - } - - static void WriteProperties(BinaryWriter writer, int version, IReadOnlyDictionary properties) - { - writer.Write(version); - writer.Write(properties.Count); - - foreach (var property in properties) - { - writer.Write(property.Key ?? string.Empty); - writer.Write(property.Value ?? string.Empty); - } - } - - static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) - => writer.Write(string.Equals(value, defaultValue, StringComparison.Ordinal) ? "\0" : value); - - static void SetProperty(IDictionary properties, string name, string value) - { - if (string.IsNullOrEmpty(value)) - { - properties.Remove(name); - } - - else - { - properties[name] = value; - } - } - - static void SetArrayProperty(IDictionary properties, string name, IEnumerable values) - { - var array = new JArray(values); - if (array.Count == 0) - { - properties.Remove(name); - } - - else - { - properties[name] = array.ToString(Formatting.None); - } - } - } - } - - /// - /// Contains the logic responsible of unprotecting a Data Protection token. - /// - public class DeserializeDataProtectionToken : IOpenIddictServerHandler where TContext : BaseDeserializingContext - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler>() - .SetOrder(DeserializeJwtBearerToken.Descriptor.Order - 5000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] TContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!context.Properties.TryGetValue(Properties.DataProtector, out var property) || - !(property is IDataProtector protector)) - { - throw new InvalidOperationException(new StringBuilder() - .Append("No suitable data protector was found for the specified token type.") - .Append("This may indicate that the OpenIddict Data Protection services were not correctly registered.") - .ToString()); - } - - try - { - using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(context.Token))); - using var reader = new BinaryReader(buffer); - - var (principal, properties) = Read(reader, version: 5); - if (principal == null) - { - return default; - } - - context.Principal = principal; - - // Tokens serialized using the ASP.NET Core Data Protection stack are compound - // of both claims and special authentication properties. To ensure existing tokens - // can be reused, well-known properties are manually mapped to their claims equivalents. - - context.Principal - .SetAudiences(GetArrayProperty(properties, Properties.Audiences)) - .SetCreationDate(GetDateProperty(properties, Properties.Issued)) - .SetExpirationDate(GetDateProperty(properties, Properties.Expires)) - .SetPresenters(GetArrayProperty(properties, Properties.Presenters)) - .SetScopes(GetArrayProperty(properties, Properties.Scopes)) - - .SetClaim(Claims.Private.AccessTokenLifetime, GetProperty(properties, Properties.AccessTokenLifetime)) - .SetClaim(Claims.Private.AuthorizationCodeLifetime, GetProperty(properties, Properties.AuthorizationCodeLifetime)) - .SetClaim(Claims.Private.AuthorizationId, GetProperty(properties, Properties.InternalAuthorizationId)) - .SetClaim(Claims.Private.CodeChallenge, GetProperty(properties, Properties.CodeChallenge)) - .SetClaim(Claims.Private.CodeChallengeMethod, GetProperty(properties, Properties.CodeChallengeMethod)) - .SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) - .SetClaim(Claims.Private.RedirectUri, GetProperty(properties, Properties.OriginalRedirectUri)) - .SetClaim(Claims.Private.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)) - .SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)) - - // Note: since the data format relies on a data protector using different "purposes" strings - // per token type, the token processed at this stage is guaranteed to be of the expected type. - .SetClaim(Claims.Private.TokenUsage, (string) context.Properties[Properties.TokenUsage]); - - context.HandleDeserialization(); - - return default; - } - - catch (Exception exception) - { - context.Logger.LogTrace(exception, "An exception occured while deserializing a token."); - - return default; - } - - static (ClaimsPrincipal principal, ImmutableDictionary properties) Read(BinaryReader reader, int version) - { - if (version != reader.ReadInt32()) - { - return (null, ImmutableDictionary.Create()); - } - - // Read the authentication scheme associated to the ticket. - _ = reader.ReadString(); - - // Read the number of identities stored in the serialized payload. - var count = reader.ReadInt32(); - if (count < 0) - { - return (null, ImmutableDictionary.Create()); - } - - var identities = new ClaimsIdentity[count]; - for (var index = 0; index != count; ++index) - { - identities[index] = ReadIdentity(reader); - } - - var properties = ReadProperties(reader, version); - - return (new ClaimsPrincipal(identities), properties); - } - - static ClaimsIdentity ReadIdentity(BinaryReader reader) - { - var identity = new ClaimsIdentity( - authenticationType: reader.ReadString(), - nameType: ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType), - roleType: ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType)); - - // Read the number of claims contained in the serialized identity. - var count = reader.ReadInt32(); - - for (int index = 0; index != count; ++index) - { - var claim = ReadClaim(reader, identity); - - identity.AddClaim(claim); - } - - // Determine whether the identity has a bootstrap context attached. - if (reader.ReadBoolean()) - { - identity.BootstrapContext = reader.ReadString(); - } - - // Determine whether the identity has an actor identity attached. - if (reader.ReadBoolean()) - { - identity.Actor = ReadIdentity(reader); - } - - return identity; - } - - static Claim ReadClaim(BinaryReader reader, ClaimsIdentity identity) - { - var type = ReadWithDefault(reader, identity.NameClaimType); - var value = reader.ReadString(); - var valueType = ReadWithDefault(reader, ClaimValueTypes.String); - var issuer = ReadWithDefault(reader, ClaimsIdentity.DefaultIssuer); - var originalIssuer = ReadWithDefault(reader, issuer); - - var claim = new Claim(type, value, valueType, issuer, originalIssuer, identity); - - // Read the number of properties stored in the claim. - var count = reader.ReadInt32(); - - for (var index = 0; index != count; ++index) - { - var key = reader.ReadString(); - var propertyValue = reader.ReadString(); - - claim.Properties.Add(key, propertyValue); - } - - return claim; - } - - static ImmutableDictionary ReadProperties(BinaryReader reader, int version) - { - if (version != reader.ReadInt32()) - { - return ImmutableDictionary.Create(); - } - - var properties = ImmutableDictionary.CreateBuilder(StringComparer.Ordinal); - var count = reader.ReadInt32(); - for (var index = 0; index != count; ++index) - { - properties.Add(reader.ReadString(), reader.ReadString()); - } - - return properties.ToImmutable(); - } - - static string ReadWithDefault(BinaryReader reader, string defaultValue) - { - var value = reader.ReadString(); - - if (string.Equals(value, "\0", StringComparison.Ordinal)) - { - return defaultValue; - } - - return value; - } - - static string GetProperty(IReadOnlyDictionary properties, string name) - => properties.TryGetValue(name, out var value) ? value : null; - - static IEnumerable GetArrayProperty(IReadOnlyDictionary properties, string name) - => properties.TryGetValue(name, out var value) ? JArray.Parse(value).Values() : Enumerable.Empty(); - - static DateTimeOffset? GetDateProperty(IReadOnlyDictionary properties, string name) - => properties.TryGetValue(name, out var value) ? (DateTimeOffset?) - DateTimeOffset.ParseExact(value, "r", CultureInfo.InvariantCulture) : null; - } - } - - /// - /// Contains the logic responsible of populating the data protector needed to generate an access token. - /// - public class AttachAccessTokenSerializationProtector : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public AttachAccessTokenSerializationProtector([NotNull] IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] SerializeAccessTokenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - // Note: the protector MUST be created with the same purposes used by the - // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. - var purposes = new List(capacity: 4) - { - "OpenIdConnectServerHandler", - "AccessTokenFormat", - "ASOS" - }; - - if (context.Options.UseReferenceTokens) - { - purposes.Insert(index: 2, "UseReferenceTokens"); - } - - var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[Properties.DataProtector] = protector; - context.Properties[Properties.TokenUsage] = TokenUsages.AccessToken; - - return default; - } - } - - /// - /// Contains the logic responsible of populating the data protector needed to generate an authorization code. - /// - public class AttachAuthorizationCodeSerializationProtector : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public AttachAuthorizationCodeSerializationProtector([NotNull] IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] SerializeAuthorizationCodeContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - // Note: the protector MUST be created with the same purposes used by the - // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. - var purposes = new List(capacity: 4) - { - "OpenIdConnectServerHandler", - "AuthorizationCodeFormat", - "ASOS" - }; - - if (context.Options.UseReferenceTokens) - { - purposes.Insert(index: 2, "UseReferenceTokens"); - } - - var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[Properties.DataProtector] = protector; - context.Properties[Properties.TokenUsage] = TokenUsages.AuthorizationCode; - - return default; - } - } - - /// - /// Contains the logic responsible of populating the data protector needed to generate a refresh token. - /// - public class AttachRefreshTokenSerializationProtector : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public AttachRefreshTokenSerializationProtector([NotNull] IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] SerializeRefreshTokenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - // Note: the protector MUST be created with the same purposes used by the - // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. - var purposes = new List(capacity: 4) - { - "OpenIdConnectServerHandler", - "RefreshTokenFormat", - "ASOS" - }; - - if (context.Options.UseReferenceTokens) - { - purposes.Insert(index: 2, "UseReferenceTokens"); - } - - var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[Properties.DataProtector] = protector; - context.Properties[Properties.TokenUsage] = TokenUsages.RefreshToken; - - return default; - } - } - - /// - /// Contains the logic responsible of populating the data protector needed to unprotect an access token. - /// - public class AttachAccessTokenDeserializationProtector : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public AttachAccessTokenDeserializationProtector([NotNull] IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] DeserializeAccessTokenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - // Note: the protector MUST be created with the same purposes used by the - // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. - var purposes = new List(capacity: 4) - { - "OpenIdConnectServerHandler", - "AccessTokenFormat", - "ASOS" - }; - - if (context.Options.UseReferenceTokens) - { - purposes.Insert(index: 2, "UseReferenceTokens"); - } - - var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[Properties.DataProtector] = protector; - context.Properties[Properties.TokenUsage] = TokenUsages.AccessToken; - - return default; - } - } - - /// - /// Contains the logic responsible of populating the data protector needed to unprotect an authorization code. - /// - public class AttachAuthorizationCodeDeserializationProtector : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public AttachAuthorizationCodeDeserializationProtector([NotNull] IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] DeserializeAuthorizationCodeContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - // Note: the protector MUST be created with the same purposes used by the - // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. - var purposes = new List(capacity: 4) - { - "OpenIdConnectServerHandler", - "AuthorizationCodeFormat", - "ASOS" - }; - - if (context.Options.UseReferenceTokens) - { - purposes.Insert(index: 2, "UseReferenceTokens"); - } - - var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[Properties.DataProtector] = protector; - context.Properties[Properties.TokenUsage] = TokenUsages.AuthorizationCode; - - return default; - } - } - - /// - /// Contains the logic responsible of populating the data protector needed to unprotect a refresh token. - /// - public class AttachRefreshTokenDeserializationProtector : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public AttachRefreshTokenDeserializationProtector([NotNull] IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] DeserializeRefreshTokenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - // Note: the protector MUST be created with the same purposes used by the - // OpenID Connect server middleware (aka ASOS) to guarantee compatibility. - var purposes = new List(capacity: 4) - { - "OpenIdConnectServerHandler", - "RefreshTokenFormat", - "ASOS" - }; - - if (context.Options.UseReferenceTokens) - { - purposes.Insert(index: 2, "UseReferenceTokens"); - } - - var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); - context.Properties[Properties.DataProtector] = protector; - context.Properties[Properties.TokenUsage] = TokenUsages.RefreshToken; - - return default; - } - } - } - } -} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs index 6ffa71f4..15db354e 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs @@ -4,12 +4,885 @@ * the license and the contributors participating to this project. */ +using System; using System.Collections.Immutable; +using System.IO; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; +using static OpenIddict.Server.OpenIddictServerConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlerFilters; +using static OpenIddict.Server.OpenIddictServerHandlers; namespace OpenIddict.Server.DataProtection { public static partial class OpenIddictServerDataProtectionHandlers { - public static ImmutableArray DefaultHandlers { get; } = Serialization.DefaultHandlers; + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Authentication processing: + */ + ValidateReferenceDataProtectionToken.Descriptor, + ValidateSelfContainedDataProtectionToken.Descriptor, + + /* + * Sign-in processing: + */ + AttachReferenceDataProtectionAccessToken.Descriptor, + AttachReferenceDataProtectionAuthorizationCode.Descriptor, + AttachReferenceDataProtectionRefreshToken.Descriptor, + + AttachSelfContainedDataProtectionAccessToken.Descriptor, + AttachSelfContainedDataProtectionAuthorizationCode.Descriptor, + AttachSelfContainedDataProtectionRefreshToken.Descriptor); + + /// + /// Contains the logic responsible of rejecting authentication + /// demands that use an invalid reference Data Protection token. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateReferenceDataProtectionToken : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + private readonly IOptionsMonitor _options; + + public ValidateReferenceDataProtectionToken() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateReferenceDataProtectionToken( + [NotNull] IOpenIddictTokenManager tokenManager, + [NotNull] IOptionsMonitor options) + { + _tokenManager = tokenManager; + _options = options; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateReferenceToken.Descriptor.Order - 500) + .Build(); + + public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a principal was already attached, don't overwrite it. + if (context.Principal != null) + { + return; + } + + var identifier = context.EndpointType switch + { + OpenIddictServerEndpointType.Introspection => context.Request.Token, + OpenIddictServerEndpointType.Revocation => context.Request.Token, + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => context.Request.Code, + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => context.Request.RefreshToken, + + OpenIddictServerEndpointType.Userinfo => context.Request.AccessToken, + + _ => null + }; + + // If the token cannot be validated, don't return an error to allow another handle to validate it. + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + var token = await _tokenManager.FindByReferenceIdAsync(identifier); + if (token == null || !await IsTokenTypeValidAsync(token)) + { + return; + } + + var payload = await _tokenManager.GetPayloadAsync(token); + if (string.IsNullOrEmpty(payload)) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The payload associated with a reference token cannot be retrieved.") + .Append("This may indicate that the token entry was corrupted.") + .ToString()); + } + + var principal = context.EndpointType switch + { + OpenIddictServerEndpointType.Introspection => ValidateToken(payload, TokenUsages.AccessToken) ?? + ValidateToken(payload, TokenUsages.RefreshToken) ?? + ValidateToken(payload, TokenUsages.AuthorizationCode), + + OpenIddictServerEndpointType.Revocation => ValidateToken(payload, TokenUsages.AccessToken) ?? + ValidateToken(payload, TokenUsages.RefreshToken) ?? + ValidateToken(payload, TokenUsages.AuthorizationCode), + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => ValidateToken(payload, TokenUsages.AuthorizationCode), + + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => ValidateToken(payload, TokenUsages.RefreshToken), + + OpenIddictServerEndpointType.Userinfo => ValidateToken(payload, TokenUsages.AccessToken), + + _ => null + }; + + // If the token cannot be validated, don't return an error to allow another handle to validate it. + if (principal == null) + { + return; + } + + // Attach the principal extracted from the authorization code to the parent event context + // and restore the creation/expiration dates/identifiers from the token entry metadata. + context.Principal = principal + .SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) + .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) + .SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetInternalTokenId(await _tokenManager.GetIdAsync(token)) + .SetClaim(Claims.Private.TokenUsage, await _tokenManager.GetTypeAsync(token)); + + ClaimsPrincipal ValidateToken(string token, string type) + { + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + Purposes.Handlers.Server, + type switch + { + TokenUsages.AccessToken => Purposes.Formats.AccessToken, + TokenUsages.AuthorizationCode => Purposes.Formats.AuthorizationCode, + TokenUsages.RefreshToken => Purposes.Formats.RefreshToken, + + _ => throw new InvalidOperationException("The specified token type is not supported.") + }, + Purposes.Features.ReferenceTokens, + Purposes.Schemes.Server); + + try + { + using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(token))); + using var reader = new BinaryReader(buffer); + + // Note: since the data format relies on a data protector using different "purposes" strings + // per token type, the token processed at this stage is guaranteed to be of the expected type. + return _options.CurrentValue.Formatter.ReadToken(reader)?.SetClaim(Claims.Private.TokenUsage, type); + } + + catch (Exception exception) + { + context.Logger.LogTrace(exception, "An exception occured while deserializing a token."); + + return null; + } + } + + async ValueTask IsTokenTypeValidAsync(object token) => context.EndpointType switch + { + // All types of tokens are accepted by the introspection and revocation endpoints. + OpenIddictServerEndpointType.Introspection => true, + OpenIddictServerEndpointType.Revocation => true, + + OpenIddictServerEndpointType.Token => await _tokenManager.GetTypeAsync(token) switch + { + TokenUsages.AuthorizationCode when context.Request.IsAuthorizationCodeGrantType() => true, + TokenUsages.RefreshToken when context.Request.IsRefreshTokenGrantType() => true, + + _ => false + }, + + OpenIddictServerEndpointType.Userinfo => await _tokenManager.GetTypeAsync(token) switch + { + TokenUsages.AccessToken => true, + + _ => false + }, + + _ => false + }; + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands + /// that specify an invalid self-contained Data Protection token. + /// + public class ValidateSelfContainedDataProtectionToken : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public ValidateSelfContainedDataProtectionToken([NotNull] IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateSelfContainedToken.Descriptor.Order - 500) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a principal was already attached, don't overwrite it. + if (context.Principal != null) + { + return default; + } + + var token = context.EndpointType switch + { + OpenIddictServerEndpointType.Introspection => context.Request.Token, + OpenIddictServerEndpointType.Revocation => context.Request.Token, + + // This handler doesn't handle reference tokens. + _ when context.Options.UseReferenceTokens => null, + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => context.Request.Code, + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => context.Request.RefreshToken, + + OpenIddictServerEndpointType.Userinfo => context.Request.AccessToken, + + _ => null + }; + + // If the token cannot be validated, don't return an error to allow another handle to validate it. + if (string.IsNullOrEmpty(token)) + { + return default; + } + + var principal = context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => ValidateToken(token, TokenUsages.AuthorizationCode), + + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => ValidateToken(token, TokenUsages.RefreshToken), + + OpenIddictServerEndpointType.Introspection => ValidateToken(token, TokenUsages.AccessToken) ?? + ValidateToken(token, TokenUsages.RefreshToken) ?? + ValidateToken(token, TokenUsages.AuthorizationCode), + + OpenIddictServerEndpointType.Revocation => ValidateToken(token, TokenUsages.AccessToken) ?? + ValidateToken(token, TokenUsages.RefreshToken) ?? + ValidateToken(token, TokenUsages.AuthorizationCode), + + OpenIddictServerEndpointType.Userinfo => ValidateToken(token, TokenUsages.AccessToken), + + _ => null + }; + + // If the token cannot be validated, don't return an error to allow another handle to validate it. + if (principal == null) + { + return default; + } + + context.Principal = principal; + + return default; + + ClaimsPrincipal ValidateToken(string token, string type) + { + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + Purposes.Handlers.Server, + type switch + { + TokenUsages.AccessToken => Purposes.Formats.AccessToken, + TokenUsages.AuthorizationCode => Purposes.Formats.AuthorizationCode, + TokenUsages.RefreshToken => Purposes.Formats.RefreshToken, + + _ => throw new InvalidOperationException("The specified token type is not supported.") + }, + Purposes.Schemes.Server); + + try + { + using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(token))); + using var reader = new BinaryReader(buffer); + + // Note: since the data format relies on a data protector using different "purposes" strings + // per token type, the token processed at this stage is guaranteed to be of the expected type. + return _options.CurrentValue.Formatter.ReadToken(reader)?.SetClaim(Claims.Private.TokenUsage, type); + } + + catch (Exception exception) + { + context.Logger.LogTrace(exception, "An exception occured while deserializing a token."); + + return null; + } + } + } + } + + /// + /// Contains the logic responsible of generating and attaching the + /// reference Data Protection access token returned as part of the response. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class AttachReferenceDataProtectionAccessToken : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictTokenManager _tokenManager; + private readonly IOptionsMonitor _options; + + public AttachReferenceDataProtectionAccessToken() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public AttachReferenceDataProtectionAccessToken( + [NotNull] IOpenIddictApplicationManager applicationManager, + [NotNull] IOpenIddictTokenManager tokenManager, + [NotNull] IOptionsMonitor options) + { + _applicationManager = applicationManager; + _tokenManager = tokenManager; + _options = options; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachReferenceAccessToken.Descriptor.Order - 500) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an access token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.AccessToken)) + { + return; + } + + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + Purposes.Handlers.Server, + Purposes.Formats.AccessToken, + Purposes.Features.ReferenceTokens, + Purposes.Schemes.Server); + + using var buffer = new MemoryStream(); + using var writer = new BinaryWriter(buffer); + + _options.CurrentValue.Formatter.WriteToken(writer, context.AccessTokenPrincipal); + + // Generate a new crypto-secure random identifier that will be substituted to the token. + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + + var descriptor = new OpenIddictTokenDescriptor + { + AuthorizationId = context.AccessTokenPrincipal.GetInternalAuthorizationId(), + CreationDate = context.AccessTokenPrincipal.GetCreationDate(), + ExpirationDate = context.AccessTokenPrincipal.GetExpirationDate(), + Payload = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())), + Principal = context.AccessTokenPrincipal, + ReferenceId = Base64UrlEncoder.Encode(data), + Status = Statuses.Valid, + Subject = context.AccessTokenPrincipal.GetClaim(Claims.Subject), + Type = TokenUsages.AccessToken + }; + + // If the client application is known, associate it with the token. + if (!string.IsNullOrEmpty(context.Request.ClientId)) + { + var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The application entry cannot be found in the database."); + } + + descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + } + + await _tokenManager.CreateAsync(descriptor); + + context.Response.AccessToken = descriptor.ReferenceId; + } + } + + /// + /// Contains the logic responsible of generating and attaching the + /// reference Data Protection authorization code returned as part of the response. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class AttachReferenceDataProtectionAuthorizationCode : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictTokenManager _tokenManager; + private readonly IOptionsMonitor _options; + + public AttachReferenceDataProtectionAuthorizationCode() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public AttachReferenceDataProtectionAuthorizationCode( + [NotNull] IOpenIddictApplicationManager applicationManager, + [NotNull] IOpenIddictTokenManager tokenManager, + [NotNull] IOptionsMonitor options) + { + _applicationManager = applicationManager; + _tokenManager = tokenManager; + _options = options; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachReferenceAuthorizationCode.Descriptor.Order - 500) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an authorization code was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.Code)) + { + return; + } + + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + Purposes.Handlers.Server, + Purposes.Formats.AuthorizationCode, + Purposes.Features.ReferenceTokens, + Purposes.Schemes.Server); + + using var buffer = new MemoryStream(); + using var writer = new BinaryWriter(buffer); + + _options.CurrentValue.Formatter.WriteToken(writer, context.AuthorizationCodePrincipal); + + // Generate a new crypto-secure random identifier that will be substituted to the token. + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + + var descriptor = new OpenIddictTokenDescriptor + { + AuthorizationId = context.AuthorizationCodePrincipal.GetInternalAuthorizationId(), + CreationDate = context.AuthorizationCodePrincipal.GetCreationDate(), + ExpirationDate = context.AuthorizationCodePrincipal.GetExpirationDate(), + Payload = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())), + Principal = context.AuthorizationCodePrincipal, + ReferenceId = Base64UrlEncoder.Encode(data), + Status = Statuses.Valid, + Subject = context.AuthorizationCodePrincipal.GetClaim(Claims.Subject), + Type = TokenUsages.AuthorizationCode + }; + + // If the client application is known, associate it with the token. + if (!string.IsNullOrEmpty(context.Request.ClientId)) + { + var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The application entry cannot be found in the database."); + } + + descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + } + + await _tokenManager.CreateAsync(descriptor); + + context.Response.Code = descriptor.ReferenceId; + } + } + + /// + /// Contains the logic responsible of generating and attaching the + /// reference Data Protection refresh token returned as part of the response. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class AttachReferenceDataProtectionRefreshToken : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictTokenManager _tokenManager; + private readonly IOptionsMonitor _options; + + public AttachReferenceDataProtectionRefreshToken() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public AttachReferenceDataProtectionRefreshToken( + [NotNull] IOpenIddictApplicationManager applicationManager, + [NotNull] IOpenIddictTokenManager tokenManager, + [NotNull] IOptionsMonitor options) + { + _applicationManager = applicationManager; + _tokenManager = tokenManager; + _options = options; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachReferenceRefreshToken.Descriptor.Order - 500) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a refresh token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.RefreshToken)) + { + return; + } + + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + Purposes.Handlers.Server, + Purposes.Formats.RefreshToken, + Purposes.Features.ReferenceTokens, + Purposes.Schemes.Server); + + using var buffer = new MemoryStream(); + using var writer = new BinaryWriter(buffer); + + _options.CurrentValue.Formatter.WriteToken(writer, context.RefreshTokenPrincipal); + + // Generate a new crypto-secure random identifier that will be substituted to the token. + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + + var descriptor = new OpenIddictTokenDescriptor + { + AuthorizationId = context.RefreshTokenPrincipal.GetInternalAuthorizationId(), + CreationDate = context.RefreshTokenPrincipal.GetCreationDate(), + ExpirationDate = context.RefreshTokenPrincipal.GetExpirationDate(), + Payload = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())), + Principal = context.RefreshTokenPrincipal, + ReferenceId = Base64UrlEncoder.Encode(data), + Status = Statuses.Valid, + Subject = context.RefreshTokenPrincipal.GetClaim(Claims.Subject), + Type = TokenUsages.RefreshToken + }; + + // If the client application is known, associate it with the token. + if (!string.IsNullOrEmpty(context.Request.ClientId)) + { + var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The application entry cannot be found in the database."); + } + + descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + } + + await _tokenManager.CreateAsync(descriptor); + + context.Response.RefreshToken = descriptor.ReferenceId; + } + } + + /// + /// Contains the logic responsible of generating and attaching the self-contained + /// Data Protection access token returned as part of the response. + /// + public class AttachSelfContainedDataProtectionAccessToken : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public AttachSelfContainedDataProtectionAccessToken([NotNull] IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachSelfContainedAccessToken.Descriptor.Order - 500) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an access token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.AccessToken)) + { + return default; + } + + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + Purposes.Handlers.Server, + Purposes.Formats.AccessToken, + Purposes.Schemes.Server); + + using var buffer = new MemoryStream(); + using var writer = new BinaryWriter(buffer); + + _options.CurrentValue.Formatter.WriteToken(writer, context.AccessTokenPrincipal); + + context.Response.AccessToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); + + return default; + } + } + + /// + /// Contains the logic responsible of generating and attaching the self-contained + /// Data Protection authorization code returned as part of the response. + /// + public class AttachSelfContainedDataProtectionAuthorizationCode : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public AttachSelfContainedDataProtectionAuthorizationCode([NotNull] IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachSelfContainedAuthorizationCode.Descriptor.Order - 500) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an authorization code was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.Code)) + { + return default; + } + + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + Purposes.Handlers.Server, + Purposes.Formats.AuthorizationCode, + Purposes.Schemes.Server); + + using var buffer = new MemoryStream(); + using var writer = new BinaryWriter(buffer); + + _options.CurrentValue.Formatter.WriteToken(writer, context.AuthorizationCodePrincipal); + + context.Response.Code = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); + + return default; + } + } + + /// + /// Contains the logic responsible of generating and attaching the self-contained + /// Data Protection refresh token returned as part of the response. + /// + public class AttachSelfContainedDataProtectionRefreshToken : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public AttachSelfContainedDataProtectionRefreshToken([NotNull] IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachSelfContainedRefreshToken.Descriptor.Order - 500) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a refresh token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.RefreshToken)) + { + return default; + } + + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + Purposes.Handlers.Server, + Purposes.Formats.RefreshToken, + Purposes.Schemes.Server); + + using var buffer = new MemoryStream(); + using var writer = new BinaryWriter(buffer); + + _options.CurrentValue.Formatter.WriteToken(writer, context.RefreshTokenPrincipal); + + context.Response.RefreshToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); + + return default; + } + } } } diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs index 9bdf16f2..87b52828 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs @@ -27,5 +27,12 @@ namespace OpenIddict.Server.DataProtection /// and authorization codes. This property is set to false by default. /// public bool PreferDataProtectionFormat { get; set; } + + /// + /// Gets or sets the formatter used to read and write Data Protection tokens, + /// serialized using the same format as the ASP.NET Core authentication tickets. + /// + public IOpenIddictServerDataProtectionFormatter Formatter { get; set; } + = new OpenIddictServerDataProtectionFormatter(); } } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs index 2021fd3d..1e526cba 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs @@ -104,40 +104,22 @@ namespace OpenIddict.Server.Owin return false; } - protected override async Task AuthenticateCoreAsync() + protected override Task AuthenticateCoreAsync() { var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName); - if (transaction?.Request == null) + if (transaction == null) { throw new InvalidOperationException("An identity cannot be extracted from this request."); } - var context = new ProcessAuthenticationContext(transaction); - await _provider.DispatchAsync(context); - - if (context.Principal == null || context.IsRequestHandled || context.IsRequestSkipped) + if (transaction.Properties.TryGetValue(OpenIddictServerConstants.Properties.AmbientPrincipal, out var principal)) { - return null; - } - - else if (context.IsRejected) - { - _logger.LogError("An error occurred while authenticating the current request: {Error} ; {Description}", - /* Error: */ context.Error ?? Errors.InvalidToken, - /* Description: */ context.ErrorDescription); - - return new AuthenticationTicket(identity: null, new AuthenticationProperties - { - Dictionary = - { - [Parameters.Error] = context.Error, - [Parameters.ErrorDescription] = context.ErrorDescription, - [Parameters.ErrorUri] = context.ErrorUri - } - }); + return Task.FromResult(new AuthenticationTicket( + (ClaimsIdentity) ((ClaimsPrincipal) principal).Identity, + new AuthenticationProperties())); } - return new AuthenticationTicket((ClaimsIdentity) context.Principal.Identity, new AuthenticationProperties()); + return Task.FromResult(null); } protected override async Task TeardownCoreAsync() diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index d1978c2b..ed781d14 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -46,7 +46,9 @@ namespace OpenIddict.Server if (options.AuthorizationEndpointUris.Count == 0 && (options.GrantTypes.Contains(GrantTypes.AuthorizationCode) || options.GrantTypes.Contains(GrantTypes.Implicit))) { - throw new InvalidOperationException("The authorization endpoint must be enabled to use the authorization code and implicit flows."); + throw new InvalidOperationException(new StringBuilder() + .Append("The authorization endpoint must be enabled to use the authorization code and implicit flows.") + .ToString()); } // Ensure the token endpoint has been enabled when the authorization code, @@ -56,11 +58,13 @@ namespace OpenIddict.Server options.GrantTypes.Contains(GrantTypes.Password) || options.GrantTypes.Contains(GrantTypes.RefreshToken))) { - throw new InvalidOperationException( - "The token endpoint must be enabled to use the authorization code, client credentials, password and refresh token flows."); + throw new InvalidOperationException(new StringBuilder() + .Append("The token endpoint must be enabled to use the authorization code, ") + .Append("client credentials, password and refresh token flows.") + .ToString()); } - if (options.DisableTokenStorage && options.RevocationEndpointUris.Count != 0) + if (options.RevocationEndpointUris.Count != 0 && options.DisableTokenStorage) { throw new InvalidOperationException("The revocation endpoint cannot be enabled when token storage is disabled."); } @@ -70,15 +74,11 @@ namespace OpenIddict.Server throw new InvalidOperationException("Reference tokens cannot be used when disabling token storage."); } - if (options.UseReferenceTokens && options.AccessTokenHandler != null) - { - throw new InvalidOperationException("Reference tokens cannot be used when configuring JWT as the access token format."); - } - if (options.UseSlidingExpiration && options.DisableTokenStorage && !options.UseRollingTokens) { - throw new InvalidOperationException( - "Sliding expiration must be disabled when turning off token storage if rolling tokens are not used."); + throw new InvalidOperationException(new StringBuilder() + .Append("Sliding expiration must be disabled when turning off token storage if rolling tokens are not used.") + .ToString()); } if (options.EncryptionCredentials.Count == 0) diff --git a/src/OpenIddict.Server/OpenIddictServerConstants.cs b/src/OpenIddict.Server/OpenIddictServerConstants.cs index 60a62662..45bcf55b 100644 --- a/src/OpenIddict.Server/OpenIddictServerConstants.cs +++ b/src/OpenIddict.Server/OpenIddictServerConstants.cs @@ -11,8 +11,34 @@ namespace OpenIddict.Server public static class Properties { public const string AmbientPrincipal = ".ambient_principal"; + public const string OriginalPrincipal = ".original_principal"; public const string ValidatedPostLogoutRedirectUri = ".validated_post_logout_redirect_uri"; public const string ValidatedRedirectUri = ".validated_redirect_uri"; } + + public static class Purposes + { + public static class Features + { + public const string ReferenceTokens = "UseReferenceTokens"; + } + + public static class Formats + { + public const string AccessToken = "AccessTokenFormat"; + public const string AuthorizationCode = "AuthorizationCodeFormat"; + public const string RefreshToken = "RefreshTokenFormat"; + } + + public static class Handlers + { + public const string Server = "OpenIdConnectServerHandler"; + } + + public static class Schemes + { + public const string Server = "ASOS"; + } + } } } diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs index 3d03670e..542aa9c6 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs @@ -78,12 +78,6 @@ namespace OpenIddict.Server /// public IDictionary Claims { get; } = new Dictionary(StringComparer.Ordinal); - - /// - /// Gets or sets a boolean indicating whether - /// the token was successfully revoked. - /// - public bool Revoked { get; set; } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs deleted file mode 100644 index a4f3c1f7..00000000 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Security.Claims; -using JetBrains.Annotations; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; -using static OpenIddict.Abstractions.OpenIddictConstants; - -namespace OpenIddict.Server -{ - public static partial class OpenIddictServerEvents - { - /// - /// Represents an abstract base class used for certain event contexts. - /// - public abstract class BaseSerializingContext : BaseContext - { - /// - /// Creates a new instance of the class. - /// - public BaseSerializingContext([NotNull] OpenIddictServerTransaction transaction) - : base(transaction) - { - } - - /// - /// Gets or sets the security principal containing the claims to serialize. - /// - public ClaimsPrincipal Principal { get; set; } - - /// - /// Gets or sets the encrypting credentials used to encrypt the token. - /// - public EncryptingCredentials EncryptingCredentials { get; set; } - - /// - /// Gets or sets the signing credentials used to sign the token. - /// - public SigningCredentials SigningCredentials { get; set; } - - /// - /// Gets or sets the security token handler used to serialize the token. - /// - public JsonWebTokenHandler SecurityTokenHandler { get; set; } - - /// - /// Gets or sets the token returned to the client application. - /// - public string Token { get; set; } - - /// - /// Gets or sets the token usage. - /// - public string TokenUsage { get; set; } - - /// - /// Gets a boolean indicating whether the - /// method was called. - /// - public bool IsHandled { get; private set; } - - /// - /// Marks the serialization process as handled by the application code. - /// - public void HandleSerialization() => IsHandled = true; - } - - /// - /// Represents an abstract base class used for certain event contexts. - /// - public abstract class BaseDeserializingContext : BaseContext - { - /// - /// Creates a new instance of the class. - /// - public BaseDeserializingContext([NotNull] OpenIddictServerTransaction transaction) - : base(transaction) - { - } - - /// - /// Gets or sets the security principal containing the deserialized claims. - /// - public ClaimsPrincipal Principal { get; set; } - - /// - /// Gets or sets the validation parameters used to verify the authenticity of access tokens. - /// Note: this property is only used when is not null. - /// - public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters(); - - /// - /// Gets or sets the security token handler used to - /// deserialize the authentication ticket. - /// - public JsonWebTokenHandler SecurityTokenHandler { get; set; } - - /// - /// Gets or sets the token used by the client application. - /// - public string Token { get; set; } - - /// - /// Gets or sets the token usage. - /// - public string TokenUsage { get; set; } - - /// - /// Gets a boolean indicating whether the - /// method was called. - /// - public bool IsHandled { get; private set; } - - /// - /// Marks the deserialization process as handled by the application code. - /// - public void HandleDeserialization() => IsHandled = true; - } - - /// - /// Represents an event called when serializing an access token. - /// - public class SerializeAccessTokenContext : BaseSerializingContext - { - /// - /// Creates a new instance of the class. - /// - public SerializeAccessTokenContext([NotNull] OpenIddictServerTransaction transaction) - : base(transaction) - => TokenUsage = TokenUsages.AccessToken; - } - - /// - /// Represents an event called when serializing an authorization code. - /// - public class SerializeAuthorizationCodeContext : BaseSerializingContext - { - /// - /// Creates a new instance of the class. - /// - public SerializeAuthorizationCodeContext([NotNull] OpenIddictServerTransaction transaction) - : base(transaction) - => TokenUsage = TokenUsages.AuthorizationCode; - } - - /// - /// Represents an event called when serializing an identity token. - /// - public class SerializeIdentityTokenContext : BaseSerializingContext - { - /// - /// Creates a new instance of the class. - /// - public SerializeIdentityTokenContext([NotNull] OpenIddictServerTransaction transaction) - : base(transaction) - => TokenUsage = TokenUsages.IdToken; - } - - /// - /// Represents an event called when serializing a refresh token. - /// - public class SerializeRefreshTokenContext : BaseSerializingContext - { - /// - /// Creates a new instance of the class. - /// - public SerializeRefreshTokenContext([NotNull] OpenIddictServerTransaction transaction) - : base(transaction) - => TokenUsage = TokenUsages.RefreshToken; - } - - /// - /// Represents an event called when deserializing an access token. - /// - public class DeserializeAccessTokenContext : BaseDeserializingContext - { - /// - /// Creates a new instance of the class. - /// - public DeserializeAccessTokenContext([NotNull] OpenIddictServerTransaction transaction) - : base(transaction) - => TokenUsage = TokenUsages.AccessToken; - } - - /// - /// Represents an event called when deserializing an authorization code. - /// - public class DeserializeAuthorizationCodeContext : BaseDeserializingContext - { - /// - /// Creates a new instance of the class. - /// - public DeserializeAuthorizationCodeContext([NotNull] OpenIddictServerTransaction transaction) - : base(transaction) - => TokenUsage = TokenUsages.AuthorizationCode; - } - - /// - /// Represents an event called when deserializing an identity token. - /// - public class DeserializeIdentityTokenContext : BaseDeserializingContext - { - /// - /// Creates a new instance of the class. - /// - public DeserializeIdentityTokenContext([NotNull] OpenIddictServerTransaction transaction) - : base(transaction) - => TokenUsage = TokenUsages.IdToken; - } - - /// - /// Represents an event called when deserializing a refresh token. - /// - public class DeserializeRefreshTokenContext : BaseDeserializingContext - { - /// - /// Creates a new instance of the class. - /// - public DeserializeRefreshTokenContext([NotNull] OpenIddictServerTransaction transaction) - : base(transaction) - => TokenUsage = TokenUsages.RefreshToken; - } - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index 4b61cb79..4a5bda4a 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -371,6 +371,30 @@ namespace OpenIddict.Server /// recommended, except when dealing with non-standard clients. /// public bool IncludeRefreshToken { get; set; } + + /// + /// Gets or sets the principal containing the claims that + /// will be used to create the access token, if applicable. + /// + public ClaimsPrincipal AccessTokenPrincipal { get; set; } + + /// + /// Gets or sets the principal containing the claims that + /// will be used to create the authorization code, if applicable. + /// + public ClaimsPrincipal AuthorizationCodePrincipal { get; set; } + + /// + /// Gets or sets the principal containing the claims that + /// will be used to create the identity token, if applicable. + /// + public ClaimsPrincipal IdentityTokenPrincipal { get; set; } + + /// + /// Gets or sets the principal containing the claims that + /// will be used to create the refresh token, if applicable. + /// + public ClaimsPrincipal RefreshTokenPrincipal { get; set; } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index 62ebed15..f1d3663e 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -52,9 +52,15 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs index a45f7491..957d44fc 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -159,6 +159,38 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers if reference tokens are enabled. + /// + public class RequireReferenceTokensDisabled : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(!context.Options.UseReferenceTokens); + } + } + + /// + /// Represents a filter that excludes the associated handlers if reference tokens are disabled. + /// + public class RequireReferenceTokensEnabled : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.Options.UseReferenceTokens); + } + } + /// /// Represents a filter that excludes the associated handlers if no refresh token is returned. /// @@ -175,6 +207,38 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers if rolling tokens were enabled. + /// + public class RequireRollingTokensDisabled : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(!context.Options.UseRollingTokens); + } + } + + /// + /// Represents a filter that excludes the associated handlers if rolling tokens were not enabled. + /// + public class RequireRollingTokensEnabled : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.Options.UseRollingTokens); + } + } + /// /// Represents a filter that excludes the associated handlers if scope permissions were disabled. /// @@ -206,5 +270,37 @@ namespace OpenIddict.Server return new ValueTask(!context.Options.DisableScopeValidation); } } + + /// + /// Represents a filter that excludes the associated handlers if sliding expiration was disabled. + /// + public class RequireSlidingExpirationEnabled : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.Options.UseSlidingExpiration); + } + } + + /// + /// Represents a filter that excludes the associated handlers if token storage was not enabled. + /// + public class RequireTokenStorageEnabled : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(!context.Options.DisableTokenStorage); + } + } } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index b8178226..ac0498e7 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -1015,23 +1015,29 @@ namespace OpenIddict.Server return; } - var notification = new DeserializeIdentityTokenContext(context.Transaction) - { - Token = context.Request.IdTokenHint - }; - + var notification = new ProcessAuthenticationContext(context.Transaction); await _provider.DispatchAsync(notification); - if (notification.Principal == null) + if (notification.IsRequestHandled) { - context.Reject( - error: Errors.InvalidRequest, - description: "The specified 'id_token_hint' parameter is invalid or malformed."); + context.HandleRequest(); + return; + } + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); return; } - // Note: the expiration date associated with an identity token used as an id_token_hint is deliberately ignored. + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } // Attach the security principal extracted from the identity token to the // validation context and store it as an environment property. diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index 9b602216..fa313286 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -54,13 +54,11 @@ namespace OpenIddict.Server ValidateEndpointPermissions.Descriptor, ValidateGrantTypePermissions.Descriptor, ValidateScopePermissions.Descriptor, - ValidateAuthorizationCode.Descriptor, - ValidateRefreshToken.Descriptor, + ValidateToken.Descriptor, ValidatePresenters.Descriptor, ValidateRedirectUri.Descriptor, ValidateCodeVerifier.Descriptor, ValidateGrantedScopes.Descriptor, - ValidateAuthorization.Descriptor, /* * Token request handling: @@ -1186,13 +1184,14 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of rejecting token requests that specify an invalid authorization code. + /// Contains the logic responsible of rejecting token requests + /// that don't specify a valid authorization code or refresh token. /// - public class ValidateAuthorizationCode : IOpenIddictServerHandler + public class ValidateToken : IOpenIddictServerHandler { private readonly IOpenIddictServerProvider _provider; - public ValidateAuthorizationCode([NotNull] IOpenIddictServerProvider provider) + public ValidateToken([NotNull] IOpenIddictServerProvider provider) => _provider = provider; /// @@ -1200,12 +1199,8 @@ namespace OpenIddict.Server /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - // This handler is deliberately registered with a high order to ensure it runs - // after custom handlers registered with the default order and prevent the token - // endpoint from disclosing whether an authorization code or refresh token is - // valid before the caller's identity can first be fully verified. - .SetOrder(100_000) + .UseScopedHandler() + .SetOrder(ValidateScopePermissions.Descriptor.Order + 1_000) .Build(); /// @@ -1222,116 +1217,40 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - if (!context.Request.IsAuthorizationCodeGrantType()) + if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType()) { return; } - var notification = new DeserializeAuthorizationCodeContext(context.Transaction) - { - Token = context.Request.Code - }; - + var notification = new ProcessAuthenticationContext(context.Transaction); await _provider.DispatchAsync(notification); - if (notification.Principal == null) - { - context.Logger.LogError("The token request was rejected because the authorization code was invalid."); - - context.Reject( - error: Errors.InvalidGrant, - description: "The specified authorization code is invalid."); - - return; - } - - var date = notification.Principal.GetExpirationDate(); - if (date.HasValue && date.Value < DateTimeOffset.UtcNow) - { - context.Logger.LogError("The token request was rejected because the authorization code was expired."); - - context.Reject( - error: Errors.InvalidGrant, - description: "The specified authorization code is no longer valid."); - - return; - } - - // Attach the principal extracted from the authorization code to the parent event context. - context.Principal = notification.Principal; - } - } - - /// - /// Contains the logic responsible of rejecting token requests that specify an invalid refresh token. - /// - public class ValidateRefreshToken : IOpenIddictServerHandler - { - private readonly IOpenIddictServerProvider _provider; - - public ValidateRefreshToken([NotNull] IOpenIddictServerProvider provider) - => _provider = provider; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .SetOrder(ValidateAuthorizationCode.Descriptor.Order + 1_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public async ValueTask HandleAsync([NotNull] ValidateTokenRequestContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!context.Request.IsRefreshTokenGrantType()) + if (notification.IsRequestHandled) { + context.HandleRequest(); return; } - var notification = new DeserializeRefreshTokenContext(context.Transaction) - { - Token = context.Request.RefreshToken - }; - - await _provider.DispatchAsync(notification); - - if (notification.Principal == null) + else if (notification.IsRequestSkipped) { - context.Logger.LogError("The token request was rejected because the refresh token was invalid."); - - context.Reject( - error: Errors.InvalidGrant, - description: "The specified refresh token is invalid."); - + context.SkipRequest(); return; } - var date = notification.Principal.GetExpirationDate(); - if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + else if (notification.IsRejected) { - context.Logger.LogError("The token request was rejected because the refresh token was expired."); - context.Reject( - error: Errors.InvalidGrant, - description: "The specified refresh token is no longer valid."); - + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); return; } - // Attach the principal extracted from the refresh token to the parent event context. + // Attach the security principal extracted from the token to the + // validation context and store it as an environment property. context.Principal = notification.Principal; + context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal; + context.Transaction.Properties[Properties.OriginalPrincipal] = notification.Principal.Clone(_ => true); } } @@ -1347,7 +1266,7 @@ namespace OpenIddict.Server public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000) + .SetOrder(ValidateToken.Descriptor.Order + 1_000) .Build(); /// @@ -1688,67 +1607,6 @@ namespace OpenIddict.Server } } - /// - /// Contains the logic responsible of rejecting token requests that use an authorization code - /// or refresh token whose associated authorization is no longer valid (e.g was revoked). - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class ValidateAuthorization : IOpenIddictServerHandler - { - private readonly IOpenIddictAuthorizationManager _authorizationManager; - - public ValidateAuthorization() => throw new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") - .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") - .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") - .Append("Alternatively, you can disable the built-in database-based server features by enabling ") - .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") - .ToString()); - - public ValidateAuthorization([NotNull] IOpenIddictAuthorizationManager authorizationManager) - => _authorizationManager = authorizationManager; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateGrantedScopes.Descriptor.Order + 1_000) - .Build(); - - public async ValueTask HandleAsync([NotNull] ValidateTokenRequestContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var identifier = context.Principal.GetInternalAuthorizationId(); - if (string.IsNullOrEmpty(identifier)) - { - return; - } - - var authorization = await _authorizationManager.FindByIdAsync(identifier); - if (authorization == null || !await _authorizationManager.IsValidAsync(authorization)) - { - context.Logger.LogError("The token '{Identifier}' was rejected because the associated " + - "authorization was no longer valid.", context.Principal.GetPublicTokenId()); - - context.Reject( - error: Errors.InvalidGrant, - description: context.Request.IsAuthorizationCodeGrantType() ? - "The authorization associated with the authorization code is no longer valid." : - "The authorization associated with the refresh token is no longer valid."); - - return; - } - } - } - /// /// Contains the logic responsible of attaching the principal extracted /// from the authorization code/refresh token to the event context. diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs index 79d7df38..48b5937f 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs @@ -48,7 +48,6 @@ namespace OpenIddict.Server ValidateEndpointPermissions.Descriptor, ValidateToken.Descriptor, ValidateAuthorizedParty.Descriptor, - ValidateAuthorization.Descriptor, /* * Introspection request handling: @@ -656,7 +655,7 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of rejecting introspection requests that specify an invalid token. + /// Contains the logic responsible of rejecting introspection requests that don't specify a valid token. /// public class ValidateToken : IOpenIddictServerHandler { @@ -671,11 +670,7 @@ namespace OpenIddict.Server public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseScopedHandler() - // This handler is deliberately registered with a high order to ensure it runs - // after custom handlers registered with the default order and prevent the token - // endpoint from disclosing whether the introspected token is valid before - // the caller's identity can first be fully verified by the other handlers. - .SetOrder(100_000) + .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) .Build(); /// @@ -692,117 +687,34 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // Note: use the "token_type_hint" parameter specified by the client application - // to try to determine the type of the token sent by the client application. - // See https://tools.ietf.org/html/rfc7662#section-2.1 for more information. - var principal = context.Request.TokenTypeHint switch - { - TokenTypeHints.AccessToken => await DeserializeAccessTokenAsync(), - TokenTypeHints.AuthorizationCode => await DeserializeAuthorizationCodeAsync(), - TokenTypeHints.IdToken => await DeserializeIdentityTokenAsync(), - TokenTypeHints.RefreshToken => await DeserializeRefreshTokenAsync(), - - _ => null - }; - - // Note: if the introspected token can't be found using "token_type_hint", - // the search must be extended to all supported token types. - // See https://tools.ietf.org/html/rfc7662#section-2.1 for more information. - // To avoid calling the same deserialization methods twice, an additional check - // is made to exclude the corresponding call when a token_type_hint was specified. - principal ??= context.Request.TokenTypeHint switch - { - TokenTypeHints.AccessToken => await DeserializeAuthorizationCodeAsync() ?? - await DeserializeIdentityTokenAsync() ?? - await DeserializeRefreshTokenAsync(), - - TokenTypeHints.AuthorizationCode => await DeserializeAccessTokenAsync() ?? - await DeserializeIdentityTokenAsync() ?? - await DeserializeRefreshTokenAsync(), - - TokenTypeHints.IdToken => await DeserializeAccessTokenAsync() ?? - await DeserializeAuthorizationCodeAsync() ?? - await DeserializeRefreshTokenAsync(), - - TokenTypeHints.RefreshToken => await DeserializeAccessTokenAsync() ?? - await DeserializeAuthorizationCodeAsync() ?? - await DeserializeIdentityTokenAsync(), - - _ => await DeserializeAccessTokenAsync() ?? - await DeserializeAuthorizationCodeAsync() ?? - await DeserializeIdentityTokenAsync() ?? - await DeserializeRefreshTokenAsync() - }; + var notification = new ProcessAuthenticationContext(context.Transaction); + await _provider.DispatchAsync(notification); - if (principal == null) + if (notification.IsRequestHandled) { - context.Logger.LogError("The introspection request was rejected because the token was invalid."); - - context.Reject( - error: Errors.InvalidToken, - description: "The specified token is invalid."); - + context.HandleRequest(); return; } - var date = principal.GetExpirationDate(); - if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + else if (notification.IsRequestSkipped) { - context.Logger.LogError("The introspection request was rejected because the token was expired."); - - context.Reject( - error: Errors.InvalidToken, - description: "The specified token is no longer valid."); - + context.SkipRequest(); return; } - // Attach the principal extracted from the token to the parent event context. - context.Principal = principal; - - async ValueTask DeserializeAccessTokenAsync() - { - var notification = new DeserializeAccessTokenContext(context.Transaction) - { - Token = context.Request.Token - }; - - await _provider.DispatchAsync(notification); - return notification.Principal; - } - - async ValueTask DeserializeAuthorizationCodeAsync() - { - var notification = new DeserializeAuthorizationCodeContext(context.Transaction) - { - Token = context.Request.Token - }; - - await _provider.DispatchAsync(notification); - return notification.Principal; - } - - async ValueTask DeserializeIdentityTokenAsync() + else if (notification.IsRejected) { - var notification = new DeserializeIdentityTokenContext(context.Transaction) - { - Token = context.Request.Token - }; - - await _provider.DispatchAsync(notification); - return notification.Principal; + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; } - async ValueTask DeserializeRefreshTokenAsync() - { - var notification = new DeserializeRefreshTokenContext(context.Transaction) - { - Token = context.Request.Token - }; - - await _provider.DispatchAsync(notification); - return notification.Principal; - } + // Attach the security principal extracted from the token to the + // validation context and store it as an environment property. + context.Principal = notification.Principal; + context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal; } } @@ -822,7 +734,7 @@ namespace OpenIddict.Server // In this case, the returned claims are limited by AttachApplicationClaims to limit exposure. .AddFilter() .UseSingletonHandler() - .SetOrder(ValidateToken.Descriptor.Order + 1_000) + .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000) .Build(); /// @@ -919,65 +831,6 @@ namespace OpenIddict.Server } } - /// - /// Contains the logic responsible of rejecting introspection requests that use - /// a token whose associated authorization is no longer valid (e.g was revoked). - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class ValidateAuthorization : IOpenIddictServerHandler - { - private readonly IOpenIddictAuthorizationManager _authorizationManager; - - public ValidateAuthorization() => throw new InvalidOperationException(new StringBuilder() - .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") - .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") - .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") - .Append("Alternatively, you can disable the built-in database-based server features by enabling ") - .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") - .ToString()); - - public ValidateAuthorization([NotNull] IOpenIddictAuthorizationManager authorizationManager) - => _authorizationManager = authorizationManager; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateAuthorizedParty.Descriptor.Order + 1_000) - .Build(); - - public async ValueTask HandleAsync([NotNull] ValidateIntrospectionRequestContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var identifier = context.Principal.GetInternalAuthorizationId(); - if (string.IsNullOrEmpty(identifier)) - { - return; - } - - var authorization = await _authorizationManager.FindByIdAsync(identifier); - if (authorization == null || !await _authorizationManager.IsValidAsync(authorization)) - { - context.Logger.LogError("The token '{Identifier}' was rejected because the associated " + - "authorization was no longer valid.", context.Principal.GetPublicTokenId()); - - context.Reject( - error: Errors.InvalidGrant, - description: "The authorization associated with the token is no longer valid."); - - return; - } - } - } - /// /// Contains the logic responsible of attaching the principal /// extracted from the introspected token to the event context. @@ -1044,7 +897,7 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - context.TokenId = context.Principal.GetPublicTokenId(); + context.TokenId = context.Principal.GetClaim(Claims.JwtId); context.TokenUsage = context.Principal.GetTokenUsage(); context.Subject = context.Principal.GetClaim(Claims.Subject); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs index c7d68df1..682d8be4 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs @@ -48,6 +48,7 @@ namespace OpenIddict.Server * Revocation request handling: */ AttachPrincipal.Descriptor, + RevokeToken.Descriptor, /* * Revocation response handling: @@ -602,7 +603,7 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of rejecting revocation requests that specify an invalid token. + /// Contains the logic responsible of rejecting revocation requests that don't specify a valid token. /// public class ValidateToken : IOpenIddictServerHandler { @@ -617,11 +618,7 @@ namespace OpenIddict.Server public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseScopedHandler() - // This handler is deliberately registered with a high order to ensure it runs - // after custom handlers registered with the default order and prevent the token - // endpoint from disclosing whether the revoked token is valid before - // the caller's identity can first be fully verified by the other handlers. - .SetOrder(100_000) + .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) .Build(); /// @@ -638,117 +635,34 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // Note: use the "token_type_hint" parameter specified by the client application - // to try to determine the type of the token sent by the client application. - // See https://tools.ietf.org/html/rfc7662#section-2.1 for more information. - var principal = context.Request.TokenTypeHint switch - { - TokenTypeHints.AccessToken => await DeserializeAccessTokenAsync(), - TokenTypeHints.AuthorizationCode => await DeserializeAuthorizationCodeAsync(), - TokenTypeHints.IdToken => await DeserializeIdentityTokenAsync(), - TokenTypeHints.RefreshToken => await DeserializeRefreshTokenAsync(), - - _ => null - }; - - // Note: if the revoked token can't be found using "token_type_hint", - // the search must be extended to all supported token types. - // See https://tools.ietf.org/html/rfc7662#section-2.1 for more information. - // To avoid calling the same deserialization methods twice, an additional check - // is made to exclude the corresponding call when a token_type_hint was specified. - principal ??= context.Request.TokenTypeHint switch - { - TokenTypeHints.AccessToken => await DeserializeAuthorizationCodeAsync() ?? - await DeserializeIdentityTokenAsync() ?? - await DeserializeRefreshTokenAsync(), - - TokenTypeHints.AuthorizationCode => await DeserializeAccessTokenAsync() ?? - await DeserializeIdentityTokenAsync() ?? - await DeserializeRefreshTokenAsync(), - - TokenTypeHints.IdToken => await DeserializeAccessTokenAsync() ?? - await DeserializeAuthorizationCodeAsync() ?? - await DeserializeRefreshTokenAsync(), - - TokenTypeHints.RefreshToken => await DeserializeAccessTokenAsync() ?? - await DeserializeAuthorizationCodeAsync() ?? - await DeserializeIdentityTokenAsync(), - - _ => await DeserializeAccessTokenAsync() ?? - await DeserializeAuthorizationCodeAsync() ?? - await DeserializeIdentityTokenAsync() ?? - await DeserializeRefreshTokenAsync() - }; + var notification = new ProcessAuthenticationContext(context.Transaction); + await _provider.DispatchAsync(notification); - if (principal == null) + if (notification.IsRequestHandled) { - context.Logger.LogError("The revocation request was rejected because the token was invalid."); - - context.Reject( - error: Errors.InvalidToken, - description: "The specified token is invalid."); - + context.HandleRequest(); return; } - var date = principal.GetExpirationDate(); - if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + else if (notification.IsRequestSkipped) { - context.Logger.LogError("The revocation request was rejected because the token was expired."); - - context.Reject( - error: Errors.InvalidToken, - description: "The specified token is no longer valid."); - + context.SkipRequest(); return; } - // Attach the principal extracted from the token to the parent event context. - context.Principal = principal; - - async ValueTask DeserializeAccessTokenAsync() - { - var notification = new DeserializeAccessTokenContext(context.Transaction) - { - Token = context.Request.Token - }; - - await _provider.DispatchAsync(notification); - return notification.Principal; - } - - async ValueTask DeserializeAuthorizationCodeAsync() - { - var notification = new DeserializeAuthorizationCodeContext(context.Transaction) - { - Token = context.Request.Token - }; - - await _provider.DispatchAsync(notification); - return notification.Principal; - } - - async ValueTask DeserializeIdentityTokenAsync() + else if (notification.IsRejected) { - var notification = new DeserializeIdentityTokenContext(context.Transaction) - { - Token = context.Request.Token - }; - - await _provider.DispatchAsync(notification); - return notification.Principal; + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; } - async ValueTask DeserializeRefreshTokenAsync() - { - var notification = new DeserializeRefreshTokenContext(context.Transaction) - { - Token = context.Request.Token - }; - - await _provider.DispatchAsync(notification); - return notification.Principal; - } + // Attach the security principal extracted from the token to the + // validation context and store it as an environment property. + context.Principal = notification.Principal; + context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal; } } @@ -903,6 +817,111 @@ namespace OpenIddict.Server } } + /// + /// Contains the logic responsible of revoking the token sent by the client application. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class RevokeToken : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RevokeToken() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public RevokeToken([NotNull] IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(AttachPrincipal.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] HandleRevocationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If the received token is not an authorization code or a refresh token, + // return an error to indicate that the token cannot be revoked. + if (context.Principal.IsIdentityToken()) + { + context.Logger.LogError("The revocation request was rejected because identity tokens are not revocable."); + + context.Reject( + error: Errors.UnsupportedTokenType, + description: "The specified token cannot be revoked."); + + return; + } + + // If the received token is an access token, return an error if reference tokens are not enabled. + if (context.Principal.IsAccessToken() && !context.Options.UseReferenceTokens) + { + context.Logger.LogError("The revocation request was rejected because the access token was not revocable."); + + context.Reject( + error: Errors.UnsupportedTokenType, + description: "The specified token cannot be revoked."); + + return; + } + + // Extract the token identifier from the authentication principal. + var identifier = context.Principal.GetInternalTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + context.Logger.LogError("The revocation request was rejected because the token had no internal identifier."); + + context.Reject( + error: Errors.UnsupportedTokenType, + description: "The specified token cannot be revoked."); + + return; + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token == null || await _tokenManager.IsRevokedAsync(token)) + { + context.Logger.LogInformation("The token '{Identifier}' was not revoked because " + + "it was already marked as invalid.", identifier); + + context.Reject( + error: Errors.InvalidToken, + description: "The specified token is invalid."); + + return; + } + + // Try to revoke the token. If an error occurs, return an error. + if (!await _tokenManager.TryRevokeAsync(token)) + { + context.Reject( + error: Errors.UnsupportedTokenType, + description: "The specified token cannot be revoked."); + + return; + } + } + } + /// /// Contains the logic responsible of converting revocation errors to standard empty responses. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs deleted file mode 100644 index b27aff1c..00000000 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs +++ /dev/null @@ -1,610 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Collections.ObjectModel; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; -using static OpenIddict.Abstractions.OpenIddictConstants; -using static OpenIddict.Server.OpenIddictServerEvents; - -namespace OpenIddict.Server -{ - public static partial class OpenIddictServerHandlers - { - public static class Serialization - { - public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( - /* - * Access token serialization: - */ - AttachAccessTokenSerializationParameters.Descriptor, - SerializeJwtBearerToken.Descriptor, - - /* - * Authorization code serialization: - */ - AttachAuthorizationCodeSerializationParameters.Descriptor, - SerializeJwtBearerToken.Descriptor, - - /* - * Identity token serialization: - */ - AttachIdentityTokenSerializationParameters.Descriptor, - SerializeJwtBearerToken.Descriptor, - - /* - * Refresh token serialization: - */ - AttachRefreshTokenSerializationParameters.Descriptor, - SerializeJwtBearerToken.Descriptor, - - /* - * Access token deserialization: - */ - AttachAccessTokenDeserializationParameters.Descriptor, - DeserializeJwtBearerToken.Descriptor, - - /* - * Authorization code deserialization: - */ - AttachAuthorizationCodeDeserializationParameters.Descriptor, - DeserializeJwtBearerToken.Descriptor, - - /* - * Identity token deserialization: - */ - AttachIdentityTokenDeserializationParameters.Descriptor, - DeserializeJwtBearerToken.Descriptor, - - /* - * Authorization code deserialization: - */ - AttachRefreshTokenDeserializationParameters.Descriptor, - DeserializeJwtBearerToken.Descriptor); - - /// - /// Contains the logic responsible of generating a JWT bearer token using IdentityModel. - /// - public class SerializeJwtBearerToken : IOpenIddictServerHandler where TContext : BaseSerializingContext - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler>() - .SetOrder(int.MaxValue - 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] TContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (string.IsNullOrEmpty(context.TokenUsage)) - { - throw new InvalidOperationException("The token usage cannot be null or empty."); - } - - var claims = new Dictionary(StringComparer.Ordinal) - { - [Claims.Private.TokenUsage] = context.TokenUsage - }; - - var destinations = new Dictionary(StringComparer.Ordinal); - foreach (var group in context.Principal.Claims.GroupBy(claim => claim.Type)) - { - var collection = group.ToList(); - - // Note: destinations are attached to claims as special CLR properties. Such properties can't be serialized - // as part of classic JWT tokens. To work around this limitation, claim destinations are added to a special - // claim named oi_cl_dstn that contains a map of all the claims and their attached destinations, if any. - - var set = new HashSet(collection[0].GetDestinations(), StringComparer.OrdinalIgnoreCase); - if (set.Count != 0) - { - // Ensure the other claims of the same type use the same exact destinations. - for (var index = 0; index < collection.Count; index++) - { - if (!set.SetEquals(collection[index].GetDestinations())) - { - throw new InvalidOperationException($"Conflicting destinations for the claim '{group.Key}' were specified."); - } - } - - destinations[group.Key] = set.ToArray(); - } - } - - // Unless at least one claim was added to the claim destinations map, - // don't add the special claim to avoid adding a useless empty claim. - if (destinations.Count != 0) - { - claims[Claims.Private.ClaimDestinations] = destinations; - } - - context.Token = context.SecurityTokenHandler.CreateToken(new SecurityTokenDescriptor - { - Subject = (ClaimsIdentity) context.Principal.Identity, - Claims = new ReadOnlyDictionary(claims), - EncryptingCredentials = context.EncryptingCredentials, - Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.SigningCredentials - }); - - context.HandleSerialization(); - - return default; - } - } - - /// - /// Contains the logic responsible of unprotecting a JWT bearer token using IdentityModel. - /// - public class DeserializeJwtBearerToken : IOpenIddictServerHandler where TContext : BaseDeserializingContext - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler>() - .SetOrder(int.MaxValue - 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] TContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!context.SecurityTokenHandler.CanReadToken(context.Token)) - { - context.Logger.LogTrace("The token '{Token}' was not compatible with the JWT format.", context.Token); - - return default; - } - - try - { - var result = context.SecurityTokenHandler.ValidateToken(context.Token, context.TokenValidationParameters); - if (result == null || !result.IsValid) - { - if (result?.Exception != null) - { - context.Logger.LogTrace(result.Exception, "The JWT token '{Token}' could not be validated.", context.Token); - } - - else - { - context.Logger.LogTrace("The token '{Token}' could not be validated.", context.Token); - } - - return default; - } - - var assertion = ((JsonWebToken) result.SecurityToken)?.InnerToken ?? (JsonWebToken) result.SecurityToken; - - if (!assertion.TryGetPayloadValue(Claims.Private.TokenUsage, out string usage) || - !string.Equals(usage, context.TokenUsage, StringComparison.OrdinalIgnoreCase)) - { - context.Logger.LogDebug("The token usage associated to the token {Token} does not match the expected type."); - context.HandleDeserialization(); - - return default; - } - - context.Principal = new ClaimsPrincipal(result.ClaimsIdentity); - - // Restore the claim destinations from the special oi_cl_dstn claim (represented as a dictionary/JSON object). - if (assertion.TryGetPayloadValue(Claims.Private.ClaimDestinations, out IDictionary definitions)) - { - foreach (var definition in definitions) - { - foreach (var claim in context.Principal.Claims.Where(claim => claim.Type == definition.Key)) - { - claim.SetDestinations(definition.Value); - } - } - } - - context.HandleDeserialization(); - - return default; - } - - catch (Exception exception) - { - context.Logger.LogDebug(exception, "An exception occured while deserializing a token."); - context.HandleDeserialization(); - - return default; - } - } - } - - /// - /// Contains the logic responsible of populating the serialization parameters needed to generate an access token. - /// - public class AttachAccessTokenSerializationParameters : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] SerializeAccessTokenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Options.SigningCredentials.Count == 0) - { - throw new InvalidOperationException("No suitable signing credentials could be found."); - } - - context.EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( - credentials => credentials.Key is SymmetricSecurityKey); - context.SecurityTokenHandler = context.Options.AccessTokenHandler; - context.SigningCredentials = context.Options.SigningCredentials.FirstOrDefault( - credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(); - - return default; - } - } - - /// - /// Contains the logic responsible of populating the serialization parameters needed to generate an authorization code. - /// - public class AttachAuthorizationCodeSerializationParameters : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] SerializeAuthorizationCodeContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Options.EncryptionCredentials.Count == 0) - { - throw new InvalidOperationException("No suitable encryption credentials could be found."); - } - - if (context.Options.SigningCredentials.Count == 0) - { - throw new InvalidOperationException("No suitable signing credentials could be found."); - } - - context.EncryptingCredentials = context.Options.EncryptionCredentials[0]; - context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler; - context.SigningCredentials = context.Options.SigningCredentials.FirstOrDefault( - credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(); - - return default; - } - } - - /// - /// Contains the logic responsible of populating the serialization parameters needed to generate an identity token. - /// - public class AttachIdentityTokenSerializationParameters : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] SerializeIdentityTokenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!context.Options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey)) - { - throw new InvalidOperationException("No suitable signing credentials could be found."); - } - - context.SecurityTokenHandler = context.Options.IdentityTokenHandler; - context.SigningCredentials = context.Options.SigningCredentials.First( - credentials => credentials.Key is AsymmetricSecurityKey); - - return default; - } - } - - /// - /// Contains the logic responsible of populating the serialization parameters needed to generate a refresh token. - /// - public class AttachRefreshTokenSerializationParameters : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] SerializeRefreshTokenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Options.EncryptionCredentials.Count == 0) - { - throw new InvalidOperationException("No suitable encryption credentials could be found."); - } - - if (context.Options.SigningCredentials.Count == 0) - { - throw new InvalidOperationException("No suitable signing credentials could be found."); - } - - context.EncryptingCredentials = context.Options.EncryptionCredentials[0]; - context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler; - context.SigningCredentials = context.Options.SigningCredentials.FirstOrDefault( - credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(); - - return default; - } - } - - /// - /// Contains the logic responsible of populating the deserialization parameters needed to unprotect an access token. - /// - public class AttachAccessTokenDeserializationParameters : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] DeserializeAccessTokenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - context.SecurityTokenHandler = context.Options.AccessTokenHandler; - - context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials - .Select(credentials => credentials.Key); - context.TokenValidationParameters.NameClaimType = Claims.Name; - context.TokenValidationParameters.RoleClaimType = Claims.Role; - context.TokenValidationParameters.TokenDecryptionKeys = context.Options.EncryptionCredentials - .Select(credentials => credentials.Key) - .Where(key => key is SymmetricSecurityKey); - context.TokenValidationParameters.ValidIssuer = context.Issuer?.AbsoluteUri; - context.TokenValidationParameters.ValidateAudience = false; - context.TokenValidationParameters.ValidateLifetime = false; - - return default; - } - } - - /// - /// Contains the logic responsible of populating the deserialization parameters needed to unprotect an authorization code. - /// - public class AttachAuthorizationCodeDeserializationParameters : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] DeserializeAuthorizationCodeContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler; - - context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials - .Select(credentials => credentials.Key); - context.TokenValidationParameters.NameClaimType = Claims.Name; - context.TokenValidationParameters.RoleClaimType = Claims.Role; - context.TokenValidationParameters.TokenDecryptionKeys = context.Options.EncryptionCredentials - .Select(credentials => credentials.Key); - context.TokenValidationParameters.ValidIssuer = context.Issuer?.AbsoluteUri; - context.TokenValidationParameters.ValidateAudience = false; - context.TokenValidationParameters.ValidateLifetime = false; - - return default; - } - } - - /// - /// Contains the logic responsible of populating the deserialization parameters needed to unprotect an identity token. - /// - public class AttachIdentityTokenDeserializationParameters : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] DeserializeIdentityTokenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - context.SecurityTokenHandler = context.Options.IdentityTokenHandler; - - context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials - .Select(credentials => credentials.Key) - .OfType(); - context.TokenValidationParameters.NameClaimType = Claims.Name; - context.TokenValidationParameters.RoleClaimType = Claims.Role; - context.TokenValidationParameters.ValidIssuer = context.Issuer?.AbsoluteUri; - context.TokenValidationParameters.ValidateAudience = false; - context.TokenValidationParameters.ValidateLifetime = false; - - return default; - } - } - - /// - /// Contains the logic responsible of populating the deserialization parameters needed to unprotect a refresh token. - /// - public class AttachRefreshTokenDeserializationParameters : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] DeserializeRefreshTokenContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler; - - context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials - .Select(credentials => credentials.Key); - context.TokenValidationParameters.NameClaimType = Claims.Name; - context.TokenValidationParameters.RoleClaimType = Claims.Role; - context.TokenValidationParameters.TokenDecryptionKeys = context.Options.EncryptionCredentials - .Select(credentials => credentials.Key); - context.TokenValidationParameters.ValidIssuer = context.Issuer?.AbsoluteUri; - context.TokenValidationParameters.ValidateAudience = false; - context.TokenValidationParameters.ValidateLifetime = false; - - return default; - } - } - } - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs index f74f3f54..6f377e55 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs @@ -444,23 +444,29 @@ namespace OpenIddict.Server return; } - var notification = new DeserializeIdentityTokenContext(context.Transaction) - { - Token = context.Request.IdTokenHint - }; - + var notification = new ProcessAuthenticationContext(context.Transaction); await _provider.DispatchAsync(notification); - if (notification.Principal == null) + if (notification.IsRequestHandled) { - context.Reject( - error: Errors.InvalidRequest, - description: "The specified 'id_token_hint' parameter is invalid or malformed."); + context.HandleRequest(); + return; + } + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); return; } - // Note: the expiration date associated with an identity token used as an id_token_hint is deliberately ignored. + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } // Attach the security principal extracted from the identity token to the // validation context and store it as an environment property. diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs index 77be8653..8ae93a95 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs @@ -39,7 +39,7 @@ namespace OpenIddict.Server * Userinfo request validation: */ ValidateAccessTokenParameter.Descriptor, - ValidateAccessToken.Descriptor, + ValidateToken.Descriptor, /* * Userinfo request handling: @@ -392,13 +392,13 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of rejecting userinfo requests that specify an invalid access token. + /// Contains the logic responsible of rejecting userinfo requests that don't specify a valid token. /// - public class ValidateAccessToken : IOpenIddictServerHandler + public class ValidateToken : IOpenIddictServerHandler { private readonly IOpenIddictServerProvider _provider; - public ValidateAccessToken([NotNull] IOpenIddictServerProvider provider) + public ValidateToken([NotNull] IOpenIddictServerProvider provider) => _provider = provider; /// @@ -406,8 +406,8 @@ namespace OpenIddict.Server /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .SetOrder(100_000) + .UseScopedHandler() + .SetOrder(ValidateAccessTokenParameter.Descriptor.Order + 1_000) .Build(); /// @@ -424,38 +424,34 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - var notification = new DeserializeAccessTokenContext(context.Transaction) - { - Token = context.Request.AccessToken - }; - + var notification = new ProcessAuthenticationContext(context.Transaction); await _provider.DispatchAsync(notification); - if (notification.Principal == null) + if (notification.IsRequestHandled) { - context.Logger.LogError("The userinfo request was rejected because the access token was invalid."); - - context.Reject( - error: Errors.InvalidToken, - description: "The specified access token is invalid."); - + context.HandleRequest(); return; } - var date = notification.Principal.GetExpirationDate(); - if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + else if (notification.IsRequestSkipped) { - context.Logger.LogError("The userinfo request was rejected because the access token was expired."); + context.SkipRequest(); + return; + } + else if (notification.IsRejected) + { context.Reject( - error: Errors.InvalidToken, - description: "The specified access token is no longer valid."); - + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); return; } - // Attach the principal extracted from the authorization code to the parent event context. + // Attach the security principal extracted from the token to the + // validation context and store it as an environment property. context.Principal = notification.Principal; + context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal; } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 8b1bcade..eb51dcf8 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -5,6 +5,7 @@ */ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Linq; @@ -30,47 +31,72 @@ namespace OpenIddict.Server /* * Authentication processing: */ - AttachAmbientPrincipal.Descriptor, + ValidateAuthenticationDemand.Descriptor, + ValidateReferenceToken.Descriptor, + ValidateSelfContainedToken.Descriptor, + ValidatePrincipal.Descriptor, + ValidateTokenEntry.Descriptor, + ValidateAuthorizationEntry.Descriptor, + ValidateExpirationDate.Descriptor, /* * Challenge processing: */ AttachDefaultChallengeError.Descriptor, - + /* * Sign-in processing: */ ValidateSigninResponse.Descriptor, + RestoreInternalClaims.Descriptor, AttachDefaultScopes.Descriptor, AttachDefaultPresenters.Descriptor, InferResources.Descriptor, EvaluateReturnedTokens.Descriptor, AttachAuthorization.Descriptor, - AttachAccessToken.Descriptor, - AttachAuthorizationCode.Descriptor, - AttachRefreshToken.Descriptor, - AttachIdentityToken.Descriptor) + + PrepareAccessTokenPrincipal.Descriptor, + PrepareAuthorizationCodePrincipal.Descriptor, + PrepareRefreshTokenPrincipal.Descriptor, + PrepareIdentityTokenPrincipal.Descriptor, + + RedeemTokenEntry.Descriptor, + RevokeRollingTokenEntries.Descriptor, + ExtendRefreshTokenEntry.Descriptor, + + AttachReferenceAccessToken.Descriptor, + AttachReferenceAuthorizationCode.Descriptor, + AttachReferenceRefreshToken.Descriptor, + + CreateSelfContainedAuthorizationCodeEntry.Descriptor, + CreateSelfContainedRefreshTokenEntry.Descriptor, + + AttachSelfContainedAccessToken.Descriptor, + AttachSelfContainedAuthorizationCode.Descriptor, + AttachSelfContainedRefreshToken.Descriptor, + + AttachTokenDigests.Descriptor, + AttachSelfContainedIdentityToken.Descriptor) .AddRange(Authentication.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Introspection.DefaultHandlers) .AddRange(Revocation.DefaultHandlers) - .AddRange(Serialization.DefaultHandlers) .AddRange(Session.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers); /// - /// Contains the logic responsible of attaching the ambient principal resolved for the current request. + /// Contains the logic responsible of rejecting authentication demands made from unsupported endpoints. /// - public class AttachAmbientPrincipal : IOpenIddictServerHandler + public class ValidateAuthenticationDemand : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); @@ -91,93 +117,250 @@ namespace OpenIddict.Server switch (context.EndpointType) { case OpenIddictServerEndpointType.Authorization: + case OpenIddictServerEndpointType.Introspection: case OpenIddictServerEndpointType.Logout: + case OpenIddictServerEndpointType.Revocation: case OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType(): case OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType(): case OpenIddictServerEndpointType.Userinfo: - { - if (context.Transaction.Properties.TryGetValue(Properties.AmbientPrincipal, out var principal)) - { - context.Principal = (ClaimsPrincipal) principal; - } - return default; - } - default: throw new InvalidOperationException("An identity cannot be extracted from this request."); + default: throw new InvalidOperationException("No identity cannot be extracted from this request."); } } } /// - /// Contains the logic responsible of ensuring that the challenge response contains an appropriate error. + /// Contains the logic responsible of rejecting authentication demands that use an invalid reference token. + /// Note: this handler is not used when the degraded mode is enabled. /// - public class AttachDefaultChallengeError : IOpenIddictServerHandler + public class ValidateReferenceToken : IOpenIddictServerHandler { + private readonly IOpenIddictTokenManager _tokenManager; + + public ValidateReferenceToken() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateReferenceToken([NotNull] IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateAuthenticationDemand.Descriptor.Order + 1_000) .Build(); - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public ValueTask HandleAsync([NotNull] ProcessChallengeContext context) + public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - if (string.IsNullOrEmpty(context.Response.Error)) + // If a principal was already attached, don't overwrite it. + if (context.Principal != null) { - context.Response.Error = context.EndpointType switch + return; + } + + var identifier = context.EndpointType switch + { + OpenIddictServerEndpointType.Introspection => context.Request.Token, + OpenIddictServerEndpointType.Revocation => context.Request.Token, + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => context.Request.Code, + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => context.Request.RefreshToken, + + OpenIddictServerEndpointType.Userinfo => context.Request.AccessToken, + + _ => null + }; + + // If the token cannot be validated, don't return an error to allow another handle to validate it. + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + // If the reference token cannot be found, return a generic error. + var token = await _tokenManager.FindByReferenceIdAsync(identifier); + if (token == null || !await IsTokenTypeValidAsync(token)) + { + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => "The specified authorization code is not valid.", + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => "The specified refresh token is not valid.", + + _ => "The specified token is not valid." + }); + + return; + } + + var payload = await _tokenManager.GetPayloadAsync(token); + if (string.IsNullOrEmpty(payload)) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The payload associated with a reference token cannot be retrieved.") + .Append("This may indicate that the token entry was corrupted.") + .ToString()); + } + + // If the token cannot be validated, don't return an error to allow another handle to validate it. + if (!context.Options.SecurityTokenHandler.CanReadToken(payload)) + { + return; + } + + var result = context.EndpointType switch + { + OpenIddictServerEndpointType.Introspection => await ValidateAnyTokenAsync(payload), + OpenIddictServerEndpointType.Revocation => await ValidateAnyTokenAsync(payload), + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => await ValidateTokenAsync(payload, TokenUsages.AuthorizationCode), + + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => await ValidateTokenAsync(payload, TokenUsages.RefreshToken), + + OpenIddictServerEndpointType.Userinfo => await ValidateTokenAsync(payload, TokenUsages.AccessToken), + + _ => new TokenValidationResult { IsValid = false } + }; + + // If the token cannot be validated, don't return an error to allow another handle to validate it. + if (result.ClaimsIdentity == null) + { + return; + } + + // Attach the principal extracted from the authorization code to the parent event context + // and restore the creation/expiration dates/identifiers from the token entry metadata. + context.Principal = new ClaimsPrincipal(result.ClaimsIdentity) + .SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) + .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) + .SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetInternalTokenId(await _tokenManager.GetIdAsync(token)) + .SetClaim(Claims.Private.TokenUsage, await _tokenManager.GetTypeAsync(token)); + + async ValueTask ValidateTokenAsync(string token, string type) + { + var parameters = new TokenValidationParameters { - OpenIddictServerEndpointType.Authorization => Errors.AccessDenied, - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - OpenIddictServerEndpointType.Userinfo => Errors.InvalidToken, + NameClaimType = Claims.Name, + PropertyBag = new Dictionary { [Claims.Private.TokenUsage] = type }, + RoleClaimType = Claims.Role, + ValidIssuer = context.Issuer?.AbsoluteUri, + ValidateAudience = false, + ValidateLifetime = false + }; - _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") + parameters.IssuerSigningKeys = type switch + { + TokenUsages.AccessToken => context.Options.SigningCredentials.Select(credentials => credentials.Key), + TokenUsages.AuthorizationCode => context.Options.SigningCredentials.Select(credentials => credentials.Key), + TokenUsages.RefreshToken => context.Options.SigningCredentials.Select(credentials => credentials.Key), + + _ => Array.Empty() + }; + + parameters.TokenDecryptionKeys = type switch + { + TokenUsages.AuthorizationCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), + TokenUsages.RefreshToken => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), + + TokenUsages.AccessToken => context.Options.EncryptionCredentials + .Select(credentials => credentials.Key) + .Where(key => key is SymmetricSecurityKey), + + _ => Array.Empty() }; + + return await context.Options.SecurityTokenHandler.ValidateTokenStringAsync(token, parameters); } - if (string.IsNullOrEmpty(context.Response.ErrorDescription)) + async ValueTask ValidateAnyTokenAsync(string token) { - context.Response.ErrorDescription = context.EndpointType switch + var result = await ValidateTokenAsync(token, TokenUsages.AccessToken); + if (result.IsValid) { - OpenIddictServerEndpointType.Authorization => "The authorization was denied by the resource owner.", - OpenIddictServerEndpointType.Token => "The token request was rejected by the authorization server.", - OpenIddictServerEndpointType.Userinfo => "The access token is not valid or cannot be used to retrieve user information.", + return result; + } - _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") - }; + result = await ValidateTokenAsync(token, TokenUsages.RefreshToken); + if (result.IsValid) + { + return result; + } + + result = await ValidateTokenAsync(token, TokenUsages.AuthorizationCode); + if (result.IsValid) + { + return result; + } + + return new TokenValidationResult { IsValid = false }; } - return default; + async ValueTask IsTokenTypeValidAsync(object token) => context.EndpointType switch + { + // All types of tokens are accepted by the introspection and revocation endpoints. + OpenIddictServerEndpointType.Introspection => true, + OpenIddictServerEndpointType.Revocation => true, + + OpenIddictServerEndpointType.Token => await _tokenManager.GetTypeAsync(token) switch + { + TokenUsages.AuthorizationCode when context.Request.IsAuthorizationCodeGrantType() => true, + TokenUsages.RefreshToken when context.Request.IsRefreshTokenGrantType() => true, + + _ => false + }, + + OpenIddictServerEndpointType.Userinfo => await _tokenManager.GetTypeAsync(token) switch + { + TokenUsages.AccessToken => true, + + _ => false + }, + + _ => false + }; } } /// - /// Contains the logic responsible of ensuring that the sign-in response - /// is compatible with the type of the endpoint that handled the request. + /// Contains the logic responsible of rejecting authentication demands that specify an invalid self-contained token. /// - public class ValidateSigninResponse : IOpenIddictServerHandler + public class ValidateSelfContainedToken : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateReferenceToken.Descriptor.Order + 1_000) .Build(); /// @@ -187,54 +370,166 @@ namespace OpenIddict.Server /// /// A that can be used to monitor the asynchronous operation. /// - public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - switch (context.EndpointType) + // If a principal was already attached, don't overwrite it. + if (context.Principal != null) { - case OpenIddictServerEndpointType.Authorization: - case OpenIddictServerEndpointType.Token: - break; + return; + } - default: throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); + var token = context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization => context.Request.IdTokenHint, + OpenIddictServerEndpointType.Logout => context.Request.IdTokenHint, + + OpenIddictServerEndpointType.Introspection => context.Request.Token, + OpenIddictServerEndpointType.Revocation => context.Request.Token, + + // This handler doesn't handle reference tokens. + _ when context.Options.UseReferenceTokens => null, + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => context.Request.Code, + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => context.Request.RefreshToken, + + OpenIddictServerEndpointType.Userinfo => context.Request.AccessToken, + + _ => null + }; + + // If the token cannot be validated, don't return an error to allow another handle to validate it. + if (string.IsNullOrEmpty(token) || !context.Options.SecurityTokenHandler.CanReadToken(token)) + { + return; } - if (context.Principal.Identity == null || !context.Principal.Identity.IsAuthenticated) + var result = context.EndpointType switch { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The specified principal doesn't contain a valid or authenticated identity.") - .Append("Make sure that both 'ClaimsPrincipal.Identity' and 'ClaimsPrincipal.Identity.AuthenticationType' ") - .Append("are not null and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'true'.") - .ToString()); + OpenIddictServerEndpointType.Authorization => await ValidateTokenAsync(token, TokenUsages.IdToken), + OpenIddictServerEndpointType.Logout => await ValidateTokenAsync(token, TokenUsages.IdToken), + + // When reference tokens are enabled, this handler can only validate id_tokens. + OpenIddictServerEndpointType.Introspection when context.Options.UseReferenceTokens + => await ValidateTokenAsync(token, TokenUsages.IdToken), + + OpenIddictServerEndpointType.Revocation when context.Options.UseReferenceTokens + => await ValidateTokenAsync(token, TokenUsages.IdToken), + + _ when context.Options.UseReferenceTokens => new TokenValidationResult { IsValid = false }, + + OpenIddictServerEndpointType.Introspection => await ValidateAnyTokenAsync(token), + OpenIddictServerEndpointType.Revocation => await ValidateAnyTokenAsync(token), + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => await ValidateTokenAsync(token, TokenUsages.AuthorizationCode), + + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => await ValidateTokenAsync(token, TokenUsages.RefreshToken), + + OpenIddictServerEndpointType.Userinfo => await ValidateTokenAsync(token, TokenUsages.AccessToken), + + _ => new TokenValidationResult { IsValid = false } + }; + + // If the token cannot be validated, don't return an error to allow another handle to validate it. + if (result.ClaimsIdentity == null) + { + return; } - if (string.IsNullOrEmpty(context.Principal.GetClaim(Claims.Subject))) + // Attach the principal extracted from the token to the parent event context. + context.Principal = new ClaimsPrincipal(result.ClaimsIdentity); + + async ValueTask ValidateTokenAsync(string token, string type) { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The security principal was rejected because the mandatory subject claim was missing.") - .ToString()); + var parameters = new TokenValidationParameters + { + NameClaimType = Claims.Name, + PropertyBag = new Dictionary { [Claims.Private.TokenUsage] = type }, + RoleClaimType = Claims.Role, + ValidIssuer = context.Issuer?.AbsoluteUri, + ValidateAudience = false, + ValidateLifetime = false + }; + + parameters.IssuerSigningKeys = type switch + { + TokenUsages.AccessToken => context.Options.SigningCredentials.Select(credentials => credentials.Key), + TokenUsages.AuthorizationCode => context.Options.SigningCredentials.Select(credentials => credentials.Key), + TokenUsages.RefreshToken => context.Options.SigningCredentials.Select(credentials => credentials.Key), + + TokenUsages.IdToken => context.Options.SigningCredentials + .Select(credentials => credentials.Key) + .OfType(), + + _ => Array.Empty() + }; + + parameters.TokenDecryptionKeys = type switch + { + TokenUsages.AuthorizationCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), + TokenUsages.RefreshToken => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), + + TokenUsages.AccessToken => context.Options.EncryptionCredentials + .Select(credentials => credentials.Key) + .Where(key => key is SymmetricSecurityKey), + + _ => Array.Empty() + }; + + return await context.Options.SecurityTokenHandler.ValidateTokenStringAsync(token, parameters); } - return default; + async ValueTask ValidateAnyTokenAsync(string token) + { + var result = await ValidateTokenAsync(token, TokenUsages.AccessToken); + if (result.IsValid) + { + return result; + } + + result = await ValidateTokenAsync(token, TokenUsages.RefreshToken); + if (result.IsValid) + { + return result; + } + + result = await ValidateTokenAsync(token, TokenUsages.AuthorizationCode); + if (result.IsValid) + { + return result; + } + + result = await ValidateTokenAsync(token, TokenUsages.IdToken); + if (result.IsValid) + { + return result; + } + + return new TokenValidationResult { IsValid = false }; + } } } /// - /// Contains the logic responsible of attaching default scopes to the authentication principal. + /// Contains the logic responsible of rejecting authentication demands for which no valid principal was resolved. /// - public class AttachDefaultScopes : IOpenIddictServerHandler + public class ValidatePrincipal : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateSigninResponse.Descriptor.Order + 1_000) + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateSelfContainedToken.Descriptor.Order + 1_000) .Build(); /// @@ -244,19 +539,36 @@ namespace OpenIddict.Server /// /// A that can be used to monitor the asynchronous operation. /// - public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. - // Note: the application is allowed to specify a different "scopes": in this case, - // don't replace the "scopes" property stored in the authentication ticket. - if (!context.Principal.HasScope() && context.Request.HasScope(Scopes.OpenId)) + if (context.Principal == null) { - context.Principal.SetScopes(Scopes.OpenId); + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization => "The specified identity token hint is not valid.", + OpenIddictServerEndpointType.Logout => "The specified identity token hint is not valid.", + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => "The specified authorization code is not valid.", + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => "The specified refresh token is not valid.", + + _ => "The specified token is not valid." + }); + + + return default; } return default; @@ -264,17 +576,1430 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of attaching default presenters to the authentication principal. + /// Contains the logic responsible of rejecting authentication demands that + /// use a token whose entry is no longer valid (e.g was revoked). + /// Note: this handler is not used when the degraded mode is enabled. /// - public class AttachDefaultPresenters : IOpenIddictServerHandler + public class ValidateTokenEntry : IOpenIddictServerHandler { + private readonly IOpenIddictAuthorizationManager _authorizationManager; + private readonly IOpenIddictTokenManager _tokenManager; + + public ValidateTokenEntry() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateTokenEntry( + [NotNull] IOpenIddictAuthorizationManager authorizationManager, + [NotNull] IOpenIddictTokenManager tokenManager) + { + _authorizationManager = authorizationManager; + _tokenManager = tokenManager; + } + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(AttachDefaultScopes.Descriptor.Order + 1_000) + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidatePrincipal.Descriptor.Order + 1_000) + .Build(); + + public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Extract the token identifier from the authentication principal. + // If no token identifier can be found, this indicates that the token + // has no backing database entry (e.g an access token or an identity token). + var identifier = context.Principal.GetInternalTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + // If the token entry cannot be found, return a generic error. + var token = await _tokenManager.FindByIdAsync(identifier); + if (token == null) + { + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => "The specified authorization code is not valid.", + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => "The specified refresh token is not valid.", + + _ => "The specified token is not valid." + }); + + return; + } + + // If the authorization code/refresh token is already marked as redeemed, this may indicate that + // it was compromised. In this case, revoke the authorization and all the associated tokens. + // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. + if (context.EndpointType == OpenIddictServerEndpointType.Token && + (context.Request.IsAuthorizationCodeGrantType() || context.Request.IsRefreshTokenGrantType()) && + await _tokenManager.IsRedeemedAsync(token)) + { + await TryRevokeAuthorizationChainAsync(token); + + context.Logger.LogError("The token '{Identifier}' has already been redeemed.", identifier); + + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => "The specified authorization code has already been redeemed.", + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => "The specified refresh token has already been redeemed.", + + _ => "The specified token has already been redeemed." + }); + + return; + } + + if (!await _tokenManager.IsValidAsync(token)) + { + context.Logger.LogError("The token '{Identifier}' was no longer valid.", identifier); + + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => "The specified authorization code is no longer valid.", + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => "The specified refresh token is no longer valid.", + + _ => "The specified token is no longer valid." + }); + + return; + } + + // Restore the creation/expiration dates/identifiers from the token entry metadata. + context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) + .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) + .SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetInternalTokenId(await _tokenManager.GetIdAsync(token)) + .SetClaim(Claims.Private.TokenUsage, await _tokenManager.GetTypeAsync(token)); + + async ValueTask TryRevokeAuthorizationChainAsync(object token) + { + // First, mark the redeemed token submitted by the client as revoked. + await _tokenManager.TryRevokeAsync(token); + + var identifier = context.Principal.GetInternalAuthorizationId(); + if (context.Options.DisableAuthorizationStorage || string.IsNullOrEmpty(identifier)) + { + return; + } + + // Then, try to revoke the authorization and the associated token entries. + + var authorization = await _authorizationManager.FindByIdAsync(identifier); + if (authorization != null) + { + await _authorizationManager.TryRevokeAsync(authorization); + } + + await using var enumerator = _tokenManager.FindByAuthorizationIdAsync(identifier).GetAsyncEnumerator(); + while (await enumerator.MoveNextAsync()) + { + // Don't change the status of the token used in the token request. + if (string.Equals(context.Principal.GetInternalTokenId(), + await _tokenManager.GetIdAsync(enumerator.Current), StringComparison.Ordinal)) + { + continue; + } + + await _tokenManager.TryRevokeAsync(token); + } + } + } + } + + /// + /// Contains the logic responsible of authentication demands a token whose + /// associated authorization entry is no longer valid (e.g was revoked). + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateAuthorizationEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictAuthorizationManager _authorizationManager; + + public ValidateAuthorizationEntry() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateAuthorizationEntry([NotNull] IOpenIddictAuthorizationManager authorizationManager) + => _authorizationManager = authorizationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) + .Build(); + + public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var identifier = context.Principal.GetInternalAuthorizationId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + var authorization = await _authorizationManager.FindByIdAsync(identifier); + if (authorization == null || !await _authorizationManager.IsValidAsync(authorization)) + { + context.Logger.LogError("The authorization associated with token '{Identifier}' " + + "was no longer valid.", context.Principal.GetInternalTokenId()); + + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => "The authorization associated with the authorization code is no longer valid.", + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => "The authorization associated with the refresh token is no longer valid.", + + _ => "The authorization associated with the token is no longer valid." + }); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands that use an expired token. + /// + public class ValidateExpirationDate : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Don't validate the lifetime of id_tokens used as id_token_hints. + switch (context.EndpointType) + { + case OpenIddictServerEndpointType.Authorization: + case OpenIddictServerEndpointType.Logout: + return default; + } + + var date = context.Principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => "The specified authorization code is no longer valid.", + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => "The specified refresh token is no longer valid.", + + _ => "The specified token is no longer valid." + }); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of ensuring that the challenge response contains an appropriate error. + /// + public class AttachDefaultChallengeError : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessChallengeContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Response.Error)) + { + context.Response.Error = context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization => Errors.AccessDenied, + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + OpenIddictServerEndpointType.Userinfo => Errors.InvalidToken, + + _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") + }; + } + + if (string.IsNullOrEmpty(context.Response.ErrorDescription)) + { + context.Response.ErrorDescription = context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization => "The authorization was denied by the resource owner.", + OpenIddictServerEndpointType.Token => "The token request was rejected by the authorization server.", + OpenIddictServerEndpointType.Userinfo => "The access token is not valid or cannot be used to retrieve user information.", + + _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") + }; + } + + return default; + } + } + + /// + /// Contains the logic responsible of ensuring that the sign-in response + /// is compatible with the type of the endpoint that handled the request. + /// + public class ValidateSigninResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + switch (context.EndpointType) + { + case OpenIddictServerEndpointType.Authorization: + case OpenIddictServerEndpointType.Token: + break; + + default: throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); + } + + if (context.Principal.Identity == null || !context.Principal.Identity.IsAuthenticated) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The specified principal doesn't contain a valid or authenticated identity.") + .Append("Make sure that both 'ClaimsPrincipal.Identity' and 'ClaimsPrincipal.Identity.AuthenticationType' ") + .Append("are not null and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'true'.") + .ToString()); + } + + if (string.IsNullOrEmpty(context.Principal.GetClaim(Claims.Subject))) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The security principal was rejected because the mandatory subject claim was missing.") + .ToString()); + } + + return default; + } + } + + /// + /// Contains the logic responsible of re-attaching internal claims to the authentication principal. + /// + public class RestoreInternalClaims : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateSigninResponse.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Token) + { + return default; + } + + if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType()) + { + return default; + } + + if (!context.Transaction.Properties.TryGetValue(Properties.OriginalPrincipal, out var principal)) + { + throw new InvalidOperationException("The original principal cannot be resolved from the transaction."); + } + + // Restore the internal claims resolved from the authorization code/refresh token. + foreach (var claims in ((ClaimsPrincipal) principal).Claims + .Where(claim => claim.Type.StartsWith(Claims.Prefixes.Private)) + .GroupBy(claim => claim.Type)) + { + // If the specified principal already contains one claim of the iterated type, ignore them. + if (context.Principal.Claims.Any(claim => claim.Type == claims.Key)) + { + continue; + } + + ((ClaimsIdentity) context.Principal.Identity).AddClaims(claims); + } + + return default; + } + } + + /// + /// Contains the logic responsible of attaching default scopes to the authentication principal. + /// + public class AttachDefaultScopes : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(RestoreInternalClaims.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Always include the "openid" scope when the developer doesn't explicitly call SetScopes. + // Note: the application is allowed to specify a different "scopes": in this case, + // don't replace the "scopes" property stored in the authentication ticket. + if (!context.Principal.HasScope() && context.Request.HasScope(Scopes.OpenId)) + { + context.Principal.SetScopes(Scopes.OpenId); + } + + return default; + } + } + + /// + /// Contains the logic responsible of attaching default presenters to the authentication principal. + /// + public class AttachDefaultPresenters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachDefaultScopes.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Add the validated client_id to the list of authorized presenters, + // unless the presenters were explicitly set by the developer. + if (!context.Principal.HasPresenter() && !string.IsNullOrEmpty(context.ClientId)) + { + context.Principal.SetPresenters(context.ClientId); + } + + return default; + } + } + + /// + /// Contains the logic responsible of inferring resources from the audience claims if necessary. + /// + public class InferResources : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachDefaultPresenters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // When a "resources" property cannot be found in the ticket, infer it from the "audiences" property. + if (context.Principal.HasAudience() && !context.Principal.HasResource()) + { + context.Principal.SetResources(context.Principal.GetAudiences()); + } + + // Reset the audiences collection, as it's later set, based on the token type. + context.Principal.SetAudiences(Array.Empty()); + + return default; + } + } + + /// + /// Contains the logic responsible of selecting the token types returned to the client application. + /// + public class EvaluateReturnedTokens : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(InferResources.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.IncludeAccessToken = context.EndpointType switch + { + // For authorization requests, return an access token if a response type containing token was specified. + OpenIddictServerEndpointType.Authorization => context.Request.HasResponseType(ResponseTypes.Token), + + // For token requests, always return an access token. + OpenIddictServerEndpointType.Token => true, + + _ => false + }; + + context.IncludeAuthorizationCode = context.EndpointType switch + { + // For authorization requests, return an authorization code if a response type containing code was specified. + OpenIddictServerEndpointType.Authorization => context.Request.HasResponseType(ResponseTypes.Code), + + // For token requests, prevent an authorization code from being returned as this type of token + // cannot be issued from the token endpoint in the standard OAuth 2.0/OpenID Connect flows. + OpenIddictServerEndpointType.Token => false, + + _ => false + }; + + context.IncludeRefreshToken = context.EndpointType switch + { + // For authorization requests, prevent a refresh token from being returned as OAuth 2.0 + // explicitly disallows returning a refresh token from the authorization endpoint. + // See https://tools.ietf.org/html/rfc6749#section-4.2.2 for more information. + OpenIddictServerEndpointType.Authorization => false, + + // For token requests, never return a refresh token if the offline_access scope was not granted. + OpenIddictServerEndpointType.Token when !context.Principal.HasScope(Scopes.OfflineAccess) => false, + + // For grant_type=refresh_token token requests, only return a refresh token if rolling tokens are enabled. + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() => context.Options.UseRollingTokens, + + // For token requests that don't meet the previous criteria, allow a refresh token to be returned. + OpenIddictServerEndpointType.Token => true, + + _ => false + }; + + context.IncludeIdentityToken = context.EndpointType switch + { + // For authorization requests, return an identity token if a response type containing code + // was specified and if the openid scope was explicitly or implicitly granted. + OpenIddictServerEndpointType.Authorization => context.Principal.HasScope(Scopes.OpenId) && + context.Request.HasResponseType(ResponseTypes.IdToken), + + // For token requests, only return an identity token if the openid scope was granted. + OpenIddictServerEndpointType.Token => context.Principal.HasScope(Scopes.OpenId), + + _ => false + }; + + return default; + } + } + + /// + /// Contains the logic responsible of creating an ad-hoc authorization, if necessary. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class AttachAuthorization : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictAuthorizationManager _authorizationManager; + + public AttachAuthorization() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public AttachAuthorization( + [NotNull] IOpenIddictApplicationManager applicationManager, + [NotNull] IOpenIddictAuthorizationManager authorizationManager) + { + _applicationManager = applicationManager; + _authorizationManager = authorizationManager; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(EvaluateReturnedTokens.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If no authorization code or refresh token is returned, don't create an authorization. + if (!context.IncludeAuthorizationCode && !context.IncludeRefreshToken) + { + return; + } + + // If an authorization identifier was explicitly specified, don't create an ad-hoc authorization. + if (!string.IsNullOrEmpty(context.Principal.GetInternalAuthorizationId())) + { + return; + } + + var descriptor = new OpenIddictAuthorizationDescriptor + { + Principal = context.Principal, + Status = Statuses.Valid, + Subject = context.Principal.GetClaim(Claims.Subject), + Type = AuthorizationTypes.AdHoc + }; + + descriptor.Scopes.UnionWith(context.Principal.GetScopes()); + + // If the client application is known, associate it to the authorization. + if (!string.IsNullOrEmpty(context.Request.ClientId)) + { + var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The application entry cannot be found in the database."); + } + + descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + } + + var authorization = await _authorizationManager.CreateAsync(descriptor); + if (authorization == null) + { + return; + } + + var identifier = await _authorizationManager.GetIdAsync(authorization); + + if (string.IsNullOrEmpty(context.Request.ClientId)) + { + context.Logger.LogInformation("An ad hoc authorization was automatically created and " + + "associated with an unknown application: {Identifier}.", identifier); + } + + else + { + context.Logger.LogInformation("An ad hoc authorization was automatically created and " + + "associated with the '{ClientId}' application: {Identifier}.", + context.Request.ClientId, identifier); + } + + // Attach the unique identifier of the ad hoc authorization to the authentication principal + // so that it is attached to all the derived tokens, allowing batched revocations support. + context.Principal.SetInternalAuthorizationId(identifier); + } + } + + /// + /// Contains the logic responsible of preparing and attaching the claims principal + /// used to generate the access token, if one is going to be returned. + /// + public class PrepareAccessTokenPrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachAuthorization.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Create a new principal containing only the filtered claims. + // Actors identities are also filtered (delegation scenarios). + var principal = context.Principal.Clone(claim => + { + // Never exclude the subject claim. + if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Always exclude private claims, whose values must generally be kept secret. + if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Claims whose destination is not explicitly referenced or doesn't + // contain "access_token" are not included in the access token. + if (!claim.HasDestination(Destinations.AccessToken)) + { + context.Logger.LogDebug("'{Claim}' was excluded from the access token claims.", claim.Type); + + return false; + } + + return true; + }); + + // Remove the destinations from the claim properties. + foreach (var claim in principal.Claims) + { + claim.Properties.Remove(OpenIddictConstants.Properties.Destinations); + } + + // Note: the internal token identifier is automatically reset to ensure + // the identifier inherited from the parent token is not automatically reused. + principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()) + .SetCreationDate(DateTimeOffset.UtcNow) + .SetInternalTokenId(identifier: null); + + var lifetime = context.Principal.GetAccessTokenLifetime() ?? context.Options.AccessTokenLifetime; + if (lifetime.HasValue) + { + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + } + + // Set the public audiences collection using the private resource claims stored in the principal. + principal.SetAudiences(context.Principal.GetResources()); + + // Set the authorized party using the first presenters (typically the client identifier), if available. + principal.SetClaim(Claims.AuthorizedParty, context.Principal.GetPresenters().FirstOrDefault()); + + // Set the public scope claim using the private scope claims from the principal. + // Note: scopes are deliberately formatted as a single space-separated + // string to respect the usual representation of the standard scope claim. + // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-02. + principal.SetClaim(Claims.Scope, string.Join(" ", context.Principal.GetScopes())); + + // When receiving a grant_type=refresh_token request, determine whether the client application + // requests a limited set of scopes and immediately replace the scopes collection if necessary. + if (context.EndpointType == OpenIddictServerEndpointType.Token && + context.Request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(context.Request.Scope)) + { + var scopes = context.Request.GetScopes(); + if (scopes.Count != 0) + { + context.Logger.LogDebug("The access token scopes will be limited to the scopes " + + "requested by the client application: {Scopes}.", scopes); + + principal.SetClaim(Claims.Scope, string.Join(" ", scopes.Intersect(context.Principal.GetScopes()))); + } + } + + context.AccessTokenPrincipal = principal; + + return default; + } + } + + /// + /// Contains the logic responsible of preparing and attaching the claims principal + /// used to generate the authorization code, if one is going to be returned. + /// + public class PrepareAuthorizationCodePrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(PrepareAccessTokenPrincipal.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the internal token identifier is automatically reset to ensure + // the identifier inherited from the parent token is not automatically reused. + var principal = context.Principal.Clone(_ => true) + .SetClaim(Claims.JwtId, Guid.NewGuid().ToString()) + .SetCreationDate(DateTimeOffset.UtcNow) + .SetInternalTokenId(identifier: null); + + var lifetime = context.Principal.GetAuthorizationCodeLifetime() ?? context.Options.AuthorizationCodeLifetime; + if (lifetime.HasValue) + { + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + } + + // Attach the redirect_uri to allow for later comparison when + // receiving a grant_type=authorization_code token request. + if (!string.IsNullOrEmpty(context.Request.RedirectUri)) + { + principal.SetClaim(Claims.Private.RedirectUri, context.Request.RedirectUri); + } + + // Attach the code challenge and the code challenge methods to allow the ValidateCodeVerifier + // handler to validate the code verifier sent by the client as part of the token request. + if (!string.IsNullOrEmpty(context.Request.CodeChallenge)) + { + principal.SetClaim(Claims.Private.CodeChallenge, context.Request.CodeChallenge); + + // Default to S256 if no explicit code challenge method was specified. + principal.SetClaim(Claims.Private.CodeChallengeMethod, + !string.IsNullOrEmpty(context.Request.CodeChallengeMethod) ? + context.Request.CodeChallengeMethod : CodeChallengeMethods.Sha256); + } + + // Attach the nonce so that it can be later returned by + // the token endpoint as part of the JWT identity token. + if (!string.IsNullOrEmpty(context.Request.Nonce)) + { + principal.SetClaim(Claims.Private.Nonce, context.Request.Nonce); + } + + context.AuthorizationCodePrincipal = principal; + + return default; + } + } + + /// + /// Contains the logic responsible of preparing and attaching the claims principal + /// used to generate the refresh token, if one is going to be returned. + /// + public class PrepareRefreshTokenPrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(PrepareAuthorizationCodePrincipal.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the internal token identifier is automatically reset to ensure + // the identifier inherited from the parent token is not automatically reused. + var principal = context.Principal.Clone(_ => true) + .SetClaim(Claims.JwtId, Guid.NewGuid().ToString()) + .SetCreationDate(DateTimeOffset.UtcNow) + .SetInternalTokenId(identifier: null); + + // When sliding expiration is disabled, the expiration date of generated refresh tokens is fixed + // and must exactly match the expiration date of the refresh token used in the token request. + if (context.EndpointType == OpenIddictServerEndpointType.Token && + context.Request.IsRefreshTokenGrantType() && !context.Options.UseSlidingExpiration) + { + if (!context.Transaction.Properties.TryGetValue(Properties.OriginalPrincipal, out var property)) + { + throw new InvalidOperationException("The original principal cannot be resolved from the transaction."); + } + + principal.SetExpirationDate(((ClaimsPrincipal) property).GetExpirationDate()); + } + + else + { + var lifetime = context.Principal.GetRefreshTokenLifetime() ?? context.Options.RefreshTokenLifetime; + if (lifetime.HasValue) + { + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + } + } + + context.RefreshTokenPrincipal = principal; + + return default; + } + } + + /// + /// Contains the logic responsible of preparing and attaching the claims principal + /// used to generate the identity token, if one is going to be returned. + /// + public class PrepareIdentityTokenPrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(PrepareRefreshTokenPrincipal.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Replace the principal by a new one containing only the filtered claims. + // Actors identities are also filtered (delegation scenarios). + var principal = context.Principal.Clone(claim => + { + // Never exclude the subject claim. + if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // Always exclude private claims, whose values must generally be kept secret. + if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Claims whose destination is not explicitly referenced or doesn't + // contain "id_token" are not included in the identity token. + if (!claim.HasDestination(Destinations.IdentityToken)) + { + context.Logger.LogDebug("'{Claim}' was excluded from the identity token claims.", claim.Type); + + return false; + } + + return true; + }); + + // Remove the destinations from the claim properties. + foreach (var claim in principal.Claims) + { + claim.Properties.Remove(OpenIddictConstants.Properties.Destinations); + } + + // Note: the internal token identifier is automatically reset to ensure + // the identifier inherited from the parent token is not automatically reused. + principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()) + .SetCreationDate(DateTimeOffset.UtcNow) + .SetInternalTokenId(identifier: null); + + var lifetime = context.Principal.GetIdentityTokenLifetime() ?? context.Options.IdentityTokenLifetime; + if (lifetime.HasValue) + { + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + } + + if (!string.IsNullOrEmpty(context.ClientId)) + { + principal.SetAudiences(context.ClientId); + } + + // If a nonce was present in the authorization request, it MUST be included in the id_token generated + // by the token endpoint. For that, OpenIddict simply flows the nonce as an authorization code claim. + // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. + + if (context.EndpointType == OpenIddictServerEndpointType.Authorization && !string.IsNullOrEmpty(context.Request.Nonce)) + { + principal.SetClaim(Claims.Nonce, context.Request.Nonce); + } + + else if (context.EndpointType == OpenIddictServerEndpointType.Token) + { + principal.SetClaim(Claims.Nonce, context.Principal.GetClaim(Claims.Private.Nonce)); + } + + context.IdentityTokenPrincipal = principal; + + return default; + } + } + + /// + /// Contains the logic responsible of redeeming the token entry + /// corresponding to the received authorization code or refresh token. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class RedeemTokenEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RedeemTokenEntry() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public RedeemTokenEntry([NotNull] IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Token) + { + return; + } + + // Extract the token identifier from the authentication principal. + // If no token identifier can be found, this indicates that the token has no backing database entry. + var identifier = context.Principal.GetInternalTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token == null) + { + throw new InvalidOperationException("The token details cannot be found in the database."); + } + + if (!context.Options.UseRollingTokens && !context.Request.IsAuthorizationCodeGrantType()) + { + return; + } + + // If rolling tokens are enabled or if the request is a grant_type=authorization_code request, + // mark the authorization code or the refresh token as redeemed to prevent future reuses. + // If the operation fails, return an error indicating the code/token is no longer valid. + // See https://tools.ietf.org/html/rfc6749#section-6 for more information. + if (!await _tokenManager.TryRedeemAsync(token)) + { + context.Reject( + error: Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code is no longer valid." : + "The specified refresh token is no longer valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of redeeming the token entry + /// corresponding to the received authorization code or refresh token. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class RevokeRollingTokenEntries : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RevokeRollingTokenEntries() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public RevokeRollingTokenEntries([NotNull] IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(RedeemTokenEntry.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Token || !context.Request.IsRefreshTokenGrantType()) + { + return; + } + + // When rolling tokens are enabled, try to revoke all the previously issued tokens + // associated with the authorization if the request is a refresh_token request. + // If the operation fails, silently ignore the error and keep processing the request: + // this may indicate that one of the revoked tokens was modified by a concurrent request. + + var identifier = context.Principal.GetInternalAuthorizationId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + await foreach (var token in _tokenManager.FindByAuthorizationIdAsync(identifier)) + { + // Don't change the status of the token used in the token request. + if (string.Equals(context.Principal.GetInternalTokenId(), + await _tokenManager.GetIdAsync(token), StringComparison.Ordinal)) + { + continue; + } + + await _tokenManager.TryRevokeAsync(token); + } + } + } + + /// + /// Contains the logic responsible of extending the lifetime of the refresh token entry. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ExtendRefreshTokenEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public ExtendRefreshTokenEntry() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ExtendRefreshTokenEntry([NotNull] IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(RevokeRollingTokenEntries.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Token || !context.Request.IsRefreshTokenGrantType()) + { + return; + } + + // Extract the token identifier from the authentication principal. + // If no token identifier can be found, this indicates that the token has no backing database entry. + var identifier = context.Principal.GetInternalTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token == null) + { + throw new InvalidOperationException("The token details cannot be found in the database."); + } + + // Compute the new expiration date of the refresh token and update the token entry. + var lifetime = context.Principal.GetRefreshTokenLifetime() ?? context.Options.RefreshTokenLifetime; + if (lifetime.HasValue) + { + await _tokenManager.TryExtendAsync(token, DateTimeOffset.UtcNow + lifetime.Value); + } + + else + { + await _tokenManager.TryExtendAsync(token, date: null); + } + } + } + + /// + /// Contains the logic responsible of generating and attaching + /// the reference access token returned as part of the response. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class AttachReferenceAccessToken : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictTokenManager _tokenManager; + + public AttachReferenceAccessToken() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public AttachReferenceAccessToken( + [NotNull] IOpenIddictApplicationManager applicationManager, + [NotNull] IOpenIddictTokenManager tokenManager) + { + _applicationManager = applicationManager; + _tokenManager = tokenManager; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ExtendRefreshTokenEntry.Descriptor.Order + 1_000) .Build(); /// @@ -284,36 +2009,107 @@ namespace OpenIddict.Server /// /// A that can be used to monitor the asynchronous operation. /// - public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - // Add the validated client_id to the list of authorized presenters, - // unless the presenters were explicitly set by the developer. - if (!context.Principal.HasPresenter() && !string.IsNullOrEmpty(context.ClientId)) + // If an access token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.AccessToken)) { - context.Principal.SetPresenters(context.ClientId); + return; } - return default; + // Generate a new crypto-secure random identifier that will be substituted to the token. + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + var identifier = Base64UrlEncoder.Encode(data); + + var descriptor = new OpenIddictTokenDescriptor + { + AuthorizationId = context.AccessTokenPrincipal.GetInternalAuthorizationId(), + CreationDate = context.AccessTokenPrincipal.GetCreationDate(), + ExpirationDate = context.AccessTokenPrincipal.GetExpirationDate(), + Principal = context.AccessTokenPrincipal, + ReferenceId = identifier, + Status = Statuses.Valid, + Subject = context.AccessTokenPrincipal.GetClaim(Claims.Subject), + Type = TokenUsages.AccessToken + }; + + // If the client application is known, associate it with the token. + if (!string.IsNullOrEmpty(context.Request.ClientId)) + { + var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The application entry cannot be found in the database."); + } + + descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + } + + descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync(new SecurityTokenDescriptor + { + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }, + EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( + credentials => credentials.Key is SymmetricSecurityKey), + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => + credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + Subject = (ClaimsIdentity) context.AccessTokenPrincipal.Identity + }); + + await _tokenManager.CreateAsync(descriptor); + + context.Response.AccessToken = identifier; } } /// - /// Contains the logic responsible of inferring resources from the audience claims if necessary. + /// Contains the logic responsible of generating and attaching + /// the reference authorization code returned as part of the response. + /// Note: this handler is not used when the degraded mode is enabled. /// - public class InferResources : IOpenIddictServerHandler + public class AttachReferenceAuthorizationCode : IOpenIddictServerHandler { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictTokenManager _tokenManager; + + public AttachReferenceAuthorizationCode() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public AttachReferenceAuthorizationCode( + [NotNull] IOpenIddictApplicationManager applicationManager, + [NotNull] IOpenIddictTokenManager tokenManager) + { + _applicationManager = applicationManager; + _tokenManager = tokenManager; + } + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(AttachDefaultPresenters.Descriptor.Order + 1_000) + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachReferenceAccessToken.Descriptor.Order + 1_000) .Build(); /// @@ -323,38 +2119,107 @@ namespace OpenIddict.Server /// /// A that can be used to monitor the asynchronous operation. /// - public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - // When a "resources" property cannot be found in the ticket, infer it from the "audiences" property. - if (context.Principal.HasAudience() && !context.Principal.HasResource()) + // If an authorization code was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.Code)) { - context.Principal.SetResources(context.Principal.GetAudiences()); + return; } - // Reset the audiences collection, as it's later set, based on the token type. - context.Principal.SetAudiences(Array.Empty()); + // Generate a new crypto-secure random identifier that will be substituted to the token. + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + var identifier = Base64UrlEncoder.Encode(data); - return default; + var descriptor = new OpenIddictTokenDescriptor + { + AuthorizationId = context.AuthorizationCodePrincipal.GetInternalAuthorizationId(), + CreationDate = context.AuthorizationCodePrincipal.GetCreationDate(), + ExpirationDate = context.AuthorizationCodePrincipal.GetExpirationDate(), + Principal = context.AuthorizationCodePrincipal, + ReferenceId = identifier, + Status = Statuses.Valid, + Subject = context.AuthorizationCodePrincipal.GetClaim(Claims.Subject), + Type = TokenUsages.AuthorizationCode + }; + + // If the client application is known, associate it with the token. + if (!string.IsNullOrEmpty(context.Request.ClientId)) + { + var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The application entry cannot be found in the database."); + } + + descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + } + + descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync(new SecurityTokenDescriptor + { + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AuthorizationCode }, + EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( + credentials => credentials.Key is SymmetricSecurityKey), + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => + credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + Subject = (ClaimsIdentity) context.AuthorizationCodePrincipal.Identity + }); + + await _tokenManager.CreateAsync(descriptor); + + context.Response.Code = identifier; } } /// - /// Contains the logic responsible of selecting the token types returned to the client application. + /// Contains the logic responsible of generating and attaching + /// the reference refresh token returned as part of the response. + /// Note: this handler is not used when the degraded mode is enabled. /// - public class EvaluateReturnedTokens : IOpenIddictServerHandler + public class AttachReferenceRefreshToken : IOpenIddictServerHandler { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictTokenManager _tokenManager; + + public AttachReferenceRefreshToken() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public AttachReferenceRefreshToken( + [NotNull] IOpenIddictApplicationManager applicationManager, + [NotNull] IOpenIddictTokenManager tokenManager) + { + _applicationManager = applicationManager; + _tokenManager = tokenManager; + } + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(InferResources.Descriptor.Order + 1_000) + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachReferenceAuthorizationCode.Descriptor.Order + 1_000) .Build(); /// @@ -364,81 +2229,79 @@ namespace OpenIddict.Server /// /// A that can be used to monitor the asynchronous operation. /// - public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - context.IncludeAccessToken = context.EndpointType switch + // If a refresh token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.RefreshToken)) { - // For authorization requests, return an access token if a response type containing token was specified. - OpenIddictServerEndpointType.Authorization => context.Request.HasResponseType(ResponseTypes.Token), - - // For token requests, always return an access token. - OpenIddictServerEndpointType.Token => true, + return; + } - _ => false - }; + // Generate a new crypto-secure random identifier that will be substituted to the token. + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + var identifier = Base64UrlEncoder.Encode(data); - context.IncludeAuthorizationCode = context.EndpointType switch + var descriptor = new OpenIddictTokenDescriptor { - // For authorization requests, return an authorization code if a response type containing code was specified. - OpenIddictServerEndpointType.Authorization => context.Request.HasResponseType(ResponseTypes.Code), - - // For token requests, prevent an authorization code from being returned as this type of token - // cannot be issued from the token endpoint in the standard OAuth 2.0/OpenID Connect flows. - OpenIddictServerEndpointType.Token => false, - - _ => false + AuthorizationId = context.RefreshTokenPrincipal.GetInternalAuthorizationId(), + CreationDate = context.RefreshTokenPrincipal.GetCreationDate(), + ExpirationDate = context.RefreshTokenPrincipal.GetExpirationDate(), + Principal = context.RefreshTokenPrincipal, + ReferenceId = identifier, + Status = Statuses.Valid, + Subject = context.RefreshTokenPrincipal.GetClaim(Claims.Subject), + Type = TokenUsages.RefreshToken }; - context.IncludeRefreshToken = context.EndpointType switch + // If the client application is known, associate it with the token. + if (!string.IsNullOrEmpty(context.Request.ClientId)) { - // For authorization requests, prevent a refresh token from being returned as OAuth 2.0 - // explicitly disallows returning a refresh token from the authorization endpoint. - // See https://tools.ietf.org/html/rfc6749#section-4.2.2 for more information. - OpenIddictServerEndpointType.Authorization => false, - - // For token requests, don't return a refresh token is the offline_access scope was not granted. - OpenIddictServerEndpointType.Token when !context.Principal.HasScope(Scopes.OfflineAccess) => false, - - // For token requests, only return a refresh token is the offline_access scope was granted and - // if sliding expiration is disabled or if the request is not a grant_type=refresh_token request. - OpenIddictServerEndpointType.Token => context.Options.UseSlidingExpiration || - !context.Request.IsRefreshTokenGrantType(), + var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The application entry cannot be found in the database."); + } - _ => false - }; + descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + } - context.IncludeIdentityToken = context.EndpointType switch + descriptor.Payload = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync(new SecurityTokenDescriptor { - // For authorization requests, return an identity token if a response type containing code - // was specified and if the openid scope was explicitly or implicitly granted. - OpenIddictServerEndpointType.Authorization => context.Principal.HasScope(Scopes.OpenId) && - context.Request.HasResponseType(ResponseTypes.IdToken), - - // For token requests, only return an identity token if the openid scope was granted. - OpenIddictServerEndpointType.Token => context.Principal.HasScope(Scopes.OpenId), + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.RefreshToken }, + EncryptingCredentials = context.Options.EncryptionCredentials[0], + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => + credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + Subject = (ClaimsIdentity) context.RefreshTokenPrincipal.Identity + }); - _ => false - }; + await _tokenManager.CreateAsync(descriptor); - return default; + context.Response.RefreshToken = identifier; } } /// - /// Contains the logic responsible of creating an ad-hoc authorization, if necessary. + /// Contains the logic responsible of creating a token entry in the database for the authorization code. /// Note: this handler is not used when the degraded mode is enabled. /// - public class AttachAuthorization : IOpenIddictServerHandler + public class CreateSelfContainedAuthorizationCodeEntry : IOpenIddictServerHandler { private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictAuthorizationManager _authorizationManager; + private readonly IOpenIddictTokenManager _tokenManager; - public AttachAuthorization() => throw new InvalidOperationException(new StringBuilder() + public CreateSelfContainedAuthorizationCodeEntry() => throw new InvalidOperationException(new StringBuilder() .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") @@ -446,12 +2309,12 @@ namespace OpenIddict.Server .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .ToString()); - public AttachAuthorization( + public CreateSelfContainedAuthorizationCodeEntry( [NotNull] IOpenIddictApplicationManager applicationManager, - [NotNull] IOpenIddictAuthorizationManager authorizationManager) + [NotNull] IOpenIddictTokenManager tokenManager) { _applicationManager = applicationManager; - _authorizationManager = authorizationManager; + _tokenManager = tokenManager; } /// @@ -460,9 +2323,11 @@ namespace OpenIddict.Server public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(EvaluateReturnedTokens.Descriptor.Order + 1_000) + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ExtendRefreshTokenEntry.Descriptor.Order + 1_000) .Build(); /// @@ -479,29 +2344,24 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // If no authorization code or refresh token is returned, don't create an authorization. - if (!context.IncludeAuthorizationCode && !context.IncludeRefreshToken) - { - return; - } - - // If an authorization identifier was explicitly specified, don't create an ad-hoc authorization. - if (!string.IsNullOrEmpty(context.Principal.GetInternalAuthorizationId())) + // If a token identifier was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.AuthorizationCodePrincipal.GetInternalTokenId())) { return; } - var descriptor = new OpenIddictAuthorizationDescriptor + var descriptor = new OpenIddictTokenDescriptor { - Principal = context.Principal, + AuthorizationId = context.AuthorizationCodePrincipal.GetInternalAuthorizationId(), + CreationDate = context.AuthorizationCodePrincipal.GetCreationDate(), + ExpirationDate = context.AuthorizationCodePrincipal.GetExpirationDate(), + Principal = context.AuthorizationCodePrincipal, Status = Statuses.Valid, - Subject = context.Principal.GetClaim(Claims.Subject), - Type = AuthorizationTypes.AdHoc + Subject = context.AuthorizationCodePrincipal.GetClaim(Claims.Subject), + Type = TokenUsages.AuthorizationCode }; - descriptor.Scopes.UnionWith(context.Principal.GetScopes()); - - // If the client application is known, associate it to the authorization. + // If the client application is known, associate it with the token. if (!string.IsNullOrEmpty(context.Request.ClientId)) { var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); @@ -513,51 +2373,49 @@ namespace OpenIddict.Server descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); } - var authorization = await _authorizationManager.CreateAsync(descriptor); - if (authorization == null) - { - return; - } - - var identifier = await _authorizationManager.GetIdAsync(authorization); - - if (string.IsNullOrEmpty(context.Request.ClientId)) - { - context.Logger.LogInformation("An ad hoc authorization was automatically created and " + - "associated with an unknown application: {Identifier}.", identifier); - } - - else - { - context.Logger.LogInformation("An ad hoc authorization was automatically created and " + - "associated with the '{ClientId}' application: {Identifier}.", - context.Request.ClientId, identifier); - } + var token = await _tokenManager.CreateAsync(descriptor); - // Attach the unique identifier of the ad hoc authorization to the authentication principal - // so that it is attached to all the derived tokens, allowing batched revocations support. - context.Principal.SetInternalAuthorizationId(identifier); + // Set the internal token identifier so that it can be added to the serialized code. + context.AuthorizationCodePrincipal.SetInternalTokenId(await _tokenManager.GetIdAsync(token)); } } /// - /// Contains the logic responsible of generating and attaching an access token. + /// Contains the logic responsible of creating a token entry in the database for the refresh token. + /// Note: this handler is not used when the degraded mode is enabled. /// - public class AttachAccessToken : IOpenIddictServerHandler + public class CreateSelfContainedRefreshTokenEntry : IOpenIddictServerHandler { - private readonly IOpenIddictServerProvider _provider; + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictTokenManager _tokenManager; + + public CreateSelfContainedRefreshTokenEntry() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); - public AttachAccessToken([NotNull] IOpenIddictServerProvider provider) - => _provider = provider; + public CreateSelfContainedRefreshTokenEntry( + [NotNull] IOpenIddictApplicationManager applicationManager, + [NotNull] IOpenIddictTokenManager tokenManager) + { + _applicationManager = applicationManager; + _tokenManager = tokenManager; + } /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseScopedHandler() - .SetOrder(AttachAuthorization.Descriptor.Order + 1_000) + .AddFilter() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(CreateSelfContainedAuthorizationCodeEntry.Descriptor.Order + 1_000) .Build(); /// @@ -571,113 +2429,111 @@ namespace OpenIddict.Server { if (context == null) { - throw new ArgumentNullException(nameof(context)); - } - - // Create a new principal containing only the filtered claims. - // Actors identities are also filtered (delegation scenarios). - var principal = context.Principal.Clone(claim => - { - // Never exclude the subject claim. - if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // Always exclude private claims, whose values must generally be kept secret. - if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - // Claims whose destination is not explicitly referenced or doesn't - // contain "access_token" are not included in the access token. - if (!claim.HasDestination(Destinations.AccessToken)) - { - context.Logger.LogDebug("'{Claim}' was excluded from the access token claims.", claim.Type); - - return false; - } - - return true; - }); - - // Remove the destinations from the claim properties. - foreach (var claim in principal.Claims) - { - claim.Properties.Remove(OpenIddictConstants.Properties.Destinations); + throw new ArgumentNullException(nameof(context)); } - principal.SetPublicTokenId(Guid.NewGuid().ToString()).SetCreationDate(DateTimeOffset.UtcNow); - - var lifetime = context.Principal.GetAccessTokenLifetime() ?? context.Options.AccessTokenLifetime; - if (lifetime.HasValue) + // If a token identifier was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.RefreshTokenPrincipal.GetInternalTokenId())) { - principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + return; } - // Set the audiences collection using the private resource claims stored in the principal. - principal.SetAudiences(context.Principal.GetResources()); + var descriptor = new OpenIddictTokenDescriptor + { + AuthorizationId = context.RefreshTokenPrincipal.GetInternalAuthorizationId(), + CreationDate = context.RefreshTokenPrincipal.GetCreationDate(), + ExpirationDate = context.RefreshTokenPrincipal.GetExpirationDate(), + Principal = context.RefreshTokenPrincipal, + Status = Statuses.Valid, + Subject = context.RefreshTokenPrincipal.GetClaim(Claims.Subject), + Type = TokenUsages.RefreshToken + }; - // When receiving a grant_type=refresh_token request, determine whether the client application - // requests a limited set of scopes and immediately replace the scopes collection if necessary. - if (context.EndpointType == OpenIddictServerEndpointType.Token && - context.Request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(context.Request.Scope)) + // If the client application is known, associate it with the token. + if (!string.IsNullOrEmpty(context.Request.ClientId)) { - var scopes = context.Request.GetScopes(); - if (scopes.Count != 0) + var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); + if (application == null) { - context.Logger.LogDebug("The access token scopes will be limited to the scopes " + - "requested by the client application: {Scopes}.", scopes); - - principal.SetScopes(scopes.Intersect(context.Principal.GetScopes())); + throw new InvalidOperationException("The application entry cannot be found in the database."); } + + descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); } - var notification = new SerializeAccessTokenContext(context.Transaction) - { - Principal = principal - }; + var token = await _tokenManager.CreateAsync(descriptor); - await _provider.DispatchAsync(notification); + // Set the internal token identifier so that it can be added to the serialized token. + context.RefreshTokenPrincipal.SetInternalTokenId(await _tokenManager.GetIdAsync(token)); + } + } - context.Response.TokenType = TokenTypes.Bearer; - context.Response.AccessToken = notification.Token; + /// + /// Contains the logic responsible of generating and attaching + /// the self-contained access token returned as part of the response. + /// + public class AttachSelfContainedAccessToken : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(CreateSelfContainedRefreshTokenEntry.Descriptor.Order + 1_000) + .Build(); - // If an expiration date was set, return it to the client application. - var date = notification.Principal.GetExpirationDate(); - if (date.HasValue && date.Value > DateTimeOffset.UtcNow) + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) { - context.Response.ExpiresIn = (long) ((date.Value - DateTimeOffset.UtcNow).TotalSeconds + .5); + throw new ArgumentNullException(nameof(context)); } - // If the granted scopes differ from the request scopes, return the granted scopes list as a parameter. - if (context.Request.IsAuthorizationCodeGrantType() || - !context.Principal.GetScopes().SetEquals(context.Request.GetScopes())) + // If an access token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.AccessToken)) { - context.Response.Scope = string.Join(" ", context.Principal.GetScopes()); + return; } + + context.Response.AccessToken = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + new SecurityTokenDescriptor + { + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }, + EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( + credentials => credentials.Key is SymmetricSecurityKey), + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => + credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + Subject = (ClaimsIdentity) context.AccessTokenPrincipal.Identity + }); } } /// - /// Contains the logic responsible of generating and attaching an authorization code. + /// Contains the logic responsible of generating and attaching + /// the self-contained authorization code returned as part of the response. /// - public class AttachAuthorizationCode : IOpenIddictServerHandler + public class AttachSelfContainedAuthorizationCode : IOpenIddictServerHandler { - private readonly IOpenIddictServerProvider _provider; - - public AttachAuthorizationCode([NotNull] IOpenIddictServerProvider provider) - => _provider = provider; - /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() .AddFilter() - .UseScopedHandler() - .SetOrder(AttachAccessToken.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(AttachSelfContainedAccessToken.Descriptor.Order + 1_000) .Build(); /// @@ -694,71 +2550,41 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - var principal = context.Principal.Clone(_ => true) - .SetPublicTokenId(Guid.NewGuid().ToString()) - .SetCreationDate(DateTimeOffset.UtcNow); - - var lifetime = context.Principal.GetAuthorizationCodeLifetime() ?? context.Options.AuthorizationCodeLifetime; - if (lifetime.HasValue) - { - principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); - } - - // Attach the redirect_uri to allow for later comparison when - // receiving a grant_type=authorization_code token request. - if (!string.IsNullOrEmpty(context.Request.RedirectUri)) - { - principal.SetClaim(Claims.Private.RedirectUri, context.Request.RedirectUri); - } - - // Attach the code challenge and the code challenge methods to allow the ValidateCodeVerifier - // handler to validate the code verifier sent by the client as part of the token request. - if (!string.IsNullOrEmpty(context.Request.CodeChallenge)) - { - principal.SetClaim(Claims.Private.CodeChallenge, context.Request.CodeChallenge); - - // Default to S256 if no explicit code challenge method was specified. - principal.SetClaim(Claims.Private.CodeChallengeMethod, - !string.IsNullOrEmpty(context.Request.CodeChallengeMethod) ? - context.Request.CodeChallengeMethod : CodeChallengeMethods.Sha256); - } - - // Attach the nonce so that it can be later returned by - // the token endpoint as part of the JWT identity token. - if (!string.IsNullOrEmpty(context.Request.Nonce)) + // If an authorization code was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.Code)) { - principal.SetClaim(Claims.Private.Nonce, context.Request.Nonce); + return; } - var notification = new SerializeAuthorizationCodeContext(context.Transaction) - { - Principal = principal - }; - - await _provider.DispatchAsync(notification); - - context.Response.Code = notification.Token; + context.Response.Code = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + new SecurityTokenDescriptor + { + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AuthorizationCode }, + EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( + credentials => credentials.Key is SymmetricSecurityKey), + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => + credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + Subject = (ClaimsIdentity) context.AuthorizationCodePrincipal.Identity + }); } } /// - /// Contains the logic responsible of generating and attaching a refresh token. + /// Contains the logic responsible of generating and attaching + /// the self-contained refresh token returned as part of the response. /// - public class AttachRefreshToken : IOpenIddictServerHandler + public class AttachSelfContainedRefreshToken : IOpenIddictServerHandler { - private readonly IOpenIddictServerProvider _provider; - - public AttachRefreshToken([NotNull] IOpenIddictServerProvider provider) - => _provider = provider; - /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() .AddFilter() - .UseScopedHandler() - .SetOrder(AttachAuthorizationCode.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(AttachSelfContainedAuthorizationCode.Descriptor.Order + 1_000) .Build(); /// @@ -775,45 +2601,39 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - var principal = context.Principal.Clone(_ => true) - .SetPublicTokenId(Guid.NewGuid().ToString()) - .SetCreationDate(DateTimeOffset.UtcNow); - - var lifetime = context.Principal.GetRefreshTokenLifetime() ?? context.Options.RefreshTokenLifetime; - if (lifetime.HasValue) + // If a refresh token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.RefreshToken)) { - principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + return; } - var notification = new SerializeRefreshTokenContext(context.Transaction) - { - Principal = principal - }; - - await _provider.DispatchAsync(notification); - - context.Response.RefreshToken = notification.Token; + context.Response.RefreshToken = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + new SecurityTokenDescriptor + { + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.RefreshToken }, + EncryptingCredentials = context.Options.EncryptionCredentials[0], + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => + credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + Subject = (ClaimsIdentity) context.RefreshTokenPrincipal.Identity + }); } } /// - /// Contains the logic responsible of generating and attaching an identity token. + /// Contains the logic responsible of generating and attaching the hashes of + /// the access token and authorization code to the identity token principal. /// - public class AttachIdentityToken : IOpenIddictServerHandler + public class AttachTokenDigests : IOpenIddictServerHandler { - private readonly IOpenIddictServerProvider _provider; - - public AttachIdentityToken([NotNull] IOpenIddictServerProvider provider) - => _provider = provider; - /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseScopedHandler() - .SetOrder(AttachRefreshToken.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(AttachSelfContainedRefreshToken.Descriptor.Order + 1_000) .Build(); /// @@ -823,121 +2643,51 @@ namespace OpenIddict.Server /// /// A that can be used to monitor the asynchronous operation. /// - public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - - // Replace the principal by a new one containing only the filtered claims. - // Actors identities are also filtered (delegation scenarios). - var principal = context.Principal.Clone(claim => - { - // Never exclude the subject claim. - if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // Always exclude private claims, whose values must generally be kept secret. - if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - // Claims whose destination is not explicitly referenced or doesn't - // contain "id_token" are not included in the identity token. - if (!claim.HasDestination(Destinations.IdentityToken)) - { - context.Logger.LogDebug("'{Claim}' was excluded from the identity token claims.", claim.Type); - - return false; - } - - return true; - }); - - // Remove the destinations from the claim properties. - foreach (var claim in principal.Claims) + if (string.IsNullOrEmpty(context.Response.AccessToken) && + string.IsNullOrEmpty(context.Response.Code)) { - claim.Properties.Remove(OpenIddictConstants.Properties.Destinations); + return default; } - principal.SetPublicTokenId(Guid.NewGuid().ToString()).SetCreationDate(DateTimeOffset.UtcNow); - - var lifetime = context.Principal.GetIdentityTokenLifetime() ?? context.Options.IdentityTokenLifetime; - if (lifetime.HasValue) + var credentials = context.Options.SigningCredentials.FirstOrDefault( + credentials => credentials.Key is AsymmetricSecurityKey); + if (credentials == null) { - principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + throw new InvalidOperationException("No suitable signing credentials could be found."); } - if (!string.IsNullOrEmpty(context.ClientId)) + using var hash = GetHashAlgorithm(credentials); + if (hash == null || hash is KeyedHashAlgorithm) { - principal.SetAudiences(context.ClientId); + throw new InvalidOperationException("The signing credentials algorithm is not valid."); } - // If a nonce was present in the authorization request, it MUST be included in the id_token generated - // by the token endpoint. For that, OpenIddict simply flows the nonce as an authorization code claim. - // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. - - if (context.EndpointType == OpenIddictServerEndpointType.Authorization && !string.IsNullOrEmpty(context.Request.Nonce)) + if (!string.IsNullOrEmpty(context.Response.AccessToken)) { - principal.SetClaim(Claims.Nonce, context.Request.Nonce); - } + var digest = hash.ComputeHash(Encoding.ASCII.GetBytes(context.Response.AccessToken)); - else if (context.EndpointType == OpenIddictServerEndpointType.Token) - { - var nonce = context.Principal.GetClaim(Claims.Private.Nonce); - if (!string.IsNullOrEmpty(nonce)) - { - principal.SetClaim(Claims.Nonce, nonce); - } + // Note: only the left-most half of the hash is used. + // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken + context.IdentityTokenPrincipal.SetClaim(Claims.AccessTokenHash, Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); } - if (!string.IsNullOrEmpty(context.Response.AccessToken) || !string.IsNullOrEmpty(context.Response.Code)) + if (!string.IsNullOrEmpty(context.Response.Code)) { - var credentials = context.Options.SigningCredentials.FirstOrDefault( - credentials => credentials.Key is AsymmetricSecurityKey); - if (credentials == null) - { - throw new InvalidOperationException("No suitable signing credentials could be found."); - } - - using var hash = GetHashAlgorithm(credentials); - if (hash == null || hash is KeyedHashAlgorithm) - { - throw new InvalidOperationException("The signing credentials algorithm is not valid."); - } + var digest = hash.ComputeHash(Encoding.ASCII.GetBytes(context.Response.Code)); - if (!string.IsNullOrEmpty(context.Response.Code)) - { - var digest = hash.ComputeHash(Encoding.ASCII.GetBytes(context.Response.Code)); - - // Note: only the left-most half of the hash is used. - // See http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken - principal.SetClaim(Claims.CodeHash, Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); - } - - if (!string.IsNullOrEmpty(context.Response.AccessToken)) - { - var digest = hash.ComputeHash(Encoding.ASCII.GetBytes(context.Response.AccessToken)); - - // Note: only the left-most half of the hash is used. - // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken - principal.SetClaim(Claims.AccessTokenHash, Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); - } + // Note: only the left-most half of the hash is used. + // See http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken + context.IdentityTokenPrincipal.SetClaim(Claims.CodeHash, Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); } - var notification = new SerializeIdentityTokenContext(context.Transaction) - { - Principal = principal - }; - - await _provider.DispatchAsync(notification); - - context.Response.IdToken = notification.Token; + return default; static HashAlgorithm GetHashAlgorithm(SigningCredentials credentials) { @@ -1001,5 +2751,108 @@ namespace OpenIddict.Server } } } + + /// + /// Contains the logic responsible of generating and attaching + /// the self-contained identity token returned as part of the response. + /// + public class AttachSelfContainedIdentityToken : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachTokenDigests.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an identity token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.IdToken)) + { + return; + } + + context.Response.IdToken = await context.Options.SecurityTokenHandler.CreateTokenFromDescriptorAsync( + new SecurityTokenDescriptor + { + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.IdToken }, + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.Options.SigningCredentials.First(credentials => + credentials.Key is AsymmetricSecurityKey), + Subject = (ClaimsIdentity) context.IdentityTokenPrincipal.Identity + }); + } + } + + /// + /// Contains the logic responsible of attaching additional properties to the sign-in response. + /// + public class AttachAdditionalProperties : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachSelfContainedIdentityToken.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.AccessTokenPrincipal == null) + { + throw new InvalidOperationException("The access token principal couldn't be found."); + } + + context.Response.TokenType = TokenTypes.Bearer; + + // If an expiration date was set on the access token principal, return it to the client application. + var date = context.AccessTokenPrincipal.GetExpirationDate(); + if (date.HasValue && date.Value > DateTimeOffset.UtcNow) + { + context.Response.ExpiresIn = (long) ((date.Value - DateTimeOffset.UtcNow).TotalSeconds + .5); + } + + // If the granted access token scopes differ from the requested scopes, return the granted scopes + // list as a parameter to inform the client application of the fact the scopes set will be reduced. + if (context.Request.IsAuthorizationCodeGrantType() || + !context.AccessTokenPrincipal.GetScopes().SetEquals(context.Request.GetScopes())) + { + context.Response.Scope = string.Join(" ", context.AccessTokenPrincipal.GetScopes()); + } + + return default; + } + } } } diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 666e9744..97694df6 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; -using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; @@ -84,24 +83,12 @@ namespace OpenIddict.Server public IList UserinfoEndpointUris { get; } = new List(); /// - /// Gets or sets the security token handler used to protect and unprotect authorization codes. + /// Gets or sets the security token handler used to protect and unprotect tokens. /// - public JsonWebTokenHandler AuthorizationCodeHandler { get; set; } = new JsonWebTokenHandler(); - - /// - /// Gets or sets the security token handler used to protect and unprotect access tokens. - /// - public JsonWebTokenHandler AccessTokenHandler { get; set; } = new JsonWebTokenHandler(); - - /// - /// Gets or sets the security token handler used to protect and unprotect identity tokens. - /// - public JsonWebTokenHandler IdentityTokenHandler { get; set; } = new JsonWebTokenHandler(); - - /// - /// Gets or sets the security token handler used to protect and unprotect refresh tokens. - /// - public JsonWebTokenHandler RefreshTokenHandler { get; set; } = new JsonWebTokenHandler(); + public OpenIddictServerTokenHandler SecurityTokenHandler { get; set; } = new OpenIddictServerTokenHandler + { + SetDefaultTimesOnTokenCreation = false + }; /// /// Gets or sets the period of time the authorization codes remain valid after being issued. diff --git a/src/OpenIddict.Server/OpenIddictServerProvider.cs b/src/OpenIddict.Server/OpenIddictServerProvider.cs index c6e339bf..7dc1e757 100644 --- a/src/OpenIddict.Server/OpenIddictServerProvider.cs +++ b/src/OpenIddict.Server/OpenIddictServerProvider.cs @@ -72,14 +72,6 @@ namespace OpenIddict.Server _logger.LogDebug("Authentication was handled in user code."); return; - case BaseDeserializingContext notification when notification.IsHandled: - _logger.LogDebug("Token deserialization was handled in user code."); - return; - - case BaseSerializingContext notification when notification.IsHandled: - _logger.LogDebug("Token serialization was handled in user code."); - return; - default: continue; } } diff --git a/src/OpenIddict.Server/OpenIddictServerTokenHandler.cs b/src/OpenIddict.Server/OpenIddictServerTokenHandler.cs new file mode 100644 index 00000000..057901f5 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerTokenHandler.cs @@ -0,0 +1,147 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace OpenIddict.Server +{ + public class OpenIddictServerTokenHandler : JsonWebTokenHandler + { + public ValueTask CreateTokenFromDescriptorAsync(SecurityTokenDescriptor descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + if (descriptor.Subject == null) + { + throw new ArgumentException("The subject associated with a descriptor cannot be null.", nameof(descriptor)); + } + + if (descriptor.Claims == null) + { + throw new InvalidOperationException("The claims collection cannot be null or empty."); + } + + if (!descriptor.Claims.TryGetValue(Claims.Private.TokenUsage, out var type) || string.IsNullOrEmpty((string) type)) + { + throw new InvalidOperationException("The token usage cannot be null or empty."); + } + + var destinations = new Dictionary(StringComparer.Ordinal); + foreach (var group in descriptor.Subject.Claims.GroupBy(claim => claim.Type)) + { + var collection = group.ToList(); + + // Note: destinations are attached to claims as special CLR properties. Such properties can't be serialized + // as part of classic JWT tokens. To work around this limitation, claim destinations are added to a special + // claim named oi_cl_dstn that contains a map of all the claims and their attached destinations, if any. + + var set = new HashSet(collection[0].GetDestinations(), StringComparer.OrdinalIgnoreCase); + if (set.Count != 0) + { + // Ensure the other claims of the same type use the same exact destinations. + for (var index = 0; index < collection.Count; index++) + { + if (!set.SetEquals(collection[index].GetDestinations())) + { + throw new InvalidOperationException($"Conflicting destinations for the claim '{group.Key}' were specified."); + } + } + + destinations[group.Key] = set.ToArray(); + } + } + + // Unless at least one claim was added to the claim destinations map, + // don't add the special claim to avoid adding a useless empty claim. + if (destinations.Count != 0) + { + descriptor.Claims[Claims.Private.ClaimDestinations] = destinations; + } + + return new ValueTask(base.CreateToken(descriptor)); + } + + public ValueTask ValidateTokenStringAsync(string token, TokenValidationParameters parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + if (!parameters.PropertyBag.TryGetValue(Claims.Private.TokenUsage, out var type) || string.IsNullOrEmpty((string) type)) + { + throw new InvalidOperationException("The token usage cannot be null or empty."); + } + + if (!CanReadToken(token)) + { + return new ValueTask(new TokenValidationResult + { + Exception = new SecurityTokenException("The token was not compatible with the JWT format."), + IsValid = false + }); + } + + try + { + var result = base.ValidateToken(token, parameters); + if (result == null || !result.IsValid) + { + return new ValueTask(new TokenValidationResult + { + Exception = result?.Exception, + IsValid = false + }); + } + + var assertion = ((JsonWebToken) result.SecurityToken)?.InnerToken ?? (JsonWebToken) result.SecurityToken; + + if (!assertion.TryGetPayloadValue(Claims.Private.TokenUsage, out string usage) || + !string.Equals(usage, (string) type, StringComparison.OrdinalIgnoreCase)) + { + return new ValueTask(new TokenValidationResult + { + Exception = new SecurityTokenException("The token usage associated to the token does not match the expected type."), + IsValid = false + }); + } + + // Restore the claim destinations from the special oi_cl_dstn claim (represented as a dictionary/JSON object). + if (assertion.TryGetPayloadValue(Claims.Private.ClaimDestinations, out IDictionary definitions)) + { + foreach (var definition in definitions) + { + foreach (var claim in result.ClaimsIdentity.Claims.Where(claim => claim.Type == definition.Key)) + { + claim.SetDestinations(definition.Value); + } + } + } + + return new ValueTask(result); + } + + catch (Exception exception) + { + return new ValueTask(new TokenValidationResult + { + Exception = exception, + IsValid = false + }); + } + } + } +}