Browse Source

Reintroduce token storage, reference tokens, non-sliding expiration, rolling tokens and token revocation support

pull/804/head
Kévin Chalet 6 years ago
committed by GitHub
parent
commit
858740818e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
  2. 52
      src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
  3. 4
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  4. 72
      src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
  5. 1
      src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj
  6. 74
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  7. 224
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  8. 57
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  9. 18
      src/OpenIddict.Server.DataProtection/IOpenIddictServerDataProtectionFormatter.cs
  10. 15
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs
  11. 379
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs
  12. 836
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs
  13. 875
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs
  14. 7
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs
  15. 32
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  16. 22
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  17. 26
      src/OpenIddict.Server/OpenIddictServerConstants.cs
  18. 6
      src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs
  19. 229
      src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs
  20. 24
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  21. 6
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  22. 96
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  23. 26
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  24. 186
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  25. 187
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  26. 229
      src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
  27. 610
      src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs
  28. 26
      src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs
  29. 46
      src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
  30. 2747
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  31. 23
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  32. 8
      src/OpenIddict.Server/OpenIddictServerProvider.cs
  33. 147
      src/OpenIddict.Server/OpenIddictServerTokenHandler.cs

16
src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs

@ -365,14 +365,6 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask PruneAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Revokes an authorization.
/// </summary>
/// <param name="authorization">The authorization to revoke.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask RevokeAsync([NotNull] object authorization, CancellationToken cancellationToken = default);
/// <summary>
/// Sets the application identifier associated with an authorization.
/// </summary>
@ -384,6 +376,14 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask SetApplicationIdAsync([NotNull] object authorization, [CanBeNull] string identifier, CancellationToken cancellationToken = default);
/// <summary>
/// Tries to revoke an authorization.
/// </summary>
/// <param name="authorization">The authorization to revoke.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization was successfully revoked, <c>false</c> otherwise.</returns>
ValueTask<bool> TryRevokeAsync([NotNull] object authorization, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing authorization.
/// </summary>

52
src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs

@ -71,17 +71,6 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask DeleteAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary>
/// Extends the specified token by replacing its expiration date.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The date on which the token will no longer be considered valid.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
ValueTask ExtendAsync([NotNull] object token, [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves the tokens corresponding to the specified
/// subject and associated with the application identifier.
@ -394,22 +383,6 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask PruneAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Redeems a token.
/// </summary>
/// <param name="token">The token to redeem.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask RedeemAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary>
/// Revokes a token.
/// </summary>
/// <param name="token">The token to revoke.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
ValueTask RevokeAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary>
/// Sets the application identifier associated with a token.
/// </summary>
@ -432,6 +405,31 @@ namespace OpenIddict.Abstractions
/// </returns>
ValueTask SetAuthorizationIdAsync([NotNull] object token, [CanBeNull] string identifier, CancellationToken cancellationToken = default);
/// <summary>
/// Tries to extend the specified token by replacing its expiration date.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The date on which the token will no longer be considered valid.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token was successfully extended, <c>false</c> otherwise.</returns>
ValueTask<bool> TryExtendAsync([NotNull] object token, [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken = default);
/// <summary>
/// Tries to redeem a token.
/// </summary>
/// <param name="token">The token to redeem.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token was successfully redemeed, <c>false</c> otherwise.</returns>
ValueTask<bool> TryRedeemAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary>
/// Tries to revoke a token.
/// </summary>
/// <param name="token">The token to revoke.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token was successfully revoked, <c>false</c> otherwise.</returns>
ValueTask<bool> TryRevokeAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary>
/// Updates an existing token.
/// </summary>

4
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";
}

72
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));
}
/// <summary>
@ -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));
}
/// <summary>
@ -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<string>(StringComparer.Ordinal);
}
return ImmutableHashSet.CreateRange(StringComparer.Ordinal, GetValues(value, Separators.Space));
return ImmutableHashSet.CreateRange(StringComparer.Ordinal, principal.GetClaims(Claims.Private.Scopes));
}
/// <summary>
@ -1275,21 +1266,6 @@ namespace OpenIddict.Abstractions
return principal.GetClaim(Claims.Private.TokenId);
}
/// <summary>
/// Gets the public token identifier associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <returns>The unique identifier or <c>null</c> if the claim cannot be found.</returns>
public static string GetPublicTokenId([NotNull] this ClaimsPrincipal principal)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
return principal.GetClaim(Claims.JwtId);
}
/// <summary>
/// Gets the token usage associated with the claims principal.
/// </summary>
@ -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();
}
/// <summary>
@ -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();
}
/// <summary>
@ -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();
}
/// <summary>
@ -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));
}
/// <summary>
@ -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));
}
/// <summary>
@ -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));
}
/// <summary>
@ -1792,22 +1762,6 @@ namespace OpenIddict.Abstractions
return principal.SetClaim(Claims.Private.TokenId, identifier);
}
/// <summary>
/// Sets the public token identifier associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <param name="identifier">The unique identifier to store.</param>
/// <returns>The claims principal.</returns>
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<string> GetValues(string source, char[] separators)
{
Debug.Assert(!string.IsNullOrEmpty(source), "The source string shouldn't be null or empty.");

1
src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj

@ -14,6 +14,7 @@
<ItemGroup>
<ProjectReference Include="..\OpenIddict\OpenIddict.csproj" />
<ProjectReference Include="..\OpenIddict.Server.AspNetCore\OpenIddict.Server.AspNetCore.csproj" />
<ProjectReference Include="..\OpenIddict.Server.DataProtection\OpenIddict.Server.DataProtection.csproj" />
</ItemGroup>
</Project>

74
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);
/// <summary>
/// Revokes an authorization.
/// Sets the application identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization to revoke.</param>
/// <param name="authorization">The authorization.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
public virtual async ValueTask RevokeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the application identifier associated with an authorization.
/// Tries to revoke an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="authorization">The authorization to revoke.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async ValueTask SetApplicationIdAsync(
[NotNull] TAuthorization authorization, [CanBeNull] string identifier, CancellationToken cancellationToken = default)
/// <returns><c>true</c> if the authorization was successfully revoked, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> 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;
}
}
/// <summary>
@ -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<bool> IOpenIddictAuthorizationManager.TryRevokeAsync(object authorization, CancellationToken cancellationToken)
=> TryRevokeAsync((TAuthorization) authorization, cancellationToken);
ValueTask IOpenIddictAuthorizationManager.UpdateAsync(object authorization, CancellationToken cancellationToken)
=> UpdateAsync((TAuthorization) authorization, cancellationToken);

224
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);
}
/// <summary>
/// Extends the specified token by replacing its expiration date.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The date on which the token will no longer be considered valid.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Retrieves the tokens corresponding to the specified
/// subject and associated with the application identifier.
@ -949,87 +929,201 @@ namespace OpenIddict.Core
=> Store.PruneAsync(cancellationToken);
/// <summary>
/// Redeems a token.
/// Sets the application identifier associated with a token.
/// </summary>
/// <param name="token">The token to redeem.</param>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
public virtual async ValueTask RedeemAsync([NotNull] TToken token, CancellationToken cancellationToken = default)
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Sets the authorization identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
/// <summary>
/// Revokes a token.
/// Tries to extend the specified token by replacing its expiration date.
/// </summary>
/// <param name="token">The token to revoke.</param>
/// <param name="token">The token.</param>
/// <param name="date">The date on which the token will no longer be considered valid.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.</returns>
public virtual async ValueTask RevokeAsync([NotNull] TToken token, CancellationToken cancellationToken = default)
/// <returns><c>true</c> if the token was successfully extended, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> 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;
}
}
/// <summary>
/// Sets the application identifier associated with a token.
/// Tries to redeem a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="token">The token to redeem.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async ValueTask SetApplicationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken = default)
/// <returns><c>true</c> if the token was successfully redemeed, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> 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;
}
}
/// <summary>
/// Sets the authorization identifier associated with a token.
/// Tries to revoke a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="token">The token to revoke.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async ValueTask SetAuthorizationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken = default)
/// <returns><c>true</c> if the token was successfully revoked, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> 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;
}
}
/// <summary>
@ -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<object> IOpenIddictTokenManager.FindAsync(string subject, string client, CancellationToken cancellationToken)
=> FindAsync(subject, client, cancellationToken).OfType<object>();
@ -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<bool> IOpenIddictTokenManager.TryExtendAsync(object token, DateTimeOffset? date, CancellationToken cancellationToken)
=> TryExtendAsync((TToken) token, date, cancellationToken);
ValueTask<bool> IOpenIddictTokenManager.TryRedeemAsync(object token, CancellationToken cancellationToken)
=> TryRedeemAsync((TToken) token, cancellationToken);
ValueTask<bool> IOpenIddictTokenManager.TryRevokeAsync(object token, CancellationToken cancellationToken)
=> TryRevokeAsync((TToken) token, cancellationToken);
ValueTask IOpenIddictTokenManager.UpdateAsync(object token, CancellationToken cancellationToken)
=> UpdateAsync((TToken) token, cancellationToken);

57
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs

@ -106,65 +106,22 @@ namespace OpenIddict.Server.AspNetCore
return false;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var transaction = Context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.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)

18
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);
}
}

15
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionBuilder.cs

@ -65,6 +65,21 @@ namespace Microsoft.Extensions.DependencyInjection
return Configure(options => options.DataProtectionProvider = provider);
}
/// <summary>
/// Configures OpenIddict to use a specific formatter instead of relying on the default instance.
/// </summary>
/// <param name="formatter">The formatter used to read and write tokens.</param>
/// <returns>The <see cref="OpenIddictServerDataProtectionBuilder"/>.</returns>
public OpenIddictServerDataProtectionBuilder UseFormatter([NotNull] IOpenIddictServerDataProtectionFormatter formatter)
{
if (formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
return Configure(options => options.Formatter = formatter);
}
/// <summary>
/// Configures OpenIddict to use the Data Protection format when
/// issuing new access tokens, refresh tokens and authorization codes.

379
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<string, string> properties) Read(BinaryReader reader, int version)
{
if (version != reader.ReadInt32())
{
return (null, ImmutableDictionary.Create<string, string>());
}
// 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<string, string>());
}
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<string, string> ReadProperties(BinaryReader reader, int version)
{
if (version != reader.ReadInt32())
{
return ImmutableDictionary.Create<string, string>();
}
var properties = ImmutableDictionary.CreateBuilder<string, string>(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<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? value : null;
static IEnumerable<string> GetArrayProperty(IReadOnlyDictionary<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? JArray.Parse(value).Values<string>() : Enumerable.Empty<string>();
static DateTimeOffset? GetDateProperty(IReadOnlyDictionary<string, string> 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<string, string>();
// 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<string, string> 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<string, string> 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<string, string> properties, string name, string value)
{
if (string.IsNullOrEmpty(value))
{
properties.Remove(name);
}
else
{
properties[name] = value;
}
}
static void SetArrayProperty(IDictionary<string, string> properties, string name, IEnumerable<string> values)
{
var array = new JArray(values);
if (array.Count == 0)
{
properties.Remove(name);
}
else
{
properties[name] = array.ToString(Formatting.None);
}
}
}
}
}

836
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Serialization.cs

@ -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<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Access token serialization:
*/
AttachAccessTokenSerializationProtector.Descriptor,
SerializeDataProtectionToken<SerializeAccessTokenContext>.Descriptor,
/*
* Authorization code serialization:
*/
AttachAuthorizationCodeSerializationProtector.Descriptor,
SerializeDataProtectionToken<SerializeAuthorizationCodeContext>.Descriptor,
/*
* Refresh token serialization:
*/
AttachRefreshTokenSerializationProtector.Descriptor,
SerializeDataProtectionToken<SerializeRefreshTokenContext>.Descriptor,
/*
* Access token deserialization:
*/
AttachAccessTokenDeserializationProtector.Descriptor,
DeserializeDataProtectionToken<DeserializeAccessTokenContext>.Descriptor,
/*
* Authorization code deserialization:
*/
AttachAuthorizationCodeDeserializationProtector.Descriptor,
DeserializeDataProtectionToken<DeserializeAuthorizationCodeContext>.Descriptor,
/*
* Refresh token deserialization:
*/
AttachRefreshTokenDeserializationProtector.Descriptor,
DeserializeDataProtectionToken<DeserializeRefreshTokenContext>.Descriptor);
/// <summary>
/// Contains the logic responsible of generating a Data Protection token.
/// </summary>
public class SerializeDataProtectionToken<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseSerializingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseSingletonHandler<SerializeDataProtectionToken<TContext>>()
.SetOrder(SerializeJwtBearerToken<TContext>.Descriptor.Order - 5000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string, string>();
// 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<string, string> 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<string, string> 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<string, string> properties, string name, string value)
{
if (string.IsNullOrEmpty(value))
{
properties.Remove(name);
}
else
{
properties[name] = value;
}
}
static void SetArrayProperty(IDictionary<string, string> properties, string name, IEnumerable<string> values)
{
var array = new JArray(values);
if (array.Count == 0)
{
properties.Remove(name);
}
else
{
properties[name] = array.ToString(Formatting.None);
}
}
}
}
/// <summary>
/// Contains the logic responsible of unprotecting a Data Protection token.
/// </summary>
public class DeserializeDataProtectionToken<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseDeserializingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.UseSingletonHandler<DeserializeDataProtectionToken<TContext>>()
.SetOrder(DeserializeJwtBearerToken<TContext>.Descriptor.Order - 5000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string, string> properties) Read(BinaryReader reader, int version)
{
if (version != reader.ReadInt32())
{
return (null, ImmutableDictionary.Create<string, string>());
}
// 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<string, string>());
}
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<string, string> ReadProperties(BinaryReader reader, int version)
{
if (version != reader.ReadInt32())
{
return ImmutableDictionary.Create<string, string>();
}
var properties = ImmutableDictionary.CreateBuilder<string, string>(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<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? value : null;
static IEnumerable<string> GetArrayProperty(IReadOnlyDictionary<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? JArray.Parse(value).Values<string>() : Enumerable.Empty<string>();
static DateTimeOffset? GetDateProperty(IReadOnlyDictionary<string, string> properties, string name)
=> properties.TryGetValue(name, out var value) ? (DateTimeOffset?)
DateTimeOffset.ParseExact(value, "r", CultureInfo.InvariantCulture) : null;
}
}
/// <summary>
/// Contains the logic responsible of populating the data protector needed to generate an access token.
/// </summary>
public class AttachAccessTokenSerializationProtector : IOpenIddictServerHandler<SerializeAccessTokenContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachAccessTokenSerializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeAccessTokenContext>()
.UseSingletonHandler<AttachAccessTokenSerializationProtector>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string>(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;
}
}
/// <summary>
/// Contains the logic responsible of populating the data protector needed to generate an authorization code.
/// </summary>
public class AttachAuthorizationCodeSerializationProtector : IOpenIddictServerHandler<SerializeAuthorizationCodeContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachAuthorizationCodeSerializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeAuthorizationCodeContext>()
.UseSingletonHandler<AttachAuthorizationCodeSerializationProtector>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string>(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;
}
}
/// <summary>
/// Contains the logic responsible of populating the data protector needed to generate a refresh token.
/// </summary>
public class AttachRefreshTokenSerializationProtector : IOpenIddictServerHandler<SerializeRefreshTokenContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachRefreshTokenSerializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeRefreshTokenContext>()
.UseSingletonHandler<AttachRefreshTokenSerializationProtector>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string>(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;
}
}
/// <summary>
/// Contains the logic responsible of populating the data protector needed to unprotect an access token.
/// </summary>
public class AttachAccessTokenDeserializationProtector : IOpenIddictServerHandler<DeserializeAccessTokenContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachAccessTokenDeserializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeAccessTokenContext>()
.UseSingletonHandler<AttachAccessTokenDeserializationProtector>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string>(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;
}
}
/// <summary>
/// Contains the logic responsible of populating the data protector needed to unprotect an authorization code.
/// </summary>
public class AttachAuthorizationCodeDeserializationProtector : IOpenIddictServerHandler<DeserializeAuthorizationCodeContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachAuthorizationCodeDeserializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeAuthorizationCodeContext>()
.UseSingletonHandler<AttachAuthorizationCodeDeserializationProtector>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string>(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;
}
}
/// <summary>
/// Contains the logic responsible of populating the data protector needed to unprotect a refresh token.
/// </summary>
public class AttachRefreshTokenDeserializationProtector : IOpenIddictServerHandler<DeserializeRefreshTokenContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachRefreshTokenDeserializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeRefreshTokenContext>()
.UseSingletonHandler<AttachRefreshTokenDeserializationProtector>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string>(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;
}
}
}
}
}

875
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<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = Serialization.DefaultHandlers;
public static ImmutableArray<OpenIddictServerHandlerDescriptor> 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);
/// <summary>
/// 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.
/// </summary>
public class ValidateReferenceDataProtectionToken : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _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<OpenIddictServerDataProtectionOptions> options)
{
_tokenManager = tokenManager;
_options = options;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireReferenceTokensEnabled>()
.UseScopedHandler<ValidateReferenceDataProtectionToken>()
.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<bool> 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
};
}
}
/// <summary>
/// Contains the logic responsible of rejecting authentication demands
/// that specify an invalid self-contained Data Protection token.
/// </summary>
public class ValidateSelfContainedDataProtectionToken : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public ValidateSelfContainedDataProtectionToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireReferenceTokensDisabled>()
.UseSingletonHandler<ValidateSelfContainedDataProtectionToken>()
.SetOrder(ValidateSelfContainedToken.Descriptor.Order - 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
}
}
/// <summary>
/// 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.
/// </summary>
public class AttachReferenceDataProtectionAccessToken : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictTokenManager _tokenManager;
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _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<OpenIddictServerDataProtectionOptions> options)
{
_applicationManager = applicationManager;
_tokenManager = tokenManager;
_options = options;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireAccessTokenIncluded>()
.AddFilter<RequireReferenceTokensEnabled>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseScopedHandler<AttachReferenceDataProtectionAccessToken>()
.SetOrder(AttachReferenceAccessToken.Descriptor.Order - 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class AttachReferenceDataProtectionAuthorizationCode : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictTokenManager _tokenManager;
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _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<OpenIddictServerDataProtectionOptions> options)
{
_applicationManager = applicationManager;
_tokenManager = tokenManager;
_options = options;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireAuthorizationCodeIncluded>()
.AddFilter<RequireReferenceTokensEnabled>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseScopedHandler<AttachReferenceDataProtectionAuthorizationCode>()
.SetOrder(AttachReferenceAuthorizationCode.Descriptor.Order - 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
public class AttachReferenceDataProtectionRefreshToken : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictTokenManager _tokenManager;
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _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<OpenIddictServerDataProtectionOptions> options)
{
_applicationManager = applicationManager;
_tokenManager = tokenManager;
_options = options;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireRefreshTokenIncluded>()
.AddFilter<RequireReferenceTokensEnabled>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseScopedHandler<AttachReferenceDataProtectionRefreshToken>()
.SetOrder(AttachReferenceRefreshToken.Descriptor.Order - 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// Contains the logic responsible of generating and attaching the self-contained
/// Data Protection access token returned as part of the response.
/// </summary>
public class AttachSelfContainedDataProtectionAccessToken : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachSelfContainedDataProtectionAccessToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireReferenceTokensDisabled>()
.AddFilter<RequireAccessTokenIncluded>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseSingletonHandler<AttachSelfContainedDataProtectionAccessToken>()
.SetOrder(AttachSelfContainedAccessToken.Descriptor.Order - 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// Contains the logic responsible of generating and attaching the self-contained
/// Data Protection authorization code returned as part of the response.
/// </summary>
public class AttachSelfContainedDataProtectionAuthorizationCode : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachSelfContainedDataProtectionAuthorizationCode([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireReferenceTokensDisabled>()
.AddFilter<RequireAuthorizationCodeIncluded>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseSingletonHandler<AttachSelfContainedDataProtectionAuthorizationCode>()
.SetOrder(AttachSelfContainedAuthorizationCode.Descriptor.Order - 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// Contains the logic responsible of generating and attaching the self-contained
/// Data Protection refresh token returned as part of the response.
/// </summary>
public class AttachSelfContainedDataProtectionRefreshToken : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachSelfContainedDataProtectionRefreshToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireReferenceTokensDisabled>()
.AddFilter<RequireRefreshTokenIncluded>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseSingletonHandler<AttachSelfContainedDataProtectionRefreshToken>()
.SetOrder(AttachSelfContainedRefreshToken.Descriptor.Order - 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
}
}

7
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionOptions.cs

@ -27,5 +27,12 @@ namespace OpenIddict.Server.DataProtection
/// and authorization codes. This property is set to <c>false</c> by default.
/// </summary>
public bool PreferDataProtectionFormat { get; set; }
/// <summary>
/// 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.
/// </summary>
public IOpenIddictServerDataProtectionFormatter Formatter { get; set; }
= new OpenIddictServerDataProtectionFormatter();
}
}

32
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs

@ -104,40 +104,22 @@ namespace OpenIddict.Server.Owin
return false;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var transaction = Context.Get<OpenIddictServerTransaction>(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<AuthenticationTicket>(null);
}
protected override async Task TeardownCoreAsync()

22
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)

26
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";
}
}
}
}

6
src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs

@ -78,12 +78,6 @@ namespace OpenIddict.Server
/// </summary>
public IDictionary<string, object> Claims { get; }
= new Dictionary<string, object>(StringComparer.Ordinal);
/// <summary>
/// Gets or sets a boolean indicating whether
/// the token was successfully revoked.
/// </summary>
public bool Revoked { get; set; }
}
/// <summary>

229
src/OpenIddict.Server/OpenIddictServerEvents.Serialization.cs

@ -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
{
/// <summary>
/// Represents an abstract base class used for certain event contexts.
/// </summary>
public abstract class BaseSerializingContext : BaseContext
{
/// <summary>
/// Creates a new instance of the <see cref="BaseSerializingContext"/> class.
/// </summary>
public BaseSerializingContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the security principal containing the claims to serialize.
/// </summary>
public ClaimsPrincipal Principal { get; set; }
/// <summary>
/// Gets or sets the encrypting credentials used to encrypt the token.
/// </summary>
public EncryptingCredentials EncryptingCredentials { get; set; }
/// <summary>
/// Gets or sets the signing credentials used to sign the token.
/// </summary>
public SigningCredentials SigningCredentials { get; set; }
/// <summary>
/// Gets or sets the security token handler used to serialize the token.
/// </summary>
public JsonWebTokenHandler SecurityTokenHandler { get; set; }
/// <summary>
/// Gets or sets the token returned to the client application.
/// </summary>
public string Token { get; set; }
/// <summary>
/// Gets or sets the token usage.
/// </summary>
public string TokenUsage { get; set; }
/// <summary>
/// Gets a boolean indicating whether the
/// <see cref="HandleSerialization()"/> method was called.
/// </summary>
public bool IsHandled { get; private set; }
/// <summary>
/// Marks the serialization process as handled by the application code.
/// </summary>
public void HandleSerialization() => IsHandled = true;
}
/// <summary>
/// Represents an abstract base class used for certain event contexts.
/// </summary>
public abstract class BaseDeserializingContext : BaseContext
{
/// <summary>
/// Creates a new instance of the <see cref="BaseDeserializingContext"/> class.
/// </summary>
public BaseDeserializingContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the security principal containing the deserialized claims.
/// </summary>
public ClaimsPrincipal Principal { get; set; }
/// <summary>
/// Gets or sets the validation parameters used to verify the authenticity of access tokens.
/// Note: this property is only used when <see cref="SecurityTokenHandler"/> is not <c>null</c>.
/// </summary>
public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters();
/// <summary>
/// Gets or sets the security token handler used to
/// deserialize the authentication ticket.
/// </summary>
public JsonWebTokenHandler SecurityTokenHandler { get; set; }
/// <summary>
/// Gets or sets the token used by the client application.
/// </summary>
public string Token { get; set; }
/// <summary>
/// Gets or sets the token usage.
/// </summary>
public string TokenUsage { get; set; }
/// <summary>
/// Gets a boolean indicating whether the
/// <see cref="HandleDeserialization()"/> method was called.
/// </summary>
public bool IsHandled { get; private set; }
/// <summary>
/// Marks the deserialization process as handled by the application code.
/// </summary>
public void HandleDeserialization() => IsHandled = true;
}
/// <summary>
/// Represents an event called when serializing an access token.
/// </summary>
public class SerializeAccessTokenContext : BaseSerializingContext
{
/// <summary>
/// Creates a new instance of the <see cref="SerializeAccessTokenContext"/> class.
/// </summary>
public SerializeAccessTokenContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
=> TokenUsage = TokenUsages.AccessToken;
}
/// <summary>
/// Represents an event called when serializing an authorization code.
/// </summary>
public class SerializeAuthorizationCodeContext : BaseSerializingContext
{
/// <summary>
/// Creates a new instance of the <see cref="SerializeAuthorizationCodeContext"/> class.
/// </summary>
public SerializeAuthorizationCodeContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
=> TokenUsage = TokenUsages.AuthorizationCode;
}
/// <summary>
/// Represents an event called when serializing an identity token.
/// </summary>
public class SerializeIdentityTokenContext : BaseSerializingContext
{
/// <summary>
/// Creates a new instance of the <see cref="SerializeIdentityTokenContext"/> class.
/// </summary>
public SerializeIdentityTokenContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
=> TokenUsage = TokenUsages.IdToken;
}
/// <summary>
/// Represents an event called when serializing a refresh token.
/// </summary>
public class SerializeRefreshTokenContext : BaseSerializingContext
{
/// <summary>
/// Creates a new instance of the <see cref="SerializeRefreshTokenContext"/> class.
/// </summary>
public SerializeRefreshTokenContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
=> TokenUsage = TokenUsages.RefreshToken;
}
/// <summary>
/// Represents an event called when deserializing an access token.
/// </summary>
public class DeserializeAccessTokenContext : BaseDeserializingContext
{
/// <summary>
/// Creates a new instance of the <see cref="DeserializeAccessTokenContext"/> class.
/// </summary>
public DeserializeAccessTokenContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
=> TokenUsage = TokenUsages.AccessToken;
}
/// <summary>
/// Represents an event called when deserializing an authorization code.
/// </summary>
public class DeserializeAuthorizationCodeContext : BaseDeserializingContext
{
/// <summary>
/// Creates a new instance of the <see cref="DeserializeAuthorizationCodeContext"/> class.
/// </summary>
public DeserializeAuthorizationCodeContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
=> TokenUsage = TokenUsages.AuthorizationCode;
}
/// <summary>
/// Represents an event called when deserializing an identity token.
/// </summary>
public class DeserializeIdentityTokenContext : BaseDeserializingContext
{
/// <summary>
/// Creates a new instance of the <see cref="DeserializeIdentityTokenContext"/> class.
/// </summary>
public DeserializeIdentityTokenContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
=> TokenUsage = TokenUsages.IdToken;
}
/// <summary>
/// Represents an event called when deserializing a refresh token.
/// </summary>
public class DeserializeRefreshTokenContext : BaseDeserializingContext
{
/// <summary>
/// Creates a new instance of the <see cref="DeserializeRefreshTokenContext"/> class.
/// </summary>
public DeserializeRefreshTokenContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
=> TokenUsage = TokenUsages.RefreshToken;
}
}
}

24
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -371,6 +371,30 @@ namespace OpenIddict.Server
/// recommended, except when dealing with non-standard clients.
/// </summary>
public bool IncludeRefreshToken { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the access token, if applicable.
/// </summary>
public ClaimsPrincipal AccessTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the authorization code, if applicable.
/// </summary>
public ClaimsPrincipal AuthorizationCodePrincipal { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the identity token, if applicable.
/// </summary>
public ClaimsPrincipal IdentityTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the refresh token, if applicable.
/// </summary>
public ClaimsPrincipal RefreshTokenPrincipal { get; set; }
}
/// <summary>

6
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -52,9 +52,15 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton<RequireGrantTypePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireIdentityTokenIncluded>();
builder.Services.TryAddSingleton<RequirePostLogoutRedirectUriParameter>();
builder.Services.TryAddSingleton<RequireReferenceTokensDisabled>();
builder.Services.TryAddSingleton<RequireReferenceTokensEnabled>();
builder.Services.TryAddSingleton<RequireRefreshTokenIncluded>();
builder.Services.TryAddSingleton<RequireRollingTokensDisabled>();
builder.Services.TryAddSingleton<RequireRollingTokensEnabled>();
builder.Services.TryAddSingleton<RequireSlidingExpirationEnabled>();
builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireScopeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenStorageEnabled>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<

96
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -159,6 +159,38 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if reference tokens are enabled.
/// </summary>
public class RequireReferenceTokensDisabled : IOpenIddictServerHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(!context.Options.UseReferenceTokens);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if reference tokens are disabled.
/// </summary>
public class RequireReferenceTokensEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.UseReferenceTokens);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no refresh token is returned.
/// </summary>
@ -175,6 +207,38 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if rolling tokens were enabled.
/// </summary>
public class RequireRollingTokensDisabled : IOpenIddictServerHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(!context.Options.UseRollingTokens);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if rolling tokens were not enabled.
/// </summary>
public class RequireRollingTokensEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.UseRollingTokens);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if scope permissions were disabled.
/// </summary>
@ -206,5 +270,37 @@ namespace OpenIddict.Server
return new ValueTask<bool>(!context.Options.DisableScopeValidation);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if sliding expiration was disabled.
/// </summary>
public class RequireSlidingExpirationEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.UseSlidingExpiration);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if token storage was not enabled.
/// </summary>
public class RequireTokenStorageEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(!context.Options.DisableTokenStorage);
}
}
}
}

26
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.

186
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
}
/// <summary>
/// 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.
/// </summary>
public class ValidateAuthorizationCode : IOpenIddictServerHandler<ValidateTokenRequestContext>
public class ValidateToken : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public ValidateAuthorizationCode([NotNull] IOpenIddictServerProvider provider)
public ValidateToken([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
@ -1200,12 +1199,8 @@ namespace OpenIddict.Server
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseScopedHandler<ValidateAuthorizationCode>()
// 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<ValidateToken>()
.SetOrder(ValidateScopePermissions.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -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;
}
}
/// <summary>
/// Contains the logic responsible of rejecting token requests that specify an invalid refresh token.
/// </summary>
public class ValidateRefreshToken : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public ValidateRefreshToken([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseScopedHandler<ValidateRefreshToken>()
.SetOrder(ValidateAuthorizationCode.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<ValidateTokenRequestContext>()
.UseSingletonHandler<ValidatePresenters>()
.SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000)
.SetOrder(ValidateToken.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -1688,67 +1607,6 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// 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.
/// </summary>
public class ValidateAuthorization : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
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;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireAuthorizationStorageEnabled>()
.UseScopedHandler<ValidateAuthorization>()
.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;
}
}
}
/// <summary>
/// Contains the logic responsible of attaching the principal extracted
/// from the authorization code/refresh token to the event context.

187
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
}
/// <summary>
/// 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.
/// </summary>
public class ValidateToken : IOpenIddictServerHandler<ValidateIntrospectionRequestContext>
{
@ -671,11 +670,7 @@ namespace OpenIddict.Server
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateIntrospectionRequestContext>()
.UseScopedHandler<ValidateToken>()
// 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();
/// <summary>
@ -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<ClaimsPrincipal> DeserializeAccessTokenAsync()
{
var notification = new DeserializeAccessTokenContext(context.Transaction)
{
Token = context.Request.Token
};
await _provider.DispatchAsync(notification);
return notification.Principal;
}
async ValueTask<ClaimsPrincipal> DeserializeAuthorizationCodeAsync()
{
var notification = new DeserializeAuthorizationCodeContext(context.Transaction)
{
Token = context.Request.Token
};
await _provider.DispatchAsync(notification);
return notification.Principal;
}
async ValueTask<ClaimsPrincipal> 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<ClaimsPrincipal> 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<RequireClientIdParameter>()
.UseSingletonHandler<ValidateAuthorizedParty>()
.SetOrder(ValidateToken.Descriptor.Order + 1_000)
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -919,65 +831,6 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// 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.
/// </summary>
public class ValidateAuthorization : IOpenIddictServerHandler<ValidateIntrospectionRequestContext>
{
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;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateIntrospectionRequestContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireAuthorizationStorageEnabled>()
.UseScopedHandler<ValidateAuthorization>()
.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;
}
}
}
/// <summary>
/// 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);

229
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
}
/// <summary>
/// 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.
/// </summary>
public class ValidateToken : IOpenIddictServerHandler<ValidateRevocationRequestContext>
{
@ -617,11 +618,7 @@ namespace OpenIddict.Server
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateRevocationRequestContext>()
.UseScopedHandler<ValidateToken>()
// 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();
/// <summary>
@ -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<ClaimsPrincipal> DeserializeAccessTokenAsync()
{
var notification = new DeserializeAccessTokenContext(context.Transaction)
{
Token = context.Request.Token
};
await _provider.DispatchAsync(notification);
return notification.Principal;
}
async ValueTask<ClaimsPrincipal> DeserializeAuthorizationCodeAsync()
{
var notification = new DeserializeAuthorizationCodeContext(context.Transaction)
{
Token = context.Request.Token
};
await _provider.DispatchAsync(notification);
return notification.Principal;
}
async ValueTask<ClaimsPrincipal> 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<ClaimsPrincipal> 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
}
}
/// <summary>
/// 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.
/// </summary>
public class RevokeToken : IOpenIddictServerHandler<HandleRevocationRequestContext>
{
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;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleRevocationRequestContext>()
.UseScopedHandler<RevokeToken>()
.SetOrder(AttachPrincipal.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
}
/// <summary>
/// Contains the logic responsible of converting revocation errors to standard empty responses.
/// </summary>

610
src/OpenIddict.Server/OpenIddictServerHandlers.Serialization.cs

@ -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<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Access token serialization:
*/
AttachAccessTokenSerializationParameters.Descriptor,
SerializeJwtBearerToken<SerializeAccessTokenContext>.Descriptor,
/*
* Authorization code serialization:
*/
AttachAuthorizationCodeSerializationParameters.Descriptor,
SerializeJwtBearerToken<SerializeAuthorizationCodeContext>.Descriptor,
/*
* Identity token serialization:
*/
AttachIdentityTokenSerializationParameters.Descriptor,
SerializeJwtBearerToken<SerializeIdentityTokenContext>.Descriptor,
/*
* Refresh token serialization:
*/
AttachRefreshTokenSerializationParameters.Descriptor,
SerializeJwtBearerToken<SerializeRefreshTokenContext>.Descriptor,
/*
* Access token deserialization:
*/
AttachAccessTokenDeserializationParameters.Descriptor,
DeserializeJwtBearerToken<DeserializeAccessTokenContext>.Descriptor,
/*
* Authorization code deserialization:
*/
AttachAuthorizationCodeDeserializationParameters.Descriptor,
DeserializeJwtBearerToken<DeserializeAuthorizationCodeContext>.Descriptor,
/*
* Identity token deserialization:
*/
AttachIdentityTokenDeserializationParameters.Descriptor,
DeserializeJwtBearerToken<DeserializeIdentityTokenContext>.Descriptor,
/*
* Authorization code deserialization:
*/
AttachRefreshTokenDeserializationParameters.Descriptor,
DeserializeJwtBearerToken<DeserializeRefreshTokenContext>.Descriptor);
/// <summary>
/// Contains the logic responsible of generating a JWT bearer token using IdentityModel.
/// </summary>
public class SerializeJwtBearerToken<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseSerializingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.UseSingletonHandler<SerializeJwtBearerToken<TContext>>()
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string, object>(StringComparer.Ordinal)
{
[Claims.Private.TokenUsage] = context.TokenUsage
};
var destinations = new Dictionary<string, string[]>(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<string>(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<string, object>(claims),
EncryptingCredentials = context.EncryptingCredentials,
Issuer = context.Issuer?.AbsoluteUri,
SigningCredentials = context.SigningCredentials
});
context.HandleSerialization();
return default;
}
}
/// <summary>
/// Contains the logic responsible of unprotecting a JWT bearer token using IdentityModel.
/// </summary>
public class DeserializeJwtBearerToken<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseDeserializingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.UseSingletonHandler<DeserializeJwtBearerToken<TContext>>()
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<string, string[]> 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;
}
}
}
/// <summary>
/// Contains the logic responsible of populating the serialization parameters needed to generate an access token.
/// </summary>
public class AttachAccessTokenSerializationParameters : IOpenIddictServerHandler<SerializeAccessTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeAccessTokenContext>()
.UseSingletonHandler<AttachAccessTokenSerializationParameters>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// Contains the logic responsible of populating the serialization parameters needed to generate an authorization code.
/// </summary>
public class AttachAuthorizationCodeSerializationParameters : IOpenIddictServerHandler<SerializeAuthorizationCodeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeAuthorizationCodeContext>()
.UseSingletonHandler<AttachAuthorizationCodeSerializationParameters>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// Contains the logic responsible of populating the serialization parameters needed to generate an identity token.
/// </summary>
public class AttachIdentityTokenSerializationParameters : IOpenIddictServerHandler<SerializeIdentityTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeIdentityTokenContext>()
.UseSingletonHandler<AttachIdentityTokenSerializationParameters>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// Contains the logic responsible of populating the serialization parameters needed to generate a refresh token.
/// </summary>
public class AttachRefreshTokenSerializationParameters : IOpenIddictServerHandler<SerializeRefreshTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeRefreshTokenContext>()
.UseSingletonHandler<AttachRefreshTokenSerializationParameters>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// Contains the logic responsible of populating the deserialization parameters needed to unprotect an access token.
/// </summary>
public class AttachAccessTokenDeserializationParameters : IOpenIddictServerHandler<DeserializeAccessTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeAccessTokenContext>()
.UseSingletonHandler<AttachAccessTokenDeserializationParameters>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// Contains the logic responsible of populating the deserialization parameters needed to unprotect an authorization code.
/// </summary>
public class AttachAuthorizationCodeDeserializationParameters : IOpenIddictServerHandler<DeserializeAuthorizationCodeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeAuthorizationCodeContext>()
.UseSingletonHandler<AttachAuthorizationCodeDeserializationParameters>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
/// <summary>
/// Contains the logic responsible of populating the deserialization parameters needed to unprotect an identity token.
/// </summary>
public class AttachIdentityTokenDeserializationParameters : IOpenIddictServerHandler<DeserializeIdentityTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeIdentityTokenContext>()
.UseSingletonHandler<AttachIdentityTokenDeserializationParameters>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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<AsymmetricSecurityKey>();
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;
}
}
/// <summary>
/// Contains the logic responsible of populating the deserialization parameters needed to unprotect a refresh token.
/// </summary>
public class AttachRefreshTokenDeserializationParameters : IOpenIddictServerHandler<DeserializeRefreshTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeRefreshTokenContext>()
.UseSingletonHandler<AttachRefreshTokenDeserializationParameters>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
}
}
}
}

26
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.

46
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
}
/// <summary>
/// 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.
/// </summary>
public class ValidateAccessToken : IOpenIddictServerHandler<ValidateUserinfoRequestContext>
public class ValidateToken : IOpenIddictServerHandler<ValidateUserinfoRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public ValidateAccessToken([NotNull] IOpenIddictServerProvider provider)
public ValidateToken([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
@ -406,8 +406,8 @@ namespace OpenIddict.Server
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateUserinfoRequestContext>()
.UseScopedHandler<ValidateAccessToken>()
.SetOrder(100_000)
.UseScopedHandler<ValidateToken>()
.SetOrder(ValidateAccessTokenParameter.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -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;
}
}

2747
src/OpenIddict.Server/OpenIddictServerHandlers.cs

File diff suppressed because it is too large

23
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<Uri> UserinfoEndpointUris { get; } = new List<Uri>();
/// <summary>
/// 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.
/// </summary>
public JsonWebTokenHandler AuthorizationCodeHandler { get; set; } = new JsonWebTokenHandler();
/// <summary>
/// Gets or sets the security token handler used to protect and unprotect access tokens.
/// </summary>
public JsonWebTokenHandler AccessTokenHandler { get; set; } = new JsonWebTokenHandler();
/// <summary>
/// Gets or sets the security token handler used to protect and unprotect identity tokens.
/// </summary>
public JsonWebTokenHandler IdentityTokenHandler { get; set; } = new JsonWebTokenHandler();
/// <summary>
/// Gets or sets the security token handler used to protect and unprotect refresh tokens.
/// </summary>
public JsonWebTokenHandler RefreshTokenHandler { get; set; } = new JsonWebTokenHandler();
public OpenIddictServerTokenHandler SecurityTokenHandler { get; set; } = new OpenIddictServerTokenHandler
{
SetDefaultTimesOnTokenCreation = false
};
/// <summary>
/// Gets or sets the period of time the authorization codes remain valid after being issued.

8
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;
}
}

147
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<string> 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<string, string[]>(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<string>(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<string>(base.CreateToken(descriptor));
}
public ValueTask<TokenValidationResult> 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<TokenValidationResult>(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<TokenValidationResult>(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<TokenValidationResult>(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<string, string[]> 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<TokenValidationResult>(result);
}
catch (Exception exception)
{
return new ValueTask<TokenValidationResult>(new TokenValidationResult
{
Exception = exception,
IsValid = false
});
}
}
}
}
Loading…
Cancel
Save