From 8342dd20ceeea193309c0ec37d0200ae3ddc5381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 7 Oct 2022 19:51:15 +0200 Subject: [PATCH] Support resolving client registrations based on the provider name --- ...OpenIddictClientWebIntegrationGenerator.cs | 13 ++-- .../Controllers/AuthenticationController.cs | 73 +++++++++++-------- .../Startup.cs | 1 + .../Views/Home/Index.cshtml | 10 +-- .../Controllers/AuthorizationController.cs | 15 +--- .../Controllers/AuthenticationController.cs | 68 ++++++++++------- .../Startup.cs | 1 + .../Views/Home/Index.cshtml | 12 +-- .../Controllers/AuthorizationController.cs | 16 ++-- .../OpenIddictResources.resx | 15 +++- .../OpenIddictClientAspNetCoreConstants.cs | 1 + .../OpenIddictClientAspNetCoreHandlers.cs | 14 ++++ .../OpenIddictClientOwinConstants.cs | 1 + .../OpenIddictClientOwinHandlers.cs | 14 ++++ .../OpenIddictClientWebIntegrationBuilder.cs | 20 ++++- ...OpenIddictClientWebIntegrationConstants.cs | 1 - ...tClientWebIntegrationHandlers.Discovery.cs | 6 +- ...ClientWebIntegrationHandlers.Protection.cs | 2 +- ...ctClientWebIntegrationHandlers.Userinfo.cs | 4 +- .../OpenIddictClientWebIntegrationHandlers.cs | 8 +- .../OpenIddictClientWebIntegrationHelpers.cs | 18 ----- .../OpenIddictClientConfiguration.cs | 14 ++++ .../OpenIddictClientEvents.cs | 12 +++ .../OpenIddictClientHandlers.cs | 32 ++++++++ .../OpenIddictClientRegistration.cs | 11 +++ 25 files changed, 250 insertions(+), 132 deletions(-) diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs index 5aba8985..1da605f1 100644 --- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs +++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs @@ -63,7 +63,7 @@ public partial class OpenIddictClientWebIntegrationBuilder /// /// Enables the {{ provider.name }} integration and registers the associated services in the DI container. {{~ if provider.documentation ~}} - /// For more information, visit the official website. + /// For more information, read the documentation. /// {{~ end ~}} /// This extension can be safely called multiple times. @@ -85,7 +85,7 @@ public partial class OpenIddictClientWebIntegrationBuilder /// /// Enables the {{ provider.name }} integration and registers the associated services in the DI container. {{~ if provider.documentation ~}} - /// For more information, visit the official website. + /// For more information, read the documentation. /// {{~ end ~}} /// This extension can be safely called multiple times. @@ -231,11 +231,11 @@ public partial class OpenIddictClientWebIntegrationBuilder {{~ end ~}} {{~ for setting in provider.settings ~}} - {{~ if setting.description ~}} /// /// Configures {{ setting.description }}. /// - {{~ end ~}} + /// {{ setting.description | string.capitalize }}. + /// The instance. {{~ if setting.collection ~}} public {{ provider.name }} Add{{ setting.property_name }}(params {{ setting.clr_type }}[] {{ setting.parameter_name }}) { @@ -495,6 +495,8 @@ public partial class OpenIddictClientWebIntegrationConfiguration var registration = new OpenIddictClientRegistration { + ProviderName = Providers.{{ provider.name }}, + Issuer = settings.Environment switch { {{~ for environment in provider.environments ~}} @@ -597,7 +599,6 @@ public partial class OpenIddictClientWebIntegrationConfiguration Properties = { - [Properties.ProviderName] = Providers.{{ provider.name }}, [Properties.ProviderOptions] = settings } }; @@ -796,11 +797,9 @@ public partial class OpenIddictClientWebIntegrationOptions public string? Environment { get; set; } = OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.Production; {{~ for setting in provider.settings ~}} - {{~ if setting.description ~}} /// /// Gets or sets {{ setting.description }}. /// - {{~ end ~}} {{~ if setting.collection ~}} public HashSet<{{ setting.clr_type }}> {{ setting.property_name }} { get; } = new(); {{~ else ~}} diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs index 4bda9212..bdd0d2ae 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs @@ -9,6 +9,7 @@ using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using OpenIddict.Client.Owin; using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; namespace OpenIddict.Sandbox.AspNet.Client.Controllers { @@ -19,46 +20,60 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers { var context = HttpContext.GetOwinContext(); - var issuer = provider switch - { - "local" or "local-github" => "https://localhost:44349/", - "github" => "https://github.com/", - "google" => "https://accounts.google.com/", - "twitter" => "https://twitter.com/", - - _ => null - }; - - if (string.IsNullOrEmpty(issuer)) + // Note: OpenIddict always validates the specified provider name when handling the challenge operation, + // but the provider can also be validated earlier to return an error page or a special HTTP error code. + if (!string.Equals(provider, "Local", StringComparison.Ordinal) && + !string.Equals(provider, "Local+GitHub", StringComparison.Ordinal) && + !string.Equals(provider, Providers.GitHub, StringComparison.Ordinal) && + !string.Equals(provider, Providers.Google, StringComparison.Ordinal) && + !string.Equals(provider, Providers.Twitter, StringComparison.Ordinal)) { return new HttpStatusCodeResult(400); } - var properties = new AuthenticationProperties(new Dictionary - { - // Note: when only one client is registered in the client options, - // setting the issuer property is not required and can be omitted. - [OpenIddictClientOwinConstants.Properties.Issuer] = issuer - }) - { - // Only allow local return URLs to prevent open redirect attacks. - RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/" - }; - // The local authorization server sample allows the client to select the external // identity provider that will be used to eventually authenticate the user. For that, // a custom "identity_provider" parameter is sent to the authorization server so that // the user is directly redirected to GitHub (in this case, no login page is shown). - if (provider is "local-github") + if (string.Equals(provider, "Local+GitHub", StringComparison.Ordinal)) { - // Note: the OWIN host requires appending the #string suffix to indicate - // that the "identity_provider" property is a public string parameter. - properties.Dictionary[Parameters.IdentityProvider + OpenIddictClientOwinConstants.PropertyTypes.String] = "github"; + var properties = new AuthenticationProperties(new Dictionary + { + // Note: when only one client is registered in the client options, + // specifying the issuer URI or the provider name is not required. + [OpenIddictClientOwinConstants.Properties.ProviderName] = "Local", + + // Note: the OWIN host requires appending the #string suffix to indicate + // that the "identity_provider" property is a public string parameter. + [Parameters.IdentityProvider + OpenIddictClientOwinConstants.PropertyTypes.String] = "GitHub" + }) + { + // Only allow local return URLs to prevent open redirect attacks. + RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/" + }; + + // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. + context.Authentication.Challenge(properties, OpenIddictClientOwinDefaults.AuthenticationType); + return new EmptyResult(); } - // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. - context.Authentication.Challenge(properties, OpenIddictClientOwinDefaults.AuthenticationType); - return new EmptyResult(); + else + { + var properties = new AuthenticationProperties(new Dictionary + { + // Note: when only one client is registered in the client options, + // specifying the issuer URI or the provider name is not required. + [OpenIddictClientOwinConstants.Properties.ProviderName] = provider + }) + { + // Only allow local return URLs to prevent open redirect attacks. + RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/" + }; + + // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. + context.Authentication.Challenge(properties, OpenIddictClientOwinDefaults.AuthenticationType); + return new EmptyResult(); + } } [HttpPost, Route("~/logout"), ValidateAntiForgeryToken] diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs index 55860da2..9d9b850f 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs @@ -101,6 +101,7 @@ namespace OpenIddict.Sandbox.AspNet.Client // Add a client registration matching the client application definition in the server project. options.AddRegistration(new OpenIddictClientRegistration { + ProviderName = "Local", Issuer = new Uri("https://localhost:44349/", UriKind.Absolute), ClientId = "mvc", diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml b/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml index 6f35bc29..d996692f 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml @@ -39,14 +39,14 @@ {

Welcome, anonymous

@Html.ActionLink("Sign in using the local OIDC server", "Login", "Authentication", - new { provider = "local" }, new { @class = "btn btn-lg btn-success" }) + new { provider = "Local" }, new { @class = "btn btn-lg btn-success" }) @Html.ActionLink("Sign in using the local OIDC server (using GitHub delegation)", "Login", "Authentication", - new { provider = "local-github" }, new { @class = "btn btn-lg btn-success" }) + new { provider = "Local+GitHub" }, new { @class = "btn btn-lg btn-success" }) @Html.ActionLink("Sign in using GitHub", "Login", "Authentication", - new { provider = "github" }, new { @class = "btn btn-lg btn-success" }) + new { provider = "GitHub" }, new { @class = "btn btn-lg btn-success" }) @Html.ActionLink("Sign in using Google", "Login", "Authentication", - new { provider = "google" }, new { @class = "btn btn-lg btn-success" }) + new { provider = "Google" }, new { @class = "btn btn-lg btn-success" }) @Html.ActionLink("Sign in using Twitter", "Login", "Authentication", - new { provider = "twitter" }, new { @class = "btn btn-lg btn-success" }) + new { provider = "Twitter" }, new { @class = "btn btn-lg btn-success" }) } \ No newline at end of file diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs index 2054222c..853cb078 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs @@ -14,7 +14,6 @@ using System.Web; using System.Web.Mvc; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; -using Microsoft.IdentityModel.Tokens; using Microsoft.Owin.Security; using OpenIddict.Abstractions; using OpenIddict.Client.Owin; @@ -23,6 +22,7 @@ using OpenIddict.Sandbox.AspNet.Server.ViewModels.Authorization; using OpenIddict.Server.Owin; using Owin; using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; namespace OpenIddict.Sandbox.AspNet.Server.Controllers { @@ -60,14 +60,7 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers // that will be used to authenticate the user, the identity_provider parameter can be used for that. if (!string.IsNullOrEmpty(request.IdentityProvider)) { - var issuer = request.IdentityProvider switch - { - "github" => "https://github.com/", - - _ => null - }; - - if (string.IsNullOrEmpty(issuer)) + if (!string.Equals(request.IdentityProvider, Providers.GitHub, StringComparison.Ordinal)) { context.Authentication.Challenge( authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType, @@ -84,8 +77,8 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers var properties = new AuthenticationProperties(new Dictionary { // Note: when only one client is registered in the client options, - // setting the issuer property is not required and can be omitted. - [OpenIddictClientOwinConstants.Properties.Issuer] = issuer + // specifying the issuer URI or the provider name is not required. + [OpenIddictClientOwinConstants.Properties.ProviderName] = request.IdentityProvider }) { // Once the callback is handled, redirect the user agent to the ASP.NET Identity diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs index 53403da1..6ce2bc4b 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Mvc; using OpenIddict.Client.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; namespace OpenIddict.Sandbox.AspNetCore.Client.Controllers; @@ -12,44 +13,57 @@ public class AuthenticationController : Controller [HttpGet("~/login")] public ActionResult LogIn(string provider, string returnUrl) { - var issuer = provider switch - { - "local" or "local-github" => "https://localhost:44395/", - "github" => "https://github.com/", - "google" => "https://accounts.google.com/", - "reddit" => "https://www.reddit.com/", - "twitter" => "https://twitter.com/", - - _ => null - }; - - if (string.IsNullOrEmpty(issuer)) + // Note: OpenIddict always validates the specified provider name when handling the challenge operation, + // but the provider can also be validated earlier to return an error page or a special HTTP error code. + if (!string.Equals(provider, "Local", StringComparison.Ordinal) && + !string.Equals(provider, "Local+GitHub", StringComparison.Ordinal) && + !string.Equals(provider, Providers.GitHub, StringComparison.Ordinal) && + !string.Equals(provider, Providers.Google, StringComparison.Ordinal) && + !string.Equals(provider, Providers.Reddit, StringComparison.Ordinal) && + !string.Equals(provider, Providers.Twitter, StringComparison.Ordinal)) { return BadRequest(); } - var properties = new AuthenticationProperties(new Dictionary - { - // Note: when only one client is registered in the client options, - // setting the issuer property is not required and can be omitted. - [OpenIddictClientAspNetCoreConstants.Properties.Issuer] = issuer - }) - { - // Only allow local return URLs to prevent open redirect attacks. - RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/" - }; - // The local authorization server sample allows the client to select the external // identity provider that will be used to eventually authenticate the user. For that, // a custom "identity_provider" parameter is sent to the authorization server so that // the user is directly redirected to GitHub (in this case, no login page is shown). - if (provider is "local-github") + if (string.Equals(provider, "Local+GitHub", StringComparison.Ordinal)) { - properties.Parameters[Parameters.IdentityProvider] = "github"; + var properties = new AuthenticationProperties(new Dictionary + { + // Note: when only one client is registered in the client options, + // specifying the issuer URI or the provider name is not required. + [OpenIddictClientAspNetCoreConstants.Properties.ProviderName] = "Local" + }) + { + // Only allow local return URLs to prevent open redirect attacks. + RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/", + + Parameters = { [Parameters.IdentityProvider] = "GitHub" } + }; + + // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. + return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme); } - // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. - return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme); + else + { + var properties = new AuthenticationProperties(new Dictionary + { + // Note: when only one client is registered in the client options, + // specifying the issuer URI or the provider name is not required. + [OpenIddictClientAspNetCoreConstants.Properties.ProviderName] = provider + }) + { + // Only allow local return URLs to prevent open redirect attacks. + RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/" + }; + + // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. + return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme); + } } [HttpPost("~/logout"), ValidateAntiForgeryToken] diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs index 698d0dda..0d3ee4dc 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs @@ -111,6 +111,7 @@ public class Startup // Add a client registration matching the client application definition in the server project. options.AddRegistration(new OpenIddictClientRegistration { + ProviderName = "Local", Issuer = new Uri("https://localhost:44395/", UriKind.Absolute), ClientId = "mvc", diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml index d511c1a0..d5393c95 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml @@ -34,16 +34,16 @@ {

Welcome, anonymous

Sign in using the local OIDC server + asp-action="Login" asp-route-provider="Local">Sign in using the local OIDC server Sign in using the local OIDC server (using GitHub delegation) + asp-action="Login" asp-route-provider="Local+GitHub">Sign in using the local OIDC server (using GitHub delegation) Sign in using GitHub + asp-action="Login" asp-route-provider="GitHub">Sign in using GitHub Sign in using Google + asp-action="Login" asp-route-provider="Google">Sign in using Google Sign in using Reddit + asp-action="Login" asp-route-provider="Reddit">Sign in using Reddit Sign in using Twitter + asp-action="Login" asp-route-provider="Twitter">Sign in using Twitter } \ No newline at end of file diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs index 497b2a27..f82559d1 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs @@ -20,6 +20,7 @@ using OpenIddict.Sandbox.AspNetCore.Server.Models; using OpenIddict.Sandbox.AspNetCore.Server.ViewModels.Authorization; using OpenIddict.Server.AspNetCore; using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; namespace OpenIddict.Sandbox.AspNetCore.Server; @@ -95,14 +96,7 @@ public class AuthorizationController : Controller // that will be used to authenticate the user, the identity_provider parameter can be used for that. if (!string.IsNullOrEmpty(request.IdentityProvider)) { - var issuer = request.IdentityProvider switch - { - "github" => "https://github.com/", - - _ => null - }; - - if (string.IsNullOrEmpty(issuer)) + if (!string.Equals(request.IdentityProvider, Providers.GitHub, StringComparison.Ordinal)) { return Forbid( authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, @@ -115,15 +109,15 @@ public class AuthorizationController : Controller } var properties = _signInManager.ConfigureExternalAuthenticationProperties( - provider: issuer, + provider: request.IdentityProvider, redirectUrl: Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = Request.PathBase + Request.Path + QueryString.Create(parameters) })); // Note: when only one client is registered in the client options, - // setting the issuer property is not required and can be omitted. - properties.SetString(OpenIddictClientAspNetCoreConstants.Properties.Issuer, issuer); + // specifying the issuer URI or the provider name is not required. + properties.SetString(OpenIddictClientAspNetCoreConstants.Properties.ProviderName, request.IdentityProvider); // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme); diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 29670068..668cefe5 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1177,7 +1177,7 @@ To apply redirection responses, create a class implementing 'IOpenIddictClientHa No client registration was found in the client options. To add a registration, use 'services.AddOpenIddict().AddClient().AddRegistration()'. - No issuer was specified in the challenge properties. When multiple clients are registered, an issuer must be specified in the challenge properties. + No issuer was specified in the challenge properties. When multiple clients are registered, an issuer (or a provider name) must be specified in the challenge properties. The specified issuer is not a valid or absolute URL. @@ -1320,10 +1320,10 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad The endpoint type associated with the state token cannot be resolved. - No issuer was specified in the sign-out properties. When multiple clients are registered, an issuer must be specified in the sign-out properties. + No issuer was specified in the sign-out properties. When multiple clients are registered, an issuer (or a provider name) must be specified in the sign-out properties. - Identical issuers cannot be used in multiple client registrations. + The same issuer cannot be used in multiple client registrations. The request forgery protection claim cannot be resolved from the challenge context. @@ -1337,6 +1337,15 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad The PEM-encoded key cannot be empty. + + The same provider name cannot be used in multiple client registrations. + + + The issuer corresponding to the specified provider name cannot be found in the client options. + + + The issuer associated with the resolved client registration doesn't match the specified provider name. + The security token is missing. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs index 6451a776..11e56b08 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs @@ -24,6 +24,7 @@ public static class OpenIddictClientAspNetCoreConstants public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; + public const string ProviderName = ".provider_name"; public const string RefreshTokenPrincipal = ".refresh_token_principal"; public const string StateTokenPrincipal = ".state_token_principal"; public const string UserinfoTokenPrincipal = ".userinfo_token_principal"; diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs index 0b9f33ea..5d0a410c 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs @@ -459,6 +459,13 @@ public static partial class OpenIddictClientAspNetCoreHandlers context.Issuer = uri; } + // If a provider name was explicitly set, update the challenge context to use it. + if (properties.Items.TryGetValue(Properties.ProviderName, out string? provider) && + !string.IsNullOrEmpty(provider)) + { + context.ProviderName = provider; + } + // If a return URL was specified, use it as the target_link_uri claim. if (!string.IsNullOrEmpty(properties.RedirectUri)) { @@ -628,6 +635,13 @@ public static partial class OpenIddictClientAspNetCoreHandlers context.Issuer = uri; } + // If a provider name was explicitly set, update the sign-out context to use it. + if (properties.Items.TryGetValue(Properties.ProviderName, out string? provider) && + !string.IsNullOrEmpty(provider)) + { + context.ProviderName = provider; + } + // If a return URL was specified, use it as the target_link_uri claim. if (!string.IsNullOrEmpty(properties.RedirectUri)) { diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs index 808893b8..95ed6568 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs @@ -32,6 +32,7 @@ public static class OpenIddictClientOwinConstants public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; + public const string ProviderName = ".provider_name"; public const string RefreshTokenPrincipal = ".refresh_token_principal"; public const string StateTokenPrincipal = ".state_token_principal"; public const string UserinfoTokenPrincipal = ".userinfo_token_principal"; diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs index d153d04d..16c8e197 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs @@ -470,6 +470,13 @@ public static partial class OpenIddictClientOwinHandlers context.Issuer = uri; } + // If a provider name was explicitly set, update the challenge context to use it. + if (properties.Dictionary.TryGetValue(Properties.ProviderName, out string? provider) && + !string.IsNullOrEmpty(provider)) + { + context.ProviderName = provider; + } + // If a return URL was specified, use it as the target_link_uri claim. if (!string.IsNullOrEmpty(properties.RedirectUri)) { @@ -666,6 +673,13 @@ public static partial class OpenIddictClientOwinHandlers context.Issuer = uri; } + // If a provider name was explicitly set, update the sign-out context to use it. + if (properties.Dictionary.TryGetValue(Properties.ProviderName, out string? provider) && + !string.IsNullOrEmpty(provider)) + { + context.ProviderName = provider; + } + // If a return URL was specified, use it as the target_link_uri claim. if (!string.IsNullOrEmpty(properties.RedirectUri)) { diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationBuilder.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationBuilder.cs index 9e674402..fbbb21c5 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationBuilder.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationBuilder.cs @@ -5,9 +5,12 @@ */ using System.ComponentModel; +using OpenIddict.Client.WebIntegration; + +#if SUPPORTS_PEM_ENCODED_KEY_IMPORT using System.Security.Cryptography; using Microsoft.IdentityModel.Tokens; -using OpenIddict.Client.WebIntegration; +#endif namespace Microsoft.Extensions.DependencyInjection; @@ -59,7 +62,10 @@ public partial class OpenIddictClientWebIntegrationBuilder /// Configures the Elliptic Curve Digital Signature Algorithm /// (ECDSA) signing key associated with the developer account. /// - /// The PEM-encoded ECDSA signing key. + /// + /// The PEM-encoded Elliptic Curve Digital Signature Algorithm + /// (ECDSA) signing key associated with the developer account. + /// /// The instance. public Apple SetSigningKey(string key) => SetSigningKey(key.AsMemory()); @@ -67,7 +73,10 @@ public partial class OpenIddictClientWebIntegrationBuilder /// Configures the Elliptic Curve Digital Signature Algorithm /// (ECDSA) signing key associated with the developer account. /// - /// The PEM-encoded ECDSA signing key. + /// + /// The PEM-encoded Elliptic Curve Digital Signature Algorithm + /// (ECDSA) signing key associated with the developer account. + /// /// The instance. public Apple SetSigningKey(ReadOnlyMemory key) => SetSigningKey(key.Span); @@ -75,7 +84,10 @@ public partial class OpenIddictClientWebIntegrationBuilder /// Configures the Elliptic Curve Digital Signature Algorithm /// (ECDSA) signing key associated with the developer account. /// - /// The PEM-encoded ECDSA signing key. + /// + /// The PEM-encoded Elliptic Curve Digital Signature Algorithm + /// (ECDSA) signing key associated with the developer account. + /// /// The instance. public Apple SetSigningKey(ReadOnlySpan key) { diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConstants.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConstants.cs index 7f8796cb..440464fa 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConstants.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConstants.cs @@ -15,7 +15,6 @@ public static partial class OpenIddictClientWebIntegrationConstants public static class Properties { - public const string ProviderName = ".provider_name"; public const string ProviderOptions = ".provider_options"; } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs index c8ecbe59..e153c927 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs @@ -51,7 +51,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers // based on the client identity. As required by RFC8414, OpenIddict would automatically reject // such responses as the issuer wouldn't match the expected value. To work around that, the issuer // is replaced by this handler to always use "https://login.microsoftonline.com/common/v2.0". - if (context.Registration.GetProviderName() is Providers.Microsoft) + if (context.Registration.ProviderName is Providers.Microsoft) { var options = context.Registration.GetMicrosoftOptions(); if (string.Equals(options.Tenant, "common", StringComparison.OrdinalIgnoreCase)) @@ -95,7 +95,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers // is the same as private_key_jwt, the configuration is amended to assume Apple supports // private_key_jwt and an event handler is responsible for populating the client_secret // parameter using the client assertion token once it has been generated by OpenIddict. - if (context.Registration.GetProviderName() is Providers.Apple) + if (context.Registration.ProviderName is Providers.Apple) { context.Configuration.TokenEndpointAuthMethodsSupported.Add( ClientAuthenticationMethods.PrivateKeyJwt); @@ -133,7 +133,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers // don't list them in the server configuration metadata. To ensure the OpenIddict // client uses Proof Key for Code Exchange for the Microsoft provider, the 2 methods // are manually added to the list of supported code challenge methods by this handler. - if (context.Registration.GetProviderName() is Providers.Microsoft) + if (context.Registration.ProviderName is Providers.Microsoft) { context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Plain); context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Sha256); diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Protection.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Protection.cs index a488f401..6f262059 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Protection.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Protection.cs @@ -50,7 +50,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers return default; } - context.TokenValidationParameters.ValidateIssuer = context.Registration.GetProviderName() switch + context.TokenValidationParameters.ValidateIssuer = context.Registration.ProviderName switch { // When the Microsoft Account provider is configured to use the "common" tenant, // the returned tokens include a dynamic issuer claim corresponding to the tenant diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index eca50931..f12c7c97 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -56,7 +56,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers // that determines what fields will be returned as part of the userinfo response. This handler is // responsible for resolving the fields from the provider settings and attaching them to the request. - if (context.Registration.GetProviderName() is Providers.Twitter) + if (context.Registration.ProviderName is Providers.Twitter) { var options = context.Registration.GetTwitterOptions(); @@ -100,7 +100,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers // logic from mapping the parameters to CLR claims. To work around that, this handler // is responsible for extracting the nested payload and replacing the userinfo response. - var parameter = context.Registration.GetProviderName() switch + var parameter = context.Registration.ProviderName switch { Providers.Twitter => "data", diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index 59625ba0..603b745e 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -64,7 +64,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers // // For more information about the custom client authentication method implemented by Apple, // see https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens. - if (context.Registration.GetProviderName() is Providers.Apple) + if (context.Registration.ProviderName is Providers.Apple) { var options = context.Registration.GetAppleOptions(); context.ClientAssertionTokenPrincipal.SetClaim(Claims.Private.Issuer, options.TeamId); @@ -109,7 +109,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers // is the same as private_key_jwt, the configuration is amended to assume Apple supports // private_key_jwt and an event handler is responsible for populating the client_secret // parameter using the client assertion token once it has been generated by OpenIddict. - if (context.Registration.GetProviderName() is Providers.Apple) + if (context.Registration.ProviderName is Providers.Apple) { context.TokenRequest.ClientSecret = context.TokenRequest.ClientAssertion; context.TokenRequest.ClientAssertion = null; @@ -146,7 +146,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers throw new ArgumentNullException(nameof(context)); } - context.ResponseMode = context.Registration.GetProviderName() switch + context.ResponseMode = context.Registration.ProviderName switch { // Note: Apple requires using form_post when the "email" or "name" scopes are requested. Providers.Apple when context.Scopes.Contains(Scopes.Email) || context.Scopes.Contains("name") @@ -184,7 +184,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers throw new ArgumentNullException(nameof(context)); } - context.Request.Scope = context.Registration.GetProviderName() switch + context.Request.Scope = context.Registration.ProviderName switch { // The following providers are known to use comma-separated scopes instead of // the standard format (that requires using a space as the scope separator): diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHelpers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHelpers.cs index 35f95a9b..5335f935 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHelpers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHelpers.cs @@ -13,24 +13,6 @@ namespace OpenIddict.Client.WebIntegration; /// public static partial class OpenIddictClientWebIntegrationHelpers { - /// - /// Resolves the name of the provider associated with the client registration or - /// if no provider information is attached to the registration. - /// - /// The client registration. - /// The provider name, if applicable. - /// is null. - public static string? GetProviderName(this OpenIddictClientRegistration registration) - { - if (registration is null) - { - throw new ArgumentNullException(nameof(registration)); - } - - return registration.Properties.TryGetValue(Properties.ProviderName, out var provider) - && provider is string value ? value : null; - } - /// /// Resolves the provider options associated with the client registration or /// if no provider information is attached to the registration or if diff --git a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs index 8d14da58..cafd67a6 100644 --- a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs +++ b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs @@ -107,6 +107,20 @@ public class OpenIddictClientConfiguration : IPostConfigureOptions !string.IsNullOrEmpty(registration.ProviderName)) + .Count() != options.Registrations.Select(registration => registration.ProviderName) + .Where(name => !string.IsNullOrEmpty(name)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Count()) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0347)); + } + // Sort the handlers collection using the order associated with each handler. options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order)); diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs index c8428500..ed7cdba2 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs @@ -693,6 +693,12 @@ public static partial class OpenIddictClientEvents set => Transaction.Response = value; } + /// + /// Gets or sets the name of the provider that will be + /// used to resolve the issuer identity, if applicable. + /// + public string? ProviderName { get; set; } + /// /// Gets the additional parameters returned to caller. /// @@ -837,6 +843,12 @@ public static partial class OpenIddictClientEvents set => Transaction.Response = value; } + /// + /// Gets or sets the name of the provider that will be + /// used to resolve the issuer identity, if applicable. + /// + public string? ProviderName { get; set; } + /// /// Gets or sets the client identifier that will be used for the sign-out demand. /// diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index e5adb13a..2c1f01c5 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -3365,6 +3365,22 @@ public static partial class OpenIddictClientHandlers throw new InvalidOperationException(SR.GetResourceString(SR.ID0296)); } + // If a provider name was specified, resolve the corresponding issuer. + if (!string.IsNullOrEmpty(context.ProviderName)) + { + var registration = context.Options.Registrations.Find(registration => string.Equals( + registration.ProviderName, context.ProviderName, StringComparison.Ordinal)) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0348)); + + // If an explicit issuer was also attached, ensure the two values point to the same instance. + if (context.Issuer is not null && context.Issuer != registration.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0349)); + } + + context.Issuer = registration.Issuer; + } + // If no issuer was explicitly attached and a single client is registered, use it. // Otherwise, throw an exception to indicate that setting an explicit issuer // is required when multiple clients are registered. @@ -4468,6 +4484,22 @@ public static partial class OpenIddictClientHandlers throw new InvalidOperationException(SR.GetResourceString(SR.ID0024)); } + // If a provider name was specified, resolve the corresponding issuer. + if (!string.IsNullOrEmpty(context.ProviderName)) + { + var registration = context.Options.Registrations.Find(registration => string.Equals( + registration.ProviderName, context.ProviderName, StringComparison.Ordinal)) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0348)); + + // If an explicit issuer was also attached, ensure the two values point to the same instance. + if (context.Issuer is not null && context.Issuer != registration.Issuer) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0349)); + } + + context.Issuer = registration.Issuer; + } + // If no issuer was explicitly attached and a single client is registered, use it. // Otherwise, throw an exception to indicate that setting an explicit issuer // is required when multiple clients are registered. diff --git a/src/OpenIddict.Client/OpenIddictClientRegistration.cs b/src/OpenIddict.Client/OpenIddictClientRegistration.cs index 6a5b6c36..f3c88efd 100644 --- a/src/OpenIddict.Client/OpenIddictClientRegistration.cs +++ b/src/OpenIddict.Client/OpenIddictClientRegistration.cs @@ -4,6 +4,7 @@ * the license and the contributors participating to this project. */ +using System.Diagnostics; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Tokens; @@ -12,6 +13,7 @@ namespace OpenIddict.Client; /// /// Contains the properties used to configure a client/server link. /// +[DebuggerDisplay("{Issuer,nq}")] public class OpenIddictClientRegistration { /// @@ -93,6 +95,15 @@ public class OpenIddictClientRegistration /// public Uri? Issuer { get; set; } + /// + /// Gets or sets the provider name, if applicable. + /// + /// + /// If a Web provider integration with the same name was enabled, the + /// provider-specific options will be automatically imported and applied. + /// + public string? ProviderName { get; set; } + /// /// Gets or sets the static server configuration, if applicable. ///