diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 218911a9e..8ff98b2c0 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1860,6 +1860,12 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions TLS client certificates must contain a private key. + + An existing '{0}' instance is already attached to the execution context. + + + The '{0}' attached to the execution context could not be resolved. + The security token is missing. @@ -2462,9 +2468,11 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions An existing '{0}' instance is already attached to the execution context. + This resource is no longer used and will be removed in a future version. The '{0}' attached to the execution context could not be resolved. + This resource is no longer used and will be removed in a future version. A certificate-based proof-of-possession is required to use this token. @@ -2472,6 +2480,9 @@ To use a custom policy relying on the system store, set 'OpenIddictServerOptions The specified certificate-based proof-of-possession is not valid. + + The specified TLS client certificate is not allowed or valid for this operation. + The '{0}' parameter shouldn't be null or empty at this point. @@ -3315,6 +3326,9 @@ This may indicate that the hashed entry is corrupted or malformed. 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. + + Certificate validation failed because the token binding certificate provided by an anonymous client was not valid: {Errors}. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs index ef9783bc4..685774a1e 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs @@ -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>().CurrentValue; diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs index 31e811fe2..9ad12b373 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs @@ -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 diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index dabdd762c..bca5f7a7b 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -116,8 +116,11 @@ public OpenIddictServerBuilder Configure(Action configu /// /// 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. /// + /// + /// Enabling this option is NOT recommended and should only be used for + /// backward compatibility with legacy authorization server deployments. + /// /// The instance. public OpenIddictServerBuilder AcceptAnonymousClients() => Configure(options => options.AcceptAnonymousClients = true); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index 70a946019..138ab3433 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -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); @@ -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, diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index d67b9bb15..cdd1526ab 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -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; @@ -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 @@ -1240,9 +1247,9 @@ OpenIddictServerEndpointType.EndUserVerification or /// public sealed class ValidateClientCertificate : IOpenIddictServerHandler { - 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)); @@ -1252,9 +1259,18 @@ public ValidateClientCertificate(IOpenIddictApplicationManager applicationManage /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() .AddFilter() - .AddFilter() + .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>().CurrentValue; + + return options.EnableDegradedMode ? + new ValidateClientCertificate() : + new ValidateClientCertificate(provider.GetService() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016))); + }) .UseScopedHandler() .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) @@ -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. @@ -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)); @@ -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( @@ -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( diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 63a4881e4..bd70ae9c8 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -359,6 +359,9 @@ public sealed class OpenIddictServerOptions /// Enabling this option allows client applications to communicate with the token, /// introspection and revocation endpoints without having to send their client identifier. /// + /// + /// Setting this property to is NOT recommended. + /// public bool AcceptAnonymousClients { get; set; } /// @@ -593,38 +596,50 @@ public sealed class OpenIddictServerOptions /// /// Gets or sets a boolean indicating whether audience permissions should be ignored. - /// Setting this property to is NOT recommended. /// + /// + /// Setting this property to is NOT recommended. + /// public bool IgnoreAudiencePermissions { get; set; } /// /// Gets or sets a boolean indicating whether endpoint permissions should be ignored. - /// Setting this property to is NOT recommended. /// + /// + /// Setting this property to is NOT recommended. + /// public bool IgnoreEndpointPermissions { get; set; } /// /// Gets or sets a boolean indicating whether grant type permissions should be ignored. - /// Setting this property to is NOT recommended. /// + /// + /// Setting this property to is NOT recommended. + /// public bool IgnoreGrantTypePermissions { get; set; } /// /// Gets or sets a boolean indicating whether resource permissions should be ignored. - /// Setting this property to is NOT recommended. /// + /// + /// Setting this property to is NOT recommended. + /// public bool IgnoreResourcePermissions { get; set; } /// /// Gets or sets a boolean indicating whether response type permissions should be ignored. - /// Setting this property to is NOT recommended. /// + /// + /// Setting this property to is NOT recommended. + /// public bool IgnoreResponseTypePermissions { get; set; } /// /// Gets or sets a boolean indicating whether scope permissions should be ignored. - /// Setting this property to is NOT recommended. /// + /// + /// Setting this property to is NOT recommended. + /// public bool IgnoreScopePermissions { get; set; } /// diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs index 6bd88a213..c75a49345 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs @@ -75,7 +75,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 = OpenIddictValidationSystemNetHttpContext.Current ?? - throw new InvalidOperationException(SR.FormatID2202(nameof(OpenIddictValidationSystemNetHttpContext))); + throw new InvalidOperationException(SR.FormatID0516(nameof(OpenIddictValidationSystemNetHttpContext))); var settings = _provider.GetRequiredService>().CurrentValue; diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs index 00b2bf0d1..66ddc8772 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs @@ -84,7 +84,7 @@ public ValueTask HandleAsync(TContext context) if (OpenIddictValidationSystemNetHttpContext.Current is not null) { - throw new InvalidOperationException(SR.FormatID2201(nameof(OpenIddictValidationSystemNetHttpContext))); + throw new InvalidOperationException(SR.FormatID0515(nameof(OpenIddictValidationSystemNetHttpContext))); } try diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index 9b3a45352..ab7a300aa 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -9,6 +9,7 @@ using System.Collections.Immutable; using System.Globalization; using System.Security.Claims; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System.Text.Json.Nodes; @@ -1099,13 +1100,170 @@ public async Task ProcessAuthentication_RequestIsRejectedWhenClientSecretIsInval } #if SUPPORTS_X509_CHAIN_POLICY_CUSTOM_TRUST_STORE + [Theory] + [InlineData(OpenIddictServerEndpointType.Introspection)] + [InlineData(OpenIddictServerEndpointType.Revocation)] + [InlineData(OpenIddictServerEndpointType.Token)] + public async Task ProcessAuthentication_SelfSignedClientCertificateIsRejectedWhenBaseChainPolicyIsNullWithUnknownClient(OpenIddictServerEndpointType type) + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + options.AcceptAnonymousClients(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + context.Transaction.RemoteCertificate = X509Certificate2.CreateFromPem($""" + -----BEGIN CERTIFICATE----- + MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE + AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy + NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75 + o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss + h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6 + 6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0 + I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6 + NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym + uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH + AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA + hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy + 3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp + nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp + AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl + 0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo= + -----END CERTIFICATE----- + """); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateClientType.Descriptor.Order - 500); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = type switch + { + OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest + { + Token = "2YotnFZFEjr1zCsicMWpAA" + }), + + OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + Token = "SlAV32hkKG", + TokenTypeHint = TokenTypeHints.RefreshToken + }), + + OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }), + + _ => throw new NotSupportedException() + }; + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2205), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2205), response.ErrorUri); + } + + [Theory] + [InlineData(OpenIddictServerEndpointType.Introspection)] + [InlineData(OpenIddictServerEndpointType.Revocation)] + [InlineData(OpenIddictServerEndpointType.Token)] + public async Task ProcessAuthentication_SelfSignedClientCertificateIsRejectedWhenInvalidWithUnknownClient(OpenIddictServerEndpointType type) + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + options.AcceptAnonymousClients(); + + options.Configure(options => options.SelfSignedTlsClientAuthenticationPolicy = new X509ChainPolicy + { + ApplicationPolicy = { new Oid("1.3.6.1.4.1.99999.1.1") } + }); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + context.Transaction.RemoteCertificate = X509Certificate2.CreateFromPem($""" + -----BEGIN CERTIFICATE----- + MIIC8jCCAdqgAwIBAgIIYfcknj8KXN0wDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UE + AxMXU2VsZi1zaWduZWQgY2VydGlmaWNhdGUwIBcNMjYwMjAxMTc1MjI2WhgPMjEy + NjAyMDExNzUyMjZaMCIxIDAeBgNVBAMTF1NlbGYtc2lnbmVkIGNlcnRpZmljYXRl + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA45uKd5cdlLmEBGLDEB75 + o9e3/kMmQjVhhMeBsy4m2t7zw5jZo7OPcahXiXZHttom9tJm7BWPWvYx7p0N9+ss + h/E5lzKyV7ZXg+mM+KeECtVhiy+82BuIPelCshrpaV3lIg93y47FYLIWXxdggjt6 + 6VUaxzlTeo+IpuMz8IssL7VpJnjCT5NmqPNVkv1VR1uuetVqP7546ZFw31RiGl/0 + I1uUlb7SwLwhLUK1iyLmGNA3VDB0m0DvLmlIEY3ZE5zxQp/Rxq6DfjbXm2LWJyu6 + NO7k7JixXOorEl+6HdJZHTWNFK5jCo2ZZAwWn+uUuzgmLILPJFLDVutXjuEZpsym + uQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUH + AwIwDQYJKoZIhvcNAQELBQADggEBAGHH3f/bkfViTvPE7yXJkB0bs88mYxajluMA + hgihEN5joPT6zHxMLBND2sitIozCMeeaj0rg+OaT/zDBgOLup/BM92UaPpYcgDCy + 3tHqZLOOJOR4aYnHhIQUnx+NRtKEM4q/hL/xLHeliKmV7TQXISEZlTbb0gOU7TFp + nJlP60Vo9F/WD6xcKNxBgV5aB/+2FjiTTw2pF0VUmvcZdQAN5ysfrmKNXbvv1oCp + AohiwRiPrwe3mJ8iCqzEY/qQqImEiIT8WC2Fty+UYyBwfXMObi1AO++QkaMUbUJl + 0aBuRoD85FLotjHIHXkFHERjOolheYdKt5nrGCCz/PmXBfsSCTo= + -----END CERTIFICATE----- + """); + + return ValueTask.CompletedTask; + }); + + builder.SetOrder(ValidateClientType.Descriptor.Order - 500); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = type switch + { + OpenIddictServerEndpointType.Introspection => await client.PostAsync("/connect/introspect", new OpenIddictRequest + { + Token = "2YotnFZFEjr1zCsicMWpAA" + }), + + OpenIddictServerEndpointType.Revocation => await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + Token = "SlAV32hkKG", + TokenTypeHint = TokenTypeHints.RefreshToken + }), + + OpenIddictServerEndpointType.Token => await client.PostAsync("/connect/token", new OpenIddictRequest + { + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }), + + _ => throw new NotSupportedException() + }; + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2197), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2197), response.ErrorUri); + } + [Theory] [InlineData(OpenIddictServerEndpointType.DeviceAuthorization)] [InlineData(OpenIddictServerEndpointType.Introspection)] [InlineData(OpenIddictServerEndpointType.PushedAuthorization)] [InlineData(OpenIddictServerEndpointType.Revocation)] [InlineData(OpenIddictServerEndpointType.Token)] - public async Task ProcessAuthentication_SelfSignedClientCertificateIsRejectedWhenChainPolicyIsNull(OpenIddictServerEndpointType type) + public async Task ProcessAuthentication_SelfSignedClientCertificateIsRejectedWhenApplicationChainPolicyIsNull(OpenIddictServerEndpointType type) { // Arrange var application = new OpenIddictApplication(); @@ -1459,7 +1617,7 @@ public async Task ProcessAuthentication_SelfSignedClientCertificatePolicyIsAmend [InlineData(OpenIddictServerEndpointType.PushedAuthorization)] [InlineData(OpenIddictServerEndpointType.Revocation)] [InlineData(OpenIddictServerEndpointType.Token)] - public async Task ProcessAuthentication_PkiClientCertificateIsRejectedWhenChainPolicyIsNull(OpenIddictServerEndpointType type) + public async Task ProcessAuthentication_PkiClientCertificateIsRejectedWhenApplicationChainPolicyIsNull(OpenIddictServerEndpointType type) { // Arrange var application = new OpenIddictApplication();