From ac5f302b01b93ddec44df6f6911d615203e74c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 14 Feb 2020 00:30:47 +0100 Subject: [PATCH] Add MapInternalClaims and handle multiple public "scope" claims for backward compatibility --- .../Controllers/AuthorizationController.cs | 8 +- .../OpenIddictConstants.cs | 8 +- .../Primitives/OpenIddictExtensions.cs | 58 +---- ...OpenIddictServerDataProtectionFormatter.cs | 6 +- .../OpenIddictServerHandlers.cs | 98 +++++-- .../OpenIddictValidationHandlers.cs | 73 +++++- .../OpenIddictValidationOptions.cs | 9 +- .../Primitives/OpenIddictExtensionsTests.cs | 28 +- ...nIddictServerAspNetCoreIntegrationTests.cs | 4 +- .../OpenIddictServerIntegrationTests.cs | 245 ++++++++++++++++++ .../OpenIddictServerOwinIntegrationTests.cs | 4 +- 11 files changed, 434 insertions(+), 107 deletions(-) diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index 6d70d5bc..526dce21 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -150,7 +150,7 @@ namespace Mvc.Server client : await _applicationManager.GetIdAsync(application), status : Statuses.Valid, type : AuthorizationTypes.Permanent, - scopes : ImmutableArray.CreateRange(request.GetScopes())).ToListAsync(); + scopes : request.GetScopes()).ToListAsync(); switch (await _applicationManager.GetConsentTypeAsync(application)) { @@ -189,7 +189,7 @@ namespace Mvc.Server subject : await _userManager.GetUserIdAsync(user), client : await _applicationManager.GetIdAsync(application), type : AuthorizationTypes.Permanent, - scopes : ImmutableArray.CreateRange(principal.GetScopes())); + scopes : principal.GetScopes()); } principal.SetInternalAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); @@ -245,7 +245,7 @@ namespace Mvc.Server client : await _applicationManager.GetIdAsync(application), status : Statuses.Valid, type : AuthorizationTypes.Permanent, - scopes : ImmutableArray.CreateRange(request.GetScopes())).ToListAsync(); + scopes : request.GetScopes()).ToListAsync(); // Note: the same check is already made in the other action but is repeated // here to ensure a malicious user can't abuse this POST-only endpoint and @@ -281,7 +281,7 @@ namespace Mvc.Server subject : await _userManager.GetUserIdAsync(user), client : await _applicationManager.GetIdAsync(application), type : AuthorizationTypes.Permanent, - scopes : ImmutableArray.CreateRange(principal.GetScopes())); + scopes : principal.GetScopes()); } principal.SetInternalAuthorizationId(await _authorizationManager.GetIdAsync(authorization)); diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 169fc837..03e2c953 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -90,18 +90,18 @@ namespace OpenIddict.Abstractions public const string AccessTokenLifetime = "oi_act_lft"; public const string AuthorizationId = "oi_au_id"; public const string AuthorizationCodeLifetime = "oi_auc_lft"; - public const string ClaimDestinations = "oi_cl_dstn"; + 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 DeviceCodeId = "oi_dvc_id"; public const string DeviceCodeLifetime = "oi_dvc_lft"; public const string IdentityTokenLifetime = "oi_idt_lft"; public const string Nonce = "oi_nce"; - public const string Presenters = "oi_prst"; + public const string Presenter = "oi_prst"; public const string RedirectUri = "oi_reduri"; public const string RefreshTokenLifetime = "oi_reft_lft"; - public const string Resources = "oi_rsrc"; - public const string Scopes = "oi_scp"; + public const string Resource = "oi_rsrc"; + public const string Scope = "oi_scp"; public const string TokenId = "oi_tkn_id"; public const string TokenType = "oi_tkn_typ"; public const string UserCodeLifetime = "oi_usrc_lft"; diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs index 9070af2c..83c4660d 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -1167,7 +1167,7 @@ namespace OpenIddict.Abstractions /// The claims principal. /// The presenters list or an empty set if the claims cannot be found. public static ImmutableArray GetPresenters([NotNull] this ClaimsPrincipal principal) - => principal.GetClaims(Claims.Private.Presenters); + => principal.GetClaims(Claims.Private.Presenter); /// /// Gets the resources list stored in the claims principal. @@ -1175,7 +1175,7 @@ namespace OpenIddict.Abstractions /// The claims principal. /// The resources list or an empty set if the claims cannot be found. public static ImmutableArray GetResources([NotNull] this ClaimsPrincipal principal) - => principal.GetClaims(Claims.Private.Resources); + => principal.GetClaims(Claims.Private.Resource); /// /// Gets the scopes list stored in the claims principal. @@ -1183,7 +1183,7 @@ namespace OpenIddict.Abstractions /// The claims principal. /// The scopes list or an empty set if the claim cannot be found. public static ImmutableArray GetScopes([NotNull] this ClaimsPrincipal principal) - => principal.GetClaims(Claims.Private.Scopes); + => principal.GetClaims(Claims.Private.Scope); /// /// Gets the access token lifetime associated with the claims principal. @@ -1290,15 +1290,7 @@ namespace OpenIddict.Abstractions throw new ArgumentException("The audience cannot be null or empty.", nameof(audience)); } - foreach (var claim in principal.FindAll(Claims.Audience)) - { - if (string.Equals(claim.Value, audience, StringComparison.Ordinal)) - { - return true; - } - } - - return false; + return principal.HasClaim(Claims.Audience, audience); } /// @@ -1313,7 +1305,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - return principal.FindAll(Claims.Private.Presenters).Any(); + return principal.FindAll(Claims.Private.Presenter).Any(); } /// @@ -1334,15 +1326,7 @@ namespace OpenIddict.Abstractions throw new ArgumentException("The presenter cannot be null or empty.", nameof(presenter)); } - foreach (var claim in principal.FindAll(Claims.Private.Presenters)) - { - if (string.Equals(claim.Value, presenter, StringComparison.Ordinal)) - { - return true; - } - } - - return false; + return principal.HasClaim(Claims.Private.Presenter, presenter); } /// @@ -1357,7 +1341,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - return principal.FindAll(Claims.Private.Resources).Any(); + return principal.FindAll(Claims.Private.Resource).Any(); } /// @@ -1378,15 +1362,7 @@ namespace OpenIddict.Abstractions throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); } - foreach (var claim in principal.FindAll(Claims.Private.Resources)) - { - if (string.Equals(claim.Value, resource, StringComparison.Ordinal)) - { - return true; - } - } - - return false; + return principal.HasClaim(Claims.Private.Resource, resource); } /// @@ -1401,7 +1377,7 @@ namespace OpenIddict.Abstractions throw new ArgumentNullException(nameof(principal)); } - return principal.FindAll(Claims.Private.Scopes).Any(); + return principal.FindAll(Claims.Private.Scope).Any(); } /// @@ -1422,15 +1398,7 @@ namespace OpenIddict.Abstractions throw new ArgumentException("The scope cannot be null or empty.", nameof(scope)); } - foreach (var claim in principal.FindAll(Claims.Private.Scopes)) - { - if (string.Equals(claim.Value, scope, StringComparison.Ordinal)) - { - return true; - } - } - - return false; + return principal.HasClaim(Claims.Private.Scope, scope); } /// @@ -1514,7 +1482,7 @@ namespace OpenIddict.Abstractions /// The claims principal. public static ClaimsPrincipal SetPresenters( [NotNull] this ClaimsPrincipal principal, [CanBeNull] ImmutableArray presenters) - => principal.SetClaims(Claims.Private.Presenters, presenters); + => principal.SetClaims(Claims.Private.Presenter, presenters); /// /// Sets the presenters list in the claims principal. @@ -1547,7 +1515,7 @@ namespace OpenIddict.Abstractions /// The claims principal. public static ClaimsPrincipal SetResources( [NotNull] this ClaimsPrincipal principal, [CanBeNull] ImmutableArray resources) - => principal.SetClaims(Claims.Private.Resources, resources); + => principal.SetClaims(Claims.Private.Resource, resources); /// /// Sets the resources list in the claims principal. @@ -1580,7 +1548,7 @@ namespace OpenIddict.Abstractions /// The claims principal. public static ClaimsPrincipal SetScopes( [NotNull] this ClaimsPrincipal principal, [CanBeNull] ImmutableArray scopes) - => principal.SetClaims(Claims.Private.Scopes, scopes); + => principal.SetClaims(Claims.Private.Scope, scopes); /// /// Sets the scopes list in the claims principal. diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs index ae5310a5..d8a0947d 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs @@ -237,11 +237,11 @@ namespace OpenIddict.Server.DataProtection Claims.Private.CodeChallengeMethod => false, Claims.Private.IdentityTokenLifetime => false, Claims.Private.Nonce => false, - Claims.Private.Presenters => false, + Claims.Private.Presenter => false, Claims.Private.RedirectUri => false, Claims.Private.RefreshTokenLifetime => false, - Claims.Private.Resources => false, - Claims.Private.Scopes => false, + Claims.Private.Resource => false, + Claims.Private.Scope => false, Claims.Private.TokenId => false, _ => true diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 4691f715..3932bfa8 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -38,6 +38,7 @@ namespace OpenIddict.Server NormalizeUserCode.Descriptor, ValidateReferenceTokenIdentifier.Descriptor, ValidateIdentityModelToken.Descriptor, + MapInternalClaims.Descriptor, RestoreReferenceTokenProperties.Descriptor, ValidatePrincipal.Descriptor, ValidateTokenEntry.Descriptor, @@ -496,26 +497,79 @@ namespace OpenIddict.Server }); // Restore the claim destinations from the special oi_cl_dstn claim (represented as a dictionary/JSON object). - if (token.TryGetPayloadValue(Claims.Private.ClaimDestinations, out ImmutableDictionary destinations)) + if (token.TryGetPayloadValue(Claims.Private.ClaimDestinationsMap, out ImmutableDictionary destinations)) { context.Principal.SetDestinations(destinations); } - if (context.Principal.HasTokenType(TokenTypeHints.AccessToken)) + context.Logger.LogTrace("The token '{Token}' was successfully validated and the following claims " + + "could be extracted: {Claims}.", context.Token, context.Principal.Claims); + + return default; + } + } + + /// + /// Contains the logic responsible of mapping internal claims used by OpenIddict. + /// + public class MapInternalClaims : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) { - // Map the standardized "azp" and "scope" claims to their "oi_" equivalent so that - // the ClaimsPrincipal extensions exposed by OpenIddict return consistent results. - context.Principal.SetPresenters(context.Principal.GetClaims(Claims.AuthorizedParty)); + throw new ArgumentNullException(nameof(context)); + } - // Note: starting in OpenIddict 3.0, the public "scope" claim is formatted - // as a unique space-separated string containing all the granted scopes. - // Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-03 for more information. - context.Principal.SetScopes(context.Principal.GetClaim(Claims.Scope) - ?.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)); + if (context.Principal == null) + { + return default; } - context.Logger.LogTrace("The token '{Token}' was successfully validated and the following claims " + - "could be extracted: {Claims}.", context.Token, context.Principal.Claims); + if (!context.Principal.HasTokenType(TokenTypeHints.AccessToken)) + { + return default; + } + + // Map the standardized "azp" and "scope" claims to their "oi_" equivalent so that + // the ClaimsPrincipal extensions exposed by OpenIddict return consistent results. + if (!context.Principal.HasPresenter()) + { + context.Principal.SetPresenters(context.Principal.GetClaims(Claims.AuthorizedParty)); + } + + // 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. + if (!context.Principal.HasScope()) + { + var scopes = context.Principal.GetClaims(Claims.Scope); + if (scopes.Length == 1) + { + scopes = scopes[0].Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray(); + } + + context.Principal.SetScopes(scopes); + } return default; } @@ -548,7 +602,7 @@ namespace OpenIddict.Server .AddFilter() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) .Build(); public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) @@ -1378,7 +1432,7 @@ namespace OpenIddict.Server // When the request is a verification request, don't flow the copy from the user code. if (context.EndpointType == OpenIddictServerEndpointType.Verification && - string.Equals(claims.Key, Claims.Private.Scopes, StringComparison.OrdinalIgnoreCase)) + string.Equals(claims.Key, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase)) { continue; } @@ -1763,8 +1817,8 @@ namespace OpenIddict.Server } // Never exclude the presenters and scope private claims. - if (string.Equals(claim.Type, Claims.Private.Presenters, StringComparison.OrdinalIgnoreCase) || - string.Equals(claim.Type, Claims.Private.Scopes, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -2708,9 +2762,9 @@ namespace OpenIddict.Server // that are manually mapped to public standard azp/scope JWT claims. var principal = context.AccessTokenPrincipal.Clone(claim => claim.Type switch { - Claims.Private.Presenters => false, - Claims.Private.Scopes => false, - Claims.Private.TokenId => false, + Claims.Private.Presenter => false, + Claims.Private.Scope => false, + Claims.Private.TokenId => false, _ => true }); @@ -2913,7 +2967,7 @@ namespace OpenIddict.Server { descriptor.Claims = new Dictionary(StringComparer.Ordinal) { - [Claims.Private.ClaimDestinations] = destinations + [Claims.Private.ClaimDestinationsMap] = destinations }; } @@ -3092,7 +3146,7 @@ namespace OpenIddict.Server { descriptor.Claims = new Dictionary(StringComparer.Ordinal) { - [Claims.Private.ClaimDestinations] = destinations + [Claims.Private.ClaimDestinationsMap] = destinations }; } @@ -3372,7 +3426,7 @@ namespace OpenIddict.Server { descriptor.Claims = new Dictionary(StringComparer.Ordinal) { - [Claims.Private.ClaimDestinations] = destinations + [Claims.Private.ClaimDestinationsMap] = destinations }; } diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 76cab093..b1396a10 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -31,6 +31,7 @@ namespace OpenIddict.Validation ValidateAccessTokenParameter.Descriptor, ValidateReferenceTokenIdentifier.Descriptor, ValidateIdentityModelToken.Descriptor, + MapInternalClaims.Descriptor, RestoreReferenceTokenProperties.Descriptor, ValidatePrincipal.Descriptor, ValidateExpirationDate.Descriptor, @@ -207,7 +208,6 @@ namespace OpenIddict.Validation parameters = parameters.Clone(); parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key); parameters.ValidIssuer = context.Issuer?.AbsoluteUri; - parameters.ValidTypes = new[] { JsonWebTokenTypes.AccessToken }; // If the token cannot be validated, don't return an error to allow another handle to validate it. var result = context.Options.JsonWebTokenHandler.ValidateToken(context.Token, parameters); @@ -220,20 +220,75 @@ namespace OpenIddict.Validation // Attach the principal extracted from the token to the parent event context. context.Principal = new ClaimsPrincipal(result.ClaimsIdentity); + + // Note: tokens that are considered valid at this point are guaranteed to be access tokens, + // as a "typ" header validation is performed by the JWT handler, based on the valid values + // set in the token validation parameters (by default, only "at+jwt" is considered valid). context.Principal.SetClaim(Claims.Private.TokenType, TokenTypeHints.AccessToken); + context.Logger.LogTrace("The self-contained JWT token '{Token}' was successfully validated and the following " + + "claims could be extracted: {Claims}.", context.Token, context.Principal.Claims); + + return default; + } + } + + /// + /// Contains the logic responsible of mapping internal claims used by OpenIddict. + /// + public class MapInternalClaims : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal == null) + { + return default; + } + // Map the standardized "azp" and "scope" claims to their "oi_" equivalent so that // the ClaimsPrincipal extensions exposed by OpenIddict return consistent results. - context.Principal.SetPresenters(context.Principal.GetClaims(Claims.AuthorizedParty)); + if (!context.Principal.HasPresenter()) + { + context.Principal.SetPresenters(context.Principal.GetClaims(Claims.AuthorizedParty)); + } - // Note: starting in OpenIddict 3.0, the public "scope" claim is formatted - // as a unique space-separated string containing all the granted scopes. + // 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. - context.Principal.SetScopes(context.Principal.GetClaim(Claims.Scope) - ?.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)); + if (!context.Principal.HasScope()) + { + var scopes = context.Principal.GetClaims(Claims.Scope); + if (scopes.Length == 1) + { + scopes = scopes[0].Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray(); + } - context.Logger.LogTrace("The self-contained JWT token '{Token}' was successfully validated and the following " + - "claims could be extracted: {Claims}.", context.Token, context.Principal.Claims); + context.Principal.SetScopes(scopes); + } return default; } @@ -263,7 +318,7 @@ namespace OpenIddict.Validation = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) .Build(); public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 7185f823..5b6f93d0 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -8,7 +8,7 @@ using System; using System.Collections.Generic; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; namespace OpenIddict.Validation { @@ -90,11 +90,12 @@ namespace OpenIddict.Validation public TokenValidationParameters TokenValidationParameters { get; } = new TokenValidationParameters { ClockSkew = TimeSpan.Zero, - NameClaimType = OpenIddictConstants.Claims.Name, - RoleClaimType = OpenIddictConstants.Claims.Role, + NameClaimType = Claims.Name, + RoleClaimType = Claims.Role, // Note: audience and lifetime are manually validated by OpenIddict itself. ValidateAudience = false, - ValidateLifetime = false + ValidateLifetime = false, + ValidTypes = new[] { JsonWebTokenTypes.AccessToken } }; } } diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs index 53958b22..acd2607c 100644 --- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs @@ -1659,7 +1659,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Private.Presenters, presenter.ToImmutableArray()); + principal.SetClaims(Claims.Private.Presenter, presenter.ToImmutableArray()); // Act and assert Assert.Equal(presenters, principal.GetPresenters()); @@ -1689,7 +1689,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Private.Resources, resource.ToImmutableArray()); + principal.SetClaims(Claims.Private.Resource, resource.ToImmutableArray()); // Act and assert Assert.Equal(resources, principal.GetResources()); @@ -1719,7 +1719,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Private.Scopes, scope.ToImmutableArray()); + principal.SetClaims(Claims.Private.Scope, scope.ToImmutableArray()); // Act and assert Assert.Equal(scopes, principal.GetScopes()); @@ -2068,7 +2068,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Private.Presenters, presenter.ToImmutableArray()); + principal.SetClaims(Claims.Private.Presenter, presenter.ToImmutableArray()); // Act and assert Assert.Equal(result, principal.HasPresenter()); @@ -2090,7 +2090,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Private.Presenters, presenter.ToImmutableArray()); + principal.SetClaims(Claims.Private.Presenter, presenter.ToImmutableArray()); // Act and assert Assert.Equal(result, principal.HasPresenter("fabrikam")); @@ -2132,7 +2132,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Private.Resources, resource.ToImmutableArray()); + principal.SetClaims(Claims.Private.Resource, resource.ToImmutableArray()); // Act and assert Assert.Equal(result, principal.HasResource()); @@ -2154,7 +2154,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Private.Resources, resource.ToImmutableArray()); + principal.SetClaims(Claims.Private.Resource, resource.ToImmutableArray()); // Act and assert Assert.Equal(result, principal.HasResource("fabrikam")); @@ -2196,7 +2196,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Private.Scopes, scope.ToImmutableArray()); + principal.SetClaims(Claims.Private.Scope, scope.ToImmutableArray()); // Act and assert Assert.Equal(result, principal.HasScope()); @@ -2218,7 +2218,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives var identity = new ClaimsIdentity(); var principal = new ClaimsPrincipal(identity); - principal.SetClaims(Claims.Private.Scopes, scope.ToImmutableArray()); + principal.SetClaims(Claims.Private.Scope, scope.ToImmutableArray()); // Act and assert Assert.Equal(result, principal.HasScope(Scopes.OpenId)); @@ -2523,7 +2523,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives principal.SetPresenters(presenters); // Assert - Assert.Equal(presenter, principal.GetClaims(Claims.Private.Presenters)); + Assert.Equal(presenter, principal.GetClaims(Claims.Private.Presenter)); } [Fact] @@ -2555,7 +2555,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives principal.SetResources(resources); // Assert - Assert.Equal(resource, principal.GetClaims(Claims.Private.Resources)); + Assert.Equal(resource, principal.GetClaims(Claims.Private.Resource)); } [Fact] @@ -2587,7 +2587,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives principal.SetScopes(scopes); // Assert - Assert.Equal(scope, principal.GetClaims(Claims.Private.Scopes)); + Assert.Equal(scope, principal.GetClaims(Claims.Private.Scope)); } [Theory] @@ -2607,7 +2607,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives principal.SetScopes((IEnumerable)scopes); // Assert - Assert.Equal(scope, principal.GetClaims(Claims.Private.Scopes)); + Assert.Equal(scope, principal.GetClaims(Claims.Private.Scope)); } [Theory] @@ -2627,7 +2627,7 @@ namespace OpenIddict.Abstractions.Tests.Primitives principal.SetScopes(ImmutableArray.Create(scopes)); // Assert - Assert.Equal(scope, principal.GetClaims(Claims.Private.Scopes)); + Assert.Equal(scope, principal.GetClaims(Claims.Private.Scope)); } [Fact] diff --git a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs index e0237176..5382cc93 100644 --- a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs +++ b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs @@ -340,7 +340,9 @@ namespace OpenIddict.Server.AspNetCore.FunctionalTests context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonSerializer.Serialize( - result.Principal.Claims.ToDictionary(claim => claim.Type, claim => claim.Value))); + new OpenIddictResponse(result.Principal.Claims.GroupBy(claim => claim.Type) + .Select(group => new KeyValuePair( + group.Key, group.Select(claim => claim.Value).ToArray()))))); return; } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index c81c380c..7f5dd377 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -5,6 +5,7 @@ */ using System; +using System.Collections.Immutable; using System.Linq; using System.Security.Claims; using System.Text; @@ -115,6 +116,250 @@ namespace OpenIddict.Server.FunctionalTests .ToString(), exception.Message); } + [Fact] + public async Task ProcessAuthentication_MissingAccessTokenReturnsNull() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetLogoutEndpointUris("/authenticate"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.GetAsync("/authenticate", new OpenIddictRequest + { + AccessToken = null + }); + + // Assert + Assert.Null((string) response[Claims.Subject]); + } + + [Fact] + public async Task ProcessAuthentication_InvalidAccessTokenReturnsNull() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetLogoutEndpointUris("/authenticate"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.GetAsync("/authenticate", new OpenIddictRequest + { + AccessToken = "38323A4B-6CB2-41B8-B457-1951987CB383" + }); + + // Assert + Assert.Null((string) response[Claims.Subject]); + } + + [Fact] + public async Task ProcessAuthentication_ValidAccessTokenReturnsExpectedIdentity() + { + // 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")) + .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]); + } + + [Fact] + public async Task ProcessAuthentication_AuthorizedPartyIsMappedToPresenter() + { + // 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.AuthorizedParty, "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.AuthorizedParty]); + Assert.Equal("Fabrikam", (string) response[Claims.Private.Presenter]); + } + + [Fact] + public async Task ProcessAuthentication_SinglePublicScopeIsMappedToPrivateClaims() + { + // 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.Scope, "openid profile"); + + 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("openid profile", (string) response[Claims.Scope]); + Assert.Equal(new[] { Scopes.OpenId, Scopes.Profile }, (string[]) response[Claims.Private.Scope]); + } + + [Fact] + public async Task ProcessAuthentication_MultiplePublicScopesAreMappedToPrivateClaims() + { + // 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.Scope, ImmutableArray.Create(Scopes.OpenId, Scopes.Profile)); + + 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[] { Scopes.OpenId, Scopes.Profile }, (string[]) response[Claims.Scope]); + Assert.Equal(new[] { Scopes.OpenId, Scopes.Profile }, (string[]) response[Claims.Private.Scope]); + } + [Fact] public async Task ProcessAuthentication_MissingIdTokenHintReturnsNull() { diff --git a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs index 3ea9ea85..1372077e 100644 --- a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs +++ b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs @@ -351,7 +351,9 @@ namespace OpenIddict.Server.Owin.FunctionalTests context.Response.ContentType = "application/json"; await context.Response.WriteAsync(JsonSerializer.Serialize( - result.Identity.Claims.ToDictionary(claim => claim.Type, claim => claim.Value))); + new OpenIddictResponse(result.Identity.Claims.GroupBy(claim => claim.Type) + .Select(group => new KeyValuePair( + group.Key, group.Select(claim => claim.Value).ToArray()))))); return; }