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