Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/OpenIddict.Abstractions/OpenIddictResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1860,6 +1860,12 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions
<data name="ID0514" xml:space="preserve">
<value>TLS client certificates must contain a private key.</value>
</data>
<data name="ID0515" xml:space="preserve">
<value>An existing '{0}' instance is already attached to the execution context.</value>
</data>
<data name="ID0516" xml:space="preserve">
<value>The '{0}' attached to the execution context could not be resolved.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
Expand Down Expand Up @@ -2462,16 +2468,21 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions
</data>
<data name="ID2201" xml:space="preserve">
<value>An existing '{0}' instance is already attached to the execution context.</value>
<comment>This resource is no longer used and will be removed in a future version.</comment>
</data>
<data name="ID2202" xml:space="preserve">
<value>The '{0}' attached to the execution context could not be resolved.</value>
<comment>This resource is no longer used and will be removed in a future version.</comment>
</data>
<data name="ID2203" xml:space="preserve">
<value>A certificate-based proof-of-possession is required to use this token.</value>
</data>
<data name="ID2204" xml:space="preserve">
<value>The specified certificate-based proof-of-possession is not valid.</value>
</data>
<data name="ID2205" xml:space="preserve">
<value>The specified TLS client certificate is not allowed or valid for this operation.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>
Expand Down Expand Up @@ -3315,6 +3326,9 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6292" xml:space="preserve">
<value>The payload of the '{0}' reference token was used instead of its reference identifier, which may indicate that the payload stored in the database has leaked and is being used as a regular token.</value>
</data>
<data name="ID6293" xml:space="preserve">
<value>Certificate validation failed because the token binding certificate provided by an anonymous client was not valid: {Errors}.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public void Configure(string? name, HttpClientFactoryOptions options)
// an async-local context to flow per-instance properties and uses dynamic client
// names to ensure the inner HttpClientHandler is not reused if the context differs.
var context = OpenIddictClientSystemNetHttpContext.Current ??
throw new InvalidOperationException(SR.FormatID2202(nameof(OpenIddictClientSystemNetHttpContext)));
throw new InvalidOperationException(SR.FormatID0516(nameof(OpenIddictClientSystemNetHttpContext)));

var settings = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientSystemNetHttpOptions>>().CurrentValue;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public ValueTask HandleAsync(TContext context)

if (OpenIddictClientSystemNetHttpContext.Current is not null)
{
throw new InvalidOperationException(SR.FormatID2201(nameof(OpenIddictClientSystemNetHttpContext)));
throw new InvalidOperationException(SR.FormatID0515(nameof(OpenIddictClientSystemNetHttpContext)));
}

try
Expand Down
5 changes: 4 additions & 1 deletion src/OpenIddict.Server/OpenIddictServerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,11 @@ public OpenIddictServerBuilder Configure(Action<OpenIddictServerOptions> configu
/// <summary>
/// Makes client identification optional so that token, introspection and revocation
/// requests that don't specify a client_id are not automatically rejected.
/// Enabling this option is NOT recommended.
/// </summary>
/// <remarks>
/// Enabling this option is NOT recommended and should only be used for
/// backward compatibility with legacy authorization server deployments.
/// </remarks>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder AcceptAnonymousClients()
=> Configure(options => options.AcceptAnonymousClients = true);
Expand Down
29 changes: 9 additions & 20 deletions src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -412,30 +412,18 @@ public ValueTask HandleAsync(ValidateTokenRequestContext context)
{
ArgumentNullException.ThrowIfNull(context);

// Reject grant_type=authorization_code requests that don't specify a client_id or a client_assertion,
// as the client identifier MUST be sent by the client application in the request body if it cannot
// be inferred from the client authentication method (e.g the username when using basic).
//
// See https://tools.ietf.org/html/rfc6749#section-4.1.3 for more information.
if (context.Request.IsAuthorizationCodeGrantType() &&
string.IsNullOrEmpty(context.Request.ClientId) &&
string.IsNullOrEmpty(context.Request.ClientAssertion))
if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsClientCredentialsGrantType())
{
context.Logger.LogInformation(6077, SR.GetResourceString(SR.ID6077), Parameters.ClientId);

context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2029(Parameters.ClientId),
uri: SR.FormatID8000(SR.ID2029));

return ValueTask.CompletedTask;
}

// Reject grant_type=client_credentials requests that don't specify a client_id or a client_assertion.
// Reject grant_type=authorization_code and grant_type=client_credentials requests that
// don't specify a client_id or a client_assertion, as the client identity MUST be sent
// by the client application (even when using mTLS OAuth 2.0 client authentication).
//
// See https://tools.ietf.org/html/rfc6749#section-4.4.1 for more information.
if (context.Request.IsClientCredentialsGrantType() &&
string.IsNullOrEmpty(context.Request.ClientId) &&
// See https://tools.ietf.org/html/rfc6749#section-4.1.3
// and https://tools.ietf.org/html/rfc6749#section-4.4.1 for more information.
if (string.IsNullOrEmpty(context.Request.ClientId) &&
string.IsNullOrEmpty(context.Request.ClientAssertion))
{
context.Logger.LogInformation(6077, SR.GetResourceString(SR.ID6077), Parameters.ClientId);
Expand Down Expand Up @@ -568,7 +556,8 @@ public ValueTask HandleAsync(ValidateTokenRequestContext context)
// See https://tools.ietf.org/html/rfc6749#section-4.4.1 for more information.
if (context.Request.IsClientCredentialsGrantType() &&
string.IsNullOrEmpty(context.Request.ClientAssertion) &&
string.IsNullOrEmpty(context.Request.ClientSecret))
string.IsNullOrEmpty(context.Request.ClientSecret) &&
context.Transaction.RemoteCertificate is null)
{
context.Reject(
error: Errors.InvalidRequest,
Expand Down
141 changes: 133 additions & 8 deletions src/OpenIddict.Server/OpenIddictServerHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
Expand Down Expand Up @@ -1004,7 +1005,13 @@ public async ValueTask HandleAsync(ProcessAuthenticationContext context)

case OpenIddictServerEndpointType.Introspection when context.Options.AcceptAnonymousClients:
case OpenIddictServerEndpointType.Revocation when context.Options.AcceptAnonymousClients:
case OpenIddictServerEndpointType.Token when context.Options.AcceptAnonymousClients:
return;

// Note: the authorization code and client credentials grant types never
// allow anonymous clients, even if the corresponding option is enabled.
case OpenIddictServerEndpointType.Token when context.Options.AcceptAnonymousClients &&
!context.Request.IsAuthorizationCodeGrantType() &&
!context.Request.IsClientCredentialsGrantType():
return;

// Note: despite being conceptually similar to the token endpoint, the pushed authorization
Expand Down Expand Up @@ -1240,9 +1247,9 @@ OpenIddictServerEndpointType.EndUserVerification or
/// </summary>
public sealed class ValidateClientCertificate : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictApplicationManager? _applicationManager;

public ValidateClientCertificate() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public ValidateClientCertificate() { }

public ValidateClientCertificate(IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
Expand All @@ -1252,9 +1259,18 @@ public ValidateClientCertificate(IOpenIddictApplicationManager applicationManage
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientIdParameter>()
.AddFilter<RequireClientCertificate>()
.AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler(static provider =>
{
// Note: the application manager is only resolved if the degraded mode was not enabled to ensure
// invalid core configuration exceptions are not thrown even if the managers were registered.
var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>().CurrentValue;

return options.EnableDegradedMode ?
new ValidateClientCertificate() :
new ValidateClientCertificate(provider.GetService<IOpenIddictApplicationManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
.UseScopedHandler<ValidateClientCertificate>()
.SetOrder(ValidateClientSecret.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
Expand All @@ -1265,7 +1281,6 @@ public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
ArgumentNullException.ThrowIfNull(context);

Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
Debug.Assert(context.Transaction.RemoteCertificate is not null, SR.GetResourceString(SR.ID4020));

// Don't validate the client certificate on endpoints that don't support client authentication/token binding.
Expand All @@ -1277,6 +1292,106 @@ OpenIddictServerEndpointType.EndUserVerification or
return;
}

// If the client is anonymous, assume the provided certificate will exclusively be used for mTLS token
// binding, validate the certificate using the base chain policy specified in the options and ensure
// the certificate is self-signed as PKI certificates cannot be used for token binding exclusively.
if (string.IsNullOrEmpty(context.ClientId))
{
// Note: to avoid building and introspecting a X.509 certificate chain and reduce the cost
// of this check, a certificate is always assumed to be self-signed when it is self-issued.
//
// A second pass is performed once the chain is built to validate whether the certificate is self-signed or not.
if (context.Options.SelfSignedTlsClientAuthenticationPolicy is not X509ChainPolicy policy ||
!OpenIddictHelpers.IsSelfIssuedCertificate(context.Transaction.RemoteCertificate))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2205),
uri: SR.FormatID8000(SR.ID2205));

return;
}

// Always clone the X.509 chain policy to ensure the original instance is never mutated.
policy = policy.Clone();

// Note: to allow validating certificates that are exclusively used for mTLS token binding, the chain policy
// is amended to consider the specified self-signed certificate as a trusted root and basically disable chain
// validation while still validating the other aspects of the certificate (e.g expiration date, key usage, etc).
#if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE
policy.CustomTrustStore.Add(context.Transaction.RemoteCertificate);
#else
policy.ExtraStore.Add(context.Transaction.RemoteCertificate);
#endif

using var chain = new X509Chain()
{
ChainPolicy = policy
};

try
{
// Ensure the specified certificate is valid based on the chain policy.
if (!chain.Build(context.Transaction.RemoteCertificate))
{
context.Logger.LogInformation(6293, SR.GetResourceString(SR.ID6293),
chain.ChainStatus.Select(static status => status.Status).ToArray());

context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2197),
uri: SR.FormatID8000(SR.ID2197));

return;
}

if (chain.ChainElements is not [X509ChainElement])
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2205),
uri: SR.FormatID8000(SR.ID2205));

return;
}
}

catch (CryptographicException exception) when (!OpenIddictHelpers.IsFatal(exception))
{
context.Logger.LogWarning(6288, exception, SR.GetResourceString(SR.ID6288));

context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2197),
uri: SR.FormatID8000(SR.ID2197));

return;
}

finally
{
// Dispose the certificates instantiated internally while building the chain.
for (var index = 0; index < chain.ChainElements.Count; index++)
{
chain.ChainElements[index].Certificate.Dispose();
}
}

return;
}

// Note: when the degraded mode is enabled, the application is responsible for manually
// validating the client certificate provided by the client using a custom event handler.
if (context.Options.EnableDegradedMode)
{
return;
}

if (_applicationManager is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
}

var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));

Expand All @@ -1291,7 +1406,12 @@ OpenIddictServerEndpointType.EndUserVerification or
{
if (context.Options.SelfSignedTlsClientAuthenticationPolicy is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0506));
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2205),
uri: SR.FormatID8000(SR.ID2205));

return;
}

if (await _applicationManager.GetSelfSignedTlsClientAuthenticationPolicyAsync(
Expand Down Expand Up @@ -1346,7 +1466,12 @@ OpenIddictServerEndpointType.EndUserVerification or
{
if (context.Options.PublicKeyInfrastructureTlsClientAuthenticationPolicy is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0505));
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2205),
uri: SR.FormatID8000(SR.ID2205));

return;
}

if (await _applicationManager.GetPublicKeyInfrastructureTlsClientAuthenticationPolicyAsync(
Expand Down
Loading
Loading