|
|
|
@ -47,7 +47,6 @@ public static partial class OpenIddictServerHandlers |
|
|
|
ValidateClientAssertion.Descriptor, |
|
|
|
ValidateClientAssertionWellknownClaims.Descriptor, |
|
|
|
ValidateClientAssertionIssuer.Descriptor, |
|
|
|
ValidateClientAssertionAudience.Descriptor, |
|
|
|
ValidateClientId.Descriptor, |
|
|
|
ValidateClientType.Descriptor, |
|
|
|
ValidateClientSecret.Descriptor, |
|
|
|
@ -637,10 +636,15 @@ public static partial class OpenIddictServerHandlers |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Throw an exception if the issuer cannot be retrieved or is not valid.
|
|
|
|
var issuer = context.Options.Issuer ?? context.BaseUri; |
|
|
|
if (issuer is not { IsAbsoluteUri: true }) |
|
|
|
{ |
|
|
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0496)); |
|
|
|
} |
|
|
|
|
|
|
|
var notification = new ValidateTokenContext(context.Transaction) |
|
|
|
{ |
|
|
|
// Note: for client authentication assertions, audience validation is enforced by a specialized handler.
|
|
|
|
DisableAudienceValidation = true, |
|
|
|
DisablePresenterValidation = true, |
|
|
|
Token = context.ClientAssertion, |
|
|
|
TokenFormat = context.ClientAssertionType switch |
|
|
|
@ -653,6 +657,31 @@ public static partial class OpenIddictServerHandlers |
|
|
|
ValidTokenTypes = { TokenTypeIdentifiers.Private.ClientAssertion } |
|
|
|
}; |
|
|
|
|
|
|
|
// Note: for client assertions, the audience MUST be the issuer URI.
|
|
|
|
if (issuer is { AbsolutePath: "/", Query.Length: 0, Fragment.Length: 0 }) |
|
|
|
{ |
|
|
|
// If the issuer URI doesn't contain any query/fragment, allow both http://www.fabrikam.com
|
|
|
|
// and http://www.fabrikam.com/ (the recommended URI representation) to be considered valid.
|
|
|
|
// See https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.3 for more information.
|
|
|
|
notification.ValidAudiences.Add(issuer.AbsoluteUri); // Uri.AbsoluteUri is normalized and always contains a trailing slash.
|
|
|
|
notification.ValidAudiences.Add(issuer.AbsoluteUri[..^1]); |
|
|
|
} |
|
|
|
|
|
|
|
else if (issuer is { AbsolutePath.Length: 0, Query.Length: 0, Fragment.Length: 0 }) |
|
|
|
{ |
|
|
|
// When properly normalized, Uri.AbsolutePath should never be empty and should at least
|
|
|
|
// contain a leading slash. While dangerous, System.Uri now offers a way to create a URI
|
|
|
|
// instance without applying the default canonicalization logic. To support such URIs,
|
|
|
|
// a special case is added here to add back the missing trailing slash when necessary.
|
|
|
|
notification.ValidAudiences.Add(issuer.AbsoluteUri); |
|
|
|
notification.ValidAudiences.Add(issuer.AbsoluteUri + "/"); |
|
|
|
} |
|
|
|
|
|
|
|
else |
|
|
|
{ |
|
|
|
notification.ValidAudiences.Add(issuer.AbsoluteUri); |
|
|
|
} |
|
|
|
|
|
|
|
await _dispatcher.DispatchAsync(notification); |
|
|
|
|
|
|
|
if (notification.IsRequestHandled) |
|
|
|
@ -721,9 +750,11 @@ public static partial class OpenIddictServerHandlers |
|
|
|
return ValueTask.CompletedTask; |
|
|
|
} |
|
|
|
|
|
|
|
// Client assertions MUST contain an "iss" claim. For more information,
|
|
|
|
// see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
|
|
|
// and https://datatracker.ietf.org/doc/html/rfc7523#section-3.
|
|
|
|
// Client assertions MUST contain an issuer.
|
|
|
|
//
|
|
|
|
// For more information, see
|
|
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication and
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc7523#section-3.
|
|
|
|
if (!context.ClientAssertionPrincipal.HasClaim(Claims.Issuer)) |
|
|
|
{ |
|
|
|
context.Reject( |
|
|
|
@ -734,9 +765,11 @@ public static partial class OpenIddictServerHandlers |
|
|
|
return ValueTask.CompletedTask; |
|
|
|
} |
|
|
|
|
|
|
|
// Client assertions MUST contain a "sub" claim. For more information,
|
|
|
|
// see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
|
|
|
// and https://datatracker.ietf.org/doc/html/rfc7523#section-3.
|
|
|
|
// Client assertions MUST contain a subject.
|
|
|
|
//
|
|
|
|
// For more information, see
|
|
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication and
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc7523#section-3.
|
|
|
|
if (!context.ClientAssertionPrincipal.HasClaim(Claims.Subject)) |
|
|
|
{ |
|
|
|
context.Reject( |
|
|
|
@ -747,10 +780,12 @@ public static partial class OpenIddictServerHandlers |
|
|
|
return ValueTask.CompletedTask; |
|
|
|
} |
|
|
|
|
|
|
|
// Client assertions MUST contain an "aud" claim. For more information,
|
|
|
|
// see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
|
|
|
// and https://datatracker.ietf.org/doc/html/rfc7523#section-3.
|
|
|
|
if (!context.ClientAssertionPrincipal.HasClaim(Claims.Audience)) |
|
|
|
// Client assertions MUST contain a unique audience.
|
|
|
|
//
|
|
|
|
// For more information, see
|
|
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication and
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc7523#section-3.
|
|
|
|
if (context.ClientAssertionPrincipal.GetAudiences() is not [_]) |
|
|
|
{ |
|
|
|
context.Reject( |
|
|
|
error: Errors.InvalidRequest, |
|
|
|
@ -760,10 +795,12 @@ public static partial class OpenIddictServerHandlers |
|
|
|
return ValueTask.CompletedTask; |
|
|
|
} |
|
|
|
|
|
|
|
// Client assertions MUST contain contain a "exp" claim. For more information,
|
|
|
|
// see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
|
|
|
// and https://datatracker.ietf.org/doc/html/rfc7523#section-3.
|
|
|
|
if (!context.ClientAssertionPrincipal.HasClaim(Claims.ExpiresAt)) |
|
|
|
// Client assertions MUST contain contain an expiration date.
|
|
|
|
//
|
|
|
|
// For more information, see
|
|
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication and
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc7523#section-3.
|
|
|
|
if (context.ClientAssertionPrincipal.GetExpirationDate() is null) |
|
|
|
{ |
|
|
|
context.Reject( |
|
|
|
error: Errors.InvalidRequest, |
|
|
|
@ -873,89 +910,6 @@ public static partial class OpenIddictServerHandlers |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Contains the logic responsible for validating the audience contained in the client assertion principal.
|
|
|
|
/// </summary>
|
|
|
|
public sealed class ValidateClientAssertionAudience : IOpenIddictServerHandler<ProcessAuthenticationContext> |
|
|
|
{ |
|
|
|
/// <summary>
|
|
|
|
/// Gets the default descriptor definition assigned to this handler.
|
|
|
|
/// </summary>
|
|
|
|
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
|
|
|
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() |
|
|
|
.AddFilter<RequireClientAssertionPrincipal>() |
|
|
|
.UseSingletonHandler<ValidateClientAssertionAudience>() |
|
|
|
.SetOrder(ValidateClientAssertionIssuer.Descriptor.Order + 1_000) |
|
|
|
.SetType(OpenIddictServerHandlerType.BuiltIn) |
|
|
|
.Build(); |
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public ValueTask HandleAsync(ProcessAuthenticationContext context) |
|
|
|
{ |
|
|
|
ArgumentNullException.ThrowIfNull(context); |
|
|
|
|
|
|
|
Debug.Assert(context.ClientAssertionPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); |
|
|
|
|
|
|
|
// Important: client assertions with multiple audiences was initially deliberately supported by
|
|
|
|
// the OpenID Connect and Assertion Framework for OAuth 2.0 Client Authentication specifications.
|
|
|
|
// Since 2025, using multiple audiences is no longer allowed for security reasons: as such, a single
|
|
|
|
// audience is allowed here and an exception is thrown if multiple claims are present in the principal.
|
|
|
|
//
|
|
|
|
// See https://www.ietf.org/archive/id/draft-ietf-oauth-rfc7523bis-01.html#section-4 for more information.
|
|
|
|
var audience = context.ClientAssertionPrincipal.GetClaim(Claims.Audience); |
|
|
|
if (string.IsNullOrEmpty(audience) || |
|
|
|
!Uri.TryCreate(audience, UriKind.Absolute, out Uri? uri) || OpenIddictHelpers.IsImplicitFileUri(uri)) |
|
|
|
{ |
|
|
|
context.Reject( |
|
|
|
error: Errors.InvalidGrant, |
|
|
|
description: SR.FormatID2172(Claims.Audience), |
|
|
|
uri: SR.FormatID8000(SR.ID2172)); |
|
|
|
|
|
|
|
return ValueTask.CompletedTask; |
|
|
|
} |
|
|
|
|
|
|
|
// Throw an exception if the issuer cannot be retrieved or is not valid.
|
|
|
|
var issuer = context.Options.Issuer ?? context.BaseUri; |
|
|
|
if (issuer is not { IsAbsoluteUri: true }) |
|
|
|
{ |
|
|
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0496)); |
|
|
|
} |
|
|
|
|
|
|
|
if (!UriEquals(uri, issuer)) |
|
|
|
{ |
|
|
|
context.Reject( |
|
|
|
error: Errors.InvalidGrant, |
|
|
|
description: SR.FormatID2173(Claims.Audience), |
|
|
|
uri: SR.FormatID8000(SR.ID2173)); |
|
|
|
|
|
|
|
return ValueTask.CompletedTask; |
|
|
|
} |
|
|
|
|
|
|
|
return ValueTask.CompletedTask; |
|
|
|
|
|
|
|
static bool UriEquals(Uri left, Uri right) |
|
|
|
{ |
|
|
|
if (string.Equals(left.AbsolutePath, right.AbsolutePath, StringComparison.Ordinal)) |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
// Consider the two URIs identical if they only differ by the trailing slash.
|
|
|
|
|
|
|
|
if (left.AbsolutePath.Length == right.AbsolutePath.Length + 1 && |
|
|
|
left.AbsolutePath.StartsWith(right.AbsolutePath, StringComparison.Ordinal) && |
|
|
|
left.AbsolutePath[^1] is '/') |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
return right.AbsolutePath.Length == left.AbsolutePath.Length + 1 && |
|
|
|
right.AbsolutePath.StartsWith(left.AbsolutePath, StringComparison.Ordinal) && |
|
|
|
right.AbsolutePath[^1] is '/'; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Contains the logic responsible for rejecting authentication demands that use an invalid client_id.
|
|
|
|
/// </summary>
|
|
|
|
@ -967,7 +921,7 @@ public static partial class OpenIddictServerHandlers |
|
|
|
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
|
|
|
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() |
|
|
|
.UseSingletonHandler<ValidateClientId>() |
|
|
|
.SetOrder(ValidateClientAssertionAudience.Descriptor.Order + 1_000) |
|
|
|
.SetOrder(ValidateClientAssertionIssuer.Descriptor.Order + 1_000) |
|
|
|
.SetType(OpenIddictServerHandlerType.BuiltIn) |
|
|
|
.Build(); |
|
|
|
|
|
|
|
|