Contributing a new Web provider
+ +As part of the OpenIddict 4.0 effort, a new client stack has been added to OpenIddict.
+To simplify integrating with social and enterprise providers that offer OAuth 2.0 and OpenID Connect services, a companion package
+(named OpenIddict.Client.WebIntegration
) has been added to the client stack. While it shares some similarities with the
+existing aspnet-contrib OAuth 2.0 providers, there are actually important technical differences:
-
+
Unlike the ASP.NET Core OAuth 2.0 base handler by the aspnet-contrib providers, the OpenIddict client is a dual-protocol OAuth 2.0 + OpenID Connect stack, +which means it can support both protocols while enforcing all the security checks required by these protocols.
+
+Unlike the aspnet-contrib providers, the source code needed to materialize the OpenIddict web providers is created dynamically using +Roslyn Source Generators and +a XML file that includes all the supported providers +with the configuration needed to properly generate them. By eliminating all the plumbing code, the OpenIddict web providers are much easier to maintain and update.
+
+To guarantee interoperability and make the best security choices, the OpenIddict client heavily relies on server configuration metadata, which differs from +the approach used by the ASP.NET Core OAuth 2.0 base handler, that doesn't support the OpenID Connect discovery and OAuth 2.0 authorization server metadata specifications.
+
+
Due to these differences, contributing a new provider to the OpenIddict stack is quite different from adding an aspnet-contrib provider:
+Add a new <Provider>
node for the new provider
+To add a new OpenIddict web provider, the first step is to add a new <Provider>
node to the OpenIddictClientWebIntegrationProviders.xml file. For instance:
<Provider Name="Zendesk" Documentation="https://developer.zendesk.com/documentation/live-chat/getting-started/auth/">
+</Provider>
+
If available, a link to the official documentation MUST be added. If multiple languages are available, the following order SHOULD be used:
+-
+
- English +
- French +
- Spanish +
- Any other language +
Warning
The added provider MUST be placed in the XML file such that the alphabetical order is respected.
+Add an <Environment>
node per supported environment
+The second step is to determine whether the service offers multiple environments (e.g Production, Testing or Development).
+-
+
- If the provider supports multiple environments, multiple
<Environment>
nodes - one per environment - MUST be added under<Provider>
:
+
<Provider Name="Salesforce">
+ <Environment Name="Production" />
+
+ <Environment Name="Development" />
+</Provider>
+
Warning
When specifying multiple environments, the production environment MUST always appear first.
+-
+
- If the provider doesn't support multiple environment, a single
<Environment>
MUST be added (theName
attribute SHOULD be omitted):
+
<Provider Name="Google">
+ <Environment />
+</Provider>
+
Add the appropriate configuration for each environment
+The third step is the most complicated one: adding the appropriate configuration for each of the added environments.
+For that, you MUST first determine whether the environment supports OpenID Connect discovery or OAuth 2.0 authorization server metadata.
+In some cases, this information will be mentioned in the official documentation, but it's not always true. By convention, the server metadata
+is typically served from https://provider/.well-known/openid-configuration
: if you get a valid JSON document from this endpoint, the server
+supports OpenID Connect/OAuth 2.0 server metadata.
-
+
- If the server supports OpenID Connect/OAuth 2.0 server metadata, add an
Issuer
attribute to<Environment>
corresponding to the provider address +without the/.well-known/openid-configuration
part. For instance, Google exposes its discovery document athttps://accounts.google.com/.well-known/openid-configuration
+so the correct issuer to use ishttps://accounts.google.com/
:
+
<Provider Name="Google">
+ <Environment Issuer="https://accounts.google.com/" />
+</Provider>
+
-
+
- If the server doesn't support OpenID Connect/OAuth 2.0 server metadata, you MUST add an
Issuer
attribute (corresponding to either +the value given in the documentation or the base address of the server) and a<Configuration>
node with the static configuration needed by +the OpenIddict client to communicate with the remote authorization server. For instance:
+
<Provider Name="Reddit">
+ <Environment Issuer="https://www.reddit.com/">
+ <Configuration AuthorizationEndpoint="https://www.reddit.com/api/v1/authorize"
+ TokenEndpoint="https://www.reddit.com/api/v1/access_token"
+ UserinfoEndpoint="https://oauth.reddit.com/api/v1/me" />
+ </Environment>
+</Provider>
+
Caution
If the provider doesn't support server metadata but is known to support Proof Key for Code Exchange (PKCE), a <CodeChallengeMethod>
node MUST
+be added under <Configuration>
to ensure the OpenIddict client will send appropriate code_challenge
/code_challenge_method
parameters:
<Provider Name="Fitbit">
+ <Environment Issuer="https://www.fitbit.com/">
+ <Configuration AuthorizationEndpoint="https://www.fitbit.com/oauth2/authorize"
+ TokenEndpoint="https://api.fitbit.com/oauth2/token"
+ UserinfoEndpoint="https://api.fitbit.com/1/user/-/profile.json">
+ <CodeChallengeMethod Value="S256" />
+ </Configuration>
+ </Environment>
+</Provider>
+
Note
Some providers use a multitenant configuration that relies on a subdomain, a custom domain or a virtual path to discriminate tenant instances.
+If the provider you want to support requires adding a dynamic part in one of its URLs, a <Setting>
node MUST be added under <Provider>
to
+store the tenant name. Once added, the URLs can include a placeholder of the same name:
<Provider Name="Zendesk">
+ <!--
+ Note: Zendesk is a multitenant provider that relies on subdomains to identify instances.
+ As such, the following URLs all include a {tenant} placeholder that will be dynamically
+ replaced by OpenIddict at runtime by the tenant configured in the Zendesk settings.
+ -->
+
+ <Environment Issuer="https://{tenant}.zendesk.com/">
+ <Configuration AuthorizationEndpoint="https://{tenant}.zendesk.com/oauth/authorizations/new"
+ TokenEndpoint="https://{tenant}.zendesk.com/oauth/tokens"
+ UserinfoEndpoint="https://{tenant}.zendesk.com/api/v2/users/me" />
+ </Environment>
+
+ <Setting Name="Tenant" Type="String" Required="true"
+ Description="Gets or sets the tenant used to identify the Zendesk instance." />
+</Provider>
+
Test the generated provider
+If the targeted service is fully standard-compliant, no additional configuration should be required at this point.
+To confirm it, build the solution and add the new provider to the OpenIddict.Sandbox.AspNetCore.Client
sandbox:
-
+
- Update
Startup.cs
to use your new provider:
+
options.SetRedirectionEndpointUris(
+ // ... other providers...
+ "/signin-[provider name]");
+
// Register the Web providers integrations.
+options.UseWebProviders()
+ // ... other providers...
+ .Add[provider name](new()
+ {
+ ClientId = "bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ",
+ ClientSecret = "VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS",
+ RedirectUri = new Uri("https://localhost:44381/signin-[provider name]", UriKind.Absolute)
+ });
+
-
+
- Update
AuthenticationController.cs
to allow triggering challenges pointing to the new provider:
+
var issuer = provider switch
+{
+ // ... other providers...
+ "[provider name]" => "https://[provider issuer]/",
+
+ _ => null
+};
+
-
+
- Update
Index.cshtml
underViews\Home
to include a login button for the new provider:
+
<a class="btn btn-lg btn-success" asp-controller="Authentication"
+ asp-action="Login" asp-route-provider="[provider name]">Sign in using [provider name]</a>
+
Note
Unless you agree to share your sandbox credentials with the OpenIddict developers, the changes +made to the sandbox project don't need to be committed and included in your pull request.
+If necessary, add the necessary workarounds for the provider to work correctly
+If an error occurs during the authentication process, the provider MAY require one or multiple workarounds for the integration to work correctly:
+-
+
- The provider MAY require sending the client credentials as part of the
Authorization
header using basic authentication (i.eclient_secret_basic
). +Providers that implement OpenID Connect discovery or OAuth 2.0 authorization server metadata will typically return the client authentication methods they support. +If the provider doesn't expose its metadata, the supported methods MUST be added manually to the static configuration using one or multiple<TokenEndpointAuthMethod>
:
+
<Provider Name="Twitter">
+ <Environment Issuer="https://twitter.com/">
+ <Configuration AuthorizationEndpoint="https://twitter.com/i/oauth2/authorize"
+ TokenEndpoint="https://api.twitter.com/2/oauth2/token"
+ UserinfoEndpoint="https://api.twitter.com/2/users/me">
+ <CodeChallengeMethod Value="S256" />
+
+ <TokenEndpointAuthMethod Value="client_secret_basic" />
+ </Configuration>
+ </Environment>
+</Provider>
+
-
+
- The provider MAY require sending one or multiple default or required scopes. If so, the default/required scopes MUST be added to the
<Environment>
node:
+
<Provider Name="Twitter" Documentation="https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code">
+ <Environment Issuer="https://twitter.com/">
+ <Configuration AuthorizationEndpoint="https://twitter.com/i/oauth2/authorize"
+ TokenEndpoint="https://api.twitter.com/2/oauth2/token"
+ UserinfoEndpoint="https://api.twitter.com/2/users/me">
+ <CodeChallengeMethod Value="S256" />
+
+ <TokenEndpointAuthMethod Value="client_secret_basic" />
+ </Configuration>
+
+ <!--
+ Note: Twitter requires requesting the "tweet.read" and "users.read" scopes for the
+ userinfo endpoint to work correctly. As such, these 2 scopes are marked as required
+ so they are always sent even if they were not explicitly added by the user.
+ -->
+
+ <Scope Name="tweet.read" Default="true" Required="true" />
+ <Scope Name="users.read" Default="true" Required="true" />
+ </Environment>
+</Provider>
+
-
+
- The provider MAY require sending the scopes using a different separator than the standard one. While the OAuth 2.0 specification requires using a space
+to separate multiple scopes, some providers require using a different separator (typically, a comma). If the provider you're adding requires such a hack,
+update the
FormatNonStandardScopeParameter
event handler present in +OpenIddictClientWebIntegrationHandlers.cs to use the correct separator required by the provider.
+
/// <summary>
+/// Contains the logic responsible for overriding the standard "scope"
+/// parameter for providers that are known to use a non-standard format.
+/// </summary>
+public class FormatNonStandardScopeParameter : IOpenIddictClientHandler<ProcessChallengeContext>
+{
+ /// <summary>
+ /// Gets the default descriptor definition assigned to this handler.
+ /// </summary>
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
+ .AddFilter<RequireInteractiveGrantType>()
+ .UseSingletonHandler<FormatNonStandardScopeParameter>()
+ .SetOrder(AttachChallengeParameters.Descriptor.Order + 500)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ /// <inheritdoc/>
+ public ValueTask HandleAsync(ProcessChallengeContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.Request.Scope = context.Registration.GetProviderName() 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):
+ Providers.Reddit => string.Join(",", context.Scopes),
+
+ _ => context.Request.Scope
+ };
+
+ return default;
+ }
+}
+
Note
If the provider still doesn't work, it's unfortunately very likely more complex workarounds will be required.
+If you're not familiar with the OpenIddict events model, open a ticket in the
+openiddict-core
repository to get help.
Send a pull request against the openiddict-core
repository
+Once you've been able to confirm that your provider works correctly, all you need to do is send a PR so that it can be added to the
+openiddict-repo
and ship with the already supported providers as part of the next update.