Browse Source

Support relative redirect_uri/post_logout_redirect_uri in the client stack and revisit how OpenIddict handles URIs

pull/1614/head
Kévin Chalet 3 years ago
parent
commit
2a987bcebe
  1. 19
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
  2. 25
      sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs
  3. 18
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  4. 29
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
  5. 18
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
  6. 78
      shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs
  7. 2
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  8. 249
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
  9. 1
      src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs
  10. 248
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
  11. 17
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs
  12. 16
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  13. 20
      src/OpenIddict.Client/OpenIddictClientConfiguration.cs
  14. 42
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  15. 3
      src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
  16. 31
      src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
  17. 244
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  18. 6
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  19. 16
      src/OpenIddict.Client/OpenIddictClientService.cs
  20. 9
      src/OpenIddict.Client/OpenIddictClientTransaction.cs
  21. 2
      src/OpenIddict.Owin/OpenIddict.Owin.csproj
  22. 8
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
  23. 8
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
  24. 160
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  25. 8
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
  26. 8
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
  27. 162
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  28. 4
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  29. 5
      src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
  30. 5
      src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs
  31. 5
      src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs
  32. 25
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  33. 1
      src/OpenIddict.Server/OpenIddictServerFactory.cs
  34. 17
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  35. 101
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  36. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  37. 20
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  38. 1
      src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
  39. 151
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  40. 8
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  41. 9
      src/OpenIddict.Server/OpenIddictServerTransaction.cs
  42. 93
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs
  43. 91
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs
  44. 17
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs
  45. 4
      src/OpenIddict.Validation/OpenIddict.Validation.csproj
  46. 2
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  47. 18
      src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs
  48. 17
      src/OpenIddict.Validation/OpenIddictValidationEvents.cs
  49. 1
      src/OpenIddict.Validation/OpenIddictValidationFactory.cs
  50. 5
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs
  51. 5
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
  52. 6
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  53. 10
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  54. 14
      src/OpenIddict.Validation/OpenIddictValidationService.cs
  55. 9
      src/OpenIddict.Validation/OpenIddictValidationTransaction.cs
  56. 406
      test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs
  57. 4
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs
  58. 128
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
  59. 404
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
  60. 406
      test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs
  61. 23
      test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs

19
gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs

@ -184,11 +184,6 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentNullException(nameof(address)); 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); return Configure(options => options.RedirectUri = address);
} }
@ -204,12 +199,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentException(SR.GetResourceString(SR.ID0143), nameof(address)); throw new ArgumentException(SR.GetResourceString(SR.ID0143), nameof(address));
} }
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri)) return SetRedirectUri(new Uri(address, UriKind.RelativeOrAbsolute));
{
throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address));
}
return SetRedirectUri(uri);
} }
/// <summary> /// <summary>
@ -285,12 +275,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentException(SR.GetResourceString(SR.ID0143), nameof({{ setting.parameter_name }})); throw new ArgumentException(SR.GetResourceString(SR.ID0143), nameof({{ setting.parameter_name }}));
} }
if (!Uri.TryCreate({{ setting.parameter_name }}, UriKind.Absolute, out Uri? uri)) return Set{{ setting.property_name }}(new Uri({{ setting.parameter_name }}, UriKind.RelativeOrAbsolute));
{
throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof({{ setting.parameter_name }}));
}
return Set{{ setting.property_name }}(uri);
} }
{{~ else ~}} {{~ else ~}}
/// <summary> /// <summary>

25
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, // 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. // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris( options.SetRedirectionEndpointUris(
"/callback/login/local", "callback/login/local",
"/callback/login/github", "callback/login/github",
"/callback/login/google", "callback/login/google",
"/callback/login/twitter"); "callback/login/twitter");
// Enable the post-logout redirection endpoints needed to handle the callback stage. // Enable the post-logout redirection endpoint needed to handle the callback stage.
options.SetPostLogoutRedirectionEndpointUris( options.SetPostLogoutRedirectionEndpointUris("callback/logout/local");
"/callback/logout/local");
// Note: this sample uses the authorization code and refresh token // Note: this sample uses the authorization code and refresh token
// flows, but you can enable the other flows if necessary. // 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. // Add a client registration matching the client application definition in the server project.
options.AddRegistration(new OpenIddictClientRegistration options.AddRegistration(new OpenIddictClientRegistration
{ {
ProviderName = "Local",
Issuer = new Uri("https://localhost:44349/", UriKind.Absolute), Issuer = new Uri("https://localhost:44349/", UriKind.Absolute),
ProviderName = "Local",
ClientId = "mvc", ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }, Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },
RedirectUri = new Uri("https://localhost:44378/callback/login/local", UriKind.Absolute), RedirectUri = new Uri("callback/login/local", UriKind.Relative),
PostLogoutRedirectUri = new Uri("https://localhost:44378/callback/logout/local", UriKind.Absolute) PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative)
}); });
// Register the Web providers integrations. // Register the Web providers integrations.
@ -123,13 +122,13 @@ namespace OpenIddict.Sandbox.AspNet.Client
{ {
options.SetClientId("c4ade52327b01ddacff3") options.SetClientId("c4ade52327b01ddacff3")
.SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122")
.SetRedirectUri("https://localhost:44378/callback/login/github"); .SetRedirectUri("callback/login/github");
}) })
.UseGoogle(options => .UseGoogle(options =>
{ {
options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com") options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com")
.SetClientSecret("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") .SetClientSecret("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")
.SetRedirectUri("https://localhost:44378/callback/login/google") .SetRedirectUri("callback/login/google")
.SetAccessType("offline") .SetAccessType("offline")
.AddScopes(Scopes.Profile); .AddScopes(Scopes.Profile);
}) })
@ -137,7 +136,7 @@ namespace OpenIddict.Sandbox.AspNet.Client
{ {
options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") .SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS")
.SetRedirectUri("https://localhost:44378/callback/login/twitter"); .SetRedirectUri("callback/login/twitter");
}); });
}); });

18
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" // address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information, // 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. // 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. // Note: this sample uses the code flow, but you can enable the other flows if necessary.
options.AllowAuthorizationCodeFlow(); options.AllowAuthorizationCodeFlow();
@ -183,7 +183,7 @@ namespace OpenIddict.Sandbox.AspNet.Server
{ {
options.SetClientId("c4ade52327b01ddacff3") options.SetClientId("c4ade52327b01ddacff3")
.SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") .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, // Enable the authorization, device, introspection,
// logout, token, userinfo and verification endpoints. // logout, token, userinfo and verification endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize") options.SetAuthorizationEndpointUris("connect/authorize")
.SetDeviceEndpointUris("/connect/device") .SetDeviceEndpointUris("connect/device")
.SetIntrospectionEndpointUris("/connect/introspect") .SetIntrospectionEndpointUris("connect/introspect")
.SetLogoutEndpointUris("/connect/logout") .SetLogoutEndpointUris("connect/logout")
.SetTokenEndpointUris("/connect/token") .SetTokenEndpointUris("connect/token")
.SetUserinfoEndpointUris("/connect/userinfo") .SetUserinfoEndpointUris("connect/userinfo")
.SetVerificationEndpointUris("/connect/verify"); .SetVerificationEndpointUris("connect/verify");
// Note: this sample uses the code, device code, password and refresh token flows, but you // 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. // can enable the other flows if you need to support implicit or client credentials.

29
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, // 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. // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris( options.SetRedirectionEndpointUris(
"/callback/login/local", "callback/login/local",
"/callback/login/github", "callback/login/github",
"/callback/login/google", "callback/login/google",
"/callback/login/reddit", "callback/login/reddit",
"/callback/login/twitter"); "callback/login/twitter");
// Enable the post-logout redirection endpoints needed to handle the callback stage. // Enable the post-logout redirection endpoint needed to handle the callback stage.
options.SetPostLogoutRedirectionEndpointUris( options.SetPostLogoutRedirectionEndpointUris("callback/logout/local");
"/callback/logout/local");
// Note: this sample uses the authorization code and refresh token // Note: this sample uses the authorization code and refresh token
// flows, but you can enable the other flows if necessary. // 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. // Add a client registration matching the client application definition in the server project.
options.AddRegistration(new OpenIddictClientRegistration options.AddRegistration(new OpenIddictClientRegistration
{ {
ProviderName = "Local",
Issuer = new Uri("https://localhost:44395/", UriKind.Absolute), Issuer = new Uri("https://localhost:44395/", UriKind.Absolute),
ProviderName = "Local",
ClientId = "mvc", ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }, Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },
RedirectUri = new Uri("https://localhost:44381/callback/login/local", UriKind.Absolute), RedirectUri = new Uri("callback/login/local", UriKind.Relative),
PostLogoutRedirectUri = new Uri("https://localhost:44381/callback/logout/local", UriKind.Absolute) PostLogoutRedirectUri = new Uri("callback/logout/local", UriKind.Relative)
}); });
// Register the Web providers integrations. // Register the Web providers integrations.
@ -133,13 +132,13 @@ public class Startup
{ {
options.SetClientId("c4ade52327b01ddacff3") options.SetClientId("c4ade52327b01ddacff3")
.SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") .SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122")
.SetRedirectUri("https://localhost:44381/callback/login/github"); .SetRedirectUri("callback/login/github");
}) })
.UseGoogle(options => .UseGoogle(options =>
{ {
options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com") options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com")
.SetClientSecret("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") .SetClientSecret("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")
.SetRedirectUri("https://localhost:44381/callback/login/google") .SetRedirectUri("callback/login/google")
.SetAccessType("offline") .SetAccessType("offline")
.AddScopes(Scopes.Profile); .AddScopes(Scopes.Profile);
}) })
@ -147,14 +146,14 @@ public class Startup
{ {
options.SetClientId("vDLNqhrkwrvqHgnoBWF3og") options.SetClientId("vDLNqhrkwrvqHgnoBWF3og")
.SetClientSecret("Tpab28Dz0upyZLqn7AN3GFD1O-zaAw") .SetClientSecret("Tpab28Dz0upyZLqn7AN3GFD1O-zaAw")
.SetRedirectUri("https://localhost:44381/callback/login/reddit") .SetRedirectUri("callback/login/reddit")
.SetDuration("permanent"); .SetDuration("permanent");
}) })
.UseTwitter(options => .UseTwitter(options =>
{ {
options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") .SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS")
.SetRedirectUri("https://localhost:44381/callback/login/twitter") .SetRedirectUri("callback/login/twitter")
.AddScopes("offline.access"); .AddScopes("offline.access");
}); });
}); });

18
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" // address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information, // 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. // 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. // Note: this sample uses the code flow, but you can enable the other flows if necessary.
options.AllowAuthorizationCodeFlow(); options.AllowAuthorizationCodeFlow();
@ -101,7 +101,7 @@ public class Startup
{ {
options.SetClientId("c4ade52327b01ddacff3") options.SetClientId("c4ade52327b01ddacff3")
.SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122") .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, // Enable the authorization, device, introspection,
// logout, token, userinfo and verification endpoints. // logout, token, userinfo and verification endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize") options.SetAuthorizationEndpointUris("connect/authorize")
.SetDeviceEndpointUris("/connect/device") .SetDeviceEndpointUris("connect/device")
.SetIntrospectionEndpointUris("/connect/introspect") .SetIntrospectionEndpointUris("connect/introspect")
.SetLogoutEndpointUris("/connect/logout") .SetLogoutEndpointUris("connect/logout")
.SetTokenEndpointUris("/connect/token") .SetTokenEndpointUris("connect/token")
.SetUserinfoEndpointUris("/connect/userinfo") .SetUserinfoEndpointUris("connect/userinfo")
.SetVerificationEndpointUris("/connect/verify"); .SetVerificationEndpointUris("connect/verify");
// Note: this sample uses the code, device code, password and refresh token flows, but you // 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. // can enable the other flows if you need to support implicit or client credentials.

78
shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs

@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -91,6 +92,83 @@ internal static class OpenIddictHelpers
=> new(source ?? throw new ArgumentNullException(nameof(source)), comparer); => new(source ?? throw new ArgumentNullException(nameof(source)), comparer);
#endif #endif
/// <summary>
/// Computes an absolute URI from the specified <paramref name="left"/> and <paramref name="right"/> URIs.
/// Note: if the <paramref name="right"/> URI is already absolute, it is directly returned.
/// </summary>
/// <param name="left">The left part.</param>
/// <param name="right">The right part.</param>
/// <returns>An absolute URI from the specified <paramref name="left"/> and <paramref name="right"/>.</returns>
/// <exception cref="InvalidOperationException"><paramref name="left"/> is not an absolute URI.</exception>
[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);
}
/// <summary>
/// Determines whether the <paramref name="left"/> URI is a base of the <paramref name="right"/> URI.
/// </summary>
/// <param name="left">The left part.</param>
/// <param name="right">The right part.</param>
/// <returns><see langword="true"/> if <paramref name="left"/> is base of
/// <paramref name="right"/>, <see langword="false"/> otherwise.</returns>
/// <exception cref="ArgumentNullException"><paramref name="left"/> or
/// <paramref name="right"/> is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException"><paramref name="left"/> is not an absolute URI.</exception>
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);
}
/// <summary> /// <summary>
/// Adds a query string parameter to the specified <see cref="Uri"/>. /// Adds a query string parameter to the specified <see cref="Uri"/>.
/// </summary> /// </summary>

2
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -547,7 +547,7 @@ To register the server services, use 'services.AddOpenIddict().AddServer()'.</va
<value>The issuer cannot be null or empty.</value> <value>The issuer cannot be null or empty.</value>
</data> </data>
<data name="ID0127" xml:space="preserve"> <data name="ID0127" xml:space="preserve">
<value>The issuer must be a valid absolute URL.</value> <value>The base URI or request URI cannot be retrieved from the request context or are now valid absolute URIs.</value>
</data> </data>
<data name="ID0128" xml:space="preserve"> <data name="ID0128" xml:space="preserve">
<value>An OAuth 2.0/OpenID Connect server configuration or an issuer address must be registered. <value>An OAuth 2.0/OpenID Connect server configuration or an issuer address must be registered.

249
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs

@ -13,7 +13,7 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -33,14 +33,14 @@ public static partial class OpenIddictClientAspNetCoreHandlers
/* /*
* Top-level request processing: * Top-level request processing:
*/ */
InferEndpointType.Descriptor, ResolveRequestUri.Descriptor,
ValidateTransportSecurityRequirement.Descriptor, ValidateTransportSecurityRequirement.Descriptor,
ValidateHostHeader.Descriptor,
/* /*
* Authentication processing: * Authentication processing:
*/ */
ResolveRequestForgeryProtection.Descriptor, ResolveRequestForgeryProtection.Descriptor,
ValidateEndpointUri.Descriptor,
/* /*
* Challenge processing: * Challenge processing:
@ -59,10 +59,10 @@ public static partial class OpenIddictClientAspNetCoreHandlers
.AddRange(Session.DefaultHandlers); .AddRange(Session.DefaultHandlers);
/// <summary> /// <summary>
/// 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. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary> /// </summary>
public sealed class InferEndpointType : IOpenIddictClientHandler<ProcessRequestContext> public sealed class ResolveRequestUri : IOpenIddictClientHandler<ProcessRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -70,8 +70,9 @@ public static partial class OpenIddictClientAspNetCoreHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<InferEndpointType>() .UseSingletonHandler<ResolveRequestUri>()
.SetOrder(int.MinValue + 50_000) .SetOrder(int.MinValue + 50_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
@ -87,63 +88,37 @@ public static partial class OpenIddictClientAspNetCoreHandlers
var request = context.Transaction.GetHttpRequest() ?? var request = context.Transaction.GetHttpRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
context.EndpointType = // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute
Matches(request, context.Options.PostLogoutRedirectionEndpointUris) ? OpenIddictClientEndpointType.PostLogoutRedirection : // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized
Matches(request, context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection : // to './' once the URI is fully constructed). At this stage of the request processing, rejecting
OpenIddictClientEndpointType.Unknown; // 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
return default; // 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
static bool Matches(HttpRequest request, IReadOnlyList<Uri> addresses) // 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++) { HasValue: true } host => (
{ BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute),
var address = addresses[index]; RequestUri: new Uri(request.GetEncodedUrl(), UriKind.Absolute)),
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;
}
}
else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { HasValue: false } => (
BaseUri: new UriBuilder
{ {
var path = new PathString(address.OriginalString); Scheme = request.Scheme,
if (AreEquivalent(path, request.Path)) Path = request.PathBase.ToUriComponent()
{ }.Uri,
return true; RequestUri: new UriBuilder
} {
} Scheme = request.Scheme,
} Path = (request.PathBase + request.Path).ToUriComponent(),
Query = request.QueryString.ToUriComponent()
return false; }.Uri)
};
// ASP.NET Core's routing system ignores trailing slashes when determining return default;
// 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);
}
} }
} }
@ -161,7 +136,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.AddFilter<RequireTransportSecurityRequirementEnabled>() .AddFilter<RequireTransportSecurityRequirementEnabled>()
.UseSingletonHandler<ValidateTransportSecurityRequirement>() .UseSingletonHandler<ValidateTransportSecurityRequirement>()
.SetOrder(InferEndpointType.Descriptor.Order + 1_000) .SetOrder(InferEndpointType.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -178,18 +153,58 @@ public static partial class OpenIddictClientAspNetCoreHandlers
var request = context.Transaction.GetHttpRequest() ?? var request = context.Transaction.GetHttpRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// 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) 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; return default;
} }
if (!request.IsHttps) return default;
}
}
/// <summary>
/// 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.
/// </summary>
public sealed class ValidateHostHeader : IOpenIddictClientHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ValidateHostHeader>()
.SetOrder(ValidateTransportSecurityRequirement.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2083), description: SR.FormatID2081(HeaderNames.Host),
uri: SR.FormatID8000(SR.ID2083)); uri: SR.FormatID8000(SR.ID2081));
return default; return default;
} }
@ -401,110 +416,6 @@ public static partial class OpenIddictClientAspNetCoreHandlers
} }
} }
/// <summary>
/// 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.
/// </summary>
public sealed class ValidateEndpointUri : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateEndpointUri>()
.SetOrder(ResolveRequestForgeryProtection.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for resolving the context-specific properties and parameters stored in the /// 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. /// ASP.NET Core authentication properties specified by the application that triggered the challenge operation.

1
src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs

@ -16,6 +16,7 @@ public static class OpenIddictClientOwinConstants
public const string CacheControl = "Cache-Control"; public const string CacheControl = "Cache-Control";
public const string ContentType = "Content-Type"; public const string ContentType = "Content-Type";
public const string Expires = "Expires"; public const string Expires = "Expires";
public const string Host = "Host";
public const string Pragma = "Pragma"; public const string Pragma = "Pragma";
} }

248
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs

@ -15,7 +15,6 @@ using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using Owin; using Owin;
using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants; using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants;
using Properties = OpenIddict.Client.Owin.OpenIddictClientOwinConstants.Properties; using Properties = OpenIddict.Client.Owin.OpenIddictClientOwinConstants.Properties;
@ -29,14 +28,14 @@ public static partial class OpenIddictClientOwinHandlers
/* /*
* Top-level request processing: * Top-level request processing:
*/ */
InferEndpointType.Descriptor, ResolveRequestUri.Descriptor,
ValidateTransportSecurityRequirement.Descriptor, ValidateTransportSecurityRequirement.Descriptor,
ValidateHostHeader.Descriptor,
/* /*
* Authentication processing: * Authentication processing:
*/ */
ResolveRequestForgeryProtection.Descriptor, ResolveRequestForgeryProtection.Descriptor,
ValidateEndpointUri.Descriptor,
/* /*
* Challenge processing: * Challenge processing:
@ -55,10 +54,10 @@ public static partial class OpenIddictClientOwinHandlers
.AddRange(Session.DefaultHandlers); .AddRange(Session.DefaultHandlers);
/// <summary> /// <summary>
/// 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. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary> /// </summary>
public sealed class InferEndpointType : IOpenIddictClientHandler<ProcessRequestContext> public sealed class ResolveRequestUri : IOpenIddictClientHandler<ProcessRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -66,9 +65,7 @@ public static partial class OpenIddictClientOwinHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.UseSingletonHandler<InferEndpointType>() .UseSingletonHandler<ResolveRequestUri>()
// Note: this handler must be invoked before any other handler,
// including the built-in handlers defined in OpenIddict.Client.
.SetOrder(int.MinValue + 50_000) .SetOrder(int.MinValue + 50_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -86,63 +83,37 @@ public static partial class OpenIddictClientOwinHandlers
var request = context.Transaction.GetOwinRequest() ?? var request = context.Transaction.GetOwinRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
context.EndpointType = // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute
Matches(request, context.Options.PostLogoutRedirectionEndpointUris) ? OpenIddictClientEndpointType.PostLogoutRedirection : // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized
Matches(request, context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection : // to './' once the URI is fully constructed). At this stage of the request processing, rejecting
OpenIddictClientEndpointType.Unknown; // 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; (context.BaseUri, context.RequestUri) = request.Host switch
static bool Matches(IOwinRequest request, IReadOnlyList<Uri> addresses)
{ {
for (var index = 0; index < addresses.Count; index++) { Value.Length: > 0 } host => (
{ BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute),
var address = addresses[index]; RequestUri: request.Uri),
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;
}
}
else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { Value: null or { Length: 0 } } => (
BaseUri: new UriBuilder
{ {
var path = new PathString(address.OriginalString); Scheme = request.Scheme,
if (AreEquivalent(path, request.Path)) Path = request.PathBase.ToUriComponent()
{ }.Uri,
return true; RequestUri: new UriBuilder
} {
} Scheme = request.Scheme,
} Path = (request.PathBase + request.Path).ToUriComponent(),
Query = request.QueryString.ToUriComponent()
return false; }.Uri)
};
// ASP.NET MVC's routing system ignores trailing slashes when determining return default;
// 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);
}
} }
} }
@ -160,7 +131,7 @@ public static partial class OpenIddictClientOwinHandlers
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.AddFilter<RequireTransportSecurityRequirementEnabled>() .AddFilter<RequireTransportSecurityRequirementEnabled>()
.UseSingletonHandler<ValidateTransportSecurityRequirement>() .UseSingletonHandler<ValidateTransportSecurityRequirement>()
.SetOrder(InferEndpointType.Descriptor.Order + 1_000) .SetOrder(InferEndpointType.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -177,7 +148,7 @@ public static partial class OpenIddictClientOwinHandlers
var request = context.Transaction.GetOwinRequest() ?? var request = context.Transaction.GetOwinRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); 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) if (context.EndpointType is OpenIddictClientEndpointType.Unknown)
{ {
return default; return default;
@ -197,6 +168,52 @@ public static partial class OpenIddictClientOwinHandlers
} }
} }
/// <summary>
/// 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.
/// </summary>
public sealed class ValidateHostHeader : IOpenIddictClientHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ValidateHostHeader>()
.SetOrder(InferEndpointType.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for extracting OpenID Connect requests from GET or POST HTTP requests. /// 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. /// 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
} }
} }
/// <summary>
/// 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.
/// </summary>
public sealed class ValidateEndpointUri : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireOwinRequest>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateEndpointUri>()
.SetOrder(ResolveRequestForgeryProtection.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for resolving the context-specific properties and parameters stored in the /// 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. /// OWIN authentication properties specified by the application that triggered the challenge operation.

17
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Exchange.cs

@ -53,7 +53,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask HandleAsync(PrepareTokenRequestContext context) public ValueTask HandleAsync(PrepareTokenRequestContext context)
{ {
if (context is null) 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 no client identifier was attached to the request, skip the following logic.
if (string.IsNullOrEmpty(context.Request.ClientId)) if (string.IsNullOrEmpty(context.Request.ClientId))
{ {
return; return default;
}
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));
} }
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication. // 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 // See https://tools.ietf.org/html/rfc8414#section-2
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information. // 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. // Important: the credentials MUST be formURL-encoded before being base64-encoded.
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder() 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; context.Request.ClientId = context.Request.ClientSecret = null;
} }
return default;
static string? EscapeDataString(string? value) static string? EscapeDataString(string? value)
=> value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null; => value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null;
} }

16
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -1126,6 +1126,22 @@ public sealed class OpenIddictClientBuilder
public OpenIddictClientBuilder SetStateTokenLifetime(TimeSpan? lifetime) public OpenIddictClientBuilder SetStateTokenLifetime(TimeSpan? lifetime)
=> Configure(options => options.StateTokenLifetime = lifetime); => Configure(options => options.StateTokenLifetime = lifetime);
/// <summary>
/// Sets the client URI, which is used as the value for the "issuer" claim.
/// </summary>
/// <param name="address">The client URI.</param>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientBuilder SetClientUri(Uri address)
{
if (address is null)
{
throw new ArgumentNullException(nameof(address));
}
return Configure(options => options.ClientUri = address);
}
/// <inheritdoc/> /// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj); public override bool Equals(object? obj) => base.Equals(obj);

20
src/OpenIddict.Client/OpenIddictClientConfiguration.cs

@ -70,23 +70,9 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0313)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0313));
} }
registration.MetadataAddress ??= new Uri(".well-known/openid-configuration", UriKind.Relative); registration.MetadataAddress = OpenIddictHelpers.CreateAbsoluteUri(
registration.Issuer,
if (!registration.MetadataAddress.IsAbsoluteUri) registration.MetadataAddress ?? new Uri(".well-known/openid-configuration", UriKind.Relative));
{
var issuer = registration.Issuer;
if (!issuer.OriginalString.EndsWith("/", StringComparison.Ordinal))
{
issuer = new Uri(issuer.OriginalString + "/", UriKind.Absolute);
}
if (registration.MetadataAddress.OriginalString.StartsWith("/", StringComparison.Ordinal))
{
registration.MetadataAddress = new Uri(registration.MetadataAddress.OriginalString[1..], UriKind.Relative);
}
registration.MetadataAddress = new Uri(issuer, registration.MetadataAddress);
}
registration.ConfigurationManager = new ConfigurationManager<OpenIddictConfiguration>( registration.ConfigurationManager = new ConfigurationManager<OpenIddictConfiguration>(
registration.MetadataAddress.AbsoluteUri, new OpenIddictClientRetriever(_service, registration)) registration.MetadataAddress.AbsoluteUri, new OpenIddictClientRetriever(_service, registration))

42
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -39,6 +39,24 @@ public static partial class OpenIddictClientEvents
set => Transaction.EndpointType = value; set => Transaction.EndpointType = value;
} }
/// <summary>
/// Gets or sets the request <see cref="Uri"/> of the current transaction, if available.
/// </summary>
public Uri? RequestUri
{
get => Transaction.RequestUri;
set => Transaction.RequestUri = value;
}
/// <summary>
/// Gets or sets the base <see cref="Uri"/> of the host, if available.
/// </summary>
public Uri? BaseUri
{
get => Transaction.BaseUri;
set => Transaction.BaseUri = value;
}
/// <summary> /// <summary>
/// Gets the logger responsible for logging processed operations. /// Gets the logger responsible for logging processed operations.
/// </summary> /// </summary>
@ -49,15 +67,6 @@ public static partial class OpenIddictClientEvents
/// </summary> /// </summary>
public OpenIddictClientOptions Options => Transaction.Options; public OpenIddictClientOptions Options => Transaction.Options;
/// <summary>
/// Gets or sets the issuer used for the current request.
/// </summary>
public Uri? Issuer
{
get => Transaction.Issuer;
set => Transaction.Issuer = value;
}
/// <summary> /// <summary>
/// Gets or sets the server configuration used for the current request. /// Gets or sets the server configuration used for the current request.
/// </summary> /// </summary>
@ -299,6 +308,11 @@ public static partial class OpenIddictClientEvents
/// </summary> /// </summary>
public Dictionary<string, string?> Properties { get; } = new(StringComparer.Ordinal); public Dictionary<string, string?> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the issuer used for the authentication demand, if applicable.
/// </summary>
public Uri? Issuer { get; set; }
/// <summary> /// <summary>
/// Gets or sets the grant type used for the authentication demand, if applicable. /// Gets or sets the grant type used for the authentication demand, if applicable.
/// </summary> /// </summary>
@ -714,6 +728,11 @@ public static partial class OpenIddictClientEvents
/// </summary> /// </summary>
public Dictionary<string, string?> Properties { get; } = new(StringComparer.Ordinal); public Dictionary<string, string?> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the issuer used for the challenge demand, if applicable.
/// </summary>
public Uri? Issuer { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the provider that will be /// Gets or sets the name of the provider that will be
/// used to resolve the issuer identity, if applicable. /// used to resolve the issuer identity, if applicable.
@ -871,6 +890,11 @@ public static partial class OpenIddictClientEvents
/// </summary> /// </summary>
public Dictionary<string, string?> Properties { get; } = new(StringComparer.Ordinal); public Dictionary<string, string?> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the issuer used for the sign-out demand, if applicable.
/// </summary>
public Uri? Issuer { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the provider that will be /// Gets or sets the name of the provider that will be
/// used to resolve the issuer identity, if applicable. /// used to resolve the issuer identity, if applicable.

3
src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs

@ -225,7 +225,8 @@ public static partial class OpenIddictClientHandlers
return default; 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( context.Reject(
error: Errors.ServerError, error: Errors.ServerError,

31
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. // When only state tokens are considered valid, use the token validation parameters of the client.
1 when context.ValidTokenTypes.Contains(TokenTypeHints.StateToken) 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. // Otherwise, use the token validation parameters of the authorization server.
_ => GetServerTokenValidationParameters(context.Registration, context.Configuration) _ => GetServerTokenValidationParameters(context.Registration, context.Configuration)
@ -94,10 +94,27 @@ public static partial class OpenIddictClientHandlers
return default; return default;
static TokenValidationParameters GetClientTokenValidationParameters(OpenIddictClientOptions options) static TokenValidationParameters GetClientTokenValidationParameters(Uri? address, OpenIddictClientOptions options)
{ {
var parameters = options.TokenValidationParameters.Clone(); 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. // For state tokens, only the short "oi_stet+jwt" form is valid.
parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.StateToken }; 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 // 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. // 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. // 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. uri.AbsoluteUri, // Uri.AbsoluteUri is normalized and always contains a trailing slash.
issuer.AbsoluteUri[..^1] uri.AbsoluteUri[..^1]
}, },
Uri issuer => new[] { issuer.AbsoluteUri } Uri uri => new[] { uri.AbsoluteUri }
}; };
parameters.ValidateIssuer = parameters.ValidIssuers is not null; parameters.ValidateIssuer = parameters.ValidIssuers is not null;

244
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -11,6 +11,7 @@ using System.Runtime.InteropServices;
using System.Security.Claims; using System.Security.Claims;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions; using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions; using static OpenIddict.Abstractions.OpenIddictExceptions;
@ -21,6 +22,11 @@ namespace OpenIddict.Client;
public static partial class OpenIddictClientHandlers public static partial class OpenIddictClientHandlers
{ {
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create( public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Top-level request processing:
*/
InferEndpointType.Descriptor,
/* /*
* Authentication processing: * Authentication processing:
*/ */
@ -33,6 +39,7 @@ public static partial class OpenIddictClientHandlers
RedeemStateTokenEntry.Descriptor, RedeemStateTokenEntry.Descriptor,
ValidateStateTokenEndpointType.Descriptor, ValidateStateTokenEndpointType.Descriptor,
ValidateRequestForgeryProtection.Descriptor, ValidateRequestForgeryProtection.Descriptor,
ValidateEndpointUri.Descriptor,
ResolveClientRegistrationFromStateToken.Descriptor, ResolveClientRegistrationFromStateToken.Descriptor,
ValidateIssuerParameter.Descriptor, ValidateIssuerParameter.Descriptor,
HandleFrontchannelErrorResponse.Descriptor, HandleFrontchannelErrorResponse.Descriptor,
@ -100,9 +107,9 @@ public static partial class OpenIddictClientHandlers
AttachScopes.Descriptor, AttachScopes.Descriptor,
AttachNonce.Descriptor, AttachNonce.Descriptor,
AttachCodeChallengeParameters.Descriptor, AttachCodeChallengeParameters.Descriptor,
PrepareStateTokenPrincipal.Descriptor, PrepareLoginStateTokenPrincipal.Descriptor,
ValidateRedirectUriParameter.Descriptor, ValidateRedirectUriParameter.Descriptor,
GenerateStateToken.Descriptor, GenerateLoginStateToken.Descriptor,
AttachChallengeParameters.Descriptor, AttachChallengeParameters.Descriptor,
AttachCustomChallengeParameters.Descriptor, AttachCustomChallengeParameters.Descriptor,
@ -134,6 +141,86 @@ public static partial class OpenIddictClientHandlers
.AddRange(Session.DefaultHandlers) .AddRange(Session.DefaultHandlers)
.AddRange(Userinfo.DefaultHandlers); .AddRange(Userinfo.DefaultHandlers);
/// <summary>
/// Contains the logic responsible for inferring the endpoint type from the request address.
/// </summary>
public sealed class InferEndpointType : IOpenIddictClientHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.UseSingletonHandler<InferEndpointType>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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<Uri> 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 '/'));
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting invalid authentication demands. /// Contains the logic responsible for rejecting invalid authentication demands.
/// </summary> /// </summary>
@ -277,12 +364,6 @@ public static partial class OpenIddictClientHandlers
context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); 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. // Ensure the selected grant type, if explicitly set, is listed as supported in the configuration.
if (!string.IsNullOrEmpty(context.GrantType) && if (!string.IsNullOrEmpty(context.GrantType) &&
!context.Configuration.GrantTypesSupported.Contains(context.GrantType)) !context.Configuration.GrantTypesSupported.Contains(context.GrantType))
@ -655,6 +736,108 @@ public static partial class OpenIddictClientHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for comparing the current request URL to the expected URL stored in the state token.
/// </summary>
public sealed class ValidateEndpointUri : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateEndpointUri>()
.SetOrder(ValidateRequestForgeryProtection.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for resolving the client registration /// Contains the logic responsible for resolving the client registration
/// based on the authorization server identity stored in the state token. /// 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. // Resolve and attach the server configuration to the context.
context.Configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? context.Configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); 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. // Resolve and attach the server configuration to the context if none has been set already.
context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); 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. // 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 // 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 // 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. // 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; return default;
} }
@ -4303,7 +4477,7 @@ public static partial class OpenIddictClientHandlers
/// Contains the logic responsible for preparing and attaching the claims principal /// Contains the logic responsible for preparing and attaching the claims principal
/// used to generate the state token, if one is going to be returned. /// used to generate the state token, if one is going to be returned.
/// </summary> /// </summary>
public sealed class PrepareStateTokenPrincipal : IOpenIddictClientHandler<ProcessChallengeContext> public sealed class PrepareLoginStateTokenPrincipal : IOpenIddictClientHandler<ProcessChallengeContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -4311,7 +4485,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireLoginStateTokenGenerated>() .AddFilter<RequireLoginStateTokenGenerated>()
.UseSingletonHandler<PrepareStateTokenPrincipal>() .UseSingletonHandler<PrepareLoginStateTokenPrincipal>()
.SetOrder(AttachCodeChallengeParameters.Descriptor.Order + 1_000) .SetOrder(AttachCodeChallengeParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -4360,6 +4534,9 @@ public static partial class OpenIddictClientHandlers
principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); 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 // 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 // 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 // are generally required to ensure the state token was not replaced with a state token
@ -4418,11 +4595,11 @@ public static partial class OpenIddictClientHandlers
/// <summary> /// <summary>
/// Contains the logic responsible for generating a state token for the current challenge operation. /// Contains the logic responsible for generating a state token for the current challenge operation.
/// </summary> /// </summary>
public sealed class GenerateStateToken : IOpenIddictClientHandler<ProcessChallengeContext> public sealed class GenerateLoginStateToken : IOpenIddictClientHandler<ProcessChallengeContext>
{ {
private readonly IOpenIddictClientDispatcher _dispatcher; private readonly IOpenIddictClientDispatcher _dispatcher;
public GenerateStateToken(IOpenIddictClientDispatcher dispatcher) public GenerateLoginStateToken(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary> /// <summary>
@ -4431,7 +4608,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireLoginStateTokenGenerated>() .AddFilter<RequireLoginStateTokenGenerated>()
.UseScopedHandler<GenerateStateToken>() .UseScopedHandler<GenerateLoginStateToken>()
.SetOrder(100_000) .SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -4493,7 +4670,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>() .AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<ValidateRedirectUriParameter>() .UseSingletonHandler<ValidateRedirectUriParameter>()
.SetOrder(GenerateStateToken.Descriptor.Order + 1_000) .SetOrder(GenerateLoginStateToken.Descriptor.Order + 1_000)
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
@ -4724,12 +4901,6 @@ public static partial class OpenIddictClientHandlers
// Resolve and attach the server configuration to the context if none has been set already. // Resolve and attach the server configuration to the context if none has been set already.
context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); 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. // 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; return default;
} }
@ -4973,6 +5146,9 @@ public static partial class OpenIddictClientHandlers
principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); 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 // Store the identity of the authorization server in the state token
// principal to allow resolving it when handling the post-logout callback. // principal to allow resolving it when handling the post-logout callback.
// //

6
src/OpenIddict.Client/OpenIddictClientOptions.cs

@ -15,6 +15,12 @@ namespace OpenIddict.Client;
/// </summary> /// </summary>
public sealed class OpenIddictClientOptions public sealed class OpenIddictClientOptions
{ {
/// <summary>
/// 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.
/// </summary>
public Uri? ClientUri { get; set; }
/// <summary> /// <summary>
/// Gets the list of the handlers responsible for processing the OpenIddict client operations. /// 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. /// Note: the list is automatically sorted based on the order assigned to each handler descriptor.

16
src/OpenIddict.Client/OpenIddictClientService.cs

@ -571,7 +571,6 @@ public sealed class OpenIddictClientService
var context = new PrepareConfigurationRequestContext(transaction) var context = new PrepareConfigurationRequestContext(transaction)
{ {
Address = address, Address = address,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -593,7 +592,6 @@ public sealed class OpenIddictClientService
var context = new ApplyConfigurationRequestContext(transaction) var context = new ApplyConfigurationRequestContext(transaction)
{ {
Address = address, Address = address,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -617,7 +615,6 @@ public sealed class OpenIddictClientService
var context = new ExtractConfigurationResponseContext(transaction) var context = new ExtractConfigurationResponseContext(transaction)
{ {
Address = address, Address = address,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -643,7 +640,6 @@ public sealed class OpenIddictClientService
var context = new HandleConfigurationResponseContext(transaction) var context = new HandleConfigurationResponseContext(transaction)
{ {
Address = address, Address = address,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request, Request = request,
Response = response Response = response
@ -730,7 +726,6 @@ public sealed class OpenIddictClientService
var context = new PrepareCryptographyRequestContext(transaction) var context = new PrepareCryptographyRequestContext(transaction)
{ {
Address = address, Address = address,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -752,7 +747,6 @@ public sealed class OpenIddictClientService
var context = new ApplyCryptographyRequestContext(transaction) var context = new ApplyCryptographyRequestContext(transaction)
{ {
Address = address, Address = address,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -776,7 +770,6 @@ public sealed class OpenIddictClientService
var context = new ExtractCryptographyResponseContext(transaction) var context = new ExtractCryptographyResponseContext(transaction)
{ {
Address = address, Address = address,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -802,7 +795,6 @@ public sealed class OpenIddictClientService
var context = new HandleCryptographyResponseContext(transaction) var context = new HandleCryptographyResponseContext(transaction)
{ {
Address = address, Address = address,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request, Request = request,
Response = response Response = response
@ -898,7 +890,6 @@ public sealed class OpenIddictClientService
{ {
Address = address, Address = address,
Configuration = configuration, Configuration = configuration,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -921,7 +912,6 @@ public sealed class OpenIddictClientService
{ {
Address = address, Address = address,
Configuration = configuration, Configuration = configuration,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -946,7 +936,6 @@ public sealed class OpenIddictClientService
{ {
Address = address, Address = address,
Configuration = configuration, Configuration = configuration,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -973,7 +962,6 @@ public sealed class OpenIddictClientService
{ {
Address = address, Address = address,
Configuration = configuration, Configuration = configuration,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request, Request = request,
Response = response Response = response
@ -1063,7 +1051,6 @@ public sealed class OpenIddictClientService
{ {
Address = address, Address = address,
Configuration = configuration, Configuration = configuration,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -1086,7 +1073,6 @@ public sealed class OpenIddictClientService
{ {
Address = address, Address = address,
Configuration = configuration, Configuration = configuration,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -1111,7 +1097,6 @@ public sealed class OpenIddictClientService
{ {
Address = address, Address = address,
Configuration = configuration, Configuration = configuration,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request Request = request
}; };
@ -1138,7 +1123,6 @@ public sealed class OpenIddictClientService
{ {
Address = address, Address = address,
Configuration = configuration, Configuration = configuration,
Issuer = registration.Issuer,
Registration = registration, Registration = registration,
Request = request, Request = request,
Response = response, Response = response,

9
src/OpenIddict.Client/OpenIddictClientTransaction.cs

@ -21,9 +21,14 @@ public sealed class OpenIddictClientTransaction
public OpenIddictClientEndpointType EndpointType { get; set; } public OpenIddictClientEndpointType EndpointType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the issuer address associated with the current transaction, if available. /// Gets or sets the request <see cref="Uri"/> of the current transaction, if available.
/// </summary> /// </summary>
public Uri? Issuer { get; set; } public Uri? RequestUri { get; set; }
/// <summary>
/// Gets or sets the base <see cref="Uri"/> of the host, if available.
/// </summary>
public Uri? BaseUri { get; set; }
/// <summary> /// <summary>
/// Gets or sets the logger associated with the current request. /// Gets or sets the logger associated with the current request.

2
src/OpenIddict.Owin/OpenIddict.Owin.csproj

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Description>Versatile OpenID Connect stack for OWIN/Katana 4.1 (compatible with ASP.NET 4.6.1 and newer).</Description> <Description>Versatile OpenID Connect stack for OWIN/Katana (compatible with ASP.NET 4.6.1 and newer).</Description>
<PackageTags>$(PackageTags);aspnet;katana;owin;client;server;validation</PackageTags> <PackageTags>$(PackageTags);aspnet;katana;owin;client;server;validation</PackageTags>
</PropertyGroup> </PropertyGroup>

8
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs

@ -113,8 +113,8 @@ public static partial class OpenIddictServerAspNetCoreHandlers
} }
var parameters = context.Options.TokenValidationParameters.Clone(); var parameters = context.Options.TokenValidationParameters.Clone();
parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri;
parameters.ValidAudience = context.Issuer?.AbsoluteUri; parameters.ValidAudience ??= parameters.ValidIssuer;
parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.AuthorizationRequest }; parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.AuthorizationRequest };
var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters); 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. // Store the serialized authorization request parameters in the distributed cache.
var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor
{ {
Audience = context.Issuer?.AbsoluteUri, Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
EncryptingCredentials = context.Options.EncryptionCredentials.First(), EncryptingCredentials = context.Options.EncryptionCredentials.First(),
Issuer = context.Issuer?.AbsoluteUri, Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(), SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType), Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType),
TokenType = JsonWebTokenTypes.Private.AuthorizationRequest TokenType = JsonWebTokenTypes.Private.AuthorizationRequest

8
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs

@ -110,8 +110,8 @@ public static partial class OpenIddictServerAspNetCoreHandlers
} }
var parameters = context.Options.TokenValidationParameters.Clone(); var parameters = context.Options.TokenValidationParameters.Clone();
parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri;
parameters.ValidAudience = context.Issuer?.AbsoluteUri; parameters.ValidAudience ??= parameters.ValidIssuer;
parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.LogoutRequest }; parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.LogoutRequest };
var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters); 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. // Store the serialized logout request parameters in the distributed cache.
var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor
{ {
Audience = context.Issuer?.AbsoluteUri, Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
EncryptingCredentials = context.Options.EncryptionCredentials.First(), EncryptingCredentials = context.Options.EncryptionCredentials.First(),
Issuer = context.Issuer?.AbsoluteUri, Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(), SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType), Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType),
TokenType = JsonWebTokenTypes.Private.LogoutRequest TokenType = JsonWebTokenTypes.Private.LogoutRequest

160
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs

@ -12,6 +12,7 @@ using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -30,9 +31,9 @@ public static partial class OpenIddictServerAspNetCoreHandlers
/* /*
* Top-level request processing: * Top-level request processing:
*/ */
InferEndpointType.Descriptor, ResolveRequestUri.Descriptor,
InferIssuerFromHost.Descriptor,
ValidateTransportSecurityRequirement.Descriptor, ValidateTransportSecurityRequirement.Descriptor,
ValidateHostHeader.Descriptor,
/* /*
* Challenge processing: * Challenge processing:
@ -59,10 +60,10 @@ public static partial class OpenIddictServerAspNetCoreHandlers
.AddRange(Userinfo.DefaultHandlers); .AddRange(Userinfo.DefaultHandlers);
/// <summary> /// <summary>
/// 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. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary> /// </summary>
public sealed class InferEndpointType : IOpenIddictServerHandler<ProcessRequestContext> public sealed class ResolveRequestUri : IOpenIddictServerHandler<ProcessRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -70,7 +71,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<InferEndpointType>() .UseSingletonHandler<ResolveRequestUri>()
.SetOrder(int.MinValue + 50_000) .SetOrder(int.MinValue + 50_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -88,84 +89,45 @@ public static partial class OpenIddictServerAspNetCoreHandlers
var request = context.Transaction.GetHttpRequest() ?? var request = context.Transaction.GetHttpRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
context.EndpointType = // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute
Matches(request, context.Options.AuthorizationEndpointUris) ? OpenIddictServerEndpointType.Authorization : // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized
Matches(request, context.Options.ConfigurationEndpointUris) ? OpenIddictServerEndpointType.Configuration : // to './' once the URI is fully constructed). At this stage of the request processing, rejecting
Matches(request, context.Options.CryptographyEndpointUris) ? OpenIddictServerEndpointType.Cryptography : // requests that lack the host information (e.g because HTTP/1.0 was used and no Host header was
Matches(request, context.Options.DeviceEndpointUris) ? OpenIddictServerEndpointType.Device : // sent by the HTTP client) is not desirable as it would affect all requests, including requests
Matches(request, context.Options.IntrospectionEndpointUris) ? OpenIddictServerEndpointType.Introspection : // that are not meant to be handled by OpenIddict itself. To avoid that, a fake host is temporarily
Matches(request, context.Options.LogoutEndpointUris) ? OpenIddictServerEndpointType.Logout : // used to build an absolute base URI and a request URI that will be used to determine whether the
Matches(request, context.Options.RevocationEndpointUris) ? OpenIddictServerEndpointType.Revocation : // received request matches one of the addresses assigned to an OpenIddict endpoint. If the request
Matches(request, context.Options.TokenEndpointUris) ? OpenIddictServerEndpointType.Token : // is later handled by OpenIddict, an additional check will be made to require the Host header.
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;
static bool Matches(HttpRequest request, IReadOnlyList<Uri> addresses) (context.BaseUri, context.RequestUri) = request.Host switch
{ {
for (var index = 0; index < addresses.Count; index++) { HasValue: true } host => (
{ BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute),
var address = addresses[index]; RequestUri: new Uri(request.GetEncodedUrl(), UriKind.Absolute)),
if (address.IsAbsoluteUri)
{ HasValue: false } => (
BaseUri: new UriBuilder
{ {
// If the request host is not available (e.g because HTTP/1.0 was used), ignore absolute URLs. Scheme = request.Scheme,
if (!request.Host.HasValue) Path = request.PathBase.ToUriComponent()
{ }.Uri,
continue; RequestUri: new UriBuilder
}
// 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))
{ {
var path = new PathString(address.OriginalString); Scheme = request.Scheme,
if (AreEquivalent(path, request.Path)) Path = (request.PathBase + request.Path).ToUriComponent(),
{ Query = request.QueryString.ToUriComponent()
return true; }.Uri)
} };
}
}
return false;
// ASP.NET Core's routing system ignores trailing slashes when determining return default;
// 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);
}
} }
} }
/// <summary> /// <summary>
/// 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. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary> /// </summary>
public sealed class InferIssuerFromHost : IOpenIddictServerHandler<ProcessRequestContext> public sealed class ValidateTransportSecurityRequirement : IOpenIddictServerHandler<ProcessRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -173,8 +135,9 @@ public static partial class OpenIddictServerAspNetCoreHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<InferIssuerFromHost>() .AddFilter<RequireTransportSecurityRequirementEnabled>()
.SetOrder(InferEndpointType.Descriptor.Order + 1_000) .UseSingletonHandler<ValidateTransportSecurityRequirement>()
.SetOrder(InferEndpointType.Descriptor.Order + 250)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -191,45 +154,26 @@ public static partial class OpenIddictServerAspNetCoreHandlers
var request = context.Transaction.GetHttpRequest() ?? var request = context.Transaction.GetHttpRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Don't require that the request host be present if the request is not handled // Don't require that transport security be used if the request is not handled by OpenIddict.
// by an OpenIddict endpoint or if an explicit issuer URL was already set. if (context.EndpointType is not OpenIddictServerEndpointType.Unknown && !request.IsHttps)
if (context.Issuer is not null || context.EndpointType is OpenIddictServerEndpointType.Unknown)
{
return default;
}
if (!request.Host.HasValue)
{ {
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,
description: SR.FormatID2081(HeaderNames.Host), description: SR.GetResourceString(SR.ID2083),
uri: SR.FormatID8000(SR.ID2081)); uri: SR.FormatID8000(SR.ID2083));
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));
return default; return default;
} }
context.Issuer = issuer;
return default; return default;
} }
} }
/// <summary> /// <summary>
/// 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. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary> /// </summary>
public sealed class ValidateTransportSecurityRequirement : IOpenIddictServerHandler<ProcessRequestContext> public sealed class ValidateHostHeader : IOpenIddictServerHandler<ProcessRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -237,9 +181,8 @@ public static partial class OpenIddictServerAspNetCoreHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.AddFilter<RequireTransportSecurityRequirementEnabled>() .UseSingletonHandler<ValidateHostHeader>()
.UseSingletonHandler<ValidateTransportSecurityRequirement>() .SetOrder(ValidateTransportSecurityRequirement.Descriptor.Order + 250)
.SetOrder(InferIssuerFromHost.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -256,18 +199,13 @@ public static partial class OpenIddictServerAspNetCoreHandlers
var request = context.Transaction.GetHttpRequest() ?? var request = context.Transaction.GetHttpRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Don't require that the host be present if the request is not handled by OpenIddict. // Don't require that the request host be present if the request is not handled by OpenIddict.
if (context.EndpointType is OpenIddictServerEndpointType.Unknown) if (context.EndpointType is not OpenIddictServerEndpointType.Unknown && !request.Host.HasValue)
{
return default;
}
if (!request.IsHttps)
{ {
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2083), description: SR.FormatID2081(HeaderNames.Host),
uri: SR.FormatID8000(SR.ID2083)); uri: SR.FormatID8000(SR.ID2081));
return default; return default;
} }

8
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs

@ -112,8 +112,8 @@ public static partial class OpenIddictServerOwinHandlers
} }
var parameters = context.Options.TokenValidationParameters.Clone(); var parameters = context.Options.TokenValidationParameters.Clone();
parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri;
parameters.ValidAudience = context.Issuer?.AbsoluteUri; parameters.ValidAudience ??= parameters.ValidIssuer;
parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.AuthorizationRequest }; parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.AuthorizationRequest };
var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters); 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. // Store the serialized authorization request parameters in the distributed cache.
var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor
{ {
Audience = context.Issuer?.AbsoluteUri, Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
EncryptingCredentials = context.Options.EncryptionCredentials.First(), EncryptingCredentials = context.Options.EncryptionCredentials.First(),
Issuer = context.Issuer?.AbsoluteUri, Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(), SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType), Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType),
TokenType = JsonWebTokenTypes.Private.AuthorizationRequest TokenType = JsonWebTokenTypes.Private.AuthorizationRequest

8
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs

@ -110,8 +110,8 @@ public static partial class OpenIddictServerOwinHandlers
} }
var parameters = context.Options.TokenValidationParameters.Clone(); var parameters = context.Options.TokenValidationParameters.Clone();
parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; parameters.ValidIssuer ??= (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri;
parameters.ValidAudience = context.Issuer?.AbsoluteUri; parameters.ValidAudience ??= parameters.ValidIssuer;
parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.LogoutRequest }; parameters.ValidTypes = new[] { JsonWebTokenTypes.Private.LogoutRequest };
var result = await context.Options.JsonWebTokenHandler.ValidateTokenAsync(token, parameters); 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. // Store the serialized logout request parameters in the distributed cache.
var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor var token = context.Options.JsonWebTokenHandler.CreateToken(new SecurityTokenDescriptor
{ {
Audience = context.Issuer?.AbsoluteUri, Audience = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
EncryptingCredentials = context.Options.EncryptionCredentials.First(), EncryptingCredentials = context.Options.EncryptionCredentials.First(),
Issuer = context.Issuer?.AbsoluteUri, Issuer = (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri,
SigningCredentials = context.Options.SigningCredentials.First(), SigningCredentials = context.Options.SigningCredentials.First(),
Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType), Subject = new ClaimsIdentity(claims, TokenValidationParameters.DefaultAuthenticationType),
TokenType = JsonWebTokenTypes.Private.LogoutRequest TokenType = JsonWebTokenTypes.Private.LogoutRequest

162
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs

@ -26,9 +26,9 @@ public static partial class OpenIddictServerOwinHandlers
/* /*
* Top-level request processing: * Top-level request processing:
*/ */
InferEndpointType.Descriptor, ResolveRequestUri.Descriptor,
InferIssuerFromHost.Descriptor,
ValidateTransportSecurityRequirement.Descriptor, ValidateTransportSecurityRequirement.Descriptor,
ValidateHostHeader.Descriptor,
/* /*
* Challenge processing: * Challenge processing:
@ -55,10 +55,10 @@ public static partial class OpenIddictServerOwinHandlers
.AddRange(Userinfo.DefaultHandlers); .AddRange(Userinfo.DefaultHandlers);
/// <summary> /// <summary>
/// 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. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary> /// </summary>
public sealed class InferEndpointType : IOpenIddictServerHandler<ProcessRequestContext> public sealed class ResolveRequestUri : IOpenIddictServerHandler<ProcessRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -66,9 +66,7 @@ public static partial class OpenIddictServerOwinHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.UseSingletonHandler<InferEndpointType>() .UseSingletonHandler<ResolveRequestUri>()
// Note: this handler must be invoked before any other handler,
// including the built-in handlers defined in OpenIddict.Server.
.SetOrder(int.MinValue + 50_000) .SetOrder(int.MinValue + 50_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -86,84 +84,45 @@ public static partial class OpenIddictServerOwinHandlers
var request = context.Transaction.GetOwinRequest() ?? var request = context.Transaction.GetOwinRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
context.EndpointType = // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute
Matches(request, context.Options.AuthorizationEndpointUris) ? OpenIddictServerEndpointType.Authorization : // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized
Matches(request, context.Options.ConfigurationEndpointUris) ? OpenIddictServerEndpointType.Configuration : // to './' once the URI is fully constructed). At this stage of the request processing, rejecting
Matches(request, context.Options.CryptographyEndpointUris) ? OpenIddictServerEndpointType.Cryptography : // requests that lack the host information (e.g because HTTP/1.0 was used and no Host header was
Matches(request, context.Options.DeviceEndpointUris) ? OpenIddictServerEndpointType.Device : // sent by the HTTP client) is not desirable as it would affect all requests, including requests
Matches(request, context.Options.IntrospectionEndpointUris) ? OpenIddictServerEndpointType.Introspection : // that are not meant to be handled by OpenIddict itself. To avoid that, a fake host is temporarily
Matches(request, context.Options.LogoutEndpointUris) ? OpenIddictServerEndpointType.Logout : // used to build an absolute base URI and a request URI that will be used to determine whether the
Matches(request, context.Options.RevocationEndpointUris) ? OpenIddictServerEndpointType.Revocation : // received request matches one of the addresses assigned to an OpenIddict endpoint. If the request
Matches(request, context.Options.TokenEndpointUris) ? OpenIddictServerEndpointType.Token : // is later handled by OpenIddict, an additional check will be made to require the Host header.
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;
static bool Matches(IOwinRequest request, IReadOnlyList<Uri> addresses) (context.BaseUri, context.RequestUri) = request.Host switch
{ {
for (var index = 0; index < addresses.Count; index++) { Value.Length: > 0 } host => (
{ BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute),
var address = addresses[index]; RequestUri: request.Uri),
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;
}
}
else if (address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { Value: null or { Length: 0 } } => (
BaseUri: new UriBuilder
{ {
var path = new PathString(address.OriginalString); Scheme = request.Scheme,
if (AreEquivalent(path, request.Path)) Path = request.PathBase.ToUriComponent()
{ }.Uri,
return true; RequestUri: new UriBuilder
} {
} Scheme = request.Scheme,
} Path = (request.PathBase + request.Path).ToUriComponent(),
Query = request.QueryString.ToUriComponent()
return false; }.Uri)
};
// ASP.NET MVC's routing system ignores trailing slashes when determining return default;
// 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);
}
} }
} }
/// <summary> /// <summary>
/// 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. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary> /// </summary>
public sealed class InferIssuerFromHost : IOpenIddictServerHandler<ProcessRequestContext> public sealed class ValidateTransportSecurityRequirement : IOpenIddictServerHandler<ProcessRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -171,8 +130,9 @@ public static partial class OpenIddictServerOwinHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.UseSingletonHandler<InferIssuerFromHost>() .AddFilter<RequireTransportSecurityRequirementEnabled>()
.SetOrder(InferEndpointType.Descriptor.Order + 1_000) .UseSingletonHandler<ValidateTransportSecurityRequirement>()
.SetOrder(InferEndpointType.Descriptor.Order + 250)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -189,45 +149,26 @@ public static partial class OpenIddictServerOwinHandlers
var request = context.Transaction.GetOwinRequest() ?? var request = context.Transaction.GetOwinRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Don't require that the request host be present if the request is not handled // Don't require that transport security be used if the request is not handled by OpenIddict.
// by an OpenIddict endpoint or if an explicit issuer URL was already set. if (context.EndpointType is not OpenIddictServerEndpointType.Unknown && !request.IsSecure)
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())
{ {
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,
description: SR.FormatID2082(Headers.Host), description: SR.GetResourceString(SR.ID2083),
uri: SR.FormatID8000(SR.ID2082)); uri: SR.FormatID8000(SR.ID2083));
return default; return default;
} }
context.Issuer = issuer;
return default; return default;
} }
} }
/// <summary> /// <summary>
/// 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. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary> /// </summary>
public sealed class ValidateTransportSecurityRequirement : IOpenIddictServerHandler<ProcessRequestContext> public sealed class ValidateHostHeader : IOpenIddictServerHandler<ProcessRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -235,9 +176,8 @@ public static partial class OpenIddictServerOwinHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.AddFilter<RequireTransportSecurityRequirementEnabled>() .UseSingletonHandler<ValidateHostHeader>()
.UseSingletonHandler<ValidateTransportSecurityRequirement>() .SetOrder(ValidateTransportSecurityRequirement.Descriptor.Order + 250)
.SetOrder(InferIssuerFromHost.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -254,18 +194,14 @@ public static partial class OpenIddictServerOwinHandlers
var request = context.Transaction.GetOwinRequest() ?? var request = context.Transaction.GetOwinRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); 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 the request host be present if the request is not handled by OpenIddict.
if (context.EndpointType is OpenIddictServerEndpointType.Unknown) if (context.EndpointType is not OpenIddictServerEndpointType.Unknown &&
{ string.IsNullOrEmpty(request.Host.Value))
return default;
}
if (!request.IsSecure)
{ {
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2083), description: SR.FormatID2081(Headers.Host),
uri: SR.FormatID8000(SR.ID2083)); uri: SR.FormatID8000(SR.ID2081));
return default; return default;
} }

4
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -1714,8 +1714,8 @@ public sealed class OpenIddictServerBuilder
=> Configure(options => options.UserCodeLifetime = lifetime); => Configure(options => options.UserCodeLifetime = lifetime);
/// <summary> /// <summary>
/// Sets the issuer address, which is used as the base address /// Sets the issuer address, which is used as the value for the "issuer" claim and
/// for the endpoint URIs returned from the discovery endpoint. /// is returned from the discovery endpoint to identify the authorization server.
/// </summary> /// </summary>
/// <param name="address">The issuer address.</param> /// <param name="address">The issuer address.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns> /// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>

5
src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs

@ -86,6 +86,11 @@ public static partial class OpenIddictServerEvents
/// </summary> /// </summary>
public Dictionary<string, OpenIddictParameter> Metadata { get; } = new(StringComparer.Ordinal); public Dictionary<string, OpenIddictParameter> Metadata { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the issuer address.
/// </summary>
public Uri? Issuer { get; set; }
/// <summary> /// <summary>
/// Gets or sets the authorization endpoint address. /// Gets or sets the authorization endpoint address.
/// </summary> /// </summary>

5
src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs

@ -125,6 +125,11 @@ public static partial class OpenIddictServerEvents
/// </summary> /// </summary>
public DateTimeOffset? IssuedAt { get; set; } public DateTimeOffset? IssuedAt { get; set; }
/// <summary>
/// Gets or sets the "iss" claim returned to the caller, if applicable.
/// </summary>
public Uri? Issuer { get; set; }
/// <summary> /// <summary>
/// Gets or sets the "nbf" claim /// Gets or sets the "nbf" claim
/// returned to the caller, if applicable. /// returned to the caller, if applicable.

5
src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs

@ -144,6 +144,11 @@ public static partial class OpenIddictServerEvents
/// </summary> /// </summary>
public string? GivenName { get; set; } public string? GivenName { get; set; }
/// <summary>
/// Gets or sets the value used for the "iss" claim.
/// </summary>
public Uri? Issuer { get; set; }
/// <summary> /// <summary>
/// Gets or sets the value used for the "phone_number" claim. /// Gets or sets the value used for the "phone_number" claim.
/// Note: this value should only be populated if the "phone" /// Note: this value should only be populated if the "phone"

25
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -30,21 +30,30 @@ public static partial class OpenIddictServerEvents
public OpenIddictServerTransaction Transaction { get; } public OpenIddictServerTransaction Transaction { get; }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public Uri? Issuer public OpenIddictServerEndpointType EndpointType
{ {
get => Transaction.Issuer; get => Transaction.EndpointType;
set => Transaction.Issuer = value; set => Transaction.EndpointType = value;
} }
/// <summary> /// <summary>
/// Gets or sets the endpoint type that handled the request, if applicable. /// Gets or sets the request <see cref="Uri"/> of the current transaction, if available.
/// </summary> /// </summary>
public OpenIddictServerEndpointType EndpointType public Uri? RequestUri
{ {
get => Transaction.EndpointType; get => Transaction.RequestUri;
set => Transaction.EndpointType = value; set => Transaction.RequestUri = value;
}
/// <summary>
/// Gets or sets the base <see cref="Uri"/> of the host, if available.
/// </summary>
public Uri? BaseUri
{
get => Transaction.BaseUri;
set => Transaction.BaseUri = value;
} }
/// <summary> /// <summary>

1
src/OpenIddict.Server/OpenIddictServerFactory.cs

@ -30,7 +30,6 @@ public sealed class OpenIddictServerFactory : IOpenIddictServerFactory
public ValueTask<OpenIddictServerTransaction> CreateTransactionAsync() public ValueTask<OpenIddictServerTransaction> CreateTransactionAsync()
=> new(new OpenIddictServerTransaction => new(new OpenIddictServerTransaction
{ {
Issuer = _options.CurrentValue.Issuer,
Logger = _logger, Logger = _logger,
Options = _options.CurrentValue Options = _options.CurrentValue
}); });

17
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. // 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. // 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. context.Response.Iss = (context.Options.Issuer ?? context.BaseUri) switch
if (context.Issuer is not { IsAbsoluteUri: true })
{ {
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. // At this stage, throw an exception if the issuer cannot be retrieved or is not valid.
if (string.IsNullOrEmpty(context.Response.Iss)) _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0023))
{ };
context.Response.Iss = context.Issuer.AbsoluteUri;
}
} }
return default; return default;

101
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -31,6 +31,7 @@ public static partial class OpenIddictServerHandlers
/* /*
* Configuration request handling: * Configuration request handling:
*/ */
AttachIssuer.Descriptor,
AttachEndpoints.Descriptor, AttachEndpoints.Descriptor,
AttachGrantTypes.Descriptor, AttachGrantTypes.Descriptor,
AttachResponseModes.Descriptor, AttachResponseModes.Descriptor,
@ -310,6 +311,35 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for attaching the issuer to the provider discovery document.
/// </summary>
public sealed class AttachIssuer : IOpenIddictServerHandler<HandleConfigurationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>()
.UseSingletonHandler<AttachIssuer>()
.SetOrder(int.MaxValue - 100_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.Issuer = context.Options.Issuer ?? context.BaseUri;
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for attaching the endpoint URLs to the provider discovery document. /// Contains the logic responsible for attaching the endpoint URLs to the provider discovery document.
/// </summary> /// </summary>
@ -321,7 +351,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>()
.UseSingletonHandler<AttachEndpoints>() .UseSingletonHandler<AttachEndpoints>()
.SetOrder(int.MaxValue - 100_000) .SetOrder(AttachIssuer.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -336,68 +366,31 @@ public static partial class OpenIddictServerHandlers
// Note: while OpenIddict allows specifying multiple endpoint addresses, the OAuth 2.0 // Note: while OpenIddict allows specifying multiple endpoint addresses, the OAuth 2.0
// and OpenID Connect discovery specifications only allow a single address per endpoint. // and OpenID Connect discovery specifications only allow a single address per endpoint.
context.AuthorizationEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, context.AuthorizationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.Options.AuthorizationEndpointUris.FirstOrDefault()); context.BaseUri, context.Options.AuthorizationEndpointUris.FirstOrDefault());
context.CryptographyEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, context.CryptographyEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.Options.CryptographyEndpointUris.FirstOrDefault()); context.BaseUri, context.Options.CryptographyEndpointUris.FirstOrDefault());
context.DeviceEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, context.DeviceEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.Options.DeviceEndpointUris.FirstOrDefault()); context.BaseUri, context.Options.DeviceEndpointUris.FirstOrDefault());
context.IntrospectionEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, context.IntrospectionEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.Options.IntrospectionEndpointUris.FirstOrDefault()); context.BaseUri, context.Options.IntrospectionEndpointUris.FirstOrDefault());
context.LogoutEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, context.LogoutEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.Options.LogoutEndpointUris.FirstOrDefault()); context.BaseUri, context.Options.LogoutEndpointUris.FirstOrDefault());
context.RevocationEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, context.RevocationEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.Options.RevocationEndpointUris.FirstOrDefault()); context.BaseUri, context.Options.RevocationEndpointUris.FirstOrDefault());
context.TokenEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, context.TokenEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.Options.TokenEndpointUris.FirstOrDefault()); context.BaseUri, context.Options.TokenEndpointUris.FirstOrDefault());
context.UserinfoEndpoint ??= GetEndpointAbsoluteUri(context.Issuer, context.UserinfoEndpoint ??= OpenIddictHelpers.CreateAbsoluteUri(
context.Options.UserinfoEndpointUris.FirstOrDefault()); context.BaseUri, context.Options.UserinfoEndpointUris.FirstOrDefault());
return default; 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);
}
} }
} }

2
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)); 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.TokenId = context.Principal.GetClaim(Claims.JwtId);
context.TokenUsage = context.Principal.GetTokenType(); context.TokenUsage = context.Principal.GetTokenType();
context.Subject = context.Principal.GetClaim(Claims.Subject); context.Subject = context.Principal.GetClaim(Claims.Subject);

20
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -68,8 +68,24 @@ public static partial class OpenIddictServerHandlers
} }
var parameters = context.Options.TokenValidationParameters.Clone(); 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 parameters.ValidTypes = context.ValidTokenTypes.Count switch
{ {

1
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)); 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); context.Subject = context.Principal.GetClaim(Claims.Subject);
// The following claims are all optional and should be excluded when // The following claims are all optional and should be excluded when

151
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -8,7 +8,6 @@ using System.Collections.Immutable;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -20,6 +19,11 @@ namespace OpenIddict.Server;
public static partial class OpenIddictServerHandlers public static partial class OpenIddictServerHandlers
{ {
public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create( public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Top-level request processing:
*/
InferEndpointType.Descriptor,
/* /*
* Authentication processing: * Authentication processing:
*/ */
@ -101,6 +105,99 @@ public static partial class OpenIddictServerHandlers
.AddRange(Session.DefaultHandlers) .AddRange(Session.DefaultHandlers)
.AddRange(Userinfo.DefaultHandlers); .AddRange(Userinfo.DefaultHandlers);
/// <summary>
/// Contains the logic responsible for inferring the endpoint type from the request address.
/// </summary>
public sealed class InferEndpointType : IOpenIddictServerHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.UseSingletonHandler<InferEndpointType>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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<Uri> 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 '/'));
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authentication demands made from unsupported endpoints. /// Contains the logic responsible for rejecting authentication demands made from unsupported endpoints.
/// </summary> /// </summary>
@ -1815,7 +1912,7 @@ public static partial class OpenIddictServerHandlers
} }
// Use the server identity as the token issuer. // 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. // Set the audiences based on the resource claims stored in the principal.
principal.SetAudiences(context.Principal.GetResources()); principal.SetAudiences(context.Principal.GetResources());
@ -1902,7 +1999,7 @@ public static partial class OpenIddictServerHandlers
} }
// Use the server identity as the token issuer. // 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 // Attach the redirect_uri to allow for later comparison when
// receiving a grant_type=authorization_code token request. // 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. // 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 // Restore the device code internal token identifier from the principal
// resolved from the user code used in the user code verification request. // 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. // 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; context.RefreshTokenPrincipal = principal;
@ -2181,7 +2278,7 @@ public static partial class OpenIddictServerHandlers
} }
// Use the server identity as the token issuer. // 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. // 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. // 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. // 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. // Store the client_id as a public client_id claim.
principal.SetClaim(Claims.ClientId, context.Request.ClientId); principal.SetClaim(Claims.ClientId, context.Request.ClientId);
@ -2976,7 +3073,8 @@ public static partial class OpenIddictServerHandlers
{ {
context.Response.UserCode = context.UserCode; 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) if (address is not null)
{ {
var builder = new UriBuilder(address) var builder = new UriBuilder(address)
@ -2990,43 +3088,6 @@ public static partial class OpenIddictServerHandlers
} }
return default; 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);
}
} }
} }

8
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -16,7 +16,7 @@ namespace OpenIddict.Server;
public sealed class OpenIddictServerOptions public sealed class OpenIddictServerOptions
{ {
/// <summary> /// <summary>
/// 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. /// The URI must be absolute and may contain a path, but no query string or fragment part.
/// </summary> /// </summary>
public Uri? Issuer { get; set; } public Uri? Issuer { get; set; }
@ -67,8 +67,8 @@ public sealed class OpenIddictServerOptions
/// </summary> /// </summary>
public List<Uri> ConfigurationEndpointUris { get; } = new() public List<Uri> ConfigurationEndpointUris { get; } = new()
{ {
new Uri("/.well-known/openid-configuration", UriKind.Relative), new Uri(".well-known/openid-configuration", UriKind.Relative),
new Uri("/.well-known/oauth-authorization-server", UriKind.Relative) new Uri(".well-known/oauth-authorization-server", UriKind.Relative)
}; };
/// <summary> /// <summary>
@ -76,7 +76,7 @@ public sealed class OpenIddictServerOptions
/// </summary> /// </summary>
public List<Uri> CryptographyEndpointUris { get; } = new() public List<Uri> CryptographyEndpointUris { get; } = new()
{ {
new Uri("/.well-known/jwks", UriKind.Relative) new Uri(".well-known/jwks", UriKind.Relative)
}; };
/// <summary> /// <summary>

9
src/OpenIddict.Server/OpenIddictServerTransaction.cs

@ -21,9 +21,14 @@ public sealed class OpenIddictServerTransaction
public OpenIddictServerEndpointType EndpointType { get; set; } public OpenIddictServerEndpointType EndpointType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the issuer address associated with the current transaction, if available. /// Gets or sets the request <see cref="Uri"/> of the current transaction, if available.
/// </summary> /// </summary>
public Uri? Issuer { get; set; } public Uri? RequestUri { get; set; }
/// <summary>
/// Gets or sets the base <see cref="Uri"/> of the host, if available.
/// </summary>
public Uri? BaseUri { get; set; }
/// <summary> /// <summary>
/// Gets or sets the logger associated with the current request. /// Gets or sets the logger associated with the current request.

93
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs

@ -10,6 +10,7 @@ using System.Diagnostics;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
@ -29,11 +30,12 @@ public static partial class OpenIddictValidationAspNetCoreHandlers
/* /*
* Request top-level processing: * Request top-level processing:
*/ */
InferIssuerFromHost.Descriptor, ResolveRequestUri.Descriptor,
/* /*
* Authentication processing: * Authentication processing:
*/ */
ValidateHostHeader.Descriptor,
ExtractAccessTokenFromAuthorizationHeader.Descriptor, ExtractAccessTokenFromAuthorizationHeader.Descriptor,
ExtractAccessTokenFromBodyForm.Descriptor, ExtractAccessTokenFromBodyForm.Descriptor,
ExtractAccessTokenFromQueryString.Descriptor, ExtractAccessTokenFromQueryString.Descriptor,
@ -58,10 +60,10 @@ public static partial class OpenIddictValidationAspNetCoreHandlers
ProcessChallengeErrorResponse<ProcessErrorContext>.Descriptor); ProcessChallengeErrorResponse<ProcessErrorContext>.Descriptor);
/// <summary> /// <summary>
/// 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. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary> /// </summary>
public sealed class InferIssuerFromHost : IOpenIddictValidationHandler<ProcessRequestContext> public sealed class ResolveRequestUri : IOpenIddictValidationHandler<ProcessRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -69,8 +71,8 @@ public static partial class OpenIddictValidationAspNetCoreHandlers
public static OpenIddictValidationHandlerDescriptor Descriptor { get; } public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<InferIssuerFromHost>() .UseSingletonHandler<ResolveRequestUri>()
.SetOrder(int.MinValue + 100_000) .SetOrder(int.MinValue + 50_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();
@ -87,36 +89,81 @@ public static partial class OpenIddictValidationAspNetCoreHandlers
var request = context.Transaction.GetHttpRequest() ?? var request = context.Transaction.GetHttpRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0114));
// Only use the current host as the issuer if the // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute
// issuer was not explicitly set in the options. // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized
if (context.Issuer is not null) // 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) { HasValue: false } => (
{ BaseUri: new UriBuilder
context.Reject( {
error: Errors.InvalidRequest, Scheme = request.Scheme,
description: SR.FormatID2081(HeaderNames.Host), Path = request.PathBase.ToUriComponent()
uri: SR.FormatID8000(SR.ID2081)); }.Uri,
RequestUri: new UriBuilder
{
Scheme = request.Scheme,
Path = (request.PathBase + request.Path).ToUriComponent(),
Query = request.QueryString.ToUriComponent()
}.Uri)
};
return default; return default;
}
}
/// <summary>
/// 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.
/// </summary>
public sealed class ValidateHostHeader : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ValidateHostHeader>()
.SetOrder(int.MinValue + 50_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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) || // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
!issuer.IsWellFormedOriginalString()) // 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( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,
description: SR.FormatID2082(HeaderNames.Host), description: SR.FormatID2081(HeaderNames.Host),
uri: SR.FormatID8000(SR.ID2082)); uri: SR.FormatID8000(SR.ID2081));
return default; return default;
} }
context.Issuer = issuer;
return default; return default;
} }
} }

91
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs

@ -25,7 +25,7 @@ public static partial class OpenIddictValidationOwinHandlers
/* /*
* Request top-level processing: * Request top-level processing:
*/ */
InferIssuerFromHost.Descriptor, ResolveRequestUri.Descriptor,
/* /*
* Authentication processing: * Authentication processing:
@ -58,10 +58,10 @@ public static partial class OpenIddictValidationOwinHandlers
ProcessChallengeErrorResponse<ProcessErrorContext>.Descriptor); ProcessChallengeErrorResponse<ProcessErrorContext>.Descriptor);
/// <summary> /// <summary>
/// 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. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary> /// </summary>
public sealed class InferIssuerFromHost : IOpenIddictValidationHandler<ProcessRequestContext> public sealed class ResolveRequestUri : IOpenIddictValidationHandler<ProcessRequestContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -69,8 +69,8 @@ public static partial class OpenIddictValidationOwinHandlers
public static OpenIddictValidationHandlerDescriptor Descriptor { get; } public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.UseSingletonHandler<InferIssuerFromHost>() .UseSingletonHandler<ResolveRequestUri>()
.SetOrder(int.MinValue + 100_000) .SetOrder(int.MinValue + 50_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();
@ -87,36 +87,81 @@ public static partial class OpenIddictValidationOwinHandlers
var request = context.Transaction.GetOwinRequest() ?? var request = context.Transaction.GetOwinRequest() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0120));
// Only use the current host as the issuer if the // OpenIddict supports both absolute and relative URIs for all its endpoints, but only absolute
// issuer was not explicitly set in the options. // URIs can be properly canonicalized by the BCL System.Uri class (e.g './path/../' is normalized
if (context.Issuer is not null) // 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
return default; // 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( { Value.Length: > 0 } host => (
error: Errors.InvalidRequest, BaseUri: new Uri(request.Scheme + Uri.SchemeDelimiter + host + request.PathBase, UriKind.Absolute),
description: SR.FormatID2081(Headers.Host), RequestUri: request.Uri),
uri: SR.FormatID8000(SR.ID2081));
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;
}
}
/// <summary>
/// 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.
/// </summary>
public sealed class ValidateHostHeader : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ValidateHostHeader>()
.SetOrder(int.MinValue + 50_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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) || // This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
!issuer.IsWellFormedOriginalString()) // 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( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidRequest,
description: SR.FormatID2082(Headers.Host), description: SR.FormatID2081(Headers.Host),
uri: SR.FormatID8000(SR.ID2082)); uri: SR.FormatID8000(SR.ID2081));
return default; return default;
} }
context.Issuer = issuer;
return default; return default;
} }
} }

17
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.Introspection.cs

@ -53,7 +53,7 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask HandleAsync(PrepareIntrospectionRequestContext context) public ValueTask HandleAsync(PrepareIntrospectionRequestContext context)
{ {
if (context is null) 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 no client identifier was attached to the request, skip the following logic.
if (string.IsNullOrEmpty(context.Request.ClientId)) if (string.IsNullOrEmpty(context.Request.ClientId))
{ {
return; return default;
}
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));
} }
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication. // 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 // See https://tools.ietf.org/html/rfc8414#section-2
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information. // 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. // Important: the credentials MUST be formURL-encoded before being base64-encoded.
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder() 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; context.Request.ClientId = context.Request.ClientSecret = null;
} }
return default;
static string? EscapeDataString(string? value) static string? EscapeDataString(string? value)
=> value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null; => value is not null ? Uri.EscapeDataString(value).Replace("%20", "+") : null;
} }

4
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
<PackageReference Include="Microsoft.IdentityModel.Protocols" /> <PackageReference Include="Microsoft.IdentityModel.Protocols" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Include="..\..\shared\OpenIddict.Extensions\*\*.cs" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Using Include="OpenIddict.Abstractions" /> <Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> <Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />

2
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -463,7 +463,7 @@ public sealed class OpenIddictValidationBuilder
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString()) 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); return SetIssuer(uri);

18
src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs

@ -7,6 +7,7 @@
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
namespace OpenIddict.Validation; namespace OpenIddict.Validation;
@ -105,28 +106,17 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
if (!options.MetadataAddress.IsAbsoluteUri) if (!options.MetadataAddress.IsAbsoluteUri)
{ {
var issuer = options.Issuer; if (options.Issuer is not { IsAbsoluteUri: true })
if (issuer is not { IsAbsoluteUri: true })
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0136)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0136));
} }
if (!string.IsNullOrEmpty(issuer.Fragment) || !string.IsNullOrEmpty(issuer.Query)) if (!string.IsNullOrEmpty(options.Issuer.Fragment) || !string.IsNullOrEmpty(options.Issuer.Query))
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0137)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0137));
} }
if (!issuer.OriginalString.EndsWith("/", StringComparison.Ordinal)) options.MetadataAddress = OpenIddictHelpers.CreateAbsoluteUri(options.Issuer, options.MetadataAddress);
{
issuer = new Uri(issuer.OriginalString + "/", UriKind.Absolute);
}
if (options.MetadataAddress.OriginalString.StartsWith("/", StringComparison.Ordinal))
{
options.MetadataAddress = new Uri(options.MetadataAddress.OriginalString[1..], UriKind.Relative);
}
options.MetadataAddress = new Uri(issuer, options.MetadataAddress);
} }
options.ConfigurationManager = new ConfigurationManager<OpenIddictConfiguration>( options.ConfigurationManager = new ConfigurationManager<OpenIddictConfiguration>(

17
src/OpenIddict.Validation/OpenIddictValidationEvents.cs

@ -39,12 +39,21 @@ public static partial class OpenIddictValidationEvents
} }
/// <summary> /// <summary>
/// Gets or sets the issuer address associated with the current transaction, if available. /// Gets or sets the request <see cref="Uri"/> of the current transaction, if available.
/// </summary> /// </summary>
public Uri? Issuer public Uri? RequestUri
{ {
get => Transaction.Issuer; get => Transaction.RequestUri;
set => Transaction.Issuer = value; set => Transaction.RequestUri = value;
}
/// <summary>
/// Gets or sets the base <see cref="Uri"/> of the host, if available.
/// </summary>
public Uri? BaseUri
{
get => Transaction.BaseUri;
set => Transaction.BaseUri = value;
} }
/// <summary> /// <summary>

1
src/OpenIddict.Validation/OpenIddictValidationFactory.cs

@ -30,7 +30,6 @@ public sealed class OpenIddictValidationFactory : IOpenIddictValidationFactory
public ValueTask<OpenIddictValidationTransaction> CreateTransactionAsync() public ValueTask<OpenIddictValidationTransaction> CreateTransactionAsync()
=> new(new OpenIddictValidationTransaction => new(new OpenIddictValidationTransaction
{ {
Issuer = _options.CurrentValue.Issuer,
Logger = _logger, Logger = _logger,
Options = _options.CurrentValue Options = _options.CurrentValue
}); });

5
src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs

@ -203,7 +203,8 @@ public static partial class OpenIddictValidationHandlers
return default; 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( context.Reject(
error: Errors.ServerError, error: Errors.ServerError,
@ -325,7 +326,7 @@ public static partial class OpenIddictValidationHandlers
/// </summary> /// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; } public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractIntrospectionEndpoint>() .UseSingletonHandler<ExtractIntrospectionEndpointClientAuthenticationMethods>()
.SetOrder(ExtractIntrospectionEndpoint.Descriptor.Order + 1_000) .SetOrder(ExtractIntrospectionEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();

5
src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs

@ -317,7 +317,8 @@ public static partial class OpenIddictValidationHandlers
return default; 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( context.Reject(
error: Errors.ServerError, 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. // to be valid, as it is guarded against unknown values by the ValidateIssuer handler.
var issuer = (string?) context.Response[Claims.Issuer] ?? var issuer = (string?) context.Response[Claims.Issuer] ??
context.Configuration.Issuer?.AbsoluteUri ?? context.Configuration.Issuer?.AbsoluteUri ??
context.Issuer?.AbsoluteUri ?? ClaimsIdentity.DefaultIssuer; context.BaseUri?.AbsoluteUri ?? ClaimsIdentity.DefaultIssuer;
foreach (var parameter in context.Response.GetParameters()) foreach (var parameter in context.Response.GetParameters())
{ {

6
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). // OpenID Connect server configuration (that can be static or retrieved using discovery).
var parameters = context.Options.TokenValidationParameters.Clone(); 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, null => null,
// If the issuer URI doesn't contain any path/query/fragment, allow both http://www.fabrikam.com // If the issuer URI doesn't contain any path/query/fragment, allow both http://www.fabrikam.com

10
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -60,16 +60,8 @@ public static partial class OpenIddictValidationHandlers
throw new ArgumentNullException(nameof(context)); 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)); 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;
} }
} }

14
src/OpenIddict.Validation/OpenIddictValidationService.cs

@ -8,6 +8,7 @@ using System.Diagnostics;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using static OpenIddict.Abstractions.OpenIddictExceptions; using static OpenIddict.Abstractions.OpenIddictExceptions;
@ -48,12 +49,17 @@ public sealed class OpenIddictValidationService
// can be disposed of asynchronously if it implements IAsyncDisposable. // can be disposed of asynchronously if it implements IAsyncDisposable.
try try
{ {
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>();
var configuration = await options.CurrentValue.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationDispatcher>(); var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>(); var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>();
var transaction = await factory.CreateTransactionAsync(); var transaction = await factory.CreateTransactionAsync();
var context = new ValidateTokenContext(transaction) var context = new ValidateTokenContext(transaction)
{ {
Configuration = configuration,
Token = token, Token = token,
ValidTokenTypes = { TokenTypeHints.AccessToken } ValidTokenTypes = { TokenTypeHints.AccessToken }
}; };
@ -409,6 +415,10 @@ public sealed class OpenIddictValidationService
// can be disposed of asynchronously if it implements IAsyncDisposable. // can be disposed of asynchronously if it implements IAsyncDisposable.
try try
{ {
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>();
var configuration = await options.CurrentValue.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationDispatcher>(); var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>(); var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>();
var transaction = await factory.CreateTransactionAsync(); var transaction = await factory.CreateTransactionAsync();
@ -426,6 +436,7 @@ public sealed class OpenIddictValidationService
var context = new PrepareIntrospectionRequestContext(transaction) var context = new PrepareIntrospectionRequestContext(transaction)
{ {
Address = address, Address = address,
Configuration = configuration,
Request = request, Request = request,
Token = token, Token = token,
TokenTypeHint = hint TokenTypeHint = hint
@ -448,6 +459,7 @@ public sealed class OpenIddictValidationService
var context = new ApplyIntrospectionRequestContext(transaction) var context = new ApplyIntrospectionRequestContext(transaction)
{ {
Address = address, Address = address,
Configuration = configuration,
Request = request Request = request
}; };
@ -470,6 +482,7 @@ public sealed class OpenIddictValidationService
var context = new ExtractIntrospectionResponseContext(transaction) var context = new ExtractIntrospectionResponseContext(transaction)
{ {
Address = address, Address = address,
Configuration = configuration,
Request = request Request = request
}; };
@ -494,6 +507,7 @@ public sealed class OpenIddictValidationService
var context = new HandleIntrospectionResponseContext(transaction) var context = new HandleIntrospectionResponseContext(transaction)
{ {
Address = address, Address = address,
Configuration = configuration,
Request = request, Request = request,
Response = response, Response = response,
Token = token Token = token

9
src/OpenIddict.Validation/OpenIddictValidationTransaction.cs

@ -21,9 +21,14 @@ public sealed class OpenIddictValidationTransaction
public OpenIddictValidationEndpointType EndpointType { get; set; } public OpenIddictValidationEndpointType EndpointType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the issuer address associated with the current transaction, if available. /// Gets or sets the request <see cref="Uri"/> of the current transaction, if available.
/// </summary> /// </summary>
public Uri? Issuer { get; set; } public Uri? RequestUri { get; set; }
/// <summary>
/// Gets or sets the base <see cref="Uri"/> of the host, if available.
/// </summary>
public Uri? BaseUri { get; set; }
/// <summary> /// <summary>
/// Gets or sets the logger associated with the current request. /// Gets or sets the logger associated with the current request.

406
test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs

@ -4,7 +4,6 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
@ -18,7 +17,6 @@ using Microsoft.Extensions.Logging;
using OpenIddict.Server.IntegrationTests; using OpenIddict.Server.IntegrationTests;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers;
using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerEvents;
using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using static OpenIddict.Server.OpenIddictServerHandlers.Protection;
@ -260,410 +258,6 @@ public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServ
Assert.Equal("custom_error_uri", response.ErrorUri); 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<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<HandleVerificationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessRequestContext>(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<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<HandleVerificationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessRequestContext>(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<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<HandleVerificationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessRequestContext>(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] [Theory]
[InlineData("/.well-known/openid-configuration")] [InlineData("/.well-known/openid-configuration")]
[InlineData("/.well-known/jwks")] [InlineData("/.well-known/jwks")]

4
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs

@ -2552,7 +2552,7 @@ public abstract partial class OpenIddictServerIntegrationTests
[Theory] [Theory]
[InlineData(null)] [InlineData(null)]
[InlineData("http://www.fabrikam.com/")] [InlineData("http://www.fabrikam.com/")]
public async Task ApplyAuthorizationResponse_SetsIssuerParameter(string issuer) public async Task ApplyAuthorizationResponse_SetsIssuerParameter(string? issuer)
{ {
// Arrange // Arrange
await using var server = await CreateServerAsync(options => await using var server = await CreateServerAsync(options =>
@ -2586,7 +2586,7 @@ public abstract partial class OpenIddictServerIntegrationTests
}); });
// Assert // Assert
Assert.Equal(issuer is not null ? issuer : "http://localhost/", response.Iss); Assert.Equal(!string.IsNullOrEmpty(issuer) ? issuer : "http://localhost/", response.Iss);
} }
[Fact] [Fact]

128
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs

@ -11,6 +11,7 @@ using Microsoft.IdentityModel.Tokens;
using Moq; using Moq;
using Xunit; using Xunit;
using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerEvents;
using static OpenIddict.Server.OpenIddictServerHandlers.Discovery;
namespace OpenIddict.Server.IntegrationTests; namespace OpenIddict.Server.IntegrationTests;
@ -304,66 +305,20 @@ public abstract partial class OpenIddictServerIntegrationTests
(string?) response[Metadata.UserinfoEndpoint]); (string?) response[Metadata.UserinfoEndpoint]);
} }
[Theory] [Fact]
[InlineData("https://www.fabrikam.com/tenant1", new[] public async Task HandleConfigurationRequest_RelativeEndpointsAreCorrectlyComputed()
{
"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)
{ {
// Arrange // Arrange
await using var server = await CreateServerAsync(options => await using var server = await CreateServerAsync(options =>
{ {
options.SetIssuer(new Uri(issuer, UriKind.Absolute)); options.SetAuthorizationEndpointUris("path/authorization_endpoint")
.SetCryptographyEndpointUris("path/cryptography_endpoint")
options.SetAuthorizationEndpointUris(endpoints[0]) .SetDeviceEndpointUris("path/device_endpoint")
.SetCryptographyEndpointUris(endpoints[1]) .SetIntrospectionEndpointUris("path/introspection_endpoint")
.SetDeviceEndpointUris(endpoints[2]) .SetLogoutEndpointUris("path/logout_endpoint")
.SetIntrospectionEndpointUris(endpoints[3]) .SetRevocationEndpointUris("path/revocation_endpoint")
.SetLogoutEndpointUris(endpoints[4]) .SetTokenEndpointUris("path/token_endpoint")
.SetRevocationEndpointUris(endpoints[5]) .SetUserinfoEndpointUris("path/userinfo_endpoint");
.SetTokenEndpointUris(endpoints[6])
.SetUserinfoEndpointUris(endpoints[7]);
}); });
await using var client = await server.CreateClientAsync(); 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"); var response = await client.GetAsync("/.well-known/openid-configuration");
// Assert // Assert
Assert.Equal("https://www.fabrikam.com/tenant1/path/authorization_endpoint", Assert.Equal("http://localhost/path/authorization_endpoint", (string?) response[Metadata.AuthorizationEndpoint]);
(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", [Fact]
(string?) response[Metadata.JwksUri]); 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", options.AddEventHandler<HandleConfigurationRequestContext>(builder =>
(string?) response[Metadata.DeviceAuthorizationEndpoint]); {
builder.UseInlineHandler(context =>
{
context.BaseUri = new Uri("https://contoso.com/issuer");
Assert.Equal("https://www.fabrikam.com/tenant1/path/introspection_endpoint", return default;
(string?) response[Metadata.IntrospectionEndpoint]); });
Assert.Equal("https://www.fabrikam.com/tenant1/path/logout_endpoint", builder.SetOrder(AttachEndpoints.Descriptor.Order - 1);
(string?) response[Metadata.EndSessionEndpoint]); });
});
Assert.Equal("https://www.fabrikam.com/tenant1/path/revocation_endpoint", await using var client = await server.CreateClientAsync();
(string?) response[Metadata.RevocationEndpoint]);
Assert.Equal("https://www.fabrikam.com/tenant1/path/token_endpoint", // Act
(string?) response[Metadata.TokenEndpoint]); var response = await client.GetAsync("/.well-known/openid-configuration");
Assert.Equal("https://www.fabrikam.com/tenant1/path/userinfo_endpoint", // Assert
(string?) response[Metadata.UserinfoEndpoint]); 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] [Fact]

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

@ -33,6 +33,410 @@ public abstract partial class OpenIddictServerIntegrationTests
protected ITestOutputHelper OutputHelper { get; } 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<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<HandleVerificationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessRequestContext>(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<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<HandleVerificationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessRequestContext>(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<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<HandleVerificationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessRequestContext>(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] [Fact]
public async Task ProcessAuthentication_UnknownEndpointCausesAnException() public async Task ProcessAuthentication_UnknownEndpointCausesAnException()
{ {

406
test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs

@ -4,7 +4,6 @@
* the license and the contributors participating to this project. * the license and the contributors participating to this project.
*/ */
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -18,7 +17,6 @@ using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerEvents;
using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using static OpenIddict.Server.OpenIddictServerHandlers.Protection;
using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlers;
namespace OpenIddict.Server.Owin.IntegrationTests; namespace OpenIddict.Server.Owin.IntegrationTests;
@ -245,410 +243,6 @@ public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerInte
Assert.Equal("custom_error_uri", response.ErrorUri); 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<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<HandleVerificationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessRequestContext>(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<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<HandleVerificationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessRequestContext>(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<HandleLogoutRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<HandleVerificationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.SkipRequest();
return default;
}));
options.AddEventHandler<ProcessRequestContext>(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] [Theory]
[InlineData("/.well-known/openid-configuration")] [InlineData("/.well-known/openid-configuration")]
[InlineData("/.well-known/jwks")] [InlineData("/.well-known/jwks")]

23
test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs

@ -32,29 +32,6 @@ public abstract partial class OpenIddictValidationIntegrationTests
protected ITestOutputHelper OutputHelper { get; } 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<OpenIddictConfiguration>(new()
{
Issuer = new Uri("https://fabrikam.com/")
});
}));
await using var client = await server.CreateClientAsync();
// Act and assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(delegate
{
return client.PostAsync("/authenticate", new OpenIddictRequest());
});
Assert.Equal(SR.GetResourceString(SR.ID0307), exception.Message);
}
[Fact] [Fact]
public async Task ProcessAuthentication_EvalutesCorrectValidatedTokens() public async Task ProcessAuthentication_EvalutesCorrectValidatedTokens()
{ {

Loading…
Cancel
Save