diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs index 993f8b60..282458f7 100644 --- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs +++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs @@ -184,11 +184,6 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentNullException(nameof(address)); } - if (!address.IsAbsoluteUri || !address.IsWellFormedOriginalString()) - { - throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address)); - } - return Configure(options => options.RedirectUri = address); } @@ -204,12 +199,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentException(SR.GetResourceString(SR.ID0143), nameof(address)); } - if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri)) - { - throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address)); - } - - return SetRedirectUri(uri); + return SetRedirectUri(new Uri(address, UriKind.RelativeOrAbsolute)); } /// @@ -285,12 +275,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder throw new ArgumentException(SR.GetResourceString(SR.ID0143), nameof({{ setting.parameter_name }})); } - if (!Uri.TryCreate({{ setting.parameter_name }}, UriKind.Absolute, out Uri? uri)) - { - throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof({{ setting.parameter_name }})); - } - - return Set{{ setting.property_name }}(uri); + return Set{{ setting.property_name }}(new Uri({{ setting.parameter_name }}, UriKind.RelativeOrAbsolute)); } {{~ else ~}} /// diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs index 4ad35853..481eece4 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs @@ -73,14 +73,13 @@ namespace OpenIddict.Sandbox.AspNet.Client // parameter containing their URL as part of authorization responses. For more information, // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4. options.SetRedirectionEndpointUris( - "/callback/login/local", - "/callback/login/github", - "/callback/login/google", - "/callback/login/twitter"); + "callback/login/local", + "callback/login/github", + "callback/login/google", + "callback/login/twitter"); - // Enable the post-logout redirection endpoints needed to handle the callback stage. - options.SetPostLogoutRedirectionEndpointUris( - "/callback/logout/local"); + // Enable the post-logout redirection endpoint needed to handle the callback stage. + options.SetPostLogoutRedirectionEndpointUris("callback/logout/local"); // Note: this sample uses the authorization code and refresh token // flows, but you can enable the other flows if necessary. @@ -106,15 +105,15 @@ namespace OpenIddict.Sandbox.AspNet.Client // Add a client registration matching the client application definition in the server project. options.AddRegistration(new OpenIddictClientRegistration { - ProviderName = "Local", Issuer = new Uri("https://localhost:44349/", UriKind.Absolute), + ProviderName = "Local", ClientId = "mvc", ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }, - RedirectUri = new Uri("https://localhost:44378/callback/login/local", UriKind.Absolute), - PostLogoutRedirectUri = new Uri("https://localhost:44378/callback/logout/local", UriKind.Absolute) + RedirectUri = new Uri("callback/login/local", UriKind.Relative), + PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative) }); // Register the Web providers integrations. @@ -123,13 +122,13 @@ namespace OpenIddict.Sandbox.AspNet.Client { options.SetClientId("c4ade52327b01ddacff3") .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") - .SetRedirectUri("https://localhost:44378/callback/login/github"); + .SetRedirectUri("callback/login/github"); }) .UseGoogle(options => { options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com") .SetClientSecret("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") - .SetRedirectUri("https://localhost:44378/callback/login/google") + .SetRedirectUri("callback/login/google") .SetAccessType("offline") .AddScopes(Scopes.Profile); }) @@ -137,7 +136,7 @@ namespace OpenIddict.Sandbox.AspNet.Client { options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") .SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") - .SetRedirectUri("https://localhost:44378/callback/login/twitter"); + .SetRedirectUri("callback/login/twitter"); }); }); diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs index 80f12a56..e555ed36 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs @@ -157,7 +157,7 @@ namespace OpenIddict.Sandbox.AspNet.Server // address per provider, unless all the registered providers support returning an "iss" // parameter containing their URL as part of authorization responses. For more information, // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4. - options.SetRedirectionEndpointUris("/callback/login/github"); + options.SetRedirectionEndpointUris("callback/login/github"); // Note: this sample uses the code flow, but you can enable the other flows if necessary. options.AllowAuthorizationCodeFlow(); @@ -183,7 +183,7 @@ namespace OpenIddict.Sandbox.AspNet.Server { options.SetClientId("c4ade52327b01ddacff3") .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") - .SetRedirectUri("https://localhost:44349/callback/login/github"); + .SetRedirectUri("callback/login/github"); }); }) @@ -192,13 +192,13 @@ namespace OpenIddict.Sandbox.AspNet.Server { // Enable the authorization, device, introspection, // logout, token, userinfo and verification endpoints. - options.SetAuthorizationEndpointUris("/connect/authorize") - .SetDeviceEndpointUris("/connect/device") - .SetIntrospectionEndpointUris("/connect/introspect") - .SetLogoutEndpointUris("/connect/logout") - .SetTokenEndpointUris("/connect/token") - .SetUserinfoEndpointUris("/connect/userinfo") - .SetVerificationEndpointUris("/connect/verify"); + options.SetAuthorizationEndpointUris("connect/authorize") + .SetDeviceEndpointUris("connect/device") + .SetIntrospectionEndpointUris("connect/introspect") + .SetLogoutEndpointUris("connect/logout") + .SetTokenEndpointUris("connect/token") + .SetUserinfoEndpointUris("connect/userinfo") + .SetVerificationEndpointUris("connect/verify"); // Note: this sample uses the code, device code, password and refresh token flows, but you // can enable the other flows if you need to support implicit or client credentials. diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs index f3583a5c..f981ddb7 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs @@ -81,15 +81,14 @@ public class Startup // parameter containing their URL as part of authorization responses. For more information, // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4. options.SetRedirectionEndpointUris( - "/callback/login/local", - "/callback/login/github", - "/callback/login/google", - "/callback/login/reddit", - "/callback/login/twitter"); + "callback/login/local", + "callback/login/github", + "callback/login/google", + "callback/login/reddit", + "callback/login/twitter"); - // Enable the post-logout redirection endpoints needed to handle the callback stage. - options.SetPostLogoutRedirectionEndpointUris( - "/callback/logout/local"); + // Enable the post-logout redirection endpoint needed to handle the callback stage. + options.SetPostLogoutRedirectionEndpointUris("callback/logout/local"); // Note: this sample uses the authorization code and refresh token // flows, but you can enable the other flows if necessary. @@ -116,15 +115,15 @@ public class Startup // Add a client registration matching the client application definition in the server project. options.AddRegistration(new OpenIddictClientRegistration { - ProviderName = "Local", Issuer = new Uri("https://localhost:44395/", UriKind.Absolute), + ProviderName = "Local", ClientId = "mvc", ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }, - RedirectUri = new Uri("https://localhost:44381/callback/login/local", UriKind.Absolute), - PostLogoutRedirectUri = new Uri("https://localhost:44381/callback/logout/local", UriKind.Absolute) + RedirectUri = new Uri("callback/login/local", UriKind.Relative), + PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative) }); // Register the Web providers integrations. @@ -133,13 +132,13 @@ public class Startup { options.SetClientId("c4ade52327b01ddacff3") .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") - .SetRedirectUri("https://localhost:44381/callback/login/github"); + .SetRedirectUri("callback/login/github"); }) .UseGoogle(options => { options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com") .SetClientSecret("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") - .SetRedirectUri("https://localhost:44381/callback/login/google") + .SetRedirectUri("callback/login/google") .SetAccessType("offline") .AddScopes(Scopes.Profile); }) @@ -147,14 +146,14 @@ public class Startup { options.SetClientId("vDLNqhrkwrvqHgnoBWF3og") .SetClientSecret("Tpab28Dz0upyZLqn7AN3GFD1O-zaAw") - .SetRedirectUri("https://localhost:44381/callback/login/reddit") + .SetRedirectUri("callback/login/reddit") .SetDuration("permanent"); }) .UseTwitter(options => { options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") .SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") - .SetRedirectUri("https://localhost:44381/callback/login/twitter") + .SetRedirectUri("callback/login/twitter") .AddScopes("offline.access"); }); }); diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs index 46a90bab..5e7949de 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs @@ -74,7 +74,7 @@ public class Startup // address per provider, unless all the registered providers support returning an "iss" // parameter containing their URL as part of authorization responses. For more information, // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4. - options.SetRedirectionEndpointUris("/callback/login/github"); + options.SetRedirectionEndpointUris("callback/login/github"); // Note: this sample uses the code flow, but you can enable the other flows if necessary. options.AllowAuthorizationCodeFlow(); @@ -101,7 +101,7 @@ public class Startup { options.SetClientId("c4ade52327b01ddacff3") .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") - .SetRedirectUri("https://localhost:44395/callback/login/github"); + .SetRedirectUri("callback/login/github"); }); }) @@ -110,13 +110,13 @@ public class Startup { // Enable the authorization, device, introspection, // logout, token, userinfo and verification endpoints. - options.SetAuthorizationEndpointUris("/connect/authorize") - .SetDeviceEndpointUris("/connect/device") - .SetIntrospectionEndpointUris("/connect/introspect") - .SetLogoutEndpointUris("/connect/logout") - .SetTokenEndpointUris("/connect/token") - .SetUserinfoEndpointUris("/connect/userinfo") - .SetVerificationEndpointUris("/connect/verify"); + options.SetAuthorizationEndpointUris("connect/authorize") + .SetDeviceEndpointUris("connect/device") + .SetIntrospectionEndpointUris("connect/introspect") + .SetLogoutEndpointUris("connect/logout") + .SetTokenEndpointUris("connect/token") + .SetUserinfoEndpointUris("connect/userinfo") + .SetVerificationEndpointUris("connect/verify"); // Note: this sample uses the code, device code, password and refresh token flows, but you // can enable the other flows if you need to support implicit or client credentials. diff --git a/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs b/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs index 548a2719..d5b39a6e 100644 --- a/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs +++ b/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Security.Claims; using System.Security.Cryptography; @@ -91,6 +92,83 @@ internal static class OpenIddictHelpers => new(source ?? throw new ArgumentNullException(nameof(source)), comparer); #endif + /// + /// Computes an absolute URI from the specified and URIs. + /// Note: if the URI is already absolute, it is directly returned. + /// + /// The left part. + /// The right part. + /// An absolute URI from the specified and . + /// is not an absolute URI. + [return: NotNullIfNotNull(nameof(right))] + public static Uri? CreateAbsoluteUri(Uri? left, Uri? right) + { + if (right is null) + { + return null; + } + + if (right.IsAbsoluteUri) + { + return right; + } + + if (left is not { IsAbsoluteUri: true }) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(left)); + } + + // Ensure the left part ends with a trailing slash, as it is necessary + // for Uri's constructor to include the last path segment in the base URI. + left = left.AbsolutePath switch + { + null or { Length: 0 } => new UriBuilder(left) { Path = "/" }.Uri, + [.., not '/'] => new UriBuilder(left) { Path = left.AbsolutePath + "/" }.Uri, + ['/'] or _ => left + }; + + return new Uri(left, right); + } + + /// + /// Determines whether the URI is a base of the URI. + /// + /// The left part. + /// The right part. + /// if is base of + /// , otherwise. + /// or + /// is . + /// is not an absolute URI. + public static bool IsBaseOf(Uri left, Uri right) + { + if (left is null) + { + throw new ArgumentNullException(nameof(left)); + } + + if (right is null) + { + throw new ArgumentNullException(nameof(right)); + } + + if (left is not { IsAbsoluteUri: true }) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(left)); + } + + // Ensure the left part ends with a trailing slash, as it is necessary + // for Uri's constructor to include the last path segment in the base URI. + left = left.AbsolutePath switch + { + null or { Length: 0 } => new UriBuilder(left) { Path = "/" }.Uri, + [.., not '/'] => new UriBuilder(left) { Path = left.AbsolutePath + "/" }.Uri, + ['/'] or _ => left + }; + + return left.IsBaseOf(right); + } + /// /// Adds a query string parameter to the specified . /// diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index d8e150ee..2f1ef58f 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -547,7 +547,7 @@ To register the server services, use 'services.AddOpenIddict().AddServer()'.The issuer cannot be null or empty. - The issuer must be a valid absolute URL. + The base URI or request URI cannot be retrieved from the request context or are now valid absolute URIs. An OAuth 2.0/OpenID Connect server configuration or an issuer address must be registered. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs index ec789efa..b39f3935 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs @@ -13,7 +13,7 @@ using System.Text; using System.Text.Json; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.WebUtilities; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; @@ -33,14 +33,14 @@ public static partial class OpenIddictClientAspNetCoreHandlers /* * Top-level request processing: */ - InferEndpointType.Descriptor, + ResolveRequestUri.Descriptor, ValidateTransportSecurityRequirement.Descriptor, + ValidateHostHeader.Descriptor, /* * Authentication processing: */ ResolveRequestForgeryProtection.Descriptor, - ValidateEndpointUri.Descriptor, /* * Challenge processing: @@ -59,10 +59,10 @@ public static partial class OpenIddictClientAspNetCoreHandlers .AddRange(Session.DefaultHandlers); /// - /// Contains the logic responsible for inferring the endpoint type from the request address. + /// Contains the logic responsible for resolving the request URI from the ASP.NET Core environment. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// - public sealed class InferEndpointType : IOpenIddictClientHandler + public sealed class ResolveRequestUri : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -70,8 +70,9 @@ public static partial class OpenIddictClientAspNetCoreHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(int.MinValue + 50_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); /// @@ -87,63 +88,37 @@ public static partial class OpenIddictClientAspNetCoreHandlers var request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); - context.EndpointType = - Matches(request, context.Options.PostLogoutRedirectionEndpointUris) ? OpenIddictClientEndpointType.PostLogoutRedirection : - Matches(request, context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection : - OpenIddictClientEndpointType.Unknown; - - return default; - - static bool Matches(HttpRequest request, IReadOnlyList addresses) + // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute + // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized + // to './' once the URI is fully constructed). At this stage of the request processing, rejecting + // requests that lack the host information (e.g because HTTP/1.0 was used and no Host header was + // sent by the HTTP client) is not desirable as it would affect all requests, including requests + // that are not meant to be handled by OpenIddict itself. To avoid that, a fake host is temporarily + // used to build an absolute base URI and a request URI that will be used to determine whether the + // received request matches one of the addresses assigned to an OpenIddict endpoint. If the request + // is later handled by OpenIddict, an additional check will be made to require the Host header. + + (context.BaseUri, context.RequestUri) = request.Host switch { - for (var index = 0; index < addresses.Count; index++) - { - var address = addresses[index]; - if (address.IsAbsoluteUri) - { - // If the request host is not available (e.g because HTTP/1.0 was used), ignore absolute URLs. - if (!request.Host.HasValue) - { - continue; - } - - // Create a Uri instance using the request scheme and raw host and compare the two base addresses. - if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host, UriKind.Absolute, out Uri? uri) || - !uri.IsWellFormedOriginalString() || uri.Port != address.Port || - !string.Equals(uri.Scheme, address.Scheme, StringComparison.OrdinalIgnoreCase) || - !string.Equals(uri.Host, address.Host, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var path = PathString.FromUriComponent(address); - if (AreEquivalent(path, request.PathBase + request.Path)) - { - return true; - } - } + { HasValue: true } host => ( + BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute), + RequestUri: new Uri(request.GetEncodedUrl(), UriKind.Absolute)), - else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { HasValue: false } => ( + BaseUri: new UriBuilder { - var path = new PathString(address.OriginalString); - if (AreEquivalent(path, request.Path)) - { - return true; - } - } - } - - return false; + Scheme = request.Scheme, + Path = request.PathBase.ToUriComponent() + }.Uri, + RequestUri: new UriBuilder + { + Scheme = request.Scheme, + Path = (request.PathBase + request.Path).ToUriComponent(), + Query = request.QueryString.ToUriComponent() + }.Uri) + }; - // ASP.NET Core's routing system ignores trailing slashes when determining - // whether the request path matches a registered route, which is not the case - // with PathString, that treats /connect/token and /connect/token/ as different - // addresses. To mitigate this inconsistency, a manual check is used here. - static bool AreEquivalent(PathString left, PathString right) - => left.Equals(right, StringComparison.OrdinalIgnoreCase) || - left.Equals(right + "/", StringComparison.OrdinalIgnoreCase) || - right.Equals(left + "/", StringComparison.OrdinalIgnoreCase); - } + return default; } } @@ -161,7 +136,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers .AddFilter() .AddFilter() .UseSingletonHandler() - .SetOrder(InferEndpointType.Descriptor.Order + 1_000) + .SetOrder(InferEndpointType.Descriptor.Order + 250) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -178,18 +153,58 @@ public static partial class OpenIddictClientAspNetCoreHandlers var request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); - // Don't require that the host be present if the request is not handled by OpenIddict. - if (context.EndpointType is OpenIddictClientEndpointType.Unknown) + // Don't require that transport security be used if the request is not handled by OpenIddict. + if (context.EndpointType is not OpenIddictClientEndpointType.Unknown && !request.IsHttps) { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2083), + uri: SR.FormatID8000(SR.ID2083)); + return default; } - if (!request.IsHttps) + return default; + } + } + + /// + /// Contains the logic responsible for validating the Host header extracted from the HTTP header. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public sealed class ValidateHostHeader : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateTransportSecurityRequirement.Descriptor.Order + 250) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + + // Don't require that the request host be present if the request is not handled by OpenIddict. + if (context.EndpointType is not OpenIddictClientEndpointType.Unknown && !request.Host.HasValue) { context.Reject( error: Errors.InvalidRequest, - description: SR.GetResourceString(SR.ID2083), - uri: SR.FormatID8000(SR.ID2083)); + description: SR.FormatID2081(HeaderNames.Host), + uri: SR.FormatID8000(SR.ID2081)); return default; } @@ -401,110 +416,6 @@ public static partial class OpenIddictClientAspNetCoreHandlers } } - /// - /// Contains the logic responsible for comparing the current request URL to the expected URL stored in the state token. - /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. - /// - public sealed class ValidateEndpointUri : IOpenIddictClientHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictClientHandlerDescriptor Descriptor { get; } - = OpenIddictClientHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(ResolveRequestForgeryProtection.Descriptor.Order + 500) - .SetType(OpenIddictClientHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - - // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, - // this may indicate that the request was incorrectly processed by another server stack. - var request = context.Transaction.GetHttpRequest() ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); - - // Resolve the endpoint type allowed to be used with the state token. - if (!Enum.TryParse(context.StateTokenPrincipal.GetClaim(Claims.Private.EndpointType), - ignoreCase: true, out OpenIddictClientEndpointType type)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0340)); - } - - // Resolve the endpoint URI from either the redirect_uri or post_logout_redirect_uri - // depending on the type of endpoint meant to be used with the specified state token. - var value = type switch - { - OpenIddictClientEndpointType.PostLogoutRedirection => - context.StateTokenPrincipal.GetClaim(Claims.Private.PostLogoutRedirectUri), - - OpenIddictClientEndpointType.Redirection => - context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri), - - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0340)) - }; - - // If the endpoint URI cannot be resolved, this likely means the authorization or - // logout request was sent without a redirect_uri/post_logout_redirect_uri attached. - if (string.IsNullOrEmpty(value)) - { - return default; - } - - // Compute the absolute URL of the current request without the query string. - var uri = new Uri(request.Scheme + Uri.SchemeDelimiter + request.Host + - request.PathBase + request.Path, UriKind.Absolute); - - // Compare the current HTTP request address to the original endpoint URI. If the two don't - // match, this may indicate a mix-up attack. While the authorization server is expected to - // abort the authorization flow by rejecting the token request that may be eventually sent - // with the original endpoint URI, many servers are known to incorrectly implement this - // endpoint URI validation logic. This check also offers limited protection as it cannot - // prevent the authorization code from being leaked to a malicious authorization server. - // By comparing the endpoint URI directly in the client, a first layer of protection is - // provided independently of whether the authorization server will enforce this check. - // - // See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-4.4.2.2 - // for more information. - var address = new Uri(value, UriKind.Absolute); - if (uri != new UriBuilder(address) { Query = null }.Uri) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.GetResourceString(SR.ID2138), - uri: SR.FormatID8000(SR.ID2138)); - - return default; - } - - // Ensure all the query string parameters that were part of the original endpoint URI - // are present in the current request (parameters that were not part of the original - // endpoint URI are assumed to be authorization response parameters and are ignored). - if (!string.IsNullOrEmpty(address.Query) && QueryHelpers.ParseQuery(address.Query) - .Any(parameter => request.Query[parameter.Key] != parameter.Value)) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.GetResourceString(SR.ID2138), - uri: SR.FormatID8000(SR.ID2138)); - - return default; - } - - return default; - } - } - /// /// Contains the logic responsible for resolving the context-specific properties and parameters stored in the /// ASP.NET Core authentication properties specified by the application that triggered the challenge operation. diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs index 95ed6568..1c54152d 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs @@ -16,6 +16,7 @@ public static class OpenIddictClientOwinConstants public const string CacheControl = "Cache-Control"; public const string ContentType = "Content-Type"; public const string Expires = "Expires"; + public const string Host = "Host"; public const string Pragma = "Pragma"; } diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs index 9aa61780..da19efc7 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs @@ -15,7 +15,6 @@ using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; -using OpenIddict.Extensions; using Owin; using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants; using Properties = OpenIddict.Client.Owin.OpenIddictClientOwinConstants.Properties; @@ -29,14 +28,14 @@ public static partial class OpenIddictClientOwinHandlers /* * Top-level request processing: */ - InferEndpointType.Descriptor, + ResolveRequestUri.Descriptor, ValidateTransportSecurityRequirement.Descriptor, + ValidateHostHeader.Descriptor, /* * Authentication processing: */ ResolveRequestForgeryProtection.Descriptor, - ValidateEndpointUri.Descriptor, /* * Challenge processing: @@ -55,10 +54,10 @@ public static partial class OpenIddictClientOwinHandlers .AddRange(Session.DefaultHandlers); /// - /// Contains the logic responsible for inferring the endpoint type from the request address. + /// Contains the logic responsible for resolving the request URI from the OWIN environment. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// - public sealed class InferEndpointType : IOpenIddictClientHandler + public sealed class ResolveRequestUri : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -66,9 +65,7 @@ public static partial class OpenIddictClientOwinHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - // Note: this handler must be invoked before any other handler, - // including the built-in handlers defined in OpenIddict.Client. + .UseSingletonHandler() .SetOrder(int.MinValue + 50_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -86,63 +83,37 @@ public static partial class OpenIddictClientOwinHandlers var request = context.Transaction.GetOwinRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); - context.EndpointType = - Matches(request, context.Options.PostLogoutRedirectionEndpointUris) ? OpenIddictClientEndpointType.PostLogoutRedirection : - Matches(request, context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection : - OpenIddictClientEndpointType.Unknown; + // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute + // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized + // to './' once the URI is fully constructed). At this stage of the request processing, rejecting + // requests that lack the host information (e.g because HTTP/1.0 was used and no Host header was + // sent by the HTTP client) is not desirable as it would affect all requests, including requests + // that are not meant to be handled by OpenIddict itself. To avoid that, a fake host is temporarily + // used to build an absolute base URI and a request URI that will be used to determine whether the + // received request matches one of the addresses assigned to an OpenIddict endpoint. If the request + // is later handled by OpenIddict, an additional check will be made to require the Host header. - return default; - - static bool Matches(IOwinRequest request, IReadOnlyList addresses) + (context.BaseUri, context.RequestUri) = request.Host switch { - for (var index = 0; index < addresses.Count; index++) - { - var address = addresses[index]; - if (address.IsAbsoluteUri) - { - // If the request host is not available (e.g because HTTP/1.0 was used), ignore absolute URLs. - if (string.IsNullOrEmpty(request.Host.Value)) - { - continue; - } - - // Create a Uri instance using the request scheme and raw host and compare the two base addresses. - if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host, UriKind.Absolute, out Uri? uri) || - !uri.IsWellFormedOriginalString() || uri.Port != address.Port || - !string.Equals(uri.Scheme, address.Scheme, StringComparison.OrdinalIgnoreCase) || - !string.Equals(uri.Host, address.Host, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var path = PathString.FromUriComponent(address); - if (AreEquivalent(path, request.PathBase + request.Path)) - { - return true; - } - } + { Value.Length: > 0 } host => ( + BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute), + RequestUri: request.Uri), - else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { Value: null or { Length: 0 } } => ( + BaseUri: new UriBuilder { - var path = new PathString(address.OriginalString); - if (AreEquivalent(path, request.Path)) - { - return true; - } - } - } - - return false; + Scheme = request.Scheme, + Path = request.PathBase.ToUriComponent() + }.Uri, + RequestUri: new UriBuilder + { + Scheme = request.Scheme, + Path = (request.PathBase + request.Path).ToUriComponent(), + Query = request.QueryString.ToUriComponent() + }.Uri) + }; - // ASP.NET MVC's routing system ignores trailing slashes when determining - // whether the request path matches a registered route, which is not the case - // with PathString, that treats /connect/token and /connect/token/ as different - // addresses. To mitigate this inconsistency, a manual check is used here. - static bool AreEquivalent(PathString left, PathString right) - => left.Equals(right, StringComparison.OrdinalIgnoreCase) || - left.Equals(right + new PathString("/"), StringComparison.OrdinalIgnoreCase) || - right.Equals(left + new PathString("/"), StringComparison.OrdinalIgnoreCase); - } + return default; } } @@ -160,7 +131,7 @@ public static partial class OpenIddictClientOwinHandlers .AddFilter() .AddFilter() .UseSingletonHandler() - .SetOrder(InferEndpointType.Descriptor.Order + 1_000) + .SetOrder(InferEndpointType.Descriptor.Order + 250) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -177,7 +148,7 @@ public static partial class OpenIddictClientOwinHandlers var request = context.Transaction.GetOwinRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); - // Don't require that the host be present if the request is not handled by OpenIddict. + // Don't require that transport security be used if the request is not handled by OpenIddict. if (context.EndpointType is OpenIddictClientEndpointType.Unknown) { return default; @@ -197,6 +168,52 @@ public static partial class OpenIddictClientOwinHandlers } } + /// + /// Contains the logic responsible for validating the Host header extracted from the HTTP header. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public sealed class ValidateHostHeader : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(InferEndpointType.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); + + // Don't require that the request host be present if the request is not handled by OpenIddict. + if (context.EndpointType is not OpenIddictClientEndpointType.Unknown && + string.IsNullOrEmpty(request.Host.Value)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2081(Headers.Host), + uri: SR.FormatID8000(SR.ID2081)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting OpenID Connect requests from GET or POST HTTP requests. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. @@ -409,113 +426,6 @@ public static partial class OpenIddictClientOwinHandlers } } - /// - /// Contains the logic responsible for comparing the current request URL to the expected URL stored in the state token. - /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. - /// - public sealed class ValidateEndpointUri : IOpenIddictClientHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictClientHandlerDescriptor Descriptor { get; } - = OpenIddictClientHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(ResolveRequestForgeryProtection.Descriptor.Order + 500) - .SetType(OpenIddictClientHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - - // This handler only applies to OWIN requests. If the HTTP context cannot be resolved, - // this may indicate that the request was incorrectly processed by another server stack. - var request = context.Transaction.GetOwinRequest() ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); - - // Resolve the endpoint type allowed to be used with the state token. - if (!Enum.TryParse(context.StateTokenPrincipal.GetClaim(Claims.Private.EndpointType), - ignoreCase: true, out OpenIddictClientEndpointType type)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0340)); - } - - // Resolve the endpoint address from either the redirect_uri or post_logout_redirect_uri - // depending on the type of endpoint allowed to receive the specified state token. - var value = type switch - { - OpenIddictClientEndpointType.PostLogoutRedirection => - context.StateTokenPrincipal.GetClaim(Claims.Private.PostLogoutRedirectUri), - - OpenIddictClientEndpointType.Redirection => - context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri), - - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0340)) - }; - - // If the endpoint URI cannot be resolved, this likely means the authorization or - // logout request was sent without a redirect_uri/post_logout_redirect_uri attached. - if (string.IsNullOrEmpty(value)) - { - return default; - } - - // Compute the absolute URL of the current request without the query string. - var uri = new Uri(request.Scheme + Uri.SchemeDelimiter + request.Host + - request.PathBase + request.Path, UriKind.Absolute); - - // Compare the current HTTP request address to the original endpoint URI. If the two don't - // match, this may indicate a mix-up attack. While the authorization server is expected to - // abort the authorization flow by rejecting the token request that may be eventually sent - // with the original endpoint URI, many servers are known to incorrectly implement this - // endpoint URI validation logic. This check also offers limited protection as it cannot - // prevent the authorization code from being leaked to a malicious authorization server. - // By comparing the endpoint URI directly in the client, a first layer of protection is - // provided independently of whether the authorization server will enforce this check. - // - // See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-4.4.2.2 - // for more information. - var address = new Uri(value, UriKind.Absolute); - if (uri != new UriBuilder(address) { Query = null }.Uri) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.GetResourceString(SR.ID2138), - uri: SR.FormatID8000(SR.ID2138)); - - return default; - } - - // Ensure all the query string parameters that were part of the original endpoint URI - // are present in the current request (parameters that were not part of the original - // endpoint URI are assumed to be authorization response parameters and are ignored). - if (!string.IsNullOrEmpty(address.Query) && OpenIddictHelpers.ParseQuery(address.Query) - // Note: ignore parameters that only include empty values - // to match the logic used by OWIN for IOwinRequest.Query. - .Where(parameter => parameter.Value.Any(value => !string.IsNullOrEmpty(value))) - .Any(parameter => request.Query[parameter.Key] != parameter.Value)) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.GetResourceString(SR.ID2138), - uri: SR.FormatID8000(SR.ID2138)); - - return default; - } - - return default; - } - } - /// /// Contains the logic responsible for resolving the context-specific properties and parameters stored in the /// OWIN authentication properties specified by the application that triggered the challenge operation. diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs index 178e3e30..12182db1 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs @@ -53,7 +53,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers .Build(); /// - public async ValueTask HandleAsync(PrepareTokenRequestContext context) + public ValueTask HandleAsync(PrepareTokenRequestContext context) { if (context is null) { @@ -70,16 +70,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers // If no client identifier was attached to the request, skip the following logic. if (string.IsNullOrEmpty(context.Request.ClientId)) { - return; - } - - var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - // Ensure the issuer resolved from the configuration matches the expected value. - if (configuration.Issuer != context.Issuer) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + return default; } // The OAuth 2.0 specification recommends sending the client credentials using basic authentication. @@ -93,7 +84,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers // // See https://tools.ietf.org/html/rfc8414#section-2 // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information. - if (!configuration.TokenEndpointAuthMethodsSupported.Contains(ClientAuthenticationMethods.ClientSecretPost)) + if (!context.Configuration.TokenEndpointAuthMethodsSupported.Contains(ClientAuthenticationMethods.ClientSecretPost)) { // Important: the credentials MUST be formURL-encoded before being base64-encoded. var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder() @@ -109,6 +100,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers context.Request.ClientId = context.Request.ClientSecret = null; } + return default; + static string? EscapeDataString(string? value) => value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null; } diff --git a/src/OpenIddict.Client/OpenIddictClientBuilder.cs b/src/OpenIddict.Client/OpenIddictClientBuilder.cs index a0a1d01f..ff267f10 100644 --- a/src/OpenIddict.Client/OpenIddictClientBuilder.cs +++ b/src/OpenIddict.Client/OpenIddictClientBuilder.cs @@ -1126,6 +1126,22 @@ public sealed class OpenIddictClientBuilder public OpenIddictClientBuilder SetStateTokenLifetime(TimeSpan? lifetime) => Configure(options => options.StateTokenLifetime = lifetime); + /// + /// Sets the client URI, which is used as the value for the "issuer" claim. + /// + /// The client URI. + /// The instance. + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictClientBuilder SetClientUri(Uri address) + { + if (address is null) + { + throw new ArgumentNullException(nameof(address)); + } + + return Configure(options => options.ClientUri = address); + } + /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) => base.Equals(obj); diff --git a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs index 7826d1f8..bdab197b 100644 --- a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs +++ b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs @@ -70,23 +70,9 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions( registration.MetadataAddress.AbsoluteUri, new OpenIddictClientRetriever(_service, registration)) diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs index c5e775fa..29593272 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs @@ -39,6 +39,24 @@ public static partial class OpenIddictClientEvents set => Transaction.EndpointType = value; } + /// + /// Gets or sets the request of the current transaction, if available. + /// + public Uri? RequestUri + { + get => Transaction.RequestUri; + set => Transaction.RequestUri = value; + } + + /// + /// Gets or sets the base of the host, if available. + /// + public Uri? BaseUri + { + get => Transaction.BaseUri; + set => Transaction.BaseUri = value; + } + /// /// Gets the logger responsible for logging processed operations. /// @@ -49,15 +67,6 @@ public static partial class OpenIddictClientEvents /// public OpenIddictClientOptions Options => Transaction.Options; - /// - /// Gets or sets the issuer used for the current request. - /// - public Uri? Issuer - { - get => Transaction.Issuer; - set => Transaction.Issuer = value; - } - /// /// Gets or sets the server configuration used for the current request. /// @@ -299,6 +308,11 @@ public static partial class OpenIddictClientEvents /// public Dictionary Properties { get; } = new(StringComparer.Ordinal); + /// + /// Gets or sets the issuer used for the authentication demand, if applicable. + /// + public Uri? Issuer { get; set; } + /// /// Gets or sets the grant type used for the authentication demand, if applicable. /// @@ -714,6 +728,11 @@ public static partial class OpenIddictClientEvents /// public Dictionary Properties { get; } = new(StringComparer.Ordinal); + /// + /// Gets or sets the issuer used for the challenge demand, if applicable. + /// + public Uri? Issuer { get; set; } + /// /// Gets or sets the name of the provider that will be /// used to resolve the issuer identity, if applicable. @@ -871,6 +890,11 @@ public static partial class OpenIddictClientEvents /// public Dictionary Properties { get; } = new(StringComparer.Ordinal); + /// + /// Gets or sets the issuer used for the sign-out demand, if applicable. + /// + public Uri? Issuer { get; set; } + /// /// Gets or sets the name of the provider that will be /// used to resolve the issuer identity, if applicable. diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs index 793b0225..7829b832 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs @@ -225,7 +225,8 @@ public static partial class OpenIddictClientHandlers return default; } - if (context.Issuer is not null && context.Issuer != address) + // Ensure the issuer matches the expected value. + if (address != context.Registration.Issuer) { context.Reject( error: Errors.ServerError, diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs index f4a31c40..16ce2a10 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs @@ -83,7 +83,7 @@ public static partial class OpenIddictClientHandlers { // When only state tokens are considered valid, use the token validation parameters of the client. 1 when context.ValidTokenTypes.Contains(TokenTypeHints.StateToken) - => GetClientTokenValidationParameters(context.Options), + => GetClientTokenValidationParameters(context.BaseUri, context.Options), // Otherwise, use the token validation parameters of the authorization server. _ => GetServerTokenValidationParameters(context.Registration, context.Configuration) @@ -94,10 +94,27 @@ public static partial class OpenIddictClientHandlers return default; - static TokenValidationParameters GetClientTokenValidationParameters(OpenIddictClientOptions options) + static TokenValidationParameters GetClientTokenValidationParameters(Uri? address, OpenIddictClientOptions options) { var parameters = options.TokenValidationParameters.Clone(); - parameters.ValidateIssuer = false; + + parameters.ValidIssuers ??= (options.ClientUri ?? address) switch + { + null => null, + + // If the client URI doesn't contain any path/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. + { AbsolutePath: "/", Query.Length: 0, Fragment.Length: 0 } uri => new[] + { + uri.AbsoluteUri, // Uri.AbsoluteUri is normalized and always contains a trailing slash. + uri.AbsoluteUri[..^1] + }, + + Uri uri => new[] { uri.AbsoluteUri } + }; + + parameters.ValidateIssuer = parameters.ValidIssuers is not null; // For state tokens, only the short "oi_stet+jwt" form is valid. parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.StateToken }; @@ -117,13 +134,13 @@ public static partial class OpenIddictClientHandlers // If the issuer URI doesn't contain any path/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. - { AbsolutePath: "/", Query.Length: 0, Fragment.Length: 0 } issuer => new[] + { AbsolutePath: "/", Query.Length: 0, Fragment.Length: 0 } uri => new[] { - issuer.AbsoluteUri, // Uri.AbsoluteUri is normalized and always contains a trailing slash. - issuer.AbsoluteUri[..^1] + uri.AbsoluteUri, // Uri.AbsoluteUri is normalized and always contains a trailing slash. + uri.AbsoluteUri[..^1] }, - Uri issuer => new[] { issuer.AbsoluteUri } + Uri uri => new[] { uri.AbsoluteUri } }; parameters.ValidateIssuer = parameters.ValidIssuers is not null; diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 273771d9..e6526697 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -11,6 +11,7 @@ using System.Runtime.InteropServices; using System.Security.Claims; using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; using OpenIddict.Extensions; using static OpenIddict.Abstractions.OpenIddictExceptions; @@ -21,6 +22,11 @@ namespace OpenIddict.Client; public static partial class OpenIddictClientHandlers { public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Top-level request processing: + */ + InferEndpointType.Descriptor, + /* * Authentication processing: */ @@ -33,6 +39,7 @@ public static partial class OpenIddictClientHandlers RedeemStateTokenEntry.Descriptor, ValidateStateTokenEndpointType.Descriptor, ValidateRequestForgeryProtection.Descriptor, + ValidateEndpointUri.Descriptor, ResolveClientRegistrationFromStateToken.Descriptor, ValidateIssuerParameter.Descriptor, HandleFrontchannelErrorResponse.Descriptor, @@ -100,9 +107,9 @@ public static partial class OpenIddictClientHandlers AttachScopes.Descriptor, AttachNonce.Descriptor, AttachCodeChallengeParameters.Descriptor, - PrepareStateTokenPrincipal.Descriptor, + PrepareLoginStateTokenPrincipal.Descriptor, ValidateRedirectUriParameter.Descriptor, - GenerateStateToken.Descriptor, + GenerateLoginStateToken.Descriptor, AttachChallengeParameters.Descriptor, AttachCustomChallengeParameters.Descriptor, @@ -134,6 +141,86 @@ public static partial class OpenIddictClientHandlers .AddRange(Session.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers); + /// + /// Contains the logic responsible for inferring the endpoint type from the request address. + /// + public sealed class InferEndpointType : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true }) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0127)); + } + + context.EndpointType = + Matches(context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection : + Matches(context.Options.PostLogoutRedirectionEndpointUris) ? OpenIddictClientEndpointType.PostLogoutRedirection : + OpenIddictClientEndpointType.Unknown; + + return default; + + bool Matches(IReadOnlyList addresses) + { + for (var index = 0; index < addresses.Count; index++) + { + var address = addresses[index]; + if (address.IsAbsoluteUri) + { + if (Equals(address, context.RequestUri)) + { + return true; + } + } + + else + { + var uri = OpenIddictHelpers.CreateAbsoluteUri(context.BaseUri, address); + if (uri.IsWellFormedOriginalString() && + OpenIddictHelpers.IsBaseOf(context.BaseUri, uri) && Equals(uri, context.RequestUri)) + { + return true; + } + } + } + + return false; + } + + static bool Equals(Uri left, Uri right) => + string.Equals(left.Scheme, right.Scheme, StringComparison.OrdinalIgnoreCase) && + string.Equals(left.Host, right.Host, StringComparison.OrdinalIgnoreCase) && + left.Port == right.Port && + // Note: paths are considered equivalent even if the casing isn't identical or if one of the two + // paths only differs by a trailing slash, which matches the classical behavior seen on ASP.NET, + // Microsoft.Owin/Katana and ASP.NET Core. Developers who prefer a different behavior can remove + // this handler and replace it by a custom version implementing a more strict comparison logic. + (string.Equals(left.AbsolutePath, right.AbsolutePath, StringComparison.OrdinalIgnoreCase) || + (left.AbsolutePath.Length == right.AbsolutePath.Length + 1 && + left.AbsolutePath.StartsWith(right.AbsolutePath, StringComparison.OrdinalIgnoreCase) && + left.AbsolutePath[^1] is '/') || + (right.AbsolutePath.Length == left.AbsolutePath.Length + 1 && + right.AbsolutePath.StartsWith(left.AbsolutePath, StringComparison.OrdinalIgnoreCase) && + right.AbsolutePath[^1] is '/')); + } + } + /// /// Contains the logic responsible for rejecting invalid authentication demands. /// @@ -277,12 +364,6 @@ public static partial class OpenIddictClientHandlers context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - // Ensure the issuer resolved from the configuration matches the expected value. - if (context.Configuration.Issuer != context.Issuer) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); - } - // Ensure the selected grant type, if explicitly set, is listed as supported in the configuration. if (!string.IsNullOrEmpty(context.GrantType) && !context.Configuration.GrantTypesSupported.Contains(context.GrantType)) @@ -655,6 +736,108 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for comparing the current request URL to the expected URL stored in the state token. + /// + public sealed class ValidateEndpointUri : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateRequestForgeryProtection.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Resolve the endpoint type allowed to be used with the state token. + if (!Enum.TryParse(context.StateTokenPrincipal.GetClaim(Claims.Private.EndpointType), + ignoreCase: true, out OpenIddictClientEndpointType type)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0340)); + } + + // Resolve the endpoint URI from either the redirect_uri or post_logout_redirect_uri + // depending on the type of endpoint meant to be used with the specified state token. + var value = type switch + { + OpenIddictClientEndpointType.PostLogoutRedirection => + context.StateTokenPrincipal.GetClaim(Claims.Private.PostLogoutRedirectUri), + + OpenIddictClientEndpointType.Redirection => + context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri), + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0340)) + }; + + // If the endpoint URI cannot be resolved, this likely means the authorization or + // logout request was sent without a redirect_uri/post_logout_redirect_uri attached. + if (string.IsNullOrEmpty(value)) + { + return default; + } + + // Compare the current HTTP request address to the original endpoint URI. If the two don't + // match, this may indicate a mix-up attack. While the authorization server is expected to + // abort the authorization flow by rejecting the token request that may be eventually sent + // with the original endpoint URI, many servers are known to incorrectly implement this + // endpoint URI validation logic. This check also offers limited protection as it cannot + // prevent the authorization code from being leaked to a malicious authorization server. + // By comparing the endpoint URI directly in the client, a first layer of protection is + // provided independently of whether the authorization server will enforce this check. + // + // See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-4.4.2.2 + // for more information. + var address = new Uri(value, UriKind.Absolute); + if (new UriBuilder(address) { Query = null }.Uri != + new UriBuilder(context.RequestUri!) { Query = null }.Uri) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2138), + uri: SR.FormatID8000(SR.ID2138)); + + return default; + } + + // Ensure all the query string parameters that were part of the original endpoint URI + // are present in the current request (parameters that were not part of the original + // endpoint URI are assumed to be authorization response parameters and are ignored). + if (!string.IsNullOrEmpty(address.Query)) + { + var parameters = OpenIddictHelpers.ParseQuery(context.RequestUri!.Query); + + foreach (var parameter in OpenIddictHelpers.ParseQuery(address.Query)) + { + if (!parameters.TryGetValue(parameter.Key, out StringValues values) || + !parameter.Value.Equals(values)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2138), + uri: SR.FormatID8000(SR.ID2138)); + + return default; + } + } + } + + return default; + } + } + /// /// Contains the logic responsible for resolving the client registration /// based on the authorization server identity stored in the state token. @@ -708,12 +891,6 @@ public static partial class OpenIddictClientHandlers // Resolve and attach the server configuration to the context. context.Configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - // Ensure the issuer resolved from the configuration matches the expected value. - if (context.Configuration.Issuer != context.Issuer) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); - } } } @@ -3617,12 +3794,6 @@ public static partial class OpenIddictClientHandlers // Resolve and attach the server configuration to the context if none has been set already. context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - // Ensure the issuer resolved from the configuration matches the expected value. - if (context.Configuration.Issuer != context.Issuer) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); - } } } @@ -4076,10 +4247,13 @@ public static partial class OpenIddictClientHandlers } // Unlike OpenID Connect, OAuth 2.0 and 2.1 don't require specifying a redirect_uri. + // // To keep OpenIddict compatible with OAuth 2.0/2.1 deployments, the redirect_uri // is not required for OAuth 2.0 requests but an exception will be thrown later // if the request that is being prepared is an OpenID Connect request. - context.RedirectUri ??= context.Registration.RedirectUri?.AbsoluteUri; + context.RedirectUri ??= OpenIddictHelpers.CreateAbsoluteUri( + context.BaseUri, + context.Registration.RedirectUri)?.AbsoluteUri; return default; } @@ -4303,7 +4477,7 @@ public static partial class OpenIddictClientHandlers /// Contains the logic responsible for preparing and attaching the claims principal /// used to generate the state token, if one is going to be returned. /// - public sealed class PrepareStateTokenPrincipal : IOpenIddictClientHandler + public sealed class PrepareLoginStateTokenPrincipal : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -4311,7 +4485,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(AttachCodeChallengeParameters.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -4360,6 +4534,9 @@ public static partial class OpenIddictClientHandlers principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); } + // Use the client identity as the token issuer. + principal.SetClaim(Claims.Private.Issuer, (context.Options.ClientUri ?? context.BaseUri)?.AbsoluteUri); + // Store the identity of the authorization server in the state token principal to allow // resolving it when handling the authorization callback. Note: additional security checks // are generally required to ensure the state token was not replaced with a state token @@ -4418,11 +4595,11 @@ public static partial class OpenIddictClientHandlers /// /// Contains the logic responsible for generating a state token for the current challenge operation. /// - public sealed class GenerateStateToken : IOpenIddictClientHandler + public sealed class GenerateLoginStateToken : IOpenIddictClientHandler { private readonly IOpenIddictClientDispatcher _dispatcher; - public GenerateStateToken(IOpenIddictClientDispatcher dispatcher) + public GenerateLoginStateToken(IOpenIddictClientDispatcher dispatcher) => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// @@ -4431,7 +4608,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(100_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -4493,7 +4670,7 @@ public static partial class OpenIddictClientHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(GenerateStateToken.Descriptor.Order + 1_000) + .SetOrder(GenerateLoginStateToken.Descriptor.Order + 1_000) .Build(); /// @@ -4724,12 +4901,6 @@ public static partial class OpenIddictClientHandlers // Resolve and attach the server configuration to the context if none has been set already. context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - // Ensure the issuer resolved from the configuration matches the expected value. - if (context.Configuration.Issuer != context.Issuer) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); - } } } @@ -4785,7 +4956,9 @@ public static partial class OpenIddictClientHandlers } // Note: the post_logout_redirect_uri parameter is optional. - context.PostLogoutRedirectUri ??= context.Registration.PostLogoutRedirectUri?.AbsoluteUri; + context.PostLogoutRedirectUri ??= OpenIddictHelpers.CreateAbsoluteUri( + context.BaseUri, + context.Registration.PostLogoutRedirectUri)?.AbsoluteUri; return default; } @@ -4973,6 +5146,9 @@ public static partial class OpenIddictClientHandlers principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); } + // Use the client identity as the token issuer. + principal.SetClaim(Claims.Private.Issuer, (context.Options.ClientUri ?? context.BaseUri)?.AbsoluteUri); + // Store the identity of the authorization server in the state token // principal to allow resolving it when handling the post-logout callback. // diff --git a/src/OpenIddict.Client/OpenIddictClientOptions.cs b/src/OpenIddict.Client/OpenIddictClientOptions.cs index 57963cd1..3d51141a 100644 --- a/src/OpenIddict.Client/OpenIddictClientOptions.cs +++ b/src/OpenIddict.Client/OpenIddictClientOptions.cs @@ -15,6 +15,12 @@ namespace OpenIddict.Client; /// public sealed class OpenIddictClientOptions { + /// + /// Gets or sets the optional address used to uniquely identify the client/relying party. + /// The URI must be absolute and may contain a path, but no query string or fragment part. + /// + public Uri? ClientUri { get; set; } + /// /// Gets the list of the handlers responsible for processing the OpenIddict client operations. /// Note: the list is automatically sorted based on the order assigned to each handler descriptor. diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs index 32a71669..20c595c1 100644 --- a/src/OpenIddict.Client/OpenIddictClientService.cs +++ b/src/OpenIddict.Client/OpenIddictClientService.cs @@ -571,7 +571,6 @@ public sealed class OpenIddictClientService var context = new PrepareConfigurationRequestContext(transaction) { Address = address, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -593,7 +592,6 @@ public sealed class OpenIddictClientService var context = new ApplyConfigurationRequestContext(transaction) { Address = address, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -617,7 +615,6 @@ public sealed class OpenIddictClientService var context = new ExtractConfigurationResponseContext(transaction) { Address = address, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -643,7 +640,6 @@ public sealed class OpenIddictClientService var context = new HandleConfigurationResponseContext(transaction) { Address = address, - Issuer = registration.Issuer, Registration = registration, Request = request, Response = response @@ -730,7 +726,6 @@ public sealed class OpenIddictClientService var context = new PrepareCryptographyRequestContext(transaction) { Address = address, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -752,7 +747,6 @@ public sealed class OpenIddictClientService var context = new ApplyCryptographyRequestContext(transaction) { Address = address, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -776,7 +770,6 @@ public sealed class OpenIddictClientService var context = new ExtractCryptographyResponseContext(transaction) { Address = address, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -802,7 +795,6 @@ public sealed class OpenIddictClientService var context = new HandleCryptographyResponseContext(transaction) { Address = address, - Issuer = registration.Issuer, Registration = registration, Request = request, Response = response @@ -898,7 +890,6 @@ public sealed class OpenIddictClientService { Address = address, Configuration = configuration, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -921,7 +912,6 @@ public sealed class OpenIddictClientService { Address = address, Configuration = configuration, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -946,7 +936,6 @@ public sealed class OpenIddictClientService { Address = address, Configuration = configuration, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -973,7 +962,6 @@ public sealed class OpenIddictClientService { Address = address, Configuration = configuration, - Issuer = registration.Issuer, Registration = registration, Request = request, Response = response @@ -1063,7 +1051,6 @@ public sealed class OpenIddictClientService { Address = address, Configuration = configuration, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -1086,7 +1073,6 @@ public sealed class OpenIddictClientService { Address = address, Configuration = configuration, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -1111,7 +1097,6 @@ public sealed class OpenIddictClientService { Address = address, Configuration = configuration, - Issuer = registration.Issuer, Registration = registration, Request = request }; @@ -1138,7 +1123,6 @@ public sealed class OpenIddictClientService { Address = address, Configuration = configuration, - Issuer = registration.Issuer, Registration = registration, Request = request, Response = response, diff --git a/src/OpenIddict.Client/OpenIddictClientTransaction.cs b/src/OpenIddict.Client/OpenIddictClientTransaction.cs index 81a10352..bdd3ecf1 100644 --- a/src/OpenIddict.Client/OpenIddictClientTransaction.cs +++ b/src/OpenIddict.Client/OpenIddictClientTransaction.cs @@ -21,9 +21,14 @@ public sealed class OpenIddictClientTransaction public OpenIddictClientEndpointType EndpointType { get; set; } /// - /// Gets or sets the issuer address associated with the current transaction, if available. + /// Gets or sets the request of the current transaction, if available. /// - public Uri? Issuer { get; set; } + public Uri? RequestUri { get; set; } + + /// + /// Gets or sets the base of the host, if available. + /// + public Uri? BaseUri { get; set; } /// /// Gets or sets the logger associated with the current request. diff --git a/src/OpenIddict.Owin/OpenIddict.Owin.csproj b/src/OpenIddict.Owin/OpenIddict.Owin.csproj index c0ddaa7d..d67ca5fc 100644 --- a/src/OpenIddict.Owin/OpenIddict.Owin.csproj +++ b/src/OpenIddict.Owin/OpenIddict.Owin.csproj @@ -7,7 +7,7 @@ - Versatile OpenID Connect stack for OWIN/Katana 4.1 (compatible with ASP.NET 4.6.1 and newer). + Versatile OpenID Connect stack for OWIN/Katana (compatible with ASP.NET 4.6.1 and newer). $(PackageTags);aspnet;katana;owin;client;server;validation diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs index 74b4c0c4..f017ed4c 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs @@ -113,8 +113,8 @@ public static partial class OpenIddictServerAspNetCoreHandlers } var parameters = context.Options.TokenValidationParameters.Clone(); - parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; - parameters.ValidAudience = context.Issuer?.AbsoluteUri; + parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri; + parameters.ValidAudience ??= parameters.ValidIssuer; parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.AuthorizationRequest }; var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters); @@ -231,9 +231,9 @@ public static partial class OpenIddictServerAspNetCoreHandlers // Store the serialized authorization request parameters in the distributed cache. var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor { - Audience = context.Issuer?.AbsoluteUri, + Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri, EncryptingCredentials = context.Options.EncryptionCredentials.First(), - Issuer = context.Issuer?.AbsoluteUri, + Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.First(), Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType), TokenType = JsonWebTokenTypes.Private.AuthorizationRequest diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs index 886546b4..deb39ec2 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs @@ -110,8 +110,8 @@ public static partial class OpenIddictServerAspNetCoreHandlers } var parameters = context.Options.TokenValidationParameters.Clone(); - parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; - parameters.ValidAudience = context.Issuer?.AbsoluteUri; + parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri; + parameters.ValidAudience ??= parameters.ValidIssuer; parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.LogoutRequest }; var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters); @@ -228,9 +228,9 @@ public static partial class OpenIddictServerAspNetCoreHandlers // Store the serialized logout request parameters in the distributed cache. var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor { - Audience = context.Issuer?.AbsoluteUri, + Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri, EncryptingCredentials = context.Options.EncryptionCredentials.First(), - Issuer = context.Issuer?.AbsoluteUri, + Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.First(), Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType), TokenType = JsonWebTokenTypes.Private.LogoutRequest diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index 73e19e4d..f69a4add 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -12,6 +12,7 @@ using System.Text.Encodings.Web; using System.Text.Json; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -30,9 +31,9 @@ public static partial class OpenIddictServerAspNetCoreHandlers /* * Top-level request processing: */ - InferEndpointType.Descriptor, - InferIssuerFromHost.Descriptor, + ResolveRequestUri.Descriptor, ValidateTransportSecurityRequirement.Descriptor, + ValidateHostHeader.Descriptor, /* * Challenge processing: @@ -59,10 +60,10 @@ public static partial class OpenIddictServerAspNetCoreHandlers .AddRange(Userinfo.DefaultHandlers); /// - /// Contains the logic responsible for inferring the endpoint type from the request address. + /// Contains the logic responsible for resolving the request URI from the ASP.NET Core environment. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// - public sealed class InferEndpointType : IOpenIddictServerHandler + public sealed class ResolveRequestUri : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -70,7 +71,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(int.MinValue + 50_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -88,84 +89,45 @@ public static partial class OpenIddictServerAspNetCoreHandlers var request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); - context.EndpointType = - Matches(request, context.Options.AuthorizationEndpointUris) ? OpenIddictServerEndpointType.Authorization : - Matches(request, context.Options.ConfigurationEndpointUris) ? OpenIddictServerEndpointType.Configuration : - Matches(request, context.Options.CryptographyEndpointUris) ? OpenIddictServerEndpointType.Cryptography : - Matches(request, context.Options.DeviceEndpointUris) ? OpenIddictServerEndpointType.Device : - Matches(request, context.Options.IntrospectionEndpointUris) ? OpenIddictServerEndpointType.Introspection : - Matches(request, context.Options.LogoutEndpointUris) ? OpenIddictServerEndpointType.Logout : - Matches(request, context.Options.RevocationEndpointUris) ? OpenIddictServerEndpointType.Revocation : - Matches(request, context.Options.TokenEndpointUris) ? OpenIddictServerEndpointType.Token : - Matches(request, context.Options.UserinfoEndpointUris) ? OpenIddictServerEndpointType.Userinfo : - Matches(request, context.Options.VerificationEndpointUris) ? OpenIddictServerEndpointType.Verification : - OpenIddictServerEndpointType.Unknown; - - if (context.EndpointType is not OpenIddictServerEndpointType.Unknown) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6053), context.EndpointType); - } - - return default; + // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute + // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized + // to './' once the URI is fully constructed). At this stage of the request processing, rejecting + // requests that lack the host information (e.g because HTTP/1.0 was used and no Host header was + // sent by the HTTP client) is not desirable as it would affect all requests, including requests + // that are not meant to be handled by OpenIddict itself. To avoid that, a fake host is temporarily + // used to build an absolute base URI and a request URI that will be used to determine whether the + // received request matches one of the addresses assigned to an OpenIddict endpoint. If the request + // is later handled by OpenIddict, an additional check will be made to require the Host header. - static bool Matches(HttpRequest request, IReadOnlyList addresses) + (context.BaseUri, context.RequestUri) = request.Host switch { - for (var index = 0; index < addresses.Count; index++) - { - var address = addresses[index]; - if (address.IsAbsoluteUri) + { HasValue: true } host => ( + BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute), + RequestUri: new Uri(request.GetEncodedUrl(), UriKind.Absolute)), + + { HasValue: false } => ( + BaseUri: new UriBuilder { - // If the request host is not available (e.g because HTTP/1.0 was used), ignore absolute URLs. - if (!request.Host.HasValue) - { - continue; - } - - // Create a Uri instance using the request scheme and raw host and compare the two base addresses. - if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host, UriKind.Absolute, out Uri? uri) || - !uri.IsWellFormedOriginalString() || uri.Port != address.Port || - !string.Equals(uri.Scheme, address.Scheme, StringComparison.OrdinalIgnoreCase) || - !string.Equals(uri.Host, address.Host, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var path = PathString.FromUriComponent(address); - if (AreEquivalent(path, request.PathBase + request.Path)) - { - return true; - } - } - - else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + Scheme = request.Scheme, + Path = request.PathBase.ToUriComponent() + }.Uri, + RequestUri: new UriBuilder { - var path = new PathString(address.OriginalString); - if (AreEquivalent(path, request.Path)) - { - return true; - } - } - } - - return false; + Scheme = request.Scheme, + Path = (request.PathBase + request.Path).ToUriComponent(), + Query = request.QueryString.ToUriComponent() + }.Uri) + }; - // ASP.NET Core's routing system ignores trailing slashes when determining - // whether the request path matches a registered route, which is not the case - // with PathString, that treats /connect/token and /connect/token/ as different - // addresses. To mitigate this inconsistency, a manual check is used here. - static bool AreEquivalent(PathString left, PathString right) - => left.Equals(right, StringComparison.OrdinalIgnoreCase) || - left.Equals(right + "/", StringComparison.OrdinalIgnoreCase) || - right.Equals(left + "/", StringComparison.OrdinalIgnoreCase); - } + return default; } } /// - /// Contains the logic responsible for infering the issuer URL from the HTTP request host and validating it. + /// Contains the logic responsible for rejecting OpenID Connect requests that don't use transport security. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// - public sealed class InferIssuerFromHost : IOpenIddictServerHandler + public sealed class ValidateTransportSecurityRequirement : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -173,8 +135,9 @@ public static partial class OpenIddictServerAspNetCoreHandlers public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - .SetOrder(InferEndpointType.Descriptor.Order + 1_000) + .AddFilter() + .UseSingletonHandler() + .SetOrder(InferEndpointType.Descriptor.Order + 250) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -191,45 +154,26 @@ public static partial class OpenIddictServerAspNetCoreHandlers var request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); - // Don't require that the request host be present if the request is not handled - // by an OpenIddict endpoint or if an explicit issuer URL was already set. - if (context.Issuer is not null || context.EndpointType is OpenIddictServerEndpointType.Unknown) - { - return default; - } - - if (!request.Host.HasValue) + // Don't require that transport security be used if the request is not handled by OpenIddict. + if (context.EndpointType is not OpenIddictServerEndpointType.Unknown && !request.IsHttps) { context.Reject( error: Errors.InvalidRequest, - description: SR.FormatID2081(HeaderNames.Host), - uri: SR.FormatID8000(SR.ID2081)); - - return default; - } - - if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host + request.PathBase, UriKind.Absolute, out Uri? issuer) || - !issuer.IsWellFormedOriginalString()) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2082(HeaderNames.Host), - uri: SR.FormatID8000(SR.ID2082)); + description: SR.GetResourceString(SR.ID2083), + uri: SR.FormatID8000(SR.ID2083)); return default; } - context.Issuer = issuer; - return default; } } /// - /// Contains the logic responsible for rejecting OpenID Connect requests that don't use transport security. + /// Contains the logic responsible for validating the Host header extracted from the HTTP header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// - public sealed class ValidateTransportSecurityRequirement : IOpenIddictServerHandler + public sealed class ValidateHostHeader : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -237,9 +181,8 @@ public static partial class OpenIddictServerAspNetCoreHandlers public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(InferIssuerFromHost.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(ValidateTransportSecurityRequirement.Descriptor.Order + 250) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -256,18 +199,13 @@ public static partial class OpenIddictServerAspNetCoreHandlers var request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); - // Don't require that the host be present if the request is not handled by OpenIddict. - if (context.EndpointType is OpenIddictServerEndpointType.Unknown) - { - return default; - } - - if (!request.IsHttps) + // Don't require that the request host be present if the request is not handled by OpenIddict. + if (context.EndpointType is not OpenIddictServerEndpointType.Unknown && !request.Host.HasValue) { context.Reject( error: Errors.InvalidRequest, - description: SR.GetResourceString(SR.ID2083), - uri: SR.FormatID8000(SR.ID2083)); + description: SR.FormatID2081(HeaderNames.Host), + uri: SR.FormatID8000(SR.ID2081)); return default; } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs index 272f9221..f8f8aa53 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs @@ -112,8 +112,8 @@ public static partial class OpenIddictServerOwinHandlers } var parameters = context.Options.TokenValidationParameters.Clone(); - parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; - parameters.ValidAudience = context.Issuer?.AbsoluteUri; + parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri; + parameters.ValidAudience ??= parameters.ValidIssuer; parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.AuthorizationRequest }; var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters); @@ -227,9 +227,9 @@ public static partial class OpenIddictServerOwinHandlers // Store the serialized authorization request parameters in the distributed cache. var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor { - Audience = context.Issuer?.AbsoluteUri, + Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri, EncryptingCredentials = context.Options.EncryptionCredentials.First(), - Issuer = context.Issuer?.AbsoluteUri, + Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.First(), Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType), TokenType = JsonWebTokenTypes.Private.AuthorizationRequest diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs index 1604376a..9a683aaa 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs @@ -110,8 +110,8 @@ public static partial class OpenIddictServerOwinHandlers } var parameters = context.Options.TokenValidationParameters.Clone(); - parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; - parameters.ValidAudience = context.Issuer?.AbsoluteUri; + parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri; + parameters.ValidAudience ??= parameters.ValidIssuer; parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.LogoutRequest }; var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters); @@ -225,9 +225,9 @@ public static partial class OpenIddictServerOwinHandlers // Store the serialized logout request parameters in the distributed cache. var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor { - Audience = context.Issuer?.AbsoluteUri, + Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri, EncryptingCredentials = context.Options.EncryptionCredentials.First(), - Issuer = context.Issuer?.AbsoluteUri, + Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.First(), Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType), TokenType = JsonWebTokenTypes.Private.LogoutRequest diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index bf1904e6..bd7f57cf 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -26,9 +26,9 @@ public static partial class OpenIddictServerOwinHandlers /* * Top-level request processing: */ - InferEndpointType.Descriptor, - InferIssuerFromHost.Descriptor, + ResolveRequestUri.Descriptor, ValidateTransportSecurityRequirement.Descriptor, + ValidateHostHeader.Descriptor, /* * Challenge processing: @@ -55,10 +55,10 @@ public static partial class OpenIddictServerOwinHandlers .AddRange(Userinfo.DefaultHandlers); /// - /// Contains the logic responsible for inferring the endpoint type from the request address. + /// Contains the logic responsible for resolving the request URI from the OWIN environment. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// - public sealed class InferEndpointType : IOpenIddictServerHandler + public sealed class ResolveRequestUri : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -66,9 +66,7 @@ public static partial class OpenIddictServerOwinHandlers public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - // Note: this handler must be invoked before any other handler, - // including the built-in handlers defined in OpenIddict.Server. + .UseSingletonHandler() .SetOrder(int.MinValue + 50_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -86,84 +84,45 @@ public static partial class OpenIddictServerOwinHandlers var request = context.Transaction.GetOwinRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); - context.EndpointType = - Matches(request, context.Options.AuthorizationEndpointUris) ? OpenIddictServerEndpointType.Authorization : - Matches(request, context.Options.ConfigurationEndpointUris) ? OpenIddictServerEndpointType.Configuration : - Matches(request, context.Options.CryptographyEndpointUris) ? OpenIddictServerEndpointType.Cryptography : - Matches(request, context.Options.DeviceEndpointUris) ? OpenIddictServerEndpointType.Device : - Matches(request, context.Options.IntrospectionEndpointUris) ? OpenIddictServerEndpointType.Introspection : - Matches(request, context.Options.LogoutEndpointUris) ? OpenIddictServerEndpointType.Logout : - Matches(request, context.Options.RevocationEndpointUris) ? OpenIddictServerEndpointType.Revocation : - Matches(request, context.Options.TokenEndpointUris) ? OpenIddictServerEndpointType.Token : - Matches(request, context.Options.UserinfoEndpointUris) ? OpenIddictServerEndpointType.Userinfo : - Matches(request, context.Options.VerificationEndpointUris) ? OpenIddictServerEndpointType.Verification : - OpenIddictServerEndpointType.Unknown; - - if (context.EndpointType is not OpenIddictServerEndpointType.Unknown) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6053), context.EndpointType); - } - - return default; + // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute + // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized + // to './' once the URI is fully constructed). At this stage of the request processing, rejecting + // requests that lack the host information (e.g because HTTP/1.0 was used and no Host header was + // sent by the HTTP client) is not desirable as it would affect all requests, including requests + // that are not meant to be handled by OpenIddict itself. To avoid that, a fake host is temporarily + // used to build an absolute base URI and a request URI that will be used to determine whether the + // received request matches one of the addresses assigned to an OpenIddict endpoint. If the request + // is later handled by OpenIddict, an additional check will be made to require the Host header. - static bool Matches(IOwinRequest request, IReadOnlyList addresses) + (context.BaseUri, context.RequestUri) = request.Host switch { - for (var index = 0; index < addresses.Count; index++) - { - var address = addresses[index]; - if (address.IsAbsoluteUri) - { - // If the request host is not available (e.g because HTTP/1.0 was used), ignore absolute URLs. - if (string.IsNullOrEmpty(request.Host.Value)) - { - continue; - } - - // Create a Uri instance using the request scheme and raw host and compare the two base addresses. - if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host, UriKind.Absolute, out Uri? uri) || - !uri.IsWellFormedOriginalString() || uri.Port != address.Port || - !string.Equals(uri.Scheme, address.Scheme, StringComparison.OrdinalIgnoreCase) || - !string.Equals(uri.Host, address.Host, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var path = PathString.FromUriComponent(address); - if (AreEquivalent(path, request.PathBase + request.Path)) - { - return true; - } - } + { Value.Length: > 0 } host => ( + BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute), + RequestUri: request.Uri), - else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { Value: null or { Length: 0 } } => ( + BaseUri: new UriBuilder { - var path = new PathString(address.OriginalString); - if (AreEquivalent(path, request.Path)) - { - return true; - } - } - } - - return false; + Scheme = request.Scheme, + Path = request.PathBase.ToUriComponent() + }.Uri, + RequestUri: new UriBuilder + { + Scheme = request.Scheme, + Path = (request.PathBase + request.Path).ToUriComponent(), + Query = request.QueryString.ToUriComponent() + }.Uri) + }; - // ASP.NET MVC's routing system ignores trailing slashes when determining - // whether the request path matches a registered route, which is not the case - // with PathString, that treats /connect/token and /connect/token/ as different - // addresses. To mitigate this inconsistency, a manual check is used here. - static bool AreEquivalent(PathString left, PathString right) - => left.Equals(right, StringComparison.OrdinalIgnoreCase) || - left.Equals(right + new PathString("/"), StringComparison.OrdinalIgnoreCase) || - right.Equals(left + new PathString("/"), StringComparison.OrdinalIgnoreCase); - } + return default; } } /// - /// Contains the logic responsible for infering the issuer URL from the HTTP request host and validating it. + /// Contains the logic responsible for rejecting OpenID Connect requests that don't use transport security. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// - public sealed class InferIssuerFromHost : IOpenIddictServerHandler + public sealed class ValidateTransportSecurityRequirement : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -171,8 +130,9 @@ public static partial class OpenIddictServerOwinHandlers public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - .SetOrder(InferEndpointType.Descriptor.Order + 1_000) + .AddFilter() + .UseSingletonHandler() + .SetOrder(InferEndpointType.Descriptor.Order + 250) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -189,45 +149,26 @@ public static partial class OpenIddictServerOwinHandlers var request = context.Transaction.GetOwinRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); - // Don't require that the request host be present if the request is not handled - // by an OpenIddict endpoint or if an explicit issuer URL was already set. - if (context.Issuer is not null || context.EndpointType is OpenIddictServerEndpointType.Unknown) - { - return default; - } - - if (string.IsNullOrEmpty(request.Host.Value)) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2081(Headers.Host), - uri: SR.FormatID8000(SR.ID2081)); - - return default; - } - - if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host + request.PathBase, UriKind.Absolute, out Uri? issuer) || - !issuer.IsWellFormedOriginalString()) + // Don't require that transport security be used if the request is not handled by OpenIddict. + if (context.EndpointType is not OpenIddictServerEndpointType.Unknown && !request.IsSecure) { context.Reject( error: Errors.InvalidRequest, - description: SR.FormatID2082(Headers.Host), - uri: SR.FormatID8000(SR.ID2082)); + description: SR.GetResourceString(SR.ID2083), + uri: SR.FormatID8000(SR.ID2083)); return default; } - context.Issuer = issuer; - return default; } } /// - /// Contains the logic responsible for rejecting OpenID Connect requests that don't use transport security. + /// Contains the logic responsible for validating the Host header extracted from the HTTP header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// - public sealed class ValidateTransportSecurityRequirement : IOpenIddictServerHandler + public sealed class ValidateHostHeader : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -235,9 +176,8 @@ public static partial class OpenIddictServerOwinHandlers public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(InferIssuerFromHost.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(ValidateTransportSecurityRequirement.Descriptor.Order + 250) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -254,18 +194,14 @@ public static partial class OpenIddictServerOwinHandlers var request = context.Transaction.GetOwinRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); - // Don't require that the host be present if the request is not handled by OpenIddict. - if (context.EndpointType is OpenIddictServerEndpointType.Unknown) - { - return default; - } - - if (!request.IsSecure) + // Don't require that the request host be present if the request is not handled by OpenIddict. + if (context.EndpointType is not OpenIddictServerEndpointType.Unknown && + string.IsNullOrEmpty(request.Host.Value)) { context.Reject( error: Errors.InvalidRequest, - description: SR.GetResourceString(SR.ID2083), - uri: SR.FormatID8000(SR.ID2083)); + description: SR.FormatID2081(Headers.Host), + uri: SR.FormatID8000(SR.ID2081)); return default; } diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index d3df0e18..1c4149e8 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -1714,8 +1714,8 @@ public sealed class OpenIddictServerBuilder => Configure(options => options.UserCodeLifetime = lifetime); /// - /// Sets the issuer address, which is used as the base address - /// for the endpoint URIs returned from the discovery endpoint. + /// Sets the issuer address, which is used as the value for the "issuer" claim and + /// is returned from the discovery endpoint to identify the authorization server. /// /// The issuer address. /// The instance. diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs index ba0d4254..46975702 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs @@ -86,6 +86,11 @@ public static partial class OpenIddictServerEvents /// public Dictionary Metadata { get; } = new(StringComparer.Ordinal); + /// + /// Gets or sets the issuer address. + /// + public Uri? Issuer { get; set; } + /// /// Gets or sets the authorization endpoint address. /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs index 62788b3a..0bb1d513 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs @@ -125,6 +125,11 @@ public static partial class OpenIddictServerEvents /// public DateTimeOffset? IssuedAt { get; set; } + /// + /// Gets or sets the "iss" claim returned to the caller, if applicable. + /// + public Uri? Issuer { get; set; } + /// /// Gets or sets the "nbf" claim /// returned to the caller, if applicable. diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs index d2202cce..545e9029 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs @@ -144,6 +144,11 @@ public static partial class OpenIddictServerEvents /// public string? GivenName { get; set; } + /// + /// Gets or sets the value used for the "iss" claim. + /// + public Uri? Issuer { get; set; } + /// /// Gets or sets the value used for the "phone_number" claim. /// Note: this value should only be populated if the "phone" diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index 399e1622..f8d60c6f 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -30,21 +30,30 @@ public static partial class OpenIddictServerEvents public OpenIddictServerTransaction Transaction { get; } /// - /// Gets or sets the issuer address associated with the current transaction, if available. + /// Gets or sets the endpoint type that handled the request, if applicable. /// - public Uri? Issuer + public OpenIddictServerEndpointType EndpointType { - get => Transaction.Issuer; - set => Transaction.Issuer = value; + get => Transaction.EndpointType; + set => Transaction.EndpointType = value; } /// - /// Gets or sets the endpoint type that handled the request, if applicable. + /// Gets or sets the request of the current transaction, if available. /// - public OpenIddictServerEndpointType EndpointType + public Uri? RequestUri { - get => Transaction.EndpointType; - set => Transaction.EndpointType = value; + get => Transaction.RequestUri; + set => Transaction.RequestUri = value; + } + + /// + /// Gets or sets the base of the host, if available. + /// + public Uri? BaseUri + { + get => Transaction.BaseUri; + set => Transaction.BaseUri = value; } /// diff --git a/src/OpenIddict.Server/OpenIddictServerFactory.cs b/src/OpenIddict.Server/OpenIddictServerFactory.cs index 360f8bb2..7e583712 100644 --- a/src/OpenIddict.Server/OpenIddictServerFactory.cs +++ b/src/OpenIddict.Server/OpenIddictServerFactory.cs @@ -30,7 +30,6 @@ public sealed class OpenIddictServerFactory : IOpenIddictServerFactory public ValueTask CreateTransactionAsync() => new(new OpenIddictServerTransaction { - Issuer = _options.CurrentValue.Issuer, Logger = _logger, Options = _options.CurrentValue }); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index bee75a51..1d9db753 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -1933,19 +1933,16 @@ public static partial class OpenIddictServerHandlers // Note: this applies to all authorization responses, whether they represent valid or errored responses. // For more information, see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-iss-auth-resp-05. - if (!string.IsNullOrEmpty(context.RedirectUri)) + // Note: don't override the issuer if one was already attached to the response instance. + if (!string.IsNullOrEmpty(context.RedirectUri) && string.IsNullOrEmpty(context.Response.Iss)) { - // At this stage, throw an exception if the issuer cannot be retrieved. - if (context.Issuer is not { IsAbsoluteUri: true }) + context.Response.Iss = (context.Options.Issuer ?? context.BaseUri) switch { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0023)); - } + { IsAbsoluteUri: true } uri => uri.AbsoluteUri, - // Note: don't override the issuer if one was already attached to the response instance. - if (string.IsNullOrEmpty(context.Response.Iss)) - { - context.Response.Iss = context.Issuer.AbsoluteUri; - } + // At this stage, throw an exception if the issuer cannot be retrieved or is not valid. + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0023)) + }; } return default; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs index 863d3d44..ba3099f6 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs @@ -31,6 +31,7 @@ public static partial class OpenIddictServerHandlers /* * Configuration request handling: */ + AttachIssuer.Descriptor, AttachEndpoints.Descriptor, AttachGrantTypes.Descriptor, AttachResponseModes.Descriptor, @@ -310,6 +311,35 @@ public static partial class OpenIddictServerHandlers } } + /// + /// Contains the logic responsible for attaching the issuer to the provider discovery document. + /// + public sealed class AttachIssuer : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Issuer = context.Options.Issuer ?? context.BaseUri; + + return default; + } + } + /// /// Contains the logic responsible for attaching the endpoint URLs to the provider discovery document. /// @@ -321,7 +351,7 @@ public static partial class OpenIddictServerHandlers public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(int.MaxValue - 100_000) + .SetOrder(AttachIssuer.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -336,68 +366,31 @@ public static partial class OpenIddictServerHandlers // Note: while OpenIddict allows specifying multiple endpoint addresses, the OAuth 2.0 // and OpenID Connect discovery specifications only allow a single address per endpoint. - context.AuthorizationEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, - context.Options.AuthorizationEndpointUris.FirstOrDefault()); + context.AuthorizationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri( + context.BaseUri, context.Options.AuthorizationEndpointUris.FirstOrDefault()); - context.CryptographyEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, - context.Options.CryptographyEndpointUris.FirstOrDefault()); + context.CryptographyEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri( + context.BaseUri, context.Options.CryptographyEndpointUris.FirstOrDefault()); - context.DeviceEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, - context.Options.DeviceEndpointUris.FirstOrDefault()); + context.DeviceEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri( + context.BaseUri, context.Options.DeviceEndpointUris.FirstOrDefault()); - context.IntrospectionEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, - context.Options.IntrospectionEndpointUris.FirstOrDefault()); + context.IntrospectionEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri( + context.BaseUri, context.Options.IntrospectionEndpointUris.FirstOrDefault()); - context.LogoutEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, - context.Options.LogoutEndpointUris.FirstOrDefault()); + context.LogoutEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri( + context.BaseUri, context.Options.LogoutEndpointUris.FirstOrDefault()); - context.RevocationEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, - context.Options.RevocationEndpointUris.FirstOrDefault()); + context.RevocationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri( + context.BaseUri, context.Options.RevocationEndpointUris.FirstOrDefault()); - context.TokenEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, - context.Options.TokenEndpointUris.FirstOrDefault()); + context.TokenEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri( + context.BaseUri, context.Options.TokenEndpointUris.FirstOrDefault()); - context.UserinfoEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, - context.Options.UserinfoEndpointUris.FirstOrDefault()); + context.UserinfoEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri( + context.BaseUri, context.Options.UserinfoEndpointUris.FirstOrDefault()); return default; - - static Uri? GetEndpointAbsoluteUri(Uri? issuer, Uri? endpoint) - { - // If the endpoint is disabled (i.e a null address is specified), return null. - if (endpoint is null) - { - return null; - } - - // If the endpoint address is already an absolute URL, return it as-is. - if (endpoint.IsAbsoluteUri) - { - return endpoint; - } - - // At this stage, throw an exception if the issuer cannot be retrieved. - if (issuer is not { IsAbsoluteUri: true }) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0023)); - } - - // Ensure the issuer ends with a trailing slash, as it is necessary - // for Uri's constructor to correctly compute correct absolute URLs. - if (!issuer.OriginalString.EndsWith("/", StringComparison.Ordinal)) - { - issuer = new Uri(issuer.OriginalString + "/", UriKind.Absolute); - } - - // Ensure the endpoint does not start with a leading slash, as it is necessary - // for Uri's constructor to correctly compute correct absolute URLs. - if (endpoint.OriginalString.StartsWith("/", StringComparison.Ordinal)) - { - endpoint = new Uri(endpoint.OriginalString[1..], UriKind.Relative); - } - - return new Uri(issuer, endpoint); - } } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs index d6d1dc99..553a9538 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs @@ -883,6 +883,8 @@ public static partial class OpenIddictServerHandlers Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + context.Issuer = context.Options.Issuer ?? context.BaseUri; + context.TokenId = context.Principal.GetClaim(Claims.JwtId); context.TokenUsage = context.Principal.GetTokenType(); context.Subject = context.Principal.GetClaim(Claims.Subject); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index c8bec667..3b32fe62 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -68,8 +68,24 @@ public static partial class OpenIddictServerHandlers } var parameters = context.Options.TokenValidationParameters.Clone(); - parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; - parameters.ValidateIssuer = !string.IsNullOrEmpty(parameters.ValidIssuer); + + parameters.ValidIssuers ??= (context.Options.Issuer ?? context.BaseUri) switch + { + null => null, + + // If the issuer URI doesn't contain any path/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. + { AbsolutePath: "/", Query.Length: 0, Fragment.Length: 0 } uri => new[] + { + uri.AbsoluteUri, // Uri.AbsoluteUri is normalized and always contains a trailing slash. + uri.AbsoluteUri[..^1] + }, + + Uri uri => new[] { uri.AbsoluteUri } + }; + + parameters.ValidateIssuer = parameters.ValidIssuers is not null; parameters.ValidTypes = context.ValidTokenTypes.Count switch { diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs index 5868bdde..275f5b75 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs @@ -497,6 +497,7 @@ public static partial class OpenIddictServerHandlers Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + context.Issuer = context.Options.Issuer ?? context.BaseUri; context.Subject = context.Principal.GetClaim(Claims.Subject); // The following claims are all optional and should be excluded when diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 9f6c054b..508e3437 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -8,7 +8,6 @@ using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; using System.Security.Claims; -using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; @@ -20,6 +19,11 @@ namespace OpenIddict.Server; public static partial class OpenIddictServerHandlers { public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Top-level request processing: + */ + InferEndpointType.Descriptor, + /* * Authentication processing: */ @@ -101,6 +105,99 @@ public static partial class OpenIddictServerHandlers .AddRange(Session.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers); + /// + /// Contains the logic responsible for inferring the endpoint type from the request address. + /// + public sealed class InferEndpointType : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true }) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0127)); + } + + context.EndpointType = + Matches(context.Options.AuthorizationEndpointUris) ? OpenIddictServerEndpointType.Authorization : + Matches(context.Options.ConfigurationEndpointUris) ? OpenIddictServerEndpointType.Configuration : + Matches(context.Options.CryptographyEndpointUris) ? OpenIddictServerEndpointType.Cryptography : + Matches(context.Options.DeviceEndpointUris) ? OpenIddictServerEndpointType.Device : + Matches(context.Options.IntrospectionEndpointUris) ? OpenIddictServerEndpointType.Introspection : + Matches(context.Options.LogoutEndpointUris) ? OpenIddictServerEndpointType.Logout : + Matches(context.Options.RevocationEndpointUris) ? OpenIddictServerEndpointType.Revocation : + Matches(context.Options.TokenEndpointUris) ? OpenIddictServerEndpointType.Token : + Matches(context.Options.UserinfoEndpointUris) ? OpenIddictServerEndpointType.Userinfo : + Matches(context.Options.VerificationEndpointUris) ? OpenIddictServerEndpointType.Verification : + OpenIddictServerEndpointType.Unknown; + + if (context.EndpointType is not OpenIddictServerEndpointType.Unknown) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6053), context.EndpointType); + } + + return default; + + bool Matches(IReadOnlyList addresses) + { + for (var index = 0; index < addresses.Count; index++) + { + var address = addresses[index]; + if (address.IsAbsoluteUri) + { + if (Equals(address, context.RequestUri)) + { + return true; + } + } + + else + { + var uri = OpenIddictHelpers.CreateAbsoluteUri(context.BaseUri, address); + if (uri.IsWellFormedOriginalString() && + OpenIddictHelpers.IsBaseOf(context.BaseUri, uri) && Equals(uri, context.RequestUri)) + { + return true; + } + } + } + + return false; + } + + static bool Equals(Uri left, Uri right) => + string.Equals(left.Scheme, right.Scheme, StringComparison.OrdinalIgnoreCase) && + string.Equals(left.Host, right.Host, StringComparison.OrdinalIgnoreCase) && + left.Port == right.Port && + // Note: paths are considered equivalent even if the casing isn't identical or if one of the two + // paths only differs by a trailing slash, which matches the classical behavior seen on ASP.NET, + // Microsoft.Owin/Katana and ASP.NET Core. Developers who prefer a different behavior can remove + // this handler and replace it by a custom version implementing a more strict comparison logic. + (string.Equals(left.AbsolutePath, right.AbsolutePath, StringComparison.OrdinalIgnoreCase) || + (left.AbsolutePath.Length == right.AbsolutePath.Length + 1 && + left.AbsolutePath.StartsWith(right.AbsolutePath, StringComparison.OrdinalIgnoreCase) && + left.AbsolutePath[^1] is '/') || + (right.AbsolutePath.Length == left.AbsolutePath.Length + 1 && + right.AbsolutePath.StartsWith(left.AbsolutePath, StringComparison.OrdinalIgnoreCase) && + right.AbsolutePath[^1] is '/')); + } + } + /// /// Contains the logic responsible for rejecting authentication demands made from unsupported endpoints. /// @@ -1815,7 +1912,7 @@ public static partial class OpenIddictServerHandlers } // Use the server identity as the token issuer. - principal.SetClaim(Claims.Private.Issuer, context.Issuer?.AbsoluteUri); + principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri); // Set the audiences based on the resource claims stored in the principal. principal.SetAudiences(context.Principal.GetResources()); @@ -1902,7 +1999,7 @@ public static partial class OpenIddictServerHandlers } // Use the server identity as the token issuer. - principal.SetClaim(Claims.Private.Issuer, context.Issuer?.AbsoluteUri); + principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri); // Attach the redirect_uri to allow for later comparison when // receiving a grant_type=authorization_code token request. @@ -1991,7 +2088,7 @@ public static partial class OpenIddictServerHandlers } // Use the server identity as the token issuer. - principal.SetClaim(Claims.Private.Issuer, context.Issuer?.AbsoluteUri); + principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri); // Restore the device code internal token identifier from the principal // resolved from the user code used in the user code verification request. @@ -2085,7 +2182,7 @@ public static partial class OpenIddictServerHandlers } // Use the server identity as the token issuer. - principal.SetClaim(Claims.Private.Issuer, context.Issuer?.AbsoluteUri); + principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri); context.RefreshTokenPrincipal = principal; @@ -2181,7 +2278,7 @@ public static partial class OpenIddictServerHandlers } // Use the server identity as the token issuer. - principal.SetClaim(Claims.Private.Issuer, context.Issuer?.AbsoluteUri); + principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri); // If available, use the client_id as both the audience and the authorized party. // See https://openid.net/specs/openid-connect-core-1_0.html#IDToken for more information. @@ -2269,7 +2366,7 @@ public static partial class OpenIddictServerHandlers } // Use the server identity as the token issuer. - principal.SetClaim(Claims.Private.Issuer, context.Issuer?.AbsoluteUri); + principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri); // Store the client_id as a public client_id claim. principal.SetClaim(Claims.ClientId, context.Request.ClientId); @@ -2976,7 +3073,8 @@ public static partial class OpenIddictServerHandlers { context.Response.UserCode = context.UserCode; - var address = GetEndpointAbsoluteUri(context.Issuer, context.Options.VerificationEndpointUris.FirstOrDefault()); + var address = OpenIddictHelpers.CreateAbsoluteUri(context.BaseUri, + context.Options.VerificationEndpointUris.FirstOrDefault()); if (address is not null) { var builder = new UriBuilder(address) @@ -2990,43 +3088,6 @@ public static partial class OpenIddictServerHandlers } return default; - - static Uri? GetEndpointAbsoluteUri(Uri? issuer, Uri? endpoint) - { - // If the endpoint is disabled (i.e a null address is specified), return null. - if (endpoint is null) - { - return null; - } - - // If the endpoint address is already an absolute URL, return it as-is. - if (endpoint.IsAbsoluteUri) - { - return endpoint; - } - - // At this stage, throw an exception if the issuer cannot be retrieved. - if (issuer is not { IsAbsoluteUri: true }) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0023)); - } - - // Ensure the issuer ends with a trailing slash, as it is necessary - // for Uri's constructor to correctly compute correct absolute URLs. - if (!issuer.OriginalString.EndsWith("/", StringComparison.Ordinal)) - { - issuer = new Uri(issuer.OriginalString + "/", UriKind.Absolute); - } - - // Ensure the endpoint does not start with a leading slash, as it is necessary - // for Uri's constructor to correctly compute correct absolute URLs. - if (endpoint.OriginalString.StartsWith("/", StringComparison.Ordinal)) - { - endpoint = new Uri(endpoint.OriginalString[1..], UriKind.Relative); - } - - return new Uri(issuer, endpoint); - } } } diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index ec5c29e4..0656c497 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -16,7 +16,7 @@ namespace OpenIddict.Server; public sealed class OpenIddictServerOptions { /// - /// Gets or sets the optional base address used to uniquely identify the authorization server. + /// Gets or sets the optional address used to uniquely identify the authorization server. /// The URI must be absolute and may contain a path, but no query string or fragment part. /// public Uri? Issuer { get; set; } @@ -67,8 +67,8 @@ public sealed class OpenIddictServerOptions /// public List ConfigurationEndpointUris { get; } = new() { - new Uri("/.well-known/openid-configuration", UriKind.Relative), - new Uri("/.well-known/oauth-authorization-server", UriKind.Relative) + new Uri(".well-known/openid-configuration", UriKind.Relative), + new Uri(".well-known/oauth-authorization-server", UriKind.Relative) }; /// @@ -76,7 +76,7 @@ public sealed class OpenIddictServerOptions /// public List CryptographyEndpointUris { get; } = new() { - new Uri("/.well-known/jwks", UriKind.Relative) + new Uri(".well-known/jwks", UriKind.Relative) }; /// diff --git a/src/OpenIddict.Server/OpenIddictServerTransaction.cs b/src/OpenIddict.Server/OpenIddictServerTransaction.cs index ed253ac1..ad778e69 100644 --- a/src/OpenIddict.Server/OpenIddictServerTransaction.cs +++ b/src/OpenIddict.Server/OpenIddictServerTransaction.cs @@ -21,9 +21,14 @@ public sealed class OpenIddictServerTransaction public OpenIddictServerEndpointType EndpointType { get; set; } /// - /// Gets or sets the issuer address associated with the current transaction, if available. + /// Gets or sets the request of the current transaction, if available. /// - public Uri? Issuer { get; set; } + public Uri? RequestUri { get; set; } + + /// + /// Gets or sets the base of the host, if available. + /// + public Uri? BaseUri { get; set; } /// /// Gets or sets the logger associated with the current request. diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs index fb82b6ae..3ee1c6df 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs @@ -10,6 +10,7 @@ using System.Diagnostics; using System.Text; using System.Text.Json; using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -29,11 +30,12 @@ public static partial class OpenIddictValidationAspNetCoreHandlers /* * Request top-level processing: */ - InferIssuerFromHost.Descriptor, + ResolveRequestUri.Descriptor, /* * Authentication processing: */ + ValidateHostHeader.Descriptor, ExtractAccessTokenFromAuthorizationHeader.Descriptor, ExtractAccessTokenFromBodyForm.Descriptor, ExtractAccessTokenFromQueryString.Descriptor, @@ -58,10 +60,10 @@ public static partial class OpenIddictValidationAspNetCoreHandlers ProcessChallengeErrorResponse.Descriptor); /// - /// Contains the logic responsible for infering the default issuer from the HTTP request host and validating it. + /// Contains the logic responsible for resolving the request URI from the ASP.NET Core environment. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// - public sealed class InferIssuerFromHost : IOpenIddictValidationHandler + public sealed class ResolveRequestUri : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -69,8 +71,8 @@ public static partial class OpenIddictValidationAspNetCoreHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) + .UseSingletonHandler() + .SetOrder(int.MinValue + 50_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -87,36 +89,81 @@ public static partial class OpenIddictValidationAspNetCoreHandlers var request = context.Transaction.GetHttpRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); - // Only use the current host as the issuer if the - // issuer was not explicitly set in the options. - if (context.Issuer is not null) + // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute + // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized + // to './' once the URI is fully constructed). At this stage of the request processing, rejecting + // requests that lack the host information (e.g because HTTP/1.0 was used and no Host header was + // sent by the HTTP client) is not desirable as it would affect all requests, including requests + // that are not meant to be handled by OpenIddict itself. To avoid that, a fake host is temporarily + // used to build an absolute base URI and a request URI that will be used to determine whether the + // received request matches one of the addresses assigned to an OpenIddict endpoint. If the request + // is later handled by OpenIddict, an additional check will be made to require the Host header. + + (context.BaseUri, context.RequestUri) = request.Host switch { - return default; - } + { HasValue: true } host => ( + BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute), + RequestUri: new Uri(request.GetEncodedUrl(), UriKind.Absolute)), - if (!request.Host.HasValue) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2081(HeaderNames.Host), - uri: SR.FormatID8000(SR.ID2081)); + { HasValue: false } => ( + BaseUri: new UriBuilder + { + Scheme = request.Scheme, + Path = request.PathBase.ToUriComponent() + }.Uri, + RequestUri: new UriBuilder + { + Scheme = request.Scheme, + Path = (request.PathBase + request.Path).ToUriComponent(), + Query = request.QueryString.ToUriComponent() + }.Uri) + }; - return default; + return default; + } + } + + /// + /// Contains the logic responsible for validating the Host header extracted from the HTTP header. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public sealed class ValidateHostHeader : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MinValue + 50_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); } - if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host + request.PathBase, UriKind.Absolute, out Uri? issuer) || - !issuer.IsWellFormedOriginalString()) + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + + // Don't require that a Host header be present if the issuer was set in the options. + if (context.Options.Issuer is null && !request.Host.HasValue) { context.Reject( error: Errors.InvalidRequest, - description: SR.FormatID2082(HeaderNames.Host), - uri: SR.FormatID8000(SR.ID2082)); + description: SR.FormatID2081(HeaderNames.Host), + uri: SR.FormatID8000(SR.ID2081)); return default; } - context.Issuer = issuer; - return default; } } diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs index a4931d50..9041fc13 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs @@ -25,7 +25,7 @@ public static partial class OpenIddictValidationOwinHandlers /* * Request top-level processing: */ - InferIssuerFromHost.Descriptor, + ResolveRequestUri.Descriptor, /* * Authentication processing: @@ -58,10 +58,10 @@ public static partial class OpenIddictValidationOwinHandlers ProcessChallengeErrorResponse.Descriptor); /// - /// Contains the logic responsible for infering the default issuer from the HTTP request host and validating it. + /// Contains the logic responsible for resolving the request URI from the OWIN environment. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// - public sealed class InferIssuerFromHost : IOpenIddictValidationHandler + public sealed class ResolveRequestUri : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -69,8 +69,8 @@ public static partial class OpenIddictValidationOwinHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) + .UseSingletonHandler() + .SetOrder(int.MinValue + 50_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -87,36 +87,81 @@ public static partial class OpenIddictValidationOwinHandlers var request = context.Transaction.GetOwinRequest() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); - // Only use the current host as the issuer if the - // issuer was not explicitly set in the options. - if (context.Issuer is not null) - { - return default; - } + // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute + // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized + // to './' once the URI is fully constructed). At this stage of the request processing, rejecting + // requests that lack the host information (e.g because HTTP/1.0 was used and no Host header was + // sent by the HTTP client) is not desirable as it would affect all requests, including requests + // that are not meant to be handled by OpenIddict itself. To avoid that, a fake host is temporarily + // used to build an absolute base URI and a request URI that will be used to determine whether the + // received request matches one of the addresses assigned to an OpenIddict endpoint. If the request + // is later handled by OpenIddict, an additional check will be made to require the Host header. - if (string.IsNullOrEmpty(request.Host.Value)) + (context.BaseUri, context.RequestUri) = request.Host switch { - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2081(Headers.Host), - uri: SR.FormatID8000(SR.ID2081)); + { Value.Length: > 0 } host => ( + BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute), + RequestUri: request.Uri), - return default; + { Value: null or { Length: 0 } } => ( + BaseUri: new UriBuilder + { + Scheme = request.Scheme, + Path = request.PathBase.ToUriComponent() + }.Uri, + RequestUri: new UriBuilder + { + Scheme = request.Scheme, + Path = (request.PathBase + request.Path).ToUriComponent(), + Query = request.QueryString.ToUriComponent() + }.Uri) + }; + + return default; + } + } + + /// + /// Contains the logic responsible for validating the Host header extracted from the HTTP header. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public sealed class ValidateHostHeader : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MinValue + 50_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); } - if (!Uri.TryCreate(request.Scheme + Uri.SchemeDelimiter + request.Host + request.PathBase, UriKind.Absolute, out Uri? issuer) || - !issuer.IsWellFormedOriginalString()) + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); + + // Don't require that a Host header be present if the issuer was set in the options. + if (context.Options.Issuer is null && string.IsNullOrEmpty(request.Host.Value)) { context.Reject( error: Errors.InvalidRequest, - description: SR.FormatID2082(Headers.Host), - uri: SR.FormatID8000(SR.ID2082)); + description: SR.FormatID2081(Headers.Host), + uri: SR.FormatID8000(SR.ID2081)); return default; } - context.Issuer = issuer; - return default; } } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs index 080505d2..8de79b19 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs @@ -53,7 +53,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers .Build(); /// - public async ValueTask HandleAsync(PrepareIntrospectionRequestContext context) + public ValueTask HandleAsync(PrepareIntrospectionRequestContext context) { if (context is null) { @@ -70,16 +70,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers // If no client identifier was attached to the request, skip the following logic. if (string.IsNullOrEmpty(context.Request.ClientId)) { - return; - } - - var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - // Ensure the issuer resolved from the configuration matches the expected value. - if (context.Options.Issuer is not null && configuration.Issuer != context.Options.Issuer) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); + return default; } // The OAuth 2.0 specification recommends sending the client credentials using basic authentication. @@ -93,7 +84,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers // // See https://tools.ietf.org/html/rfc8414#section-2 // and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information. - if (!configuration.IntrospectionEndpointAuthMethodsSupported.Contains(ClientAuthenticationMethods.ClientSecretPost)) + if (!context.Configuration.IntrospectionEndpointAuthMethodsSupported.Contains(ClientAuthenticationMethods.ClientSecretPost)) { // Important: the credentials MUST be formURL-encoded before being base64-encoded. var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder() @@ -109,6 +100,8 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers context.Request.ClientId = context.Request.ClientSecret = null; } + return default; + static string? EscapeDataString(string? value) => value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null; } diff --git a/src/OpenIddict.Validation/OpenIddict.Validation.csproj b/src/OpenIddict.Validation/OpenIddict.Validation.csproj index b26b9e16..855e4f4e 100644 --- a/src/OpenIddict.Validation/OpenIddict.Validation.csproj +++ b/src/OpenIddict.Validation/OpenIddict.Validation.csproj @@ -22,6 +22,10 @@ To use the validation feature on ASP.NET Core or OWIN/Katana, reference the Open + + + + diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index 388acbfe..a717aa25 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -463,7 +463,7 @@ public sealed class OpenIddictValidationBuilder if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString()) { - throw new ArgumentException(SR.GetResourceString(SR.ID0127), nameof(address)); + throw new ArgumentException(SR.GetResourceString(SR.ID0023), nameof(address)); } return SetIssuer(uri); diff --git a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs index 6f82a262..6c48facd 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Tokens; +using OpenIddict.Extensions; namespace OpenIddict.Validation; @@ -105,28 +106,17 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions( diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs index e97f07a3..90149c0b 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs @@ -39,12 +39,21 @@ public static partial class OpenIddictValidationEvents } /// - /// Gets or sets the issuer address associated with the current transaction, if available. + /// Gets or sets the request of the current transaction, if available. /// - public Uri? Issuer + public Uri? RequestUri { - get => Transaction.Issuer; - set => Transaction.Issuer = value; + get => Transaction.RequestUri; + set => Transaction.RequestUri = value; + } + + /// + /// Gets or sets the base of the host, if available. + /// + public Uri? BaseUri + { + get => Transaction.BaseUri; + set => Transaction.BaseUri = value; } /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationFactory.cs b/src/OpenIddict.Validation/OpenIddictValidationFactory.cs index 2cee5e5f..03ac8102 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationFactory.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationFactory.cs @@ -30,7 +30,6 @@ public sealed class OpenIddictValidationFactory : IOpenIddictValidationFactory public ValueTask CreateTransactionAsync() => new(new OpenIddictValidationTransaction { - Issuer = _options.CurrentValue.Issuer, Logger = _logger, Options = _options.CurrentValue }); diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs index bb99f3df..16c5bdea 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs @@ -203,7 +203,8 @@ public static partial class OpenIddictValidationHandlers return default; } - if (context.Issuer is not null && context.Issuer != address) + // Ensure the issuer matches the expected value. + if (address != context.Options.Issuer) { context.Reject( error: Errors.ServerError, @@ -325,7 +326,7 @@ public static partial class OpenIddictValidationHandlers /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(ExtractIntrospectionEndpoint.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs index aed91220..20fdaeab 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs @@ -317,7 +317,8 @@ public static partial class OpenIddictValidationHandlers return default; } - if (context.Issuer is not null && context.Issuer != uri) + // Ensure the issuer matches the expected value. + if (uri != context.Configuration.Issuer) { context.Reject( error: Errors.ServerError, @@ -426,7 +427,7 @@ public static partial class OpenIddictValidationHandlers // to be valid, as it is guarded against unknown values by the ValidateIssuer handler. var issuer = (string?) context.Response[Claims.Issuer] ?? context.Configuration.Issuer?.AbsoluteUri ?? - context.Issuer?.AbsoluteUri ?? ClaimsIdentity.DefaultIssuer; + context.BaseUri?.AbsoluteUri ?? ClaimsIdentity.DefaultIssuer; foreach (var parameter in context.Response.GetParameters()) { diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index 7c04c6f7..4c5a60c0 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -63,8 +63,12 @@ public static partial class OpenIddictValidationHandlers // OpenID Connect server configuration (that can be static or retrieved using discovery). var parameters = context.Options.TokenValidationParameters.Clone(); - parameters.ValidIssuers ??= context.Configuration.Issuer switch + // If the issuer was not explicitly set, assume the authorization server + // is located in the same application as the component validating tokens. + parameters.ValidIssuers ??= (context.Configuration.Issuer ?? context.BaseUri) switch { + // Note: the issuer may be null at this point (e.g when validating a token + // issued by a local authorization server outside a request context). null => null, // If the issuer URI doesn't contain any path/query/fragment, allow both http://www.fabrikam.com diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index bb42740d..285eead7 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -60,16 +60,8 @@ public static partial class OpenIddictValidationHandlers throw new ArgumentNullException(nameof(context)); } - var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? + context.Configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - // Ensure the issuer resolved from the configuration matches the expected value. - if (context.Options.Issuer is not null && configuration.Issuer != context.Options.Issuer) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0307)); - } - - context.Configuration = configuration; } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs index a19247f1..232e6af9 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationService.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationService.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Security.Claims; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using static OpenIddict.Abstractions.OpenIddictExceptions; @@ -48,12 +49,17 @@ public sealed class OpenIddictValidationService // can be disposed of asynchronously if it implements IAsyncDisposable. try { + var options = _provider.GetRequiredService>(); + var configuration = await options.CurrentValue.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + var dispatcher = scope.ServiceProvider.GetRequiredService(); var factory = scope.ServiceProvider.GetRequiredService(); var transaction = await factory.CreateTransactionAsync(); var context = new ValidateTokenContext(transaction) { + Configuration = configuration, Token = token, ValidTokenTypes = { TokenTypeHints.AccessToken } }; @@ -409,6 +415,10 @@ public sealed class OpenIddictValidationService // can be disposed of asynchronously if it implements IAsyncDisposable. try { + var options = _provider.GetRequiredService>(); + var configuration = await options.CurrentValue.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + var dispatcher = scope.ServiceProvider.GetRequiredService(); var factory = scope.ServiceProvider.GetRequiredService(); var transaction = await factory.CreateTransactionAsync(); @@ -426,6 +436,7 @@ public sealed class OpenIddictValidationService var context = new PrepareIntrospectionRequestContext(transaction) { Address = address, + Configuration = configuration, Request = request, Token = token, TokenTypeHint = hint @@ -448,6 +459,7 @@ public sealed class OpenIddictValidationService var context = new ApplyIntrospectionRequestContext(transaction) { Address = address, + Configuration = configuration, Request = request }; @@ -470,6 +482,7 @@ public sealed class OpenIddictValidationService var context = new ExtractIntrospectionResponseContext(transaction) { Address = address, + Configuration = configuration, Request = request }; @@ -494,6 +507,7 @@ public sealed class OpenIddictValidationService var context = new HandleIntrospectionResponseContext(transaction) { Address = address, + Configuration = configuration, Request = request, Response = response, Token = token diff --git a/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs b/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs index ee5c4236..26e0e3e1 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs @@ -21,9 +21,14 @@ public sealed class OpenIddictValidationTransaction public OpenIddictValidationEndpointType EndpointType { get; set; } /// - /// Gets or sets the issuer address associated with the current transaction, if available. + /// Gets or sets the request of the current transaction, if available. /// - public Uri? Issuer { get; set; } + public Uri? RequestUri { get; set; } + + /// + /// Gets or sets the base of the host, if available. + /// + public Uri? BaseUri { get; set; } /// /// Gets or sets the logger associated with the current request. diff --git a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs index e5b53aec..e95a13c5 100644 --- a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs +++ b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs @@ -4,7 +4,6 @@ * the license and the contributors participating to this project. */ -using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using System.Text.Json; using Microsoft.AspNetCore.Authentication; @@ -18,7 +17,6 @@ using Microsoft.Extensions.Logging; using OpenIddict.Server.IntegrationTests; using Xunit; using Xunit.Abstractions; -using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers.Protection; @@ -260,410 +258,6 @@ public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServ Assert.Equal("custom_error_uri", response.ErrorUri); } - [Theory] - [InlineData("/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/authorize", OpenIddictServerEndpointType.Authorization)] - [InlineData("/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Authorization)] - [InlineData("/connect/authorize/", OpenIddictServerEndpointType.Authorization)] - [InlineData("/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Authorization)] - [InlineData("/connect/authorize/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/AUTHORIZE/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/authorize/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/AUTHORIZE/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] - [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Configuration)] - [InlineData("/.well-known/openid-configuration/", OpenIddictServerEndpointType.Configuration)] - [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Configuration)] - [InlineData("/.well-known/openid-configuration/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.well-known/openid-configuration/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] - [InlineData("/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Cryptography)] - [InlineData("/.well-known/jwks/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("/.well-known/jwks/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.WELL-KNOWN/JWKS/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.well-known/jwks/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.WELL-KNOWN/JWKS/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/device", OpenIddictServerEndpointType.Device)] - [InlineData("/CONNECT/DEVICE", OpenIddictServerEndpointType.Device)] - [InlineData("/connect/device/", OpenIddictServerEndpointType.Device)] - [InlineData("/CONNECT/DEVICE/", OpenIddictServerEndpointType.Device)] - [InlineData("/connect/device/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/DEVICE/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/device/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/DEVICE/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/introspect", OpenIddictServerEndpointType.Introspection)] - [InlineData("/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Introspection)] - [InlineData("/connect/introspect/", OpenIddictServerEndpointType.Introspection)] - [InlineData("/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Introspection)] - [InlineData("/connect/introspect/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/INTROSPECT/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/introspect/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/INTROSPECT/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/logout", OpenIddictServerEndpointType.Logout)] - [InlineData("/CONNECT/LOGOUT", OpenIddictServerEndpointType.Logout)] - [InlineData("/connect/logout/", OpenIddictServerEndpointType.Logout)] - [InlineData("/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Logout)] - [InlineData("/connect/logout/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/LOGOUT/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/logout/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/LOGOUT/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/revoke", OpenIddictServerEndpointType.Revocation)] - [InlineData("/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)] - [InlineData("/connect/revoke/", OpenIddictServerEndpointType.Revocation)] - [InlineData("/CONNECT/REVOKE/", OpenIddictServerEndpointType.Revocation)] - [InlineData("/connect/revoke/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/REVOKE/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/revoke/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/REVOKE/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/token", OpenIddictServerEndpointType.Token)] - [InlineData("/CONNECT/TOKEN", OpenIddictServerEndpointType.Token)] - [InlineData("/connect/token/", OpenIddictServerEndpointType.Token)] - [InlineData("/CONNECT/TOKEN/", OpenIddictServerEndpointType.Token)] - [InlineData("/connect/token/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/TOKEN/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/token/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/TOKEN/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] - [InlineData("/CONNECT/USERINFO", OpenIddictServerEndpointType.Userinfo)] - [InlineData("/connect/userinfo/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("/CONNECT/USERINFO/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("/connect/userinfo/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/USERINFO/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/userinfo/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/USERINFO/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/verification", OpenIddictServerEndpointType.Verification)] - [InlineData("/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Verification)] - [InlineData("/connect/verification/", OpenIddictServerEndpointType.Verification)] - [InlineData("/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Verification)] - [InlineData("/connect/verification/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/VERIFICATION/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/verification/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/VERIFICATION/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - public async Task ProcessRequest_MatchesCorrespondingRelativeEndpoint(string path, OpenIddictServerEndpointType type) - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - // Assert - Assert.Equal(type, context.EndpointType); - - return default; - })); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - await client.PostAsync(path, new OpenIddictRequest()); - } - - [Theory] - [InlineData("https://localhost/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:443/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST/CONNECT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST/CONNECT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:443/connect", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:443/connect/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/authorize", OpenIddictServerEndpointType.Authorization)] - [InlineData("HTTPS://LOCALHOST/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Authorization)] - [InlineData("https://localhost/connect/authorize/", OpenIddictServerEndpointType.Authorization)] - [InlineData("HTTPS://LOCALHOST/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Authorization)] - [InlineData("https://localhost:443/connect/authorize", OpenIddictServerEndpointType.Authorization)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Authorization)] - [InlineData("https://localhost:443/connect/authorize/", OpenIddictServerEndpointType.Authorization)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Authorization)] - [InlineData("https://fabrikam.com/connect/authorize", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/authorize/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/authorize", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/authorize/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] - [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Configuration)] - [InlineData("https://localhost/.well-known/openid-configuration/", OpenIddictServerEndpointType.Configuration)] - [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Configuration)] - [InlineData("https://localhost:443/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] - [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Configuration)] - [InlineData("https://localhost:443/.well-known/openid-configuration/", OpenIddictServerEndpointType.Configuration)] - [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Configuration)] - [InlineData("https://fabrikam.com/.well-known/openid-configuration", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/.well-known/openid-configuration/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/.well-known/openid-configuration", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/.well-known/openid-configuration/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] - [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Cryptography)] - [InlineData("https://localhost/.well-known/jwks/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("https://localhost:443/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] - [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Cryptography)] - [InlineData("https://localhost:443/.well-known/jwks/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("https://fabrikam.com/.well-known/jwks", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/.well-known/jwks/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/.well-known/jwks", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/.well-known/jwks/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/device", OpenIddictServerEndpointType.Device)] - [InlineData("HTTPS://LOCALHOST/CONNECT/DEVICE", OpenIddictServerEndpointType.Device)] - [InlineData("https://localhost/connect/device/", OpenIddictServerEndpointType.Device)] - [InlineData("HTTPS://LOCALHOST/CONNECT/DEVICE/", OpenIddictServerEndpointType.Device)] - [InlineData("https://localhost:443/connect/device", OpenIddictServerEndpointType.Device)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/DEVICE", OpenIddictServerEndpointType.Device)] - [InlineData("https://localhost:443/connect/device/", OpenIddictServerEndpointType.Device)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/DEVICE/", OpenIddictServerEndpointType.Device)] - [InlineData("https://fabrikam.com/connect/device", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/DEVICE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/device/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/DEVICE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/device", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/DEVICE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/device/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/DEVICE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/introspect", OpenIddictServerEndpointType.Introspection)] - [InlineData("HTTPS://LOCALHOST/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Introspection)] - [InlineData("https://localhost/connect/introspect/", OpenIddictServerEndpointType.Introspection)] - [InlineData("HTTPS://LOCALHOST/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Introspection)] - [InlineData("https://localhost:443/connect/introspect", OpenIddictServerEndpointType.Introspection)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Introspection)] - [InlineData("https://localhost:443/connect/introspect/", OpenIddictServerEndpointType.Introspection)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Introspection)] - [InlineData("https://fabrikam.com/connect/introspect", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/introspect/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/introspect", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/introspect/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/logout", OpenIddictServerEndpointType.Logout)] - [InlineData("HTTPS://LOCALHOST/CONNECT/LOGOUT", OpenIddictServerEndpointType.Logout)] - [InlineData("https://localhost/connect/logout/", OpenIddictServerEndpointType.Logout)] - [InlineData("HTTPS://LOCALHOST/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Logout)] - [InlineData("https://localhost:443/connect/logout", OpenIddictServerEndpointType.Logout)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/LOGOUT", OpenIddictServerEndpointType.Logout)] - [InlineData("https://localhost:443/connect/logout/", OpenIddictServerEndpointType.Logout)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Logout)] - [InlineData("https://fabrikam.com/connect/logout", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/LOGOUT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/logout/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/logout", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/LOGOUT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/logout/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/revoke", OpenIddictServerEndpointType.Revocation)] - [InlineData("HTTPS://LOCALHOST/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)] - [InlineData("https://localhost/connect/revoke/", OpenIddictServerEndpointType.Revocation)] - [InlineData("HTTPS://LOCALHOST/CONNECT/REVOKE/", OpenIddictServerEndpointType.Revocation)] - [InlineData("https://localhost:443/connect/revoke", OpenIddictServerEndpointType.Revocation)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)] - [InlineData("https://localhost:443/connect/revoke/", OpenIddictServerEndpointType.Revocation)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/REVOKE/", OpenIddictServerEndpointType.Revocation)] - [InlineData("https://fabrikam.com/connect/revoke", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/REVOKE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/revoke/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/REVOKE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/revoke", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/REVOKE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/revoke/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/REVOKE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/token", OpenIddictServerEndpointType.Token)] - [InlineData("HTTPS://LOCALHOST/CONNECT/TOKEN", OpenIddictServerEndpointType.Token)] - [InlineData("https://localhost/connect/token/", OpenIddictServerEndpointType.Token)] - [InlineData("HTTPS://LOCALHOST/CONNECT/TOKEN/", OpenIddictServerEndpointType.Token)] - [InlineData("https://localhost:443/connect/token", OpenIddictServerEndpointType.Token)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/TOKEN", OpenIddictServerEndpointType.Token)] - [InlineData("https://localhost:443/connect/token/", OpenIddictServerEndpointType.Token)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/TOKEN/", OpenIddictServerEndpointType.Token)] - [InlineData("https://fabrikam.com/connect/token", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/TOKEN", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/token/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/TOKEN/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/token", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/TOKEN", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/token/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/TOKEN/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] - [InlineData("HTTPS://LOCALHOST/CONNECT/USERINFO", OpenIddictServerEndpointType.Userinfo)] - [InlineData("https://localhost/connect/userinfo/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("HTTPS://LOCALHOST/CONNECT/USERINFO/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("https://localhost:443/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/USERINFO", OpenIddictServerEndpointType.Userinfo)] - [InlineData("https://localhost:443/connect/userinfo/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/USERINFO/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("https://fabrikam.com/connect/userinfo", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/USERINFO", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/userinfo/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/USERINFO/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/userinfo", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/USERINFO", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/userinfo/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/USERINFO/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/verification", OpenIddictServerEndpointType.Verification)] - [InlineData("HTTPS://LOCALHOST/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Verification)] - [InlineData("https://localhost/connect/verification/", OpenIddictServerEndpointType.Verification)] - [InlineData("HTTPS://LOCALHOST/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Verification)] - [InlineData("https://localhost:443/connect/verification", OpenIddictServerEndpointType.Verification)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Verification)] - [InlineData("https://localhost:443/connect/verification/", OpenIddictServerEndpointType.Verification)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Verification)] - [InlineData("https://fabrikam.com/connect/verification", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/verification/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/verification", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/verification/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Unknown)] - public async Task ProcessRequest_MatchesCorrespondingAbsoluteEndpoint(string path, OpenIddictServerEndpointType type) - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.SetAuthorizationEndpointUris("https://localhost/connect/authorize") - .SetConfigurationEndpointUris("https://localhost/.well-known/openid-configuration") - .SetCryptographyEndpointUris("https://localhost/.well-known/jwks") - .SetDeviceEndpointUris("https://localhost/connect/device") - .SetIntrospectionEndpointUris("https://localhost/connect/introspect") - .SetLogoutEndpointUris("https://localhost/connect/logout") - .SetRevocationEndpointUris("https://localhost/connect/revoke") - .SetTokenEndpointUris("https://localhost/connect/token") - .SetUserinfoEndpointUris("https://localhost/connect/userinfo") - .SetVerificationEndpointUris("https://localhost/connect/verification"); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - // Assert - Assert.Equal(type, context.EndpointType); - - return default; - })); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - await client.PostAsync(path, new OpenIddictRequest()); - } - - [Theory] - [InlineData("/custom/connect/authorize", OpenIddictServerEndpointType.Authorization)] - [InlineData("/custom/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] - [InlineData("/custom/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] - [InlineData("/custom/connect/device", OpenIddictServerEndpointType.Device)] - [InlineData("/custom/connect/custom", OpenIddictServerEndpointType.Unknown)] - [InlineData("/custom/connect/introspect", OpenIddictServerEndpointType.Introspection)] - [InlineData("/custom/connect/logout", OpenIddictServerEndpointType.Logout)] - [InlineData("/custom/connect/revoke", OpenIddictServerEndpointType.Revocation)] - [InlineData("/custom/connect/token", OpenIddictServerEndpointType.Token)] - [InlineData("/custom/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] - [InlineData("/custom/connect/verification", OpenIddictServerEndpointType.Verification)] - public async Task ProcessRequest_AllowsOverridingEndpoint(string address, OpenIddictServerEndpointType type) - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - // Act - context.EndpointType = type; - - // Assert - Assert.Equal(type, context.EndpointType); - - return default; - }); - - builder.SetOrder(InferEndpointType.Descriptor.Order + 500); - }); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - await client.PostAsync(address, new OpenIddictRequest()); - } - [Theory] [InlineData("/.well-known/openid-configuration")] [InlineData("/.well-known/jwks")] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs index 96a10453..23e63a11 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs @@ -2552,7 +2552,7 @@ public abstract partial class OpenIddictServerIntegrationTests [Theory] [InlineData(null)] [InlineData("http://www.fabrikam.com/")] - public async Task ApplyAuthorizationResponse_SetsIssuerParameter(string issuer) + public async Task ApplyAuthorizationResponse_SetsIssuerParameter(string? issuer) { // Arrange await using var server = await CreateServerAsync(options => @@ -2586,7 +2586,7 @@ public abstract partial class OpenIddictServerIntegrationTests }); // Assert - Assert.Equal(issuer is not null ? issuer : "http://localhost/", response.Iss); + Assert.Equal(!string.IsNullOrEmpty(issuer) ? issuer : "http://localhost/", response.Iss); } [Fact] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs index 4907f04c..cf9c04d2 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs @@ -11,6 +11,7 @@ using Microsoft.IdentityModel.Tokens; using Moq; using Xunit; using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers.Discovery; namespace OpenIddict.Server.IntegrationTests; @@ -304,66 +305,20 @@ public abstract partial class OpenIddictServerIntegrationTests (string?) response[Metadata.UserinfoEndpoint]); } - [Theory] - [InlineData("https://www.fabrikam.com/tenant1", new[] - { - "path/authorization_endpoint", - "path/cryptography_endpoint", - "path/device_endpoint", - "path/introspection_endpoint", - "path/logout_endpoint", - "path/revocation_endpoint", - "path/token_endpoint", - "path/userinfo_endpoint" - })] - [InlineData("https://www.fabrikam.com/tenant1/", new[] - { - "path/authorization_endpoint", - "path/cryptography_endpoint", - "path/device_endpoint", - "path/introspection_endpoint", - "path/logout_endpoint", - "path/revocation_endpoint", - "path/token_endpoint", - "path/userinfo_endpoint" - })] - [InlineData("https://www.fabrikam.com/tenant1", new[] - { - "/path/authorization_endpoint", - "/path/cryptography_endpoint", - "/path/device_endpoint", - "/path/introspection_endpoint", - "/path/logout_endpoint", - "/path/revocation_endpoint", - "/path/token_endpoint", - "/path/userinfo_endpoint" - })] - [InlineData("https://www.fabrikam.com/tenant1/", new[] - { - "/path/authorization_endpoint", - "/path/cryptography_endpoint", - "/path/device_endpoint", - "/path/introspection_endpoint", - "/path/logout_endpoint", - "/path/revocation_endpoint", - "/path/token_endpoint", - "/path/userinfo_endpoint" - })] - public async Task HandleConfigurationRequest_RelativeEndpointsAreCorrectlyComputed(string issuer, string[] endpoints) + [Fact] + public async Task HandleConfigurationRequest_RelativeEndpointsAreCorrectlyComputed() { // Arrange await using var server = await CreateServerAsync(options => { - options.SetIssuer(new Uri(issuer, UriKind.Absolute)); - - options.SetAuthorizationEndpointUris(endpoints[0]) - .SetCryptographyEndpointUris(endpoints[1]) - .SetDeviceEndpointUris(endpoints[2]) - .SetIntrospectionEndpointUris(endpoints[3]) - .SetLogoutEndpointUris(endpoints[4]) - .SetRevocationEndpointUris(endpoints[5]) - .SetTokenEndpointUris(endpoints[6]) - .SetUserinfoEndpointUris(endpoints[7]); + options.SetAuthorizationEndpointUris("path/authorization_endpoint") + .SetCryptographyEndpointUris("path/cryptography_endpoint") + .SetDeviceEndpointUris("path/device_endpoint") + .SetIntrospectionEndpointUris("path/introspection_endpoint") + .SetLogoutEndpointUris("path/logout_endpoint") + .SetRevocationEndpointUris("path/revocation_endpoint") + .SetTokenEndpointUris("path/token_endpoint") + .SetUserinfoEndpointUris("path/userinfo_endpoint"); }); await using var client = await server.CreateClientAsync(); @@ -372,29 +327,58 @@ public abstract partial class OpenIddictServerIntegrationTests var response = await client.GetAsync("/.well-known/openid-configuration"); // Assert - Assert.Equal("https://www.fabrikam.com/tenant1/path/authorization_endpoint", - (string?) response[Metadata.AuthorizationEndpoint]); + Assert.Equal("http://localhost/path/authorization_endpoint", (string?) response[Metadata.AuthorizationEndpoint]); + Assert.Equal("http://localhost/path/cryptography_endpoint", (string?) response[Metadata.JwksUri]); + Assert.Equal("http://localhost/path/device_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]); + Assert.Equal("http://localhost/path/introspection_endpoint", (string?) response[Metadata.IntrospectionEndpoint]); + Assert.Equal("http://localhost/path/logout_endpoint", (string?) response[Metadata.EndSessionEndpoint]); + Assert.Equal("http://localhost/path/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]); + Assert.Equal("http://localhost/path/token_endpoint", (string?) response[Metadata.TokenEndpoint]); + Assert.Equal("http://localhost/path/userinfo_endpoint", (string?) response[Metadata.UserinfoEndpoint]); + } - Assert.Equal("https://www.fabrikam.com/tenant1/path/cryptography_endpoint", - (string?) response[Metadata.JwksUri]); + [Fact] + public async Task HandleConfigurationRequest_RelativeEndpointsWithSpecificBaseUriAreCorrectlyComputed() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.SetAuthorizationEndpointUris("path/authorization_endpoint") + .SetCryptographyEndpointUris("path/cryptography_endpoint") + .SetDeviceEndpointUris("path/device_endpoint") + .SetIntrospectionEndpointUris("path/introspection_endpoint") + .SetLogoutEndpointUris("path/logout_endpoint") + .SetRevocationEndpointUris("path/revocation_endpoint") + .SetTokenEndpointUris("path/token_endpoint") + .SetUserinfoEndpointUris("path/userinfo_endpoint"); - Assert.Equal("https://www.fabrikam.com/tenant1/path/device_endpoint", - (string?) response[Metadata.DeviceAuthorizationEndpoint]); + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + context.BaseUri = new Uri("https://contoso.com/issuer"); - Assert.Equal("https://www.fabrikam.com/tenant1/path/introspection_endpoint", - (string?) response[Metadata.IntrospectionEndpoint]); + return default; + }); - Assert.Equal("https://www.fabrikam.com/tenant1/path/logout_endpoint", - (string?) response[Metadata.EndSessionEndpoint]); + builder.SetOrder(AttachEndpoints.Descriptor.Order - 1); + }); + }); - Assert.Equal("https://www.fabrikam.com/tenant1/path/revocation_endpoint", - (string?) response[Metadata.RevocationEndpoint]); + await using var client = await server.CreateClientAsync(); - Assert.Equal("https://www.fabrikam.com/tenant1/path/token_endpoint", - (string?) response[Metadata.TokenEndpoint]); + // Act + var response = await client.GetAsync("/.well-known/openid-configuration"); - Assert.Equal("https://www.fabrikam.com/tenant1/path/userinfo_endpoint", - (string?) response[Metadata.UserinfoEndpoint]); + // Assert + Assert.Equal("https://contoso.com/issuer/path/authorization_endpoint", (string?) response[Metadata.AuthorizationEndpoint]); + Assert.Equal("https://contoso.com/issuer/path/cryptography_endpoint", (string?) response[Metadata.JwksUri]); + Assert.Equal("https://contoso.com/issuer/path/device_endpoint", (string?) response[Metadata.DeviceAuthorizationEndpoint]); + Assert.Equal("https://contoso.com/issuer/path/introspection_endpoint", (string?) response[Metadata.IntrospectionEndpoint]); + Assert.Equal("https://contoso.com/issuer/path/logout_endpoint", (string?) response[Metadata.EndSessionEndpoint]); + Assert.Equal("https://contoso.com/issuer/path/revocation_endpoint", (string?) response[Metadata.RevocationEndpoint]); + Assert.Equal("https://contoso.com/issuer/path/token_endpoint", (string?) response[Metadata.TokenEndpoint]); + Assert.Equal("https://contoso.com/issuer/path/userinfo_endpoint", (string?) response[Metadata.UserinfoEndpoint]); } [Fact] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index ecbc1bec..55fe7954 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -33,6 +33,410 @@ public abstract partial class OpenIddictServerIntegrationTests protected ITestOutputHelper OutputHelper { get; } + [Theory] + [InlineData("/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/authorize", OpenIddictServerEndpointType.Authorization)] + [InlineData("/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Authorization)] + [InlineData("/connect/authorize/", OpenIddictServerEndpointType.Authorization)] + [InlineData("/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Authorization)] + [InlineData("/connect/authorize/subpath", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/AUTHORIZE/SUBPATH", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/authorize/subpath/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/AUTHORIZE/SUBPATH/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] + [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Configuration)] + [InlineData("/.well-known/openid-configuration/", OpenIddictServerEndpointType.Configuration)] + [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Configuration)] + [InlineData("/.well-known/openid-configuration/subpath", OpenIddictServerEndpointType.Unknown)] + [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION/SUBPATH", OpenIddictServerEndpointType.Unknown)] + [InlineData("/.well-known/openid-configuration/subpath/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION/SUBPATH/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] + [InlineData("/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Cryptography)] + [InlineData("/.well-known/jwks/", OpenIddictServerEndpointType.Cryptography)] + [InlineData("/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Cryptography)] + [InlineData("/.well-known/jwks/subpath", OpenIddictServerEndpointType.Unknown)] + [InlineData("/.WELL-KNOWN/JWKS/SUBPATH", OpenIddictServerEndpointType.Unknown)] + [InlineData("/.well-known/jwks/subpath/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/.WELL-KNOWN/JWKS/SUBPATH/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/device", OpenIddictServerEndpointType.Device)] + [InlineData("/CONNECT/DEVICE", OpenIddictServerEndpointType.Device)] + [InlineData("/connect/device/", OpenIddictServerEndpointType.Device)] + [InlineData("/CONNECT/DEVICE/", OpenIddictServerEndpointType.Device)] + [InlineData("/connect/device/subpath", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/DEVICE/SUBPATH", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/device/subpath/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/DEVICE/SUBPATH/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/introspect", OpenIddictServerEndpointType.Introspection)] + [InlineData("/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Introspection)] + [InlineData("/connect/introspect/", OpenIddictServerEndpointType.Introspection)] + [InlineData("/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Introspection)] + [InlineData("/connect/introspect/subpath", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/INTROSPECT/SUBPATH", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/introspect/subpath/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/INTROSPECT/SUBPATH/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/logout", OpenIddictServerEndpointType.Logout)] + [InlineData("/CONNECT/LOGOUT", OpenIddictServerEndpointType.Logout)] + [InlineData("/connect/logout/", OpenIddictServerEndpointType.Logout)] + [InlineData("/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Logout)] + [InlineData("/connect/logout/subpath", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/LOGOUT/SUBPATH", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/logout/subpath/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/LOGOUT/SUBPATH/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/revoke", OpenIddictServerEndpointType.Revocation)] + [InlineData("/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)] + [InlineData("/connect/revoke/", OpenIddictServerEndpointType.Revocation)] + [InlineData("/CONNECT/REVOKE/", OpenIddictServerEndpointType.Revocation)] + [InlineData("/connect/revoke/subpath", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/REVOKE/SUBPATH", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/revoke/subpath/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/REVOKE/SUBPATH/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/token", OpenIddictServerEndpointType.Token)] + [InlineData("/CONNECT/TOKEN", OpenIddictServerEndpointType.Token)] + [InlineData("/connect/token/", OpenIddictServerEndpointType.Token)] + [InlineData("/CONNECT/TOKEN/", OpenIddictServerEndpointType.Token)] + [InlineData("/connect/token/subpath", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/TOKEN/SUBPATH", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/token/subpath/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/TOKEN/SUBPATH/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] + [InlineData("/CONNECT/USERINFO", OpenIddictServerEndpointType.Userinfo)] + [InlineData("/connect/userinfo/", OpenIddictServerEndpointType.Userinfo)] + [InlineData("/CONNECT/USERINFO/", OpenIddictServerEndpointType.Userinfo)] + [InlineData("/connect/userinfo/subpath", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/USERINFO/SUBPATH", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/userinfo/subpath/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/USERINFO/SUBPATH/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/verification", OpenIddictServerEndpointType.Verification)] + [InlineData("/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Verification)] + [InlineData("/connect/verification/", OpenIddictServerEndpointType.Verification)] + [InlineData("/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Verification)] + [InlineData("/connect/verification/subpath", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/VERIFICATION/SUBPATH", OpenIddictServerEndpointType.Unknown)] + [InlineData("/connect/verification/subpath/", OpenIddictServerEndpointType.Unknown)] + [InlineData("/CONNECT/VERIFICATION/SUBPATH/", OpenIddictServerEndpointType.Unknown)] + public async Task ProcessRequest_MatchesCorrespondingRelativeEndpoint(string path, OpenIddictServerEndpointType type) + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + // Assert + Assert.Equal(type, context.EndpointType); + + return default; + })); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + await client.PostAsync(path, new OpenIddictRequest()); + } + + [Theory] + [InlineData("https://localhost/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:443/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/connect", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST/CONNECT", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/connect/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST/CONNECT/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:443/connect", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:443/connect/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/connect/authorize", OpenIddictServerEndpointType.Authorization)] + [InlineData("HTTPS://LOCALHOST/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Authorization)] + [InlineData("https://localhost/connect/authorize/", OpenIddictServerEndpointType.Authorization)] + [InlineData("HTTPS://LOCALHOST/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Authorization)] + [InlineData("https://localhost:443/connect/authorize", OpenIddictServerEndpointType.Authorization)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Authorization)] + [InlineData("https://localhost:443/connect/authorize/", OpenIddictServerEndpointType.Authorization)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Authorization)] + [InlineData("https://fabrikam.com/connect/authorize", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://fabrikam.com/connect/authorize/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/authorize", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/authorize/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] + [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Configuration)] + [InlineData("https://localhost/.well-known/openid-configuration/", OpenIddictServerEndpointType.Configuration)] + [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Configuration)] + [InlineData("https://localhost:443/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] + [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Configuration)] + [InlineData("https://localhost:443/.well-known/openid-configuration/", OpenIddictServerEndpointType.Configuration)] + [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Configuration)] + [InlineData("https://fabrikam.com/.well-known/openid-configuration", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://fabrikam.com/.well-known/openid-configuration/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/.well-known/openid-configuration", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/.well-known/openid-configuration/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] + [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Cryptography)] + [InlineData("https://localhost/.well-known/jwks/", OpenIddictServerEndpointType.Cryptography)] + [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Cryptography)] + [InlineData("https://localhost:443/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] + [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Cryptography)] + [InlineData("https://localhost:443/.well-known/jwks/", OpenIddictServerEndpointType.Cryptography)] + [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Cryptography)] + [InlineData("https://fabrikam.com/.well-known/jwks", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://fabrikam.com/.well-known/jwks/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/.well-known/jwks", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/.well-known/jwks/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/connect/device", OpenIddictServerEndpointType.Device)] + [InlineData("HTTPS://LOCALHOST/CONNECT/DEVICE", OpenIddictServerEndpointType.Device)] + [InlineData("https://localhost/connect/device/", OpenIddictServerEndpointType.Device)] + [InlineData("HTTPS://LOCALHOST/CONNECT/DEVICE/", OpenIddictServerEndpointType.Device)] + [InlineData("https://localhost:443/connect/device", OpenIddictServerEndpointType.Device)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/DEVICE", OpenIddictServerEndpointType.Device)] + [InlineData("https://localhost:443/connect/device/", OpenIddictServerEndpointType.Device)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/DEVICE/", OpenIddictServerEndpointType.Device)] + [InlineData("https://fabrikam.com/connect/device", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/DEVICE", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://fabrikam.com/connect/device/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/DEVICE/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/device", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/DEVICE", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/device/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/DEVICE/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/connect/introspect", OpenIddictServerEndpointType.Introspection)] + [InlineData("HTTPS://LOCALHOST/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Introspection)] + [InlineData("https://localhost/connect/introspect/", OpenIddictServerEndpointType.Introspection)] + [InlineData("HTTPS://LOCALHOST/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Introspection)] + [InlineData("https://localhost:443/connect/introspect", OpenIddictServerEndpointType.Introspection)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Introspection)] + [InlineData("https://localhost:443/connect/introspect/", OpenIddictServerEndpointType.Introspection)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Introspection)] + [InlineData("https://fabrikam.com/connect/introspect", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://fabrikam.com/connect/introspect/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/introspect", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/introspect/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/connect/logout", OpenIddictServerEndpointType.Logout)] + [InlineData("HTTPS://LOCALHOST/CONNECT/LOGOUT", OpenIddictServerEndpointType.Logout)] + [InlineData("https://localhost/connect/logout/", OpenIddictServerEndpointType.Logout)] + [InlineData("HTTPS://LOCALHOST/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Logout)] + [InlineData("https://localhost:443/connect/logout", OpenIddictServerEndpointType.Logout)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/LOGOUT", OpenIddictServerEndpointType.Logout)] + [InlineData("https://localhost:443/connect/logout/", OpenIddictServerEndpointType.Logout)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Logout)] + [InlineData("https://fabrikam.com/connect/logout", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/LOGOUT", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://fabrikam.com/connect/logout/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/logout", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/LOGOUT", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/logout/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/connect/revoke", OpenIddictServerEndpointType.Revocation)] + [InlineData("HTTPS://LOCALHOST/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)] + [InlineData("https://localhost/connect/revoke/", OpenIddictServerEndpointType.Revocation)] + [InlineData("HTTPS://LOCALHOST/CONNECT/REVOKE/", OpenIddictServerEndpointType.Revocation)] + [InlineData("https://localhost:443/connect/revoke", OpenIddictServerEndpointType.Revocation)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)] + [InlineData("https://localhost:443/connect/revoke/", OpenIddictServerEndpointType.Revocation)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/REVOKE/", OpenIddictServerEndpointType.Revocation)] + [InlineData("https://fabrikam.com/connect/revoke", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/REVOKE", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://fabrikam.com/connect/revoke/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/REVOKE/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/revoke", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/REVOKE", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/revoke/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/REVOKE/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/connect/token", OpenIddictServerEndpointType.Token)] + [InlineData("HTTPS://LOCALHOST/CONNECT/TOKEN", OpenIddictServerEndpointType.Token)] + [InlineData("https://localhost/connect/token/", OpenIddictServerEndpointType.Token)] + [InlineData("HTTPS://LOCALHOST/CONNECT/TOKEN/", OpenIddictServerEndpointType.Token)] + [InlineData("https://localhost:443/connect/token", OpenIddictServerEndpointType.Token)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/TOKEN", OpenIddictServerEndpointType.Token)] + [InlineData("https://localhost:443/connect/token/", OpenIddictServerEndpointType.Token)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/TOKEN/", OpenIddictServerEndpointType.Token)] + [InlineData("https://fabrikam.com/connect/token", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/TOKEN", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://fabrikam.com/connect/token/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/TOKEN/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/token", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/TOKEN", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/token/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/TOKEN/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] + [InlineData("HTTPS://LOCALHOST/CONNECT/USERINFO", OpenIddictServerEndpointType.Userinfo)] + [InlineData("https://localhost/connect/userinfo/", OpenIddictServerEndpointType.Userinfo)] + [InlineData("HTTPS://LOCALHOST/CONNECT/USERINFO/", OpenIddictServerEndpointType.Userinfo)] + [InlineData("https://localhost:443/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/USERINFO", OpenIddictServerEndpointType.Userinfo)] + [InlineData("https://localhost:443/connect/userinfo/", OpenIddictServerEndpointType.Userinfo)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/USERINFO/", OpenIddictServerEndpointType.Userinfo)] + [InlineData("https://fabrikam.com/connect/userinfo", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/USERINFO", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://fabrikam.com/connect/userinfo/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/USERINFO/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/userinfo", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/USERINFO", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/userinfo/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/USERINFO/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost/connect/verification", OpenIddictServerEndpointType.Verification)] + [InlineData("HTTPS://LOCALHOST/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Verification)] + [InlineData("https://localhost/connect/verification/", OpenIddictServerEndpointType.Verification)] + [InlineData("HTTPS://LOCALHOST/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Verification)] + [InlineData("https://localhost:443/connect/verification", OpenIddictServerEndpointType.Verification)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Verification)] + [InlineData("https://localhost:443/connect/verification/", OpenIddictServerEndpointType.Verification)] + [InlineData("HTTPS://LOCALHOST:443/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Verification)] + [InlineData("https://fabrikam.com/connect/verification", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://fabrikam.com/connect/verification/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://FABRIKAM.COM/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/verification", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Unknown)] + [InlineData("https://localhost:8888/connect/verification/", OpenIddictServerEndpointType.Unknown)] + [InlineData("HTTPS://LOCALHOST:8888/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Unknown)] + public async Task ProcessRequest_MatchesCorrespondingAbsoluteEndpoint(string path, OpenIddictServerEndpointType type) + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.SetAuthorizationEndpointUris("https://localhost/connect/authorize") + .SetConfigurationEndpointUris("https://localhost/.well-known/openid-configuration") + .SetCryptographyEndpointUris("https://localhost/.well-known/jwks") + .SetDeviceEndpointUris("https://localhost/connect/device") + .SetIntrospectionEndpointUris("https://localhost/connect/introspect") + .SetLogoutEndpointUris("https://localhost/connect/logout") + .SetRevocationEndpointUris("https://localhost/connect/revoke") + .SetTokenEndpointUris("https://localhost/connect/token") + .SetUserinfoEndpointUris("https://localhost/connect/userinfo") + .SetVerificationEndpointUris("https://localhost/connect/verification"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + // Assert + Assert.Equal(type, context.EndpointType); + + return default; + })); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + await client.PostAsync(path, new OpenIddictRequest()); + } + + [Theory] + [InlineData("/custom/connect/authorize", OpenIddictServerEndpointType.Authorization)] + [InlineData("/custom/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] + [InlineData("/custom/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] + [InlineData("/custom/connect/device", OpenIddictServerEndpointType.Device)] + [InlineData("/custom/connect/custom", OpenIddictServerEndpointType.Unknown)] + [InlineData("/custom/connect/introspect", OpenIddictServerEndpointType.Introspection)] + [InlineData("/custom/connect/logout", OpenIddictServerEndpointType.Logout)] + [InlineData("/custom/connect/revoke", OpenIddictServerEndpointType.Revocation)] + [InlineData("/custom/connect/token", OpenIddictServerEndpointType.Token)] + [InlineData("/custom/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] + [InlineData("/custom/connect/verification", OpenIddictServerEndpointType.Verification)] + public async Task ProcessRequest_AllowsOverridingEndpoint(string address, OpenIddictServerEndpointType type) + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + // Act + context.EndpointType = type; + + // Assert + Assert.Equal(type, context.EndpointType); + + return default; + }); + + builder.SetOrder(InferEndpointType.Descriptor.Order + 500); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + await client.PostAsync(address, new OpenIddictRequest()); + } + [Fact] public async Task ProcessAuthentication_UnknownEndpointCausesAnException() { diff --git a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs index f5936d72..e489d04d 100644 --- a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs +++ b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs @@ -4,7 +4,6 @@ * the license and the contributors participating to this project. */ -using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; @@ -18,7 +17,6 @@ using Xunit; using Xunit.Abstractions; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers.Protection; -using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlers; namespace OpenIddict.Server.Owin.IntegrationTests; @@ -245,410 +243,6 @@ public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerInte Assert.Equal("custom_error_uri", response.ErrorUri); } - [Theory] - [InlineData("/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/authorize", OpenIddictServerEndpointType.Authorization)] - [InlineData("/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Authorization)] - [InlineData("/connect/authorize/", OpenIddictServerEndpointType.Authorization)] - [InlineData("/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Authorization)] - [InlineData("/connect/authorize/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/AUTHORIZE/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/authorize/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/AUTHORIZE/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] - [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Configuration)] - [InlineData("/.well-known/openid-configuration/", OpenIddictServerEndpointType.Configuration)] - [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Configuration)] - [InlineData("/.well-known/openid-configuration/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.well-known/openid-configuration/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.WELL-KNOWN/OPENID-CONFIGURATION/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] - [InlineData("/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Cryptography)] - [InlineData("/.well-known/jwks/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("/.well-known/jwks/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.WELL-KNOWN/JWKS/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.well-known/jwks/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/.WELL-KNOWN/JWKS/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/device", OpenIddictServerEndpointType.Device)] - [InlineData("/CONNECT/DEVICE", OpenIddictServerEndpointType.Device)] - [InlineData("/connect/device/", OpenIddictServerEndpointType.Device)] - [InlineData("/CONNECT/DEVICE/", OpenIddictServerEndpointType.Device)] - [InlineData("/connect/device/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/DEVICE/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/device/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/DEVICE/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/introspect", OpenIddictServerEndpointType.Introspection)] - [InlineData("/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Introspection)] - [InlineData("/connect/introspect/", OpenIddictServerEndpointType.Introspection)] - [InlineData("/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Introspection)] - [InlineData("/connect/introspect/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/INTROSPECT/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/introspect/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/INTROSPECT/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/logout", OpenIddictServerEndpointType.Logout)] - [InlineData("/CONNECT/LOGOUT", OpenIddictServerEndpointType.Logout)] - [InlineData("/connect/logout/", OpenIddictServerEndpointType.Logout)] - [InlineData("/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Logout)] - [InlineData("/connect/logout/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/LOGOUT/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/logout/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/LOGOUT/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/revoke", OpenIddictServerEndpointType.Revocation)] - [InlineData("/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)] - [InlineData("/connect/revoke/", OpenIddictServerEndpointType.Revocation)] - [InlineData("/CONNECT/REVOKE/", OpenIddictServerEndpointType.Revocation)] - [InlineData("/connect/revoke/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/REVOKE/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/revoke/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/REVOKE/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/token", OpenIddictServerEndpointType.Token)] - [InlineData("/CONNECT/TOKEN", OpenIddictServerEndpointType.Token)] - [InlineData("/connect/token/", OpenIddictServerEndpointType.Token)] - [InlineData("/CONNECT/TOKEN/", OpenIddictServerEndpointType.Token)] - [InlineData("/connect/token/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/TOKEN/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/token/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/TOKEN/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] - [InlineData("/CONNECT/USERINFO", OpenIddictServerEndpointType.Userinfo)] - [InlineData("/connect/userinfo/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("/CONNECT/USERINFO/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("/connect/userinfo/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/USERINFO/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/userinfo/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/USERINFO/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/verification", OpenIddictServerEndpointType.Verification)] - [InlineData("/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Verification)] - [InlineData("/connect/verification/", OpenIddictServerEndpointType.Verification)] - [InlineData("/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Verification)] - [InlineData("/connect/verification/subpath", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/VERIFICATION/SUBPATH", OpenIddictServerEndpointType.Unknown)] - [InlineData("/connect/verification/subpath/", OpenIddictServerEndpointType.Unknown)] - [InlineData("/CONNECT/VERIFICATION/SUBPATH/", OpenIddictServerEndpointType.Unknown)] - public async Task ProcessRequest_MatchesCorrespondingRelativeEndpoint(string path, OpenIddictServerEndpointType type) - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - // Assert - Assert.Equal(type, context.EndpointType); - - return default; - })); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - await client.PostAsync(path, new OpenIddictRequest()); - } - - [Theory] - [InlineData("https://localhost/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:443/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST/CONNECT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST/CONNECT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:443/connect", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:443/connect/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/authorize", OpenIddictServerEndpointType.Authorization)] - [InlineData("HTTPS://LOCALHOST/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Authorization)] - [InlineData("https://localhost/connect/authorize/", OpenIddictServerEndpointType.Authorization)] - [InlineData("HTTPS://LOCALHOST/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Authorization)] - [InlineData("https://localhost:443/connect/authorize", OpenIddictServerEndpointType.Authorization)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Authorization)] - [InlineData("https://localhost:443/connect/authorize/", OpenIddictServerEndpointType.Authorization)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Authorization)] - [InlineData("https://fabrikam.com/connect/authorize", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/authorize/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/authorize", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/AUTHORIZE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/authorize/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/AUTHORIZE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] - [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Configuration)] - [InlineData("https://localhost/.well-known/openid-configuration/", OpenIddictServerEndpointType.Configuration)] - [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Configuration)] - [InlineData("https://localhost:443/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] - [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Configuration)] - [InlineData("https://localhost:443/.well-known/openid-configuration/", OpenIddictServerEndpointType.Configuration)] - [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Configuration)] - [InlineData("https://fabrikam.com/.well-known/openid-configuration", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/.well-known/openid-configuration/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/.well-known/openid-configuration", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/OPENID-CONFIGURATION", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/.well-known/openid-configuration/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/OPENID-CONFIGURATION/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] - [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Cryptography)] - [InlineData("https://localhost/.well-known/jwks/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("HTTPS://LOCALHOST/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("https://localhost:443/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] - [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Cryptography)] - [InlineData("https://localhost:443/.well-known/jwks/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("HTTPS://LOCALHOST:443/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Cryptography)] - [InlineData("https://fabrikam.com/.well-known/jwks", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/.well-known/jwks/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/.well-known/jwks", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/JWKS", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/.well-known/jwks/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/.WELL-KNOWN/JWKS/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/device", OpenIddictServerEndpointType.Device)] - [InlineData("HTTPS://LOCALHOST/CONNECT/DEVICE", OpenIddictServerEndpointType.Device)] - [InlineData("https://localhost/connect/device/", OpenIddictServerEndpointType.Device)] - [InlineData("HTTPS://LOCALHOST/CONNECT/DEVICE/", OpenIddictServerEndpointType.Device)] - [InlineData("https://localhost:443/connect/device", OpenIddictServerEndpointType.Device)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/DEVICE", OpenIddictServerEndpointType.Device)] - [InlineData("https://localhost:443/connect/device/", OpenIddictServerEndpointType.Device)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/DEVICE/", OpenIddictServerEndpointType.Device)] - [InlineData("https://fabrikam.com/connect/device", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/DEVICE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/device/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/DEVICE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/device", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/DEVICE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/device/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/DEVICE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/introspect", OpenIddictServerEndpointType.Introspection)] - [InlineData("HTTPS://LOCALHOST/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Introspection)] - [InlineData("https://localhost/connect/introspect/", OpenIddictServerEndpointType.Introspection)] - [InlineData("HTTPS://LOCALHOST/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Introspection)] - [InlineData("https://localhost:443/connect/introspect", OpenIddictServerEndpointType.Introspection)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Introspection)] - [InlineData("https://localhost:443/connect/introspect/", OpenIddictServerEndpointType.Introspection)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Introspection)] - [InlineData("https://fabrikam.com/connect/introspect", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/introspect/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/introspect", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/INTROSPECT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/introspect/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/INTROSPECT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/logout", OpenIddictServerEndpointType.Logout)] - [InlineData("HTTPS://LOCALHOST/CONNECT/LOGOUT", OpenIddictServerEndpointType.Logout)] - [InlineData("https://localhost/connect/logout/", OpenIddictServerEndpointType.Logout)] - [InlineData("HTTPS://LOCALHOST/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Logout)] - [InlineData("https://localhost:443/connect/logout", OpenIddictServerEndpointType.Logout)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/LOGOUT", OpenIddictServerEndpointType.Logout)] - [InlineData("https://localhost:443/connect/logout/", OpenIddictServerEndpointType.Logout)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Logout)] - [InlineData("https://fabrikam.com/connect/logout", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/LOGOUT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/logout/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/logout", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/LOGOUT", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/logout/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/LOGOUT/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/revoke", OpenIddictServerEndpointType.Revocation)] - [InlineData("HTTPS://LOCALHOST/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)] - [InlineData("https://localhost/connect/revoke/", OpenIddictServerEndpointType.Revocation)] - [InlineData("HTTPS://LOCALHOST/CONNECT/REVOKE/", OpenIddictServerEndpointType.Revocation)] - [InlineData("https://localhost:443/connect/revoke", OpenIddictServerEndpointType.Revocation)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/REVOKE", OpenIddictServerEndpointType.Revocation)] - [InlineData("https://localhost:443/connect/revoke/", OpenIddictServerEndpointType.Revocation)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/REVOKE/", OpenIddictServerEndpointType.Revocation)] - [InlineData("https://fabrikam.com/connect/revoke", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/REVOKE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/revoke/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/REVOKE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/revoke", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/REVOKE", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/revoke/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/REVOKE/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/token", OpenIddictServerEndpointType.Token)] - [InlineData("HTTPS://LOCALHOST/CONNECT/TOKEN", OpenIddictServerEndpointType.Token)] - [InlineData("https://localhost/connect/token/", OpenIddictServerEndpointType.Token)] - [InlineData("HTTPS://LOCALHOST/CONNECT/TOKEN/", OpenIddictServerEndpointType.Token)] - [InlineData("https://localhost:443/connect/token", OpenIddictServerEndpointType.Token)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/TOKEN", OpenIddictServerEndpointType.Token)] - [InlineData("https://localhost:443/connect/token/", OpenIddictServerEndpointType.Token)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/TOKEN/", OpenIddictServerEndpointType.Token)] - [InlineData("https://fabrikam.com/connect/token", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/TOKEN", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/token/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/TOKEN/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/token", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/TOKEN", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/token/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/TOKEN/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] - [InlineData("HTTPS://LOCALHOST/CONNECT/USERINFO", OpenIddictServerEndpointType.Userinfo)] - [InlineData("https://localhost/connect/userinfo/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("HTTPS://LOCALHOST/CONNECT/USERINFO/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("https://localhost:443/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/USERINFO", OpenIddictServerEndpointType.Userinfo)] - [InlineData("https://localhost:443/connect/userinfo/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/USERINFO/", OpenIddictServerEndpointType.Userinfo)] - [InlineData("https://fabrikam.com/connect/userinfo", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/USERINFO", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/userinfo/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/USERINFO/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/userinfo", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/USERINFO", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/userinfo/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/USERINFO/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost/connect/verification", OpenIddictServerEndpointType.Verification)] - [InlineData("HTTPS://LOCALHOST/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Verification)] - [InlineData("https://localhost/connect/verification/", OpenIddictServerEndpointType.Verification)] - [InlineData("HTTPS://LOCALHOST/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Verification)] - [InlineData("https://localhost:443/connect/verification", OpenIddictServerEndpointType.Verification)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Verification)] - [InlineData("https://localhost:443/connect/verification/", OpenIddictServerEndpointType.Verification)] - [InlineData("HTTPS://LOCALHOST:443/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Verification)] - [InlineData("https://fabrikam.com/connect/verification", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://fabrikam.com/connect/verification/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://FABRIKAM.COM/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/verification", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/VERIFICATION", OpenIddictServerEndpointType.Unknown)] - [InlineData("https://localhost:8888/connect/verification/", OpenIddictServerEndpointType.Unknown)] - [InlineData("HTTPS://LOCALHOST:8888/CONNECT/VERIFICATION/", OpenIddictServerEndpointType.Unknown)] - public async Task ProcessRequest_MatchesCorrespondingAbsoluteEndpoint(string path, OpenIddictServerEndpointType type) - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.SetAuthorizationEndpointUris("https://localhost/connect/authorize") - .SetConfigurationEndpointUris("https://localhost/.well-known/openid-configuration") - .SetCryptographyEndpointUris("https://localhost/.well-known/jwks") - .SetDeviceEndpointUris("https://localhost/connect/device") - .SetIntrospectionEndpointUris("https://localhost/connect/introspect") - .SetLogoutEndpointUris("https://localhost/connect/logout") - .SetRevocationEndpointUris("https://localhost/connect/revoke") - .SetTokenEndpointUris("https://localhost/connect/token") - .SetUserinfoEndpointUris("https://localhost/connect/userinfo") - .SetVerificationEndpointUris("https://localhost/connect/verification"); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - // Assert - Assert.Equal(type, context.EndpointType); - - return default; - })); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - await client.PostAsync(path, new OpenIddictRequest()); - } - - [Theory] - [InlineData("/custom/connect/authorize", OpenIddictServerEndpointType.Authorization)] - [InlineData("/custom/.well-known/openid-configuration", OpenIddictServerEndpointType.Configuration)] - [InlineData("/custom/.well-known/jwks", OpenIddictServerEndpointType.Cryptography)] - [InlineData("/custom/connect/device", OpenIddictServerEndpointType.Device)] - [InlineData("/custom/connect/custom", OpenIddictServerEndpointType.Unknown)] - [InlineData("/custom/connect/introspect", OpenIddictServerEndpointType.Introspection)] - [InlineData("/custom/connect/logout", OpenIddictServerEndpointType.Logout)] - [InlineData("/custom/connect/revoke", OpenIddictServerEndpointType.Revocation)] - [InlineData("/custom/connect/token", OpenIddictServerEndpointType.Token)] - [InlineData("/custom/connect/userinfo", OpenIddictServerEndpointType.Userinfo)] - [InlineData("/custom/connect/verification", OpenIddictServerEndpointType.Verification)] - public async Task ProcessRequest_AllowsOverridingEndpoint(string address, OpenIddictServerEndpointType type) - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - // Act - context.EndpointType = type; - - // Assert - Assert.Equal(type, context.EndpointType); - - return default; - }); - - builder.SetOrder(InferEndpointType.Descriptor.Order + 500); - }); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - await client.PostAsync(address, new OpenIddictRequest()); - } - [Theory] [InlineData("/.well-known/openid-configuration")] [InlineData("/.well-known/jwks")] diff --git a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs index c057251e..0e1cc800 100644 --- a/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs +++ b/test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs @@ -32,29 +32,6 @@ public abstract partial class OpenIddictValidationIntegrationTests protected ITestOutputHelper OutputHelper { get; } - [Fact] - public async Task ProcessAuthentication_InvalidIssuerThrowsAnException() - { - // Arrange - await using var server = await CreateServerAsync(options => options.Configure(options => - { - options.ConfigurationManager = new StaticConfigurationManager(new() - { - Issuer = new Uri("https://fabrikam.com/") - }); - })); - - await using var client = await server.CreateClientAsync(); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.PostAsync("/authenticate", new OpenIddictRequest()); - }); - - Assert.Equal(SR.GetResourceString(SR.ID0307), exception.Message); - } - [Fact] public async Task ProcessAuthentication_EvalutesCorrectValidatedTokens() {