Browse Source

Normalize multiple public scope claims to a single spare-separated claim

pull/973/head
Kévin Chalet 6 years ago
parent
commit
fb406560c6
  1. 73
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  2. 73
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  3. 51
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

73
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -38,6 +38,7 @@ namespace OpenIddict.Server
NormalizeUserCode.Descriptor,
ValidateReferenceTokenIdentifier.Descriptor,
ValidateIdentityModelToken.Descriptor,
NormalizeScopeClaims.Descriptor,
MapInternalClaims.Descriptor,
RestoreReferenceTokenProperties.Descriptor,
ValidatePrincipal.Descriptor,
@ -508,6 +509,56 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Contains the logic responsible of normalizing the scope claims stored in the tokens.
/// </summary>
public class NormalizeScopeClaims : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<NormalizeScopeClaims>()
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Principal == null)
{
return default;
}
// 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.
// To achieve that, all the "scope" claims are combined into a single one containg all the values.
// Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information.
var scopes = context.Principal.GetClaims(Claims.Scope);
if (scopes.Length > 1)
{
context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes));
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of mapping internal claims used by OpenIddict.
/// </summary>
@ -519,7 +570,7 @@ namespace OpenIddict.Server
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<MapInternalClaims>()
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
.SetOrder(NormalizeScopeClaims.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -601,24 +652,14 @@ namespace OpenIddict.Server
}
// 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-04 for more information.
// If no such claim exists, try to infer them from the standard "scope" JWT claim,
// which is guaranteed to be a unique space-separated claim containing all the values.
if (!context.Principal.HasScope())
{
var scopes = context.Principal.GetClaims(Claims.Scope);
if (scopes.Length == 1)
{
scopes = scopes[0].Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray();
}
if (scopes.Any())
var scope = context.Principal.GetClaim(Claims.Scope);
if (!string.IsNullOrEmpty(scope))
{
context.Principal.SetScopes(scopes);
context.Principal.SetScopes(scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries));
}
}

73
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -34,6 +34,7 @@ namespace OpenIddict.Validation
ValidateReferenceTokenIdentifier.Descriptor,
ValidateIdentityModelToken.Descriptor,
IntrospectToken.Descriptor,
NormalizeScopeClaims.Descriptor,
MapInternalClaims.Descriptor,
RestoreReferenceTokenProperties.Descriptor,
ValidatePrincipal.Descriptor,
@ -349,6 +350,56 @@ namespace OpenIddict.Validation
}
}
/// <summary>
/// Contains the logic responsible of normalizing the scope claims stored in the tokens.
/// </summary>
public class NormalizeScopeClaims : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<NormalizeScopeClaims>()
.SetOrder(IntrospectToken.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Principal == null)
{
return default;
}
// 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.
// To achieve that, all the "scope" claims are combined into a single one containg all the values.
// Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information.
var scopes = context.Principal.GetClaims(Claims.Scope);
if (scopes.Length > 1)
{
context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes));
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of mapping internal claims used by OpenIddict.
/// </summary>
@ -360,7 +411,7 @@ namespace OpenIddict.Validation
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<MapInternalClaims>()
.SetOrder(IntrospectToken.Descriptor.Order + 1_000)
.SetOrder(NormalizeScopeClaims.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -442,24 +493,14 @@ namespace OpenIddict.Validation
}
// 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-04 for more information.
// If no such claim exists, try to infer them from the standard "scope" JWT claim,
// which is guaranteed to be a unique space-separated claim containing all the values.
if (!context.Principal.HasScope())
{
var scopes = context.Principal.GetClaims(Claims.Scope);
if (scopes.Length == 1)
{
scopes = scopes[0].Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray();
}
if (scopes.Any())
var scope = context.Principal.GetClaim(Claims.Scope);
if (!string.IsNullOrEmpty(scope))
{
context.Principal.SetScopes(scopes);
context.Principal.SetScopes(scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries));
}
}

51
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

@ -532,6 +532,55 @@ namespace OpenIddict.Server.FunctionalTests
Assert.Equal(new[] { "Fabrikam", "Contoso" }, (string[]) response[Claims.Private.Audience]);
}
[Fact]
public async Task ProcessAuthentication_MultiplePublicScopesAreNormalizedToSingleClaim()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.SetUserinfoEndpointUris("/authenticate");
options.AddEventHandler<HandleUserinfoRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessAuthenticationContext>(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);
});
});
await using var client = await server.CreateClientAsync();
// 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]);
}
[Fact]
public async Task ProcessAuthentication_SinglePublicScopeIsMappedToPrivateClaims()
{
@ -578,7 +627,6 @@ namespace OpenIddict.Server.FunctionalTests
// 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]);
}
@ -628,7 +676,6 @@ namespace OpenIddict.Server.FunctionalTests
// 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]);
}

Loading…
Cancel
Save