diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs index 03ccaaca..aa044122 100644 --- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs +++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs @@ -44,10 +44,6 @@ namespace OpenIddict.Client.WebIntegration.Generators "OpenIddictClientWebIntegrationHelpers.generated.cs", SourceText.From(GenerateHelpers(document), Encoding.UTF8)); - context.AddSource( - "OpenIddictClientWebIntegrationScopes.generated.cs", - SourceText.From(GenerateScopes(document), Encoding.UTF8)); - context.AddSource( "OpenIddictClientWebIntegrationSettings.generated.cs", SourceText.From(GenerateSettings(document), Encoding.UTF8)); @@ -230,7 +226,7 @@ public partial class OpenIddictClientWebIntegrationConfiguration {{~ for setting in provider.settings ~}} {{~ if setting.required ~}} - {{~ if setting.type == 'string' ~}} + {{~ if setting.type == 'String' ~}} if (string.IsNullOrEmpty(settings.{{ setting.name }})) {{~ else ~}} if (settings.{{ setting.name }} is null) @@ -333,7 +329,7 @@ public partial class OpenIddictClientWebIntegrationConfiguration EncryptionCredentials = { {{~ for setting in provider.settings ~}} - {{~ if setting.encryption_algorithm ~}} + {{~ if setting.type == 'EncryptionKey' ~}} new EncryptingCredentials(settings.{{ setting.name }}, ""{{ setting.encryption_algorithm }}"", SecurityAlgorithms.Aes256CbcHmacSha512), {{~ end ~}} {{~ end ~}} @@ -342,7 +338,7 @@ public partial class OpenIddictClientWebIntegrationConfiguration SigningCredentials = { {{~ for setting in provider.settings ~}} - {{~ if setting.signing_algorithm ~}} + {{~ if setting.type == 'SigningKey' ~}} new SigningCredentials(settings.{{ setting.name }}, ""{{ setting.signing_algorithm }}""), {{~ end ~}} {{~ end ~}} @@ -375,6 +371,21 @@ public partial class OpenIddictClientWebIntegrationConfiguration } {{~ end ~}} + {{~ for setting in provider.settings ~}} + {{~ for item in setting.collection_items ~}} + {{~ if item.required ~}} + settings.{{ setting.name }}.Add(""{{ item.value }}""); + {{~ end ~}} + + {{~ if item.default ~}} + if (settings.{{ setting.name }}.Count is 0) + { + settings.{{ setting.name }}.Add(""{{ item.value }}""); + } + {{~ end ~}} + {{~ end ~}} + {{~ end ~}} + options.Registrations.Add(registration); } } @@ -466,8 +477,17 @@ public partial class OpenIddictClientWebIntegrationConfiguration Name = (string) setting.Attribute("Name"), Type = (string) setting.Attribute("Type"), Required = (bool?) setting.Attribute("Required") ?? false, - EncryptionAlgorithm = (string?) setting.Attribute("EncryptionAlgorithm"), - SigningAlgorithm = (string?) setting.Attribute("SigningAlgorithm") + + EncryptionAlgorithm = (string?) setting.Element("EncryptionAlgorithm")?.Attribute("Value"), + SigningAlgorithm = (string?) setting.Element("SigningAlgorithm")?.Attribute("Value"), + + CollectionItems = setting.Elements("CollectionItem").Select(item => new + { + Value = (string) item.Attribute("Value"), + Default = (bool?) item.Attribute("Default") ?? false, + Required = (bool?) item.Attribute("Required") ?? false + }) + .ToList() }) .ToList() }) @@ -512,55 +532,6 @@ public partial class OpenIddictClientWebIntegrationHelpers }); } - static string GenerateScopes(XDocument document) - { - var template = Template.Parse(@"#nullable enable - -namespace OpenIddict.Client.WebIntegration; - -public static partial class OpenIddictClientWebIntegrationScopes -{ - {{~ for provider in providers ~}} - /// - /// Exposes the scopes supported by the {{ provider.name }} provider. - /// - public static class {{ provider.name }} - { - {{~ for scope in provider.scopes ~}} - {{~ if scope.description ~}} - /// - /// {{ scope.description }} - /// - {{~ end ~}} - public const string {{ scope.clr_name }} = ""{{ scope.name }}""; - {{~ end ~}} - } - {{~ end ~}} -} -"); - return template.Render(new - { - Providers = document.Root.Elements("Provider") - .Select(provider => new - { - Name = (string) provider.Attribute("Name"), - - Scopes = provider.Elements("Environment") - .SelectMany(environment => environment.Elements("Scope")) - .Select(scope => new - { - Name = (string) scope.Attribute("Name"), - ClrName = Regex.Replace((string) scope.Attribute("Name"), "(?:^|_| +)(.)", - match => match.Groups[1].Value.ToUpper(CultureInfo.InvariantCulture)), - Description = (string?) scope.Attribute("Description") - }) - .Distinct(scope => scope.ClrName) - .ToList() - }) - .ToList() - }); - } - static string GenerateSettings(XDocument document) { var template = Template.Parse(@"#nullable enable @@ -583,8 +554,12 @@ public partial class OpenIddictClientWebIntegrationSettings /// {{ setting.description }} /// {{~ end ~}} + {{~ if setting.collection ~}} + public HashSet<{{ setting.type }}> {{ setting.name }} { get; } = new(); + {{~ else ~}} public {{ setting.type }}? {{ setting.name }} { get; set; } {{~ end ~}} + {{~ end ~}} /// /// Gets or sets the environment that determines the endpoints to use. @@ -603,9 +578,26 @@ public partial class OpenIddictClientWebIntegrationSettings Settings = provider.Elements("Setting").Select(setting => new { - Type = (string) setting.Attribute("Type"), Name = (string) setting.Attribute("Name"), - Description = (string) setting.Attribute("Description") + Collection = (bool?) setting.Attribute("Collection") ?? false, + Description = (string) setting.Attribute("Description"), + Type = (string) setting.Attribute("Type") switch + { + "EncryptionKey" when (string) setting.Element("EncryptionAlgorithm").Attribute("Value") + is "RS256" or "RS384" or "RS512" => "RsaSecurityKey", + + "SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value") + is "ES256" or "ES384" or "ES512" => "ECDsaSecurityKey", + + "SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value") + is "PS256" or "PS384" or "PS512" or + "RS256" or "RS384" or "RS512" => "RsaSecurityKey", + + "String" => "string", + "StringHashSet" => "HashSet", + + string value => value + } }) .ToList() }) diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs index 416e036e..98f77aab 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs @@ -25,6 +25,7 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers "local" or "local-github" => "https://localhost:44349/", "github" => "https://github.com/", "google" => "https://accounts.google.com/", + "twitter" => "https://twitter.com/", _ => null }; @@ -121,7 +122,7 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers => new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer), // Applications can map non-standard claims issued by specific issuers to a standard equivalent. - { Type: "id", Issuer: "https://github.com/" } + { Type: "id", Issuer: "https://github.com/" or "https://twitter.com/" } => new Claim(Claims.Subject, claim.Value, claim.ValueType, claim.Issuer), _ => claim diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs index c18fcd9c..9212a7e3 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs @@ -75,7 +75,8 @@ namespace OpenIddict.Sandbox.AspNet.Client options.SetRedirectionEndpointUris( "/signin-local", "/signin-github", - "/signin-google"); + "/signin-google", + "/signin-twitter"); // Register the signing and encryption credentials used to protect // sensitive data like the state tokens produced by OpenIddict. @@ -114,6 +115,12 @@ namespace OpenIddict.Sandbox.AspNet.Client ClientSecret = "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", RedirectUri = new Uri("https://localhost:44378/signin-google", UriKind.Absolute), Scopes = { Scopes.Profile } + }) + .AddTwitter(new() + { + ClientId = "bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ", + ClientSecret = "VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS", + RedirectUri = new Uri("https://localhost:44378/signin-twitter", UriKind.Absolute) }); }); diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml b/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml index 2d9c7ff7..be7a8a17 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml @@ -43,5 +43,7 @@ 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" }) + @Html.ActionLink("Sign in using Twitter", "Login", "Authentication", + new { provider = "twitter" }, new { @class = "btn btn-lg btn-success" }) } \ No newline at end of file diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs index e5386622..b0304c53 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs @@ -18,6 +18,7 @@ public class AuthenticationController : Controller "github" => "https://github.com/", "google" => "https://accounts.google.com/", "reddit" => "https://www.reddit.com/", + "twitter" => "https://twitter.com/", _ => null }; @@ -100,7 +101,7 @@ public class AuthenticationController : Controller .Select(claim => claim switch { // Applications can map non-standard claims issued by specific issuers to a standard equivalent. - { Type: "id", Issuer: "https://github.com/" } + { Type: "id", Issuer: "https://github.com/" or "https://twitter.com/" } => new Claim(Claims.Subject, claim.Value, claim.ValueType, claim.Issuer), _ => claim diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs index adcd8cea..3ed21acf 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs @@ -84,7 +84,8 @@ public class Startup "/signin-local", "/signin-github", "/signin-google", - "/signin-reddit"); + "/signin-reddit", + "/signin-twitter"); // Register the signing and encryption credentials used to protect // sensitive data like the state tokens produced by OpenIddict. @@ -132,6 +133,12 @@ public class Startup RedirectUri = new Uri("https://localhost:44381/signin-reddit", UriKind.Absolute), ProductName = "DemoApp", ProductVersion = "1.0.0" + }) + .AddTwitter(new() + { + ClientId = "bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ", + ClientSecret = "VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS", + RedirectUri = new Uri("https://localhost:44381/signin-twitter", UriKind.Absolute) }); }); diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml index dd2445a5..8534cc2d 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml @@ -41,5 +41,7 @@ asp-action="Login" asp-route-provider="google">Sign in using Google Sign in using Reddit + Sign in using Twitter } \ No newline at end of file diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 26f83ebe..100d8cab 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1298,6 +1298,9 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad The '{0}' provider settings cannot be resolved from the event context. Make sure the provider was correctly registered using 'services.AddOpenIddict().AddClient().UseWebProviders().Add{0}()'. + + The '{0}' node cannot be extracted from the response. + The security token is missing. diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index 64aa67c1..639b4749 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -5,6 +5,9 @@ */ using System.Collections.Immutable; +using System.Diagnostics; +using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers; +using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; namespace OpenIddict.Client.WebIntegration; @@ -16,6 +19,96 @@ public static partial class OpenIddictClientWebIntegrationHandlers /* * Userinfo request preparation: */ - UseProductNameAsUserAgent.Descriptor); + UseProductNameAsUserAgent.Descriptor, + AttachNonStandardFieldParameter.Descriptor, + + /* + * Userinfo response extraction: + */ + UnwrapUserinfoResponse.Descriptor); + + /// + /// Contains the logic responsible for attaching non-standard field parameters for the providers that require it. + /// + public class AttachNonStandardFieldParameter : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(PrepareGetHttpRequest.Descriptor.Order - 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(PrepareUserinfoRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008)); + + // Some providers are known to limit the number of fields returned by their userinfo endpoint + // but allow returning additional information using a special parameter (generally called "fields") + // 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) + { + var settings = context.Registration.GetTwitterSettings(); + + context.Request["expansions"] = string.Join(",", settings.Expansions); + context.Request["tweet.fields"] = string.Join(",", settings.TweetFields); + context.Request["user.fields"] = string.Join(",", settings.UserFields); + } + + return default; + } + } + + /// + /// Contains the logic responsible for extracting the userinfo response + /// from nested JSON nodes (e.g "data") for the providers that require it. + /// + public class UnwrapUserinfoResponse : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractJsonHttpResponse.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ExtractUserinfoResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007)); + + // Some providers are known to wrap their userinfo payloads in top-level JSON nodes + // (generally named "d", "data" or "content"), which prevents the default extraction + // 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. + + if (context.Registration.GetProviderName() is Providers.Twitter) + { + context.Response = new OpenIddictResponse(context.Response["data"]?.GetNamedParameters() ?? + throw new InvalidOperationException(SR.FormatID0334("data"))); + } + + return default; + } + } } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index 88736ccc..34d37569 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -4,10 +4,12 @@ - + + + - @@ -38,14 +40,46 @@ is always added even if another scope was explicitly registered by the user. --> - + - - + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd index 3e4a9ddd..0af782b9 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd @@ -191,7 +191,7 @@ - + A boolean indicating whether the scope is automatically added if no other scope is added by the user. @@ -201,7 +201,7 @@ - + A boolean indicating whether the scope is always added even if another scope is already added by the user. @@ -210,12 +210,6 @@ - - - - The scope description. - - @@ -246,6 +240,95 @@ + + + + An item added by default to the collection, if applicable. + + + + + + The value of the item. + + + + + + + + + + A boolean indicating whether the item is automatically added if no other item is added by the user. + + + + + + + + + + A boolean indicating whether the item is always added even if another item is already added by the user. + + + + + + + + + + + + The encryption algorithm used with the encryption key, if applicable. + + + + + + The encryption algorithm name (e.g RSA-OAEP). + + + + + + + + + + + + + + + The signing algorithm used with the signing key, if applicable. + + + + + + The signing algorithm name (e.g RS256). + + + + + + + + + + + + + + + + + + + + The setting name. @@ -258,22 +341,9 @@ - - - The setting type. - - - - - - - - - - - + - A boolean indicating whether the setting is required. + A boolean indicating whether the setting is a collection. @@ -281,36 +351,27 @@ - + - The encryption algorithm, if applicable. + The setting type. - - + + + - + - The signing algorithm, if applicable. + A boolean indicating whether the setting is required. - - - - - - - - - - - + diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationScopes.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationScopes.cs deleted file mode 100644 index 9fce8bc8..00000000 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationScopes.cs +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -namespace OpenIddict.Client.WebIntegration; - -/// -/// Exposes the provider-specific scopes supported by the OpenIddict client Web integration services. -/// -public static partial class OpenIddictClientWebIntegrationScopes -{ - // Note: scopes are automatically generated by the source generator. -}