From 77ca35a53f8c19166687b64a6611e725aff61a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Thu, 4 Jun 2020 17:48:29 +0200 Subject: [PATCH] Use private claims for the token creation/expiration dates and introduce new Data Protection authentication properties --- .../Controllers/AuthorizationController.cs | 4 +- .../OpenIddictConstants.cs | 5 +- .../Primitives/OpenIddictExtensions.cs | 127 +++--- ...OpenIddictServerDataProtectionConstants.cs | 3 + ...OpenIddictServerDataProtectionFormatter.cs | 35 +- .../OpenIddictServerHandlers.Revocation.cs | 2 +- .../OpenIddictServerHandlers.cs | 384 +++++++++++++----- ...IddictValidationDataProtectionConstants.cs | 3 + ...IddictValidationDataProtectionFormatter.cs | 14 +- .../OpenIddictValidationHandlers.cs | 80 +++- .../Primitives/OpenIddictExtensionsTests.cs | 209 ++++++---- ...enIddictServerIntegrationTests.Exchange.cs | 66 +-- ...ictServerIntegrationTests.Introspection.cs | 16 +- ...IddictServerIntegrationTests.Revocation.cs | 6 +- .../OpenIddictServerIntegrationTests.cs | 272 ++++++++++++- 15 files changed, 883 insertions(+), 343 deletions(-) diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index 7623bd8f..2b9d07c0 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -191,7 +191,7 @@ namespace Mvc.Server scopes : principal.GetScopes()); } - principal.SetInternalAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); + principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); foreach (var claim in principal.Claims) { @@ -281,7 +281,7 @@ namespace Mvc.Server scopes : principal.GetScopes()); } - principal.SetInternalAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); + principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); foreach (var claim in principal.Claims) { diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 8685ea27..5bdb04ef 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -88,13 +88,16 @@ namespace OpenIddict.Abstractions public static class Private { public const string AccessTokenLifetime = "oi_act_lft"; - public const string AuthorizationId = "oi_au_id"; + public const string Audience = "oi_aud"; public const string AuthorizationCodeLifetime = "oi_auc_lft"; + public const string AuthorizationId = "oi_au_id"; public const string ClaimDestinationsMap = "oi_cl_dstn"; public const string CodeChallenge = "oi_cd_chlg"; public const string CodeChallengeMethod = "oi_cd_chlg_meth"; + public const string CreationDate = "oi_crt_dt"; public const string DeviceCodeId = "oi_dvc_id"; public const string DeviceCodeLifetime = "oi_dvc_lft"; + public const string ExpirationDate = "oi_exp_dt"; public const string IdentityTokenLifetime = "oi_idt_lft"; public const string Nonce = "oi_nce"; public const string Presenter = "oi_prst"; diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs index 83c4660d..a72e24a0 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -903,6 +903,27 @@ namespace OpenIddict.Abstractions return identity.FindAll(type).Select(claim => claim.Value).Distinct(StringComparer.Ordinal).ToImmutableArray(); } + /// + /// Determines whether the claims identity contains at least one claim of the specified type. + /// + /// The claims identity. + /// The claim type. + /// true if the identity contains at least one claim of the specified type. + public static bool HasClaim([NotNull] this ClaimsIdentity identity, [NotNull] string type) + { + if (identity == null) + { + throw new ArgumentNullException(nameof(identity)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + return identity.FindAll(type).Any(); + } + /// /// Gets the claim values corresponding to the given type. /// @@ -924,6 +945,27 @@ namespace OpenIddict.Abstractions return principal.FindAll(type).Select(claim => claim.Value).Distinct(StringComparer.Ordinal).ToImmutableArray(); } + /// + /// Determines whether the claims principal contains at least one claim of the specified type. + /// + /// The claims principal. + /// The claim type. + /// true if the principal contains at least one claim of the specified type. + public static bool HasClaim([NotNull] this ClaimsPrincipal principal, [NotNull] string type) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The claim type cannot be null or empty.", nameof(type)); + } + + return principal.FindAll(type).Any(); + } + /// /// Removes all the claims corresponding to the given type. /// @@ -1113,18 +1155,18 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - var claim = principal.FindFirst(Claims.IssuedAt); + var claim = principal.FindFirst(Claims.Private.CreationDate); if (claim == null) { return null; } - if (!long.TryParse(claim.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + if (!DateTimeOffset.TryParseExact(claim.Value, "r", CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)) { return null; } - return DateTimeOffset.FromUnixTimeSeconds(value); + return value; } /// @@ -1139,18 +1181,18 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - var claim = principal.FindFirst(Claims.ExpiresAt); + var claim = principal.FindFirst(Claims.Private.ExpirationDate); if (claim == null) { return null; } - if (!long.TryParse(claim.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + if (!DateTimeOffset.TryParseExact(claim.Value, "r", CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)) { return null; } - return DateTimeOffset.FromUnixTimeSeconds(value); + return value; } /// @@ -1159,7 +1201,7 @@ namespace OpenIddict.Abstractions /// The claims principal. /// The audiences list or an empty set if the claims cannot be found. public static ImmutableArray GetAudiences([NotNull] this ClaimsPrincipal principal) - => principal.GetClaims(Claims.Audience); + => principal.GetClaims(Claims.Private.Audience); /// /// Gets the presenters list stored in the claims principal. @@ -1238,7 +1280,7 @@ namespace OpenIddict.Abstractions /// /// The claims principal. /// The unique identifier or null if the claim cannot be found. - public static string GetInternalAuthorizationId([NotNull] this ClaimsPrincipal principal) + public static string GetAuthorizationId([NotNull] this ClaimsPrincipal principal) => principal.GetClaim(Claims.Private.AuthorizationId); /// @@ -1246,7 +1288,7 @@ namespace OpenIddict.Abstractions /// /// The claims principal. /// The unique identifier or null if the claim cannot be found. - public static string GetInternalTokenId([NotNull] this ClaimsPrincipal principal) + public static string GetTokenId([NotNull] this ClaimsPrincipal principal) => principal.GetClaim(Claims.Private.TokenId); /// @@ -1263,14 +1305,7 @@ namespace OpenIddict.Abstractions /// The claims principal. /// true if the principal contains at least one audience. public static bool HasAudience([NotNull] this ClaimsPrincipal principal) - { - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - - return principal.FindAll(Claims.Audience).Any(); - } + => principal.HasClaim(Claims.Private.Audience); /// /// Determines whether the claims principal contains the given audience. @@ -1290,7 +1325,7 @@ namespace OpenIddict.Abstractions throw new ArgumentException("The audience cannot be null or empty.", nameof(audience)); } - return principal.HasClaim(Claims.Audience, audience); + return principal.HasClaim(Claims.Private.Audience, audience); } /// @@ -1299,14 +1334,7 @@ namespace OpenIddict.Abstractions /// The claims principal. /// true if the principal contains at least one presenter. public static bool HasPresenter([NotNull] this ClaimsPrincipal principal) - { - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - - return principal.FindAll(Claims.Private.Presenter).Any(); - } + => principal.HasClaim(Claims.Private.Presenter); /// /// Determines whether the claims principal contains the given presenter. @@ -1335,14 +1363,7 @@ namespace OpenIddict.Abstractions /// The claims principal. /// true if the principal contains at least one resource. public static bool HasResource([NotNull] this ClaimsPrincipal principal) - { - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - - return principal.FindAll(Claims.Private.Resource).Any(); - } + => principal.HasClaim(Claims.Private.Resource); /// /// Determines whether the claims principal contains the given resource. @@ -1371,14 +1392,7 @@ namespace OpenIddict.Abstractions /// The claims principal. /// true if the principal contains at least one scope. public static bool HasScope([NotNull] this ClaimsPrincipal principal) - { - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - - return principal.FindAll(Claims.Private.Scope).Any(); - } + => principal.HasClaim(Claims.Private.Scope); /// /// Determines whether the claims principal contains the given scope. @@ -1429,7 +1443,7 @@ namespace OpenIddict.Abstractions /// The creation date /// The claims principal. public static ClaimsPrincipal SetCreationDate([NotNull] this ClaimsPrincipal principal, [CanBeNull] DateTimeOffset? date) - => SetDateClaim(principal, Claims.IssuedAt, date); + => principal.SetClaim(Claims.Private.CreationDate, date?.ToString("r", CultureInfo.InvariantCulture)); /// /// Sets the expiration date in the claims principal. @@ -1438,7 +1452,7 @@ namespace OpenIddict.Abstractions /// The expiration date /// The claims principal. public static ClaimsPrincipal SetExpirationDate([NotNull] this ClaimsPrincipal principal, [CanBeNull] DateTimeOffset? date) - => SetDateClaim(principal, Claims.ExpiresAt, date); + => principal.SetClaim(Claims.Private.ExpirationDate, date?.ToString("r", CultureInfo.InvariantCulture)); /// /// Sets the audiences list in the claims principal. @@ -1449,7 +1463,7 @@ namespace OpenIddict.Abstractions /// The claims principal. public static ClaimsPrincipal SetAudiences( [NotNull] this ClaimsPrincipal principal, [CanBeNull] ImmutableArray audiences) - => principal.SetClaims(Claims.Audience, audiences); + => principal.SetClaims(Claims.Private.Audience, audiences); /// /// Sets the audiences list in the claims principal. @@ -1632,7 +1646,7 @@ namespace OpenIddict.Abstractions /// The claims principal. /// The unique identifier to store. /// The claims principal. - public static ClaimsPrincipal SetInternalAuthorizationId([NotNull] this ClaimsPrincipal principal, string identifier) + public static ClaimsPrincipal SetAuthorizationId([NotNull] this ClaimsPrincipal principal, string identifier) => principal.SetClaim(Claims.Private.AuthorizationId, identifier); /// @@ -1641,7 +1655,7 @@ namespace OpenIddict.Abstractions /// The claims principal. /// The unique identifier to store. /// The claims principal. - public static ClaimsPrincipal SetInternalTokenId([NotNull] this ClaimsPrincipal principal, string identifier) + public static ClaimsPrincipal SetTokenId([NotNull] this ClaimsPrincipal principal, string identifier) => principal.SetClaim(Claims.Private.TokenId, identifier); /// @@ -1779,24 +1793,5 @@ namespace OpenIddict.Abstractions return null; } - - private static ClaimsPrincipal SetDateClaim(ClaimsPrincipal principal, string type, DateTimeOffset? date) - { - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - - principal.RemoveClaims(type); - - if (date.HasValue) - { - var value = date?.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture); - var claim = new Claim(type, value, ClaimValueTypes.Integer64); - ((ClaimsIdentity)principal.Identity).AddClaim(claim); - } - - return principal; - } } } diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs index 2ec90bba..c37539d8 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs @@ -16,6 +16,8 @@ namespace OpenIddict.Server.DataProtection public const string CodeChallenge = ".code_challenge"; public const string CodeChallengeMethod = ".code_challenge_method"; public const string DataProtector = ".data_protector"; + public const string DeviceCodeId = ".device_code_id"; + public const string DeviceCodeLifetime = ".device_code_lifetime"; public const string Expires = ".expires"; public const string IdentityTokenLifetime = ".identity_token_lifetime"; public const string InternalAuthorizationId = ".internal_authorization_id"; @@ -27,6 +29,7 @@ namespace OpenIddict.Server.DataProtection public const string RefreshTokenLifetime = ".refresh_token_lifetime"; public const string Resources = ".resources"; public const string Scopes = ".scopes"; + public const string UserCodeLifetime = ".user_code_lifetime"; } public static class Purposes diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs index d8a0947d..8a8a2c4a 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.IO; using System.Linq; using System.Security.Claims; @@ -41,8 +40,6 @@ namespace OpenIddict.Server.DataProtection return principal .SetAudiences(GetArrayProperty(properties, Properties.Audiences)) - .SetCreationDate(GetDateProperty(properties, Properties.Issued)) - .SetExpirationDate(GetDateProperty(properties, Properties.Expires)) .SetPresenters(GetArrayProperty(properties, Properties.Presenters)) .SetResources(GetArrayProperty(properties, Properties.Resources)) .SetScopes(GetArrayProperty(properties, Properties.Scopes)) @@ -52,11 +49,16 @@ namespace OpenIddict.Server.DataProtection .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.CreationDate, GetProperty(properties, Properties.Issued)) + .SetClaim(Claims.Private.DeviceCodeId, GetProperty(properties, Properties.DeviceCodeId)) + .SetClaim(Claims.Private.DeviceCodeLifetime, GetProperty(properties, Properties.DeviceCodeLifetime)) .SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) + .SetClaim(Claims.Private.ExpirationDate, GetProperty(properties, Properties.Expires)) .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)); + .SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)) + .SetClaim(Claims.Private.UserCodeLifetime, GetProperty(properties, Properties.UserCodeLifetime)); static (ClaimsPrincipal principal, ImmutableDictionary properties) Read(BinaryReader reader, int version) { @@ -177,10 +179,6 @@ namespace OpenIddict.Server.DataProtection static ImmutableArray GetArrayProperty(IReadOnlyDictionary properties, string name) => properties.TryGetValue(name, out var value) ? JsonSerializer.Deserialize>(value) : ImmutableArray.Create(); - - static DateTimeOffset? GetDateProperty(IReadOnlyDictionary properties, string name) - => properties.TryGetValue(name, out var value) ? (DateTimeOffset?) - DateTimeOffset.ParseExact(value, "r", CultureInfo.InvariantCulture) : null; } public void WriteToken([NotNull] BinaryWriter writer, [NotNull] ClaimsPrincipal principal) @@ -201,20 +199,23 @@ namespace OpenIddict.Server.DataProtection // can't include authentication properties. To ensure tokens can be used with previous versions // of OpenIddict (1.x/2.x), 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.Issued, principal.GetClaim(Claims.Private.CreationDate)); + SetProperty(properties, Properties.Expires, principal.GetClaim(Claims.Private.ExpirationDate)); SetProperty(properties, Properties.AccessTokenLifetime, principal.GetClaim(Claims.Private.AccessTokenLifetime)); SetProperty(properties, Properties.AuthorizationCodeLifetime, principal.GetClaim(Claims.Private.AuthorizationCodeLifetime)); + SetProperty(properties, Properties.DeviceCodeLifetime, principal.GetClaim(Claims.Private.DeviceCodeLifetime)); SetProperty(properties, Properties.IdentityTokenLifetime, principal.GetClaim(Claims.Private.IdentityTokenLifetime)); SetProperty(properties, Properties.RefreshTokenLifetime, principal.GetClaim(Claims.Private.RefreshTokenLifetime)); + SetProperty(properties, Properties.UserCodeLifetime, principal.GetClaim(Claims.Private.UserCodeLifetime)); 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.InternalAuthorizationId, principal.GetAuthorizationId()); + SetProperty(properties, Properties.InternalTokenId, principal.GetTokenId()); + SetProperty(properties, Properties.DeviceCodeId, principal.GetClaim(Claims.Private.DeviceCodeId)); SetProperty(properties, Properties.Nonce, principal.GetClaim(Claims.Private.Nonce)); SetProperty(properties, Properties.OriginalRedirectUri, principal.GetClaim(Claims.Private.RedirectUri)); @@ -226,15 +227,16 @@ namespace OpenIddict.Server.DataProtection // 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.Audience => false, Claims.Private.AuthorizationCodeLifetime => false, Claims.Private.AuthorizationId => false, Claims.Private.CodeChallenge => false, Claims.Private.CodeChallengeMethod => false, + Claims.Private.CreationDate => false, + Claims.Private.DeviceCodeId => false, + Claims.Private.DeviceCodeLifetime => false, + Claims.Private.ExpirationDate => false, Claims.Private.IdentityTokenLifetime => false, Claims.Private.Nonce => false, Claims.Private.Presenter => false, @@ -243,6 +245,7 @@ namespace OpenIddict.Server.DataProtection Claims.Private.Resource => false, Claims.Private.Scope => false, Claims.Private.TokenId => false, + Claims.Private.UserCodeLifetime => false, _ => true }); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs index 4cc3acc5..0b42edaf 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs @@ -997,7 +997,7 @@ namespace OpenIddict.Server } // Extract the token identifier from the authentication principal. - var identifier = context.Principal.GetInternalTokenId(); + var identifier = context.Principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { context.Logger.LogError("The revocation request was rejected because the token had no internal identifier."); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index f3bb8c68..42943982 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -543,24 +543,73 @@ namespace OpenIddict.Server return default; } - if (!context.Principal.HasTokenType(TokenTypeHints.AccessToken)) + // To reduce the size of tokens, some of the private claims used by OpenIddict + // are mapped to their standard equivalent before being removed from the token. + // This handler is responsible of adding back the private claims to the principal + // when receiving the token (e.g "oi_prst" is resolved from the "scope" claim). + + // In OpenIddict 3.0, the creation date of a token is stored in "oi_crt_dt". + // If the claim doesn't exist, try to infer it from the standard "iat" JWT claim. + if (!context.Principal.HasClaim(Claims.Private.CreationDate)) + { + var date = context.Principal.GetClaim(Claims.IssuedAt); + if (!string.IsNullOrEmpty(date) && + long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + context.Principal.SetCreationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + } + } + + // In OpenIddict 3.0, the expiration date of a token is stored in "oi_exp_dt". + // If the claim doesn't exist, try to infer it from the standard "exp" JWT claim. + if (!context.Principal.HasClaim(Claims.Private.ExpirationDate)) { - return default; + var date = context.Principal.GetClaim(Claims.ExpiresAt); + if (!string.IsNullOrEmpty(date) && + long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + context.Principal.SetExpirationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + } + } + + // In OpenIddict 3.0, the audiences allowed to receive a token are stored in "oi_aud". + // If no such claim exists, try to infer them from the standard "aud" JWT claims. + if (!context.Principal.HasAudience()) + { + var audiences = context.Principal.GetClaims(Claims.Audience); + if (audiences.Any()) + { + context.Principal.SetAudiences(audiences); + } } - // Map the standardized "azp" and "scope" claims to their "oi_" equivalent so that - // the ClaimsPrincipal extensions exposed by OpenIddict return consistent results. + // In OpenIddict 3.0, the presenters allowed to use a token are stored in "oi_prst". + // If no such claim exists, try to infer them from the standard "azp" and "client_id" JWT claims. + // + // Note: in previous OpenIddict versions, the presenters were represented in JWT tokens + // using the "azp" claim (defined by OpenID Connect), for which a single value could be + // specified. To ensure presenters stored in JWT tokens created by OpenIddict 1.x/2.x + // can still be read with OpenIddict 3.0, the presenter is automatically inferred from + // the "azp" or "client_id" claim if no "oi_prst" claim was found in the principal. if (!context.Principal.HasPresenter()) { - context.Principal.SetPresenters(context.Principal.GetClaims(Claims.AuthorizedParty)); + var presenter = context.Principal.GetClaim(Claims.AuthorizedParty) ?? + context.Principal.GetClaim(Claims.ClientId); + + if (!string.IsNullOrEmpty(presenter)) + { + context.Principal.SetPresenters(presenter); + } } + // In OpenIddict 3.0, the scopes granted to an application are stored in "oi_scp". + // // Note: in previous OpenIddict versions, scopes were represented as a JSON array // and deserialized as multiple claims. In OpenIddict 3.0, the public "scope" claim // is formatted as a unique space-separated string containing all the granted scopes. // To ensure access tokens generated by previous versions are still correctly handled, // both formats (unique space-separated string or multiple scope claims) must be supported. - // Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-03 for more information. + // Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information. if (!context.Principal.HasScope()) { var scopes = context.Principal.GetClaims(Claims.Scope); @@ -569,7 +618,10 @@ namespace OpenIddict.Server scopes = scopes[0].Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray(); } - context.Principal.SetScopes(scopes); + if (scopes.Any()) + { + context.Principal.SetScopes(scopes); + } } return default; @@ -634,8 +686,8 @@ namespace OpenIddict.Server context.Principal = context.Principal .SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) - .SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) - .SetInternalTokenId(await _tokenManager.GetIdAsync(token)) + .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetTokenId(await _tokenManager.GetIdAsync(token)) .SetTokenType(await _tokenManager.GetTypeAsync(token)); } } @@ -771,7 +823,7 @@ namespace OpenIddict.Server // Extract the token identifier from the authentication principal. // If no token identifier can be found, this indicates that the token // has no backing database entry (e.g an access token or an identity token). - var identifier = context.Principal.GetInternalTokenId(); + var identifier = context.Principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { return; @@ -816,7 +868,7 @@ namespace OpenIddict.Server await _tokenManager.TryRevokeAsync(token); // Then, try to revoke the authorization and the associated token entries. - await TryRevokeAuthorizationChainAsync(context.Principal.GetInternalAuthorizationId()); + await TryRevokeAuthorizationChainAsync(context.Principal.GetAuthorizationId()); context.Logger.LogError("The token '{Identifier}' has already been redeemed.", identifier); @@ -897,8 +949,8 @@ namespace OpenIddict.Server // Restore the creation/expiration dates/identifiers from the token entry metadata. context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) - .SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) - .SetInternalTokenId(await _tokenManager.GetIdAsync(token)) + .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetTokenId(await _tokenManager.GetIdAsync(token)) .SetTokenType(await _tokenManager.GetTypeAsync(token)); async ValueTask TryRevokeAuthorizationChainAsync(string identifier) @@ -919,7 +971,7 @@ namespace OpenIddict.Server await foreach (var token in _tokenManager.FindByAuthorizationIdAsync(identifier)) { // Don't change the status of the token used in the token request. - if (string.Equals(context.Principal.GetInternalTokenId(), + if (string.Equals(context.Principal.GetTokenId(), await _tokenManager.GetIdAsync(token), StringComparison.Ordinal)) { continue; @@ -969,7 +1021,7 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - var identifier = context.Principal.GetInternalAuthorizationId(); + var identifier = context.Principal.GetAuthorizationId(); if (string.IsNullOrEmpty(identifier)) { return; @@ -1286,7 +1338,7 @@ namespace OpenIddict.Server throw new InvalidOperationException("The authentication context cannot be found."); // Extract the device code identifier from the authentication principal. - var identifier = notification.Principal.GetInternalTokenId(); + var identifier = notification.Principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The token identifier cannot be extracted from the principal."); @@ -1741,7 +1793,7 @@ namespace OpenIddict.Server } // If an authorization identifier was explicitly specified, don't create an ad-hoc authorization. - if (!string.IsNullOrEmpty(context.Principal.GetInternalAuthorizationId())) + if (!string.IsNullOrEmpty(context.Principal.GetAuthorizationId())) { return; } @@ -1791,7 +1843,7 @@ namespace OpenIddict.Server // Attach the unique identifier of the ad hoc authorization to the authentication principal // so that it is attached to all the derived tokens, allowing batched revocations support. - context.Principal.SetInternalAuthorizationId(identifier); + context.Principal.SetAuthorizationId(identifier); } } @@ -1893,10 +1945,11 @@ namespace OpenIddict.Server principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); } - // Set the public audiences collection using the private resource claims stored in the principal. + // Set the audiences based on the resource claims stored in the principal. principal.SetAudiences(context.Principal.GetResources()); - // Store the client_id as a public client_id claim, if available. + // Store the client identifier in the public client_id claim, if available. + // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information. principal.SetClaim(Claims.ClientId, context.ClientId); // When receiving a grant_type=refresh_token request, determine whether the client application @@ -2271,6 +2324,10 @@ namespace OpenIddict.Server principal.SetAudiences(context.ClientId); } + // Use the client_id as the authorized party, if available. + // See https://openid.net/specs/openid-connect-core-1_0.html#IDToken for more information. + principal.SetClaim(Claims.AuthorizedParty, context.ClientId); + // If a nonce was present in the authorization request, it MUST be included in the id_token generated // by the token endpoint. For that, OpenIddict simply flows the nonce as an authorization code claim. // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. @@ -2278,6 +2335,7 @@ namespace OpenIddict.Server { OpenIddictServerEndpointType.Authorization => context.Request.Nonce, OpenIddictServerEndpointType.Token => context.Principal.GetClaim(Claims.Private.Nonce), + _ => null }); @@ -2428,7 +2486,7 @@ namespace OpenIddict.Server // Extract the token identifier from the authentication principal. // If no token identifier can be found, this indicates that the token has no backing database entry. - var identifier = context.Principal.GetInternalTokenId(); + var identifier = context.Principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { return; @@ -2517,7 +2575,7 @@ namespace OpenIddict.Server // If the operation fails, silently ignore the error and keep processing the request: // this may indicate that one of the revoked tokens was modified by a concurrent request. - var identifier = context.Principal.GetInternalAuthorizationId(); + var identifier = context.Principal.GetAuthorizationId(); if (string.IsNullOrEmpty(identifier)) { return; @@ -2526,7 +2584,7 @@ namespace OpenIddict.Server await foreach (var token in _tokenManager.FindByAuthorizationIdAsync(identifier)) { // Don't change the status of the token used in the token request. - if (string.Equals(context.Principal.GetInternalTokenId(), + if (string.Equals(context.Principal.GetTokenId(), await _tokenManager.GetIdAsync(token), StringComparison.Ordinal)) { continue; @@ -2590,7 +2648,7 @@ namespace OpenIddict.Server // Extract the token identifier from the authentication principal. // If no token identifier can be found, this indicates that the token has no backing database entry. - var identifier = context.Principal.GetInternalTokenId(); + var identifier = context.Principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { return; @@ -2670,12 +2728,12 @@ namespace OpenIddict.Server var principal = context.AccessTokenPrincipal; if (principal == null) { - throw new InvalidOperationException("A token entry cannot be created from a null principal."); + throw new InvalidOperationException("A token cannot be created from a null principal."); } var descriptor = new OpenIddictTokenDescriptor { - AuthorizationId = principal.GetInternalAuthorizationId(), + AuthorizationId = principal.GetAuthorizationId(), CreationDate = principal.GetCreationDate(), ExpirationDate = principal.GetExpirationDate(), Principal = principal, @@ -2705,7 +2763,7 @@ namespace OpenIddict.Server var identifier = await _tokenManager.GetIdAsync(token); // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetInternalTokenId(identifier); + principal.SetTokenId(identifier); context.Logger.LogTrace("The token entry for access token '{Identifier}' was successfully created.", identifier); } @@ -2746,55 +2804,78 @@ namespace OpenIddict.Server return default; } - // Copy the principal and exclude the presenters/scopes private claims, - // that are manually mapped to public standard azp/scope JWT claims. - var principal = context.AccessTokenPrincipal.Clone(claim => claim.Type switch + // Clone the principal and exclude the private claims mapped to standard JWT claims. + var principal = context.AccessTokenPrincipal?.Clone(claim => claim.Type switch { - Claims.Private.Presenter => false, - Claims.Private.Scope => false, + Claims.Private.Audience => false, + Claims.Private.CreationDate => false, + Claims.Private.ExpirationDate => false, + Claims.Private.Scope => false, + Claims.Private.TokenType => false, _ => true }); - // Set the authorized party using the first presenter (typically the client identifier), if available. - principal.SetClaim(Claims.AuthorizedParty, context.AccessTokenPrincipal.GetPresenters().FirstOrDefault()); + if (principal == null) + { + throw new InvalidOperationException("A token entry cannot be created from a null principal."); + } + + var claims = new Dictionary(StringComparer.Ordinal); + + // Set the public audience claims using the private audience claims from the principal. + // Note: when there's a single audience, represent it as a unique string claim. + var audiences = context.AccessTokenPrincipal.GetAudiences(); + if (audiences.Any()) + { + claims.Add(Claims.Audience, audiences.Length switch + { + 1 => audiences.ElementAt(0), + _ => audiences + }); + } // Set the public scope claim using the private scope claims from the principal. // Note: scopes are deliberately formatted as a single space-separated // string to respect the usual representation of the standard scope claim. - // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-02. - principal.SetClaim(Claims.Scope, string.Join(" ", context.AccessTokenPrincipal.GetScopes())); + // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04. + var scopes = context.AccessTokenPrincipal.GetScopes(); + if (scopes.Any()) + { + claims.Add(Claims.Scope, string.Join(" ", scopes)); + } - var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor + var descriptor = new SecurityTokenDescriptor { AdditionalHeaderClaims = new Dictionary(StringComparer.Ordinal) { [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.AccessToken }, + Claims = claims, + Expires = context.AccessTokenPrincipal.GetExpirationDate()?.UtcDateTime, + IssuedAt = context.AccessTokenPrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), Subject = (ClaimsIdentity) principal.Identity - }); + }; + + var token = context.Options.JsonWebTokenHandler.CreateToken(descriptor); if (!context.Options.DisableAccessTokenEncryption) { token = context.Options.JsonWebTokenHandler.EncryptToken(token, - context.Options.EncryptionCredentials.FirstOrDefault( + encryptingCredentials: context.Options.EncryptionCredentials.FirstOrDefault( credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.EncryptionCredentials.First(), - new Dictionary(StringComparer.Ordinal) - { - [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.AccessToken - }); + additionalHeaderClaims: descriptor.AdditionalHeaderClaims); } context.Response.AccessToken = token; context.Logger.LogTrace("The access token '{Identifier}' was successfully created: {Payload}. " + "The principal used to create the token contained the following claims: {Claims}.", - context.AccessTokenPrincipal.GetClaim(Claims.JwtId), - context.Response.AccessToken, principal.Claims); + principal.GetClaim(Claims.JwtId), token, principal.Claims); return default; } @@ -2857,7 +2938,7 @@ namespace OpenIddict.Server throw new InvalidOperationException("A token entry cannot be created from a null principal."); } - var identifier = principal.GetInternalTokenId(); + var identifier = principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The token identifier cannot be extracted from the principal."); @@ -2954,7 +3035,7 @@ namespace OpenIddict.Server var descriptor = new OpenIddictTokenDescriptor { - AuthorizationId = principal.GetInternalAuthorizationId(), + AuthorizationId = principal.GetAuthorizationId(), CreationDate = principal.GetCreationDate(), ExpirationDate = principal.GetExpirationDate(), Principal = principal, @@ -2984,7 +3065,7 @@ namespace OpenIddict.Server var identifier = await _tokenManager.GetIdAsync(token); // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetInternalTokenId(identifier); + principal.SetTokenId(identifier); context.Logger.LogTrace("The token entry for authorization code '{Identifier}' was successfully created.", identifier); } @@ -3025,20 +3106,37 @@ namespace OpenIddict.Server return default; } + // Clone the principal and exclude the claim mapped to standard JWT claims. + var principal = context.AuthorizationCodePrincipal?.Clone(claim => claim.Type switch + { + Claims.Private.CreationDate => false, + Claims.Private.ExpirationDate => false, + Claims.Private.TokenType => false, + + _ => true + }); + + if (principal == null) + { + throw new InvalidOperationException("A token cannot be created from a null principal."); + } + var descriptor = new SecurityTokenDescriptor { AdditionalHeaderClaims = new Dictionary(StringComparer.Ordinal) { [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.AuthorizationCode }, + Expires = context.AuthorizationCodePrincipal.GetExpirationDate()?.UtcDateTime, + IssuedAt = context.AuthorizationCodePrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.AuthorizationCodePrincipal.Identity + Subject = (ClaimsIdentity) principal.Identity }; // Attach claims destinations to the JWT claims collection. - var destinations = context.AuthorizationCodePrincipal.GetDestinations(); + var destinations = principal.GetDestinations(); if (destinations.Count != 0) { descriptor.Claims = new Dictionary(StringComparer.Ordinal) @@ -3049,21 +3147,18 @@ namespace OpenIddict.Server // Sign and encrypt the authorization code. var token = context.Options.JsonWebTokenHandler.CreateToken(descriptor); + token = context.Options.JsonWebTokenHandler.EncryptToken(token, - context.Options.EncryptionCredentials.FirstOrDefault( + encryptingCredentials: context.Options.EncryptionCredentials.FirstOrDefault( credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.EncryptionCredentials.First(), - new Dictionary(StringComparer.Ordinal) - { - [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.AuthorizationCode - }); + additionalHeaderClaims: descriptor.AdditionalHeaderClaims); context.Response.Code = token; context.Logger.LogTrace("The authorization code '{Identifier}' was successfully created: {Payload}. " + "The principal used to create the token contained the following claims: {Claims}.", - context.AuthorizationCodePrincipal.GetClaim(Claims.JwtId), token, - context.AuthorizationCodePrincipal.Claims); + principal.GetClaim(Claims.JwtId), token, principal.Claims); return default; } @@ -3126,7 +3221,7 @@ namespace OpenIddict.Server throw new InvalidOperationException("A token entry cannot be created from a null principal."); } - var identifier = principal.GetInternalTokenId(); + var identifier = principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The token identifier cannot be extracted from the principal."); @@ -3228,7 +3323,7 @@ namespace OpenIddict.Server var descriptor = new OpenIddictTokenDescriptor { - AuthorizationId = principal.GetInternalAuthorizationId(), + AuthorizationId = principal.GetAuthorizationId(), CreationDate = principal.GetCreationDate(), ExpirationDate = principal.GetExpirationDate(), Principal = principal, @@ -3258,7 +3353,7 @@ namespace OpenIddict.Server var identifier = await _tokenManager.GetIdAsync(token); // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetInternalTokenId(identifier); + principal.SetTokenId(identifier); context.Logger.LogTrace("The token entry for device code '{Identifier}' was successfully created.", identifier); } @@ -3299,20 +3394,37 @@ namespace OpenIddict.Server return default; } + // Clone the principal and exclude the claim mapped to standard JWT claims. + var principal = context.DeviceCodePrincipal?.Clone(claim => claim.Type switch + { + Claims.Private.CreationDate => false, + Claims.Private.ExpirationDate => false, + Claims.Private.TokenType => false, + + _ => true + }); + + if (principal == null) + { + throw new InvalidOperationException("A token cannot be created from a null principal."); + } + var descriptor = new SecurityTokenDescriptor { AdditionalHeaderClaims = new Dictionary(StringComparer.Ordinal) { [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.DeviceCode }, + Expires = context.DeviceCodePrincipal.GetExpirationDate()?.UtcDateTime, + IssuedAt = context.DeviceCodePrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.DeviceCodePrincipal.Identity + Subject = (ClaimsIdentity) principal.Identity }; // Attach claims destinations to the JWT claims collection. - var destinations = context.DeviceCodePrincipal.GetDestinations(); + var destinations = principal.GetDestinations(); if (destinations.Count != 0) { descriptor.Claims = new Dictionary(StringComparer.Ordinal) @@ -3323,21 +3435,18 @@ namespace OpenIddict.Server // Sign and encrypt the device code. var token = context.Options.JsonWebTokenHandler.CreateToken(descriptor); + token = context.Options.JsonWebTokenHandler.EncryptToken(token, - context.Options.EncryptionCredentials.FirstOrDefault( + encryptingCredentials: context.Options.EncryptionCredentials.FirstOrDefault( credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.EncryptionCredentials.First(), - new Dictionary(StringComparer.Ordinal) - { - [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.DeviceCode - }); + additionalHeaderClaims: descriptor.AdditionalHeaderClaims); context.Response.DeviceCode = token; context.Logger.LogTrace("The device code '{Identifier}' was successfully created: {Payload}. " + "The principal used to create the token contained the following claims: {Claims}.", - context.DeviceCodePrincipal.GetClaim(Claims.JwtId), token, - context.DeviceCodePrincipal.Claims); + principal.GetClaim(Claims.JwtId), token, principal.Claims); return default; } @@ -3405,7 +3514,7 @@ namespace OpenIddict.Server throw new InvalidOperationException("A token entry cannot be created from a null principal."); } - var identifier = principal.GetInternalTokenId(); + var identifier = principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The token identifier cannot be extracted from the principal."); @@ -3598,7 +3707,7 @@ namespace OpenIddict.Server var descriptor = new OpenIddictTokenDescriptor { - AuthorizationId = principal.GetInternalAuthorizationId(), + AuthorizationId = principal.GetAuthorizationId(), CreationDate = principal.GetCreationDate(), ExpirationDate = principal.GetExpirationDate(), Principal = principal, @@ -3628,7 +3737,7 @@ namespace OpenIddict.Server var identifier = await _tokenManager.GetIdAsync(token); // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetInternalTokenId(identifier); + principal.SetTokenId(identifier); context.Logger.LogTrace("The token entry for refresh token '{Identifier}' was successfully created.", identifier); } @@ -3669,20 +3778,37 @@ namespace OpenIddict.Server return default; } + // Clone the principal and exclude the claim mapped to standard JWT claims. + var principal = context.RefreshTokenPrincipal?.Clone(claim => claim.Type switch + { + Claims.Private.CreationDate => false, + Claims.Private.ExpirationDate => false, + Claims.Private.TokenType => false, + + _ => true + }); + + if (principal == null) + { + throw new InvalidOperationException("A token cannot be created from a null principal."); + } + var descriptor = new SecurityTokenDescriptor { AdditionalHeaderClaims = new Dictionary(StringComparer.Ordinal) { [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.RefreshToken }, + Expires = context.RefreshTokenPrincipal.GetExpirationDate()?.UtcDateTime, + IssuedAt = context.RefreshTokenPrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.RefreshTokenPrincipal.Identity + Subject = (ClaimsIdentity) principal.Identity }; // Attach claims destinations to the JWT claims collection. - var destinations = context.RefreshTokenPrincipal.GetDestinations(); + var destinations = principal.GetDestinations(); if (destinations.Count != 0) { descriptor.Claims = new Dictionary(StringComparer.Ordinal) @@ -3693,21 +3819,18 @@ namespace OpenIddict.Server // Sign and encrypt the refresh token. var token = context.Options.JsonWebTokenHandler.CreateToken(descriptor); + token = context.Options.JsonWebTokenHandler.EncryptToken(token, - context.Options.EncryptionCredentials.FirstOrDefault( + encryptingCredentials: context.Options.EncryptionCredentials.FirstOrDefault( credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.EncryptionCredentials.First(), - new Dictionary(StringComparer.Ordinal) - { - [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.RefreshToken - }); + additionalHeaderClaims: descriptor.AdditionalHeaderClaims); context.Response.RefreshToken = token; context.Logger.LogTrace("The refresh token '{Identifier}' was successfully created: {Payload}. " + "The principal used to create the token contained the following claims: {Claims}.", - context.RefreshTokenPrincipal.GetClaim(Claims.JwtId), token, - context.RefreshTokenPrincipal.Claims); + principal.GetClaim(Claims.JwtId), token, principal.Claims); return default; } @@ -3770,7 +3893,7 @@ namespace OpenIddict.Server throw new InvalidOperationException("A token entry cannot be created from a null principal."); } - var identifier = principal.GetInternalTokenId(); + var identifier = principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The token identifier cannot be extracted from the principal."); @@ -3844,7 +3967,7 @@ namespace OpenIddict.Server throw new InvalidOperationException("A token entry cannot be created from a null principal."); } - var identifier = context.DeviceCodePrincipal.GetInternalTokenId(); + var identifier = context.DeviceCodePrincipal.GetTokenId(); if (!string.IsNullOrEmpty(identifier)) { principal.SetClaim(Claims.Private.DeviceCodeId, identifier); @@ -3913,7 +4036,7 @@ namespace OpenIddict.Server var descriptor = new OpenIddictTokenDescriptor { - AuthorizationId = principal.GetInternalAuthorizationId(), + AuthorizationId = principal.GetAuthorizationId(), CreationDate = principal.GetCreationDate(), ExpirationDate = principal.GetExpirationDate(), Principal = principal, @@ -3943,7 +4066,7 @@ namespace OpenIddict.Server var identifier = await _tokenManager.GetIdAsync(token); // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetInternalTokenId(identifier); + principal.SetTokenId(identifier); context.Logger.LogTrace("The token entry for user code '{Identifier}' was successfully created.", identifier); } @@ -3984,34 +4107,49 @@ namespace OpenIddict.Server return default; } - // Sign and encrypt the user code. - var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor + // Clone the principal and exclude the claim mapped to standard JWT claims. + var principal = context.UserCodePrincipal?.Clone(claim => claim.Type switch + { + Claims.Private.CreationDate => false, + Claims.Private.ExpirationDate => false, + Claims.Private.TokenType => false, + + _ => true + }); + + if (principal == null) + { + throw new InvalidOperationException("A token cannot be created from a null principal."); + } + + var descriptor = new SecurityTokenDescriptor { AdditionalHeaderClaims = new Dictionary(StringComparer.Ordinal) { [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.UserCode }, + Expires = context.UserCodePrincipal.GetExpirationDate()?.UtcDateTime, + IssuedAt = context.UserCodePrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.UserCodePrincipal.Identity - }); + Subject = (ClaimsIdentity) principal.Identity + }; + + // Sign and encrypt the user code. + var token = context.Options.JsonWebTokenHandler.CreateToken(descriptor); token = context.Options.JsonWebTokenHandler.EncryptToken(token, - context.Options.EncryptionCredentials.FirstOrDefault( + encryptingCredentials: context.Options.EncryptionCredentials.FirstOrDefault( credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.EncryptionCredentials.First(), - new Dictionary(StringComparer.Ordinal) - { - [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.Private.UserCode - }); + additionalHeaderClaims: descriptor.AdditionalHeaderClaims); context.Response.UserCode = token; context.Logger.LogTrace("The user code '{Identifier}' was successfully created: {Payload}. " + "The principal used to create the token contained the following claims: {Claims}.", - context.UserCodePrincipal.GetClaim(Claims.JwtId), token, - context.UserCodePrincipal.Claims); + principal.GetClaim(Claims.JwtId), token, principal.Claims); return default; } @@ -4074,7 +4212,7 @@ namespace OpenIddict.Server throw new InvalidOperationException("A token entry cannot be created from a null principal."); } - var identifier = principal.GetInternalTokenId(); + var identifier = principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The token identifier cannot be extracted from the principal."); @@ -4315,7 +4453,7 @@ namespace OpenIddict.Server var descriptor = new OpenIddictTokenDescriptor { - AuthorizationId = principal.GetInternalAuthorizationId(), + AuthorizationId = principal.GetAuthorizationId(), CreationDate = principal.GetCreationDate(), ExpirationDate = principal.GetExpirationDate(), Principal = principal, @@ -4345,7 +4483,7 @@ namespace OpenIddict.Server var identifier = await _tokenManager.GetIdAsync(token); // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetInternalTokenId(identifier); + principal.SetTokenId(identifier); context.Logger.LogTrace("The token entry for identity token '{Identifier}' was successfully created.", identifier); } @@ -4386,23 +4524,59 @@ namespace OpenIddict.Server return default; } - // Sign and attach the identity token. - context.Response.IdToken = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor + // Clone the principal and exclude the claim mapped to standard JWT claims. + var principal = context.IdentityTokenPrincipal?.Clone(claim => claim.Type switch + { + Claims.Private.Audience => false, + Claims.Private.CreationDate => false, + Claims.Private.ExpirationDate => false, + Claims.Private.TokenType => false, + + _ => true + }); + + if (principal == null) + { + throw new InvalidOperationException("A token cannot be created from a null principal."); + } + + var claims = new Dictionary(StringComparer.Ordinal); + + // Set the public audience claims using the private audience claims from the principal. + // Note: when there's a single audience, represent it as a unique string claim. + var audiences = context.IdentityTokenPrincipal.GetAudiences(); + if (audiences.Any()) + { + claims.Add(Claims.Audience, audiences.Length switch + { + 1 => audiences.ElementAt(0), + _ => audiences + }); + } + + var descriptor = new SecurityTokenDescriptor { AdditionalHeaderClaims = new Dictionary(StringComparer.Ordinal) { [JwtHeaderParameterNames.Typ] = JsonWebTokenTypes.IdentityToken }, + Claims = claims, + Expires = context.IdentityTokenPrincipal.GetExpirationDate()?.UtcDateTime, + IssuedAt = context.IdentityTokenPrincipal.GetCreationDate()?.UtcDateTime, Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.First(credentials => credentials.Key is AsymmetricSecurityKey), - Subject = (ClaimsIdentity) context.IdentityTokenPrincipal.Identity - }); + Subject = (ClaimsIdentity) principal.Identity + }; + + // Sign and attach the identity token. + var token = context.Options.JsonWebTokenHandler.CreateToken(descriptor); + + context.Response.IdToken = token; context.Logger.LogTrace("The identity token '{Identifier}' was successfully created: {Payload}. " + "The principal used to create the token contained the following claims: {Claims}.", - context.IdentityTokenPrincipal.GetClaim(Claims.JwtId), - context.Response.IdToken, context.IdentityTokenPrincipal.Claims); + principal.GetClaim(Claims.JwtId), token, principal.Claims); return default; } diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs index 0d181d8d..6b7b54e4 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs @@ -16,6 +16,8 @@ namespace OpenIddict.Validation.DataProtection public const string CodeChallenge = ".code_challenge"; public const string CodeChallengeMethod = ".code_challenge_method"; public const string DataProtector = ".data_protector"; + public const string DeviceCodeId = ".device_code_id"; + public const string DeviceCodeLifetime = ".device_code_lifetime"; public const string Expires = ".expires"; public const string IdentityTokenLifetime = ".identity_token_lifetime"; public const string InternalAuthorizationId = ".internal_authorization_id"; @@ -27,6 +29,7 @@ namespace OpenIddict.Validation.DataProtection public const string RefreshTokenLifetime = ".refresh_token_lifetime"; public const string Resources = ".resources"; public const string Scopes = ".scopes"; + public const string UserCodeLifetime = ".user_code_lifetime"; } public static class Purposes diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs index b47676b1..b8704d81 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.IO; using System.Security.Claims; using System.Text.Json; @@ -39,8 +38,6 @@ namespace OpenIddict.Validation.DataProtection return principal .SetAudiences(GetArrayProperty(properties, Properties.Audiences)) - .SetCreationDate(GetDateProperty(properties, Properties.Issued)) - .SetExpirationDate(GetDateProperty(properties, Properties.Expires)) .SetPresenters(GetArrayProperty(properties, Properties.Presenters)) .SetResources(GetArrayProperty(properties, Properties.Resources)) .SetScopes(GetArrayProperty(properties, Properties.Scopes)) @@ -50,11 +47,16 @@ namespace OpenIddict.Validation.DataProtection .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.CreationDate, GetProperty(properties, Properties.Issued)) + .SetClaim(Claims.Private.DeviceCodeId, GetProperty(properties, Properties.DeviceCodeId)) + .SetClaim(Claims.Private.DeviceCodeLifetime, GetProperty(properties, Properties.DeviceCodeLifetime)) .SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) + .SetClaim(Claims.Private.ExpirationDate, GetProperty(properties, Properties.Expires)) .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)); + .SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)) + .SetClaim(Claims.Private.UserCodeLifetime, GetProperty(properties, Properties.UserCodeLifetime)); static (ClaimsPrincipal principal, ImmutableDictionary properties) Read(BinaryReader reader, int version) { @@ -175,10 +177,6 @@ namespace OpenIddict.Validation.DataProtection static ImmutableArray GetArrayProperty(IReadOnlyDictionary properties, string name) => properties.TryGetValue(name, out var value) ? JsonSerializer.Deserialize>(value) : ImmutableArray.Create(); - - static DateTimeOffset? GetDateProperty(IReadOnlyDictionary properties, string name) - => properties.TryGetValue(name, out var value) ? (DateTimeOffset?) - DateTimeOffset.ParseExact(value, "r", CultureInfo.InvariantCulture) : null; } } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index cead04db..c67ed299 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Immutable; using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Security.Claims; using System.Text; @@ -382,19 +383,73 @@ namespace OpenIddict.Validation return default; } - // Map the standardized "azp" and "scope" claims to their "oi_" equivalent so that - // the ClaimsPrincipal extensions exposed by OpenIddict return consistent results. + // To reduce the size of tokens, some of the private claims used by OpenIddict + // are mapped to their standard equivalent before being removed from the token. + // This handler is responsible of adding back the private claims to the principal + // when receiving the token (e.g "oi_prst" is resolved from the "scope" claim). + + // In OpenIddict 3.0, the creation date of a token is stored in "oi_crt_dt". + // If the claim doesn't exist, try to infer it from the standard "iat" JWT claim. + if (!context.Principal.HasClaim(Claims.Private.CreationDate)) + { + var date = context.Principal.GetClaim(Claims.IssuedAt); + if (!string.IsNullOrEmpty(date) && + long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + context.Principal.SetCreationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + } + } + + // In OpenIddict 3.0, the expiration date of a token is stored in "oi_exp_dt". + // If the claim doesn't exist, try to infer it from the standard "exp" JWT claim. + if (!context.Principal.HasClaim(Claims.Private.ExpirationDate)) + { + var date = context.Principal.GetClaim(Claims.ExpiresAt); + if (!string.IsNullOrEmpty(date) && + long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + context.Principal.SetExpirationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + } + } + + // In OpenIddict 3.0, the audiences allowed to receive a token are stored in "oi_aud". + // If no such claim exists, try to infer them from the standard "aud" JWT claims. + if (!context.Principal.HasAudience()) + { + var audiences = context.Principal.GetClaims(Claims.Audience); + if (audiences.Any()) + { + context.Principal.SetAudiences(audiences); + } + } + + // In OpenIddict 3.0, the presenters allowed to use a token are stored in "oi_prst". + // If no such claim exists, try to infer them from the standard "azp" and "client_id" JWT claims. + // + // Note: in previous OpenIddict versions, the presenters were represented in JWT tokens + // using the "azp" claim (defined by OpenID Connect), for which a single value could be + // specified. To ensure presenters stored in JWT tokens created by OpenIddict 1.x/2.x + // can still be read with OpenIddict 3.0, the presenter is automatically inferred from + // the "azp" or "client_id" claim if no "oi_prst" claim was found in the principal. if (!context.Principal.HasPresenter()) { - context.Principal.SetPresenters(context.Principal.GetClaims(Claims.AuthorizedParty)); + var presenter = context.Principal.GetClaim(Claims.AuthorizedParty) ?? + context.Principal.GetClaim(Claims.ClientId); + + if (!string.IsNullOrEmpty(presenter)) + { + context.Principal.SetPresenters(presenter); + } } + // In OpenIddict 3.0, the scopes granted to an application are stored in "oi_scp". + // // Note: in previous OpenIddict versions, scopes were represented as a JSON array // and deserialized as multiple claims. In OpenIddict 3.0, the public "scope" claim // is formatted as a unique space-separated string containing all the granted scopes. // To ensure access tokens generated by previous versions are still correctly handled, // both formats (unique space-separated string or multiple scope claims) must be supported. - // Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-03 for more information. + // Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information. if (!context.Principal.HasScope()) { var scopes = context.Principal.GetClaims(Claims.Scope); @@ -403,7 +458,10 @@ namespace OpenIddict.Validation scopes = scopes[0].Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray(); } - context.Principal.SetScopes(scopes); + if (scopes.Any()) + { + context.Principal.SetScopes(scopes); + } } return default; @@ -465,8 +523,8 @@ namespace OpenIddict.Validation context.Principal = context.Principal .SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) - .SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) - .SetInternalTokenId(await _tokenManager.GetIdAsync(token)) + .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetTokenId(await _tokenManager.GetIdAsync(token)) .SetTokenType(await _tokenManager.GetTypeAsync(token)); } } @@ -682,7 +740,7 @@ namespace OpenIddict.Validation throw new ArgumentNullException(nameof(context)); } - var identifier = context.Principal.GetInternalTokenId(); + var identifier = context.Principal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { return; @@ -703,8 +761,8 @@ namespace OpenIddict.Validation // Restore the creation/expiration dates/identifiers from the token entry metadata. context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) - .SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) - .SetInternalTokenId(await _tokenManager.GetIdAsync(token)) + .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetTokenId(await _tokenManager.GetIdAsync(token)) .SetTokenType(await _tokenManager.GetTypeAsync(token)); } } @@ -745,7 +803,7 @@ namespace OpenIddict.Validation throw new ArgumentNullException(nameof(context)); } - var identifier = context.Principal.GetInternalAuthorizationId(); + var identifier = context.Principal.GetAuthorizationId(); if (string.IsNullOrEmpty(identifier)) { return; diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs index 6d31925a..63d2fda2 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs @@ -1497,7 +1497,8 @@ namespace OpenIddict.Abstractions.Tests.Primitives public void GetClaim_ReturnsNullForMissingClaim() { // Arrange - var principal = new ClaimsPrincipal(); + var identity = new ClaimsIdentity(); + var principal = new ClaimsPrincipal(identity); // Act and assert Assert.Null(principal.GetClaim("type")); @@ -1531,33 +1532,26 @@ namespace OpenIddict.Abstractions.Tests.Primitives public void GetCreationDate_ReturnsNullIfNoClaim() { // Arrange - var principal = new ClaimsPrincipal(); - - // Act - var creationDate = principal.GetCreationDate(); + var identity = new ClaimsIdentity(); + var principal = new ClaimsPrincipal(identity); - // Assert - Assert.Null(creationDate); + // Act and assert + Assert.Null(principal.GetCreationDate()); } - [Theory] - [InlineData(null, null)] - [InlineData("", null)] - [InlineData(" ", null)] - [InlineData("62", "62")] - [InlineData("bad_data", null)] - public void GetCreationDate_ReturnsCreationDate(string issuedAt, string expected) + [Fact] + public void GetCreationDate_ReturnsCreationDate() { // Arrange var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaim(Claims.IssuedAt, issuedAt); + principal.SetClaim(Claims.Private.CreationDate, "Wed, 01 Jan 2020 04:30:30 GMT"); // Act - var creationDate = principal.GetCreationDate(); + var date = principal.GetCreationDate(); // Assert - Assert.Equal(ParseDateTimeOffset(expected), creationDate); + Assert.Equal(new DateTimeOffset(2020, 01, 01, 05, 30, 30, TimeSpan.FromHours(1)), date); } [Fact] @@ -1578,31 +1572,23 @@ namespace OpenIddict.Abstractions.Tests.Primitives // Arrange var principal = new ClaimsPrincipal(); - // Act - var expirationDate = principal.GetExpirationDate(); - - // Assert - Assert.Null(expirationDate); + // Act and assert + Assert.Null(principal.GetExpirationDate()); } - [Theory] - [InlineData(null, null)] - [InlineData("", null)] - [InlineData(" ", null)] - [InlineData("62", "62")] - [InlineData("bad_data", null)] - public void GetExpirationDate_ReturnsExpirationDate(string expiresAt, string expected) + [Fact] + public void GetExpirationDate_ReturnsExpirationDate() { // Arrange var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaim(Claims.ExpiresAt, expiresAt); + principal.SetClaim(Claims.Private.ExpirationDate, "Wed, 01 Jan 2020 04:30:30 GMT"); // Act - var expirationDate = principal.GetExpirationDate(); + var date = principal.GetExpirationDate(); // Assert - Assert.Equal(ParseDateTimeOffset(expected), expirationDate); + Assert.Equal(new DateTimeOffset(2020, 01, 01, 05, 30, 30, TimeSpan.FromHours(1)), date); } [Fact] @@ -1629,7 +1615,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Audience, audience.ToImmutableArray()); + principal.SetClaims(Claims.Private.Audience, audience.ToImmutableArray()); // Act and assert Assert.Equal(audiences, principal.GetAudiences()); @@ -1888,13 +1874,13 @@ namespace OpenIddict.Abstractions.Tests.Primitives } [Fact] - public void GetInternalAuthorizationId_ThrowsAnExceptionForNullPrincipal() + public void GetAuthorizationId_ThrowsAnExceptionForNullPrincipal() { // Arrange var principal = (ClaimsPrincipal) null; // Act and assert - var exception = Assert.Throws(() => principal.GetInternalAuthorizationId()); + var exception = Assert.Throws(() => principal.GetAuthorizationId()); Assert.Equal("principal", exception.ParamName); } @@ -1902,7 +1888,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives [Theory] [InlineData(null)] [InlineData("identifier")] - public void GetInternalAuthorizationId_ReturnsExpectedResult(string identifier) + public void GetAuthorizationId_ReturnsExpectedResult(string identifier) { // Arrange var identity = new ClaimsIdentity(); @@ -1911,17 +1897,17 @@ namespace OpenIddict.Abstractions.Tests.Primitives principal.SetClaim(Claims.Private.AuthorizationId, identifier); // Act and assert - Assert.Equal(identifier, principal.GetInternalAuthorizationId()); + Assert.Equal(identifier, principal.GetAuthorizationId()); } [Fact] - public void GetInternalTokenId_ThrowsAnExceptionForNullPrincipal() + public void GetTokenId_ThrowsAnExceptionForNullPrincipal() { // Arrange var principal = (ClaimsPrincipal) null; // Act and assert - var exception = Assert.Throws(() => principal.GetInternalTokenId()); + var exception = Assert.Throws(() => principal.GetTokenId()); Assert.Equal("principal", exception.ParamName); } @@ -1929,7 +1915,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives [Theory] [InlineData(null)] [InlineData("identifier")] - public void GetInternalTokenId_ReturnsExpectedResult(string identifier) + public void GetTokenId_ReturnsExpectedResult(string identifier) { // Arrange var identity = new ClaimsIdentity(); @@ -1938,7 +1924,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives principal.SetClaim(Claims.Private.TokenId, identifier); // Act and assert - Assert.Equal(identifier, principal.GetInternalTokenId()); + Assert.Equal(identifier, principal.GetTokenId()); } [Fact] @@ -2004,7 +1990,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Audience, audience.ToImmutableArray()); + principal.SetClaims(Claims.Private.Audience, audience.ToImmutableArray()); // Act and assert Assert.Equal(result, principal.HasAudience()); @@ -2026,7 +2012,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Audience, audience.ToImmutableArray()); + principal.SetClaims(Claims.Private.Audience, audience.ToImmutableArray()); // Act and assert Assert.Equal(result, principal.HasAudience("fabrikam")); @@ -2294,6 +2280,92 @@ namespace OpenIddict.Abstractions.Tests.Primitives Assert.Equal("value", identity.GetClaim("type")); } + [Fact] + public void GetClaims_ThrowsAnExceptionForNullPrincipal() + { + // Arrange + var principal = (ClaimsPrincipal) null; + + // Act and assert + var exception = Assert.Throws(() => principal.GetClaims("type")); + + Assert.Equal("principal", exception.ParamName); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void GetClaims_ThrowsAnExceptionForNullOrEmptyClaimType(string type) + { + // Arrange + var principal = new ClaimsPrincipal(); + + // Act and assert + var exception = Assert.Throws(() => principal.GetClaims(type)); + + Assert.Equal("type", exception.ParamName); + Assert.StartsWith("The claim type cannot be null or empty.", exception.Message); + } + + [Fact] + public void GetClaims_ReturnsExpectedResult() + { + // Arrange + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim(Claims.Name, "Bob le Bricoleur")); + identity.AddClaim(new Claim(Claims.Scope, Scopes.OpenId)); + identity.AddClaim(new Claim(Claims.Scope, Scopes.Profile)); + + var principal = new ClaimsPrincipal(identity); + + // Act and assert + Assert.Equal(new[] { Scopes.OpenId, Scopes.Profile }, principal.GetClaims(Claims.Scope)); + } + + [Fact] + public void HasClaim_ThrowsAnExceptionForNullPrincipal() + { + // Arrange + var principal = (ClaimsPrincipal) null; + + // Act and assert + var exception = Assert.Throws(() => principal.HasClaim("type")); + + Assert.Equal("principal", exception.ParamName); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void HasClaim_ThrowsAnExceptionForNullOrEmptyClaimType(string type) + { + // Arrange + var principal = new ClaimsPrincipal(); + + // Act and assert + var exception = Assert.Throws(() => principal.HasClaim(type)); + + Assert.Equal("type", exception.ParamName); + Assert.StartsWith("The claim type cannot be null or empty.", exception.Message); + } + + [Fact] + public void HasClaim_ReturnsExpectedResult() + { + // Arrange + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim(Claims.Name, "Bob le Bricoleur")); + identity.AddClaim(new Claim(Claims.Scope, Scopes.OpenId)); + identity.AddClaim(new Claim(Claims.Scope, Scopes.Profile)); + + var principal = new ClaimsPrincipal(identity); + + // Act and assert + Assert.True(principal.HasClaim(Claims.Name)); + Assert.True(principal.HasClaim(Claims.Scope)); + Assert.False(principal.HasClaim(Claims.Nickname)); + } + [Fact] public void RemoveClaims_ThrowsAnExceptionForNullPrincipal() { @@ -2413,25 +2485,23 @@ namespace OpenIddict.Abstractions.Tests.Primitives var principal = (ClaimsPrincipal) null; // Act and assert - var exception = Assert.Throws(() => principal.SetCreationDate(default(DateTimeOffset))); + var exception = Assert.Throws(() => principal.SetCreationDate(date: null)); Assert.Equal("principal", exception.ParamName); } - [Theory] - [InlineData(null)] - [InlineData("62")] - public void SetCreationDate_AddsIssuedAtClaim(string date) + [Fact] + public void SetCreationDate_AddsClaim() { // Arrange var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); // Act - principal.SetCreationDate(ParseDateTimeOffset(date)); + principal.SetCreationDate(new DateTimeOffset(2020, 01, 01, 05, 30, 30, TimeSpan.FromHours(1))); // Assert - Assert.Equal(date, principal.GetClaim(Claims.IssuedAt)); + Assert.Equal("Wed, 01 Jan 2020 04:30:30 GMT", principal.GetClaim(Claims.Private.CreationDate)); } [Fact] @@ -2441,25 +2511,23 @@ namespace OpenIddict.Abstractions.Tests.Primitives var principal = (ClaimsPrincipal) null; // Act and assert - var exception = Assert.Throws(() => principal.SetExpirationDate(default(DateTimeOffset))); + var exception = Assert.Throws(() => principal.SetExpirationDate(date: null)); Assert.Equal("principal", exception.ParamName); } - [Theory] - [InlineData(null)] - [InlineData("62")] - public void SetExpirationDate_AddsExpiresAtClaim(string date) + [Fact] + public void SetExpirationDate_AddsClaim() { // Arrange var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); // Act - principal.SetExpirationDate(ParseDateTimeOffset(date)); + principal.SetExpirationDate(new DateTimeOffset(2020, 01, 01, 05, 30, 30, TimeSpan.FromHours(1))); // Assert - Assert.Equal(date, principal.GetClaim(Claims.ExpiresAt)); + Assert.Equal("Wed, 01 Jan 2020 04:30:30 GMT", principal.GetClaim(Claims.Private.ExpirationDate)); } [Fact] @@ -2491,7 +2559,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives principal.SetAudiences(audiences); // Assert - Assert.Equal(audience, principal.GetClaims(Claims.Audience)); + Assert.Equal(audience, principal.GetClaims(Claims.Private.Audience)); } [Fact] @@ -2799,13 +2867,13 @@ namespace OpenIddict.Abstractions.Tests.Primitives } [Fact] - public void SetInternalAuthorizationId_ThrowsAnExceptionForNullPrincipal() + public void SetAuthorizationId_ThrowsAnExceptionForNullPrincipal() { // Arrange var principal = (ClaimsPrincipal) null; // Act and assert - var exception = Assert.Throws(() => principal.SetInternalAuthorizationId(null)); + var exception = Assert.Throws(() => principal.SetAuthorizationId(null)); Assert.Equal("principal", exception.ParamName); } @@ -2813,27 +2881,27 @@ namespace OpenIddict.Abstractions.Tests.Primitives [Theory] [InlineData(null)] [InlineData("identifier")] - public void SetInternalAuthorizationId_AddsScopes(string identifier) + public void SetAuthorizationId_AddsScopes(string identifier) { // Arrange var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); // Act - principal.SetInternalAuthorizationId(identifier); + principal.SetAuthorizationId(identifier); // Assert Assert.Equal(identifier, principal.GetClaim(Claims.Private.AuthorizationId)); } [Fact] - public void SetInternalTokenId_ThrowsAnExceptionForNullPrincipal() + public void SetTokenId_ThrowsAnExceptionForNullPrincipal() { // Arrange var principal = (ClaimsPrincipal) null; // Act and assert - var exception = Assert.Throws(() => principal.SetInternalTokenId(null)); + var exception = Assert.Throws(() => principal.SetTokenId(null)); Assert.Equal("principal", exception.ParamName); } @@ -2841,14 +2909,14 @@ namespace OpenIddict.Abstractions.Tests.Primitives [Theory] [InlineData(null)] [InlineData("identifier")] - public void SetInternalTokenId_AddsScopes(string identifier) + public void SetTokenId_AddsScopes(string identifier) { // Arrange var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); // Act - principal.SetInternalTokenId(identifier); + principal.SetTokenId(identifier); // Assert Assert.Equal(identifier, principal.GetClaim(Claims.Private.TokenId)); @@ -2890,14 +2958,5 @@ namespace OpenIddict.Abstractions.Tests.Primitives return lifeT; } - - private DateTimeOffset? ParseDateTimeOffset(string dateTimeOffset) - { - var dtOffset = string.IsNullOrWhiteSpace(dateTimeOffset) - ? null - : (DateTimeOffset?)DateTimeOffset.FromUnixTimeSeconds(long.Parse(dateTimeOffset, NumberStyles.Number, CultureInfo.InvariantCulture)); - - return dtOffset; - } } } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index b6a8bb17..7175a380 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -1669,7 +1669,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -1733,7 +1733,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetClaim(Claims.Subject, "Bob le Bricoleur") .SetClaim(Claims.Private.CodeChallenge, "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") .SetClaim(Claims.Private.CodeChallengeMethod, CodeChallengeMethods.Sha256); @@ -1885,7 +1885,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -1947,7 +1947,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2002,7 +2002,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2072,7 +2072,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetPresenters("Fabrikam") - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2137,7 +2137,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2215,7 +2215,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2275,8 +2275,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2368,8 +2368,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2470,8 +2470,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2568,8 +2568,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetPresenters("Fabrikam") - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2641,8 +2641,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2725,8 +2725,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetPresenters("Fabrikam") - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2784,8 +2784,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2883,8 +2883,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2965,8 +2965,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3061,8 +3061,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3152,8 +3152,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3234,8 +3234,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3332,7 +3332,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(context.TokenType) .SetPresenters("Fabrikam") - .SetInternalTokenId("0270F515-C5B1-4FBF-B673-D7CAF7CCDABC") + .SetTokenId("0270F515-C5B1-4FBF-B673-D7CAF7CCDABC") .SetClaim(Claims.Subject, "Bob le Bricoleur"); if (context.Request.IsAuthorizationCodeGrantType()) diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs index 4ba20eb9..3f7efb60 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs @@ -1219,8 +1219,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetAudiences("Fabrikam") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetTokenType(TokenTypeHints.AccessToken) .SetClaim(Claims.Subject, "Bob le Magnifique"); @@ -1310,8 +1310,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetAudiences("Fabrikam") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetTokenType(TokenTypeHints.AccessToken) .SetClaim(Claims.Subject, "Bob le Magnifique"); @@ -1408,8 +1408,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetAudiences("Fabrikam") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetTokenType(TokenTypeHints.AccessToken) .SetClaim(Claims.Subject, "Bob le Magnifique"); @@ -1519,8 +1519,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetAudiences("Fabrikam") - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetTokenType(TokenTypeHints.AccessToken) .SetClaim(Claims.Subject, "Bob le Magnifique"); diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs index afa2c260..8d89ddbd 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs @@ -674,7 +674,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); return default; }); @@ -732,7 +732,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); return default; }); @@ -793,7 +793,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56"); return default; }); diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index c9fd481d..796ac9a1 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -217,6 +217,106 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal("Bob le Magnifique", (string) response[Claims.Subject]); } + [Fact] + public async Task ProcessAuthentication_IssuedAtIsMappedToCreationDate() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetUserinfoEndpointUris("/authenticate"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("access_token", context.Token); + Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + + var identity = new ClaimsIdentity("Bearer"); + identity.AddClaim(new Claim(Claims.IssuedAt, "1577836800", ClaimValueTypes.Integer64)); + + context.Principal = new ClaimsPrincipal(identity) + .SetTokenType(TokenTypeHints.AccessToken) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.GetAsync("/authenticate", new OpenIddictRequest + { + AccessToken = "access_token" + }); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response[Claims.Subject]); + Assert.Equal(1577836800, (long) response[Claims.IssuedAt]); + Assert.Equal("Wed, 01 Jan 2020 00:00:00 GMT", (string) response[Claims.Private.CreationDate]); + } + + [Fact] + public async Task ProcessAuthentication_ExpiresAtIsMappedToExpirationDate() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetUserinfoEndpointUris("/authenticate"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("access_token", context.Token); + Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + + var identity = new ClaimsIdentity("Bearer"); + identity.AddClaim(new Claim(Claims.ExpiresAt, "2524608000", ClaimValueTypes.Integer64)); + + context.Principal = new ClaimsPrincipal(identity) + .SetTokenType(TokenTypeHints.AccessToken) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.GetAsync("/authenticate", new OpenIddictRequest + { + AccessToken = "access_token" + }); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response[Claims.Subject]); + Assert.Equal(2524608000, (long) response[Claims.ExpiresAt]); + Assert.Equal("Sat, 01 Jan 2050 00:00:00 GMT", (string) response[Claims.Private.ExpirationDate]); + } + [Fact] public async Task ProcessAuthentication_AuthorizedPartyIsMappedToPresenter() { @@ -265,6 +365,150 @@ namespace OpenIddict.Server.FunctionalTests Assert.Equal("Fabrikam", (string) response[Claims.Private.Presenter]); } + [Fact] + public async Task ProcessAuthentication_ClientIdIsMappedToPresenter() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetUserinfoEndpointUris("/authenticate"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("access_token", context.Token); + Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken) + .SetClaim(Claims.Subject, "Bob le Magnifique") + .SetClaim(Claims.ClientId, "Fabrikam"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.GetAsync("/authenticate", new OpenIddictRequest + { + AccessToken = "access_token" + }); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response[Claims.Subject]); + Assert.Equal("Fabrikam", (string) response[Claims.ClientId]); + Assert.Equal("Fabrikam", (string) response[Claims.Private.Presenter]); + } + + [Fact] + public async Task ProcessAuthentication_SinglePublicAudienceIsMappedToPrivateClaims() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetUserinfoEndpointUris("/authenticate"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("access_token", context.Token); + Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken) + .SetClaim(Claims.Subject, "Bob le Magnifique") + .SetClaim(Claims.Audience, "Fabrikam"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.GetAsync("/authenticate", new OpenIddictRequest + { + AccessToken = "access_token" + }); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response[Claims.Subject]); + Assert.Equal("Fabrikam", (string) response[Claims.Audience]); + Assert.Equal("Fabrikam", (string) response[Claims.Private.Audience]); + } + + [Fact] + public async Task ProcessAuthentication_MultiplePublicAudiencesAreMappedToPrivateClaims() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetUserinfoEndpointUris("/authenticate"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("access_token", context.Token); + Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken) + .SetClaim(Claims.Subject, "Bob le Magnifique") + .SetClaims(Claims.Audience, ImmutableArray.Create("Fabrikam", "Contoso")); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + }); + + // Act + var response = await client.GetAsync("/authenticate", new OpenIddictRequest + { + AccessToken = "access_token" + }); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response[Claims.Subject]); + Assert.Equal(new[] { "Fabrikam", "Contoso" }, (string[]) response[Claims.Audience]); + Assert.Equal(new[] { "Fabrikam", "Contoso" }, (string[]) response[Claims.Private.Audience]); + } + [Fact] public async Task ProcessAuthentication_SinglePublicScopeIsMappedToPrivateClaims() { @@ -2666,7 +2910,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2736,7 +2980,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) .SetPresenters("Fabrikam") - .SetInternalTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2821,7 +3065,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2886,7 +3130,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -2946,7 +3190,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3030,8 +3274,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3120,8 +3364,8 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) - .SetInternalAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetAuthorizationId("18D15F73-BE2B-6867-DC01-B3C1E8AFDED0") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3197,7 +3441,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3258,7 +3502,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3320,7 +3564,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3386,7 +3630,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default; @@ -3450,7 +3694,7 @@ namespace OpenIddict.Server.FunctionalTests context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) .SetScopes(Scopes.OpenId, Scopes.OfflineAccess) - .SetInternalTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") + .SetTokenId("60FFF7EA-F98E-437B-937E-5073CC313103") .SetClaim(Claims.Subject, "Bob le Bricoleur"); return default;