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.
-}