mirror of
https://gitee.com/dcren/openiddict-documentation.git
synced 2025-07-15 05:13:19 +08:00
366 lines
22 KiB
HTML
366 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<!--[if IE]><![endif]-->
|
|
<html>
|
|
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
|
<title>Contributing a new Web provider </title>
|
|
<meta name="viewport" content="width=device-width">
|
|
<meta name="title" content="Contributing a new Web provider ">
|
|
<meta name="generator" content="docfx 2.56.7.0">
|
|
|
|
<link rel="shortcut icon" href="../images/favicon.ico">
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/night-owl.min.css">
|
|
<link rel="stylesheet" href="../styles/colors.css">
|
|
<link rel="stylesheet" href="../styles/discord.css">
|
|
<link rel="stylesheet" href="../styles/main.css">
|
|
<meta property="docfx:navrel" content="../toc.html">
|
|
<meta property="docfx:tocrel" content="toc.html">
|
|
|
|
|
|
|
|
</head>
|
|
|
|
<body>
|
|
<div class="top-navbar">
|
|
|
|
<a href="javascript:void(0);" class="burger-icon" onclick="toggleMenu()">
|
|
<svg name="Hamburger" style="vertical-align: middle;" width="24" height="24" viewbox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M20 6H4V9H20V6ZM4 10.999H20V13.999H4V10.999ZM4 15.999H20V18.999H4V15.999Z"></path></svg>
|
|
</a>
|
|
|
|
|
|
<a class="brand" href="../index.html">
|
|
<img src="../images/logo.png" alt="OpenIddict" class="logomark">
|
|
<span class="brand-title">OpenIddict</span>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="body-content">
|
|
|
|
<div id="blackout" class="blackout" onclick="toggleMenu()"></div>
|
|
|
|
<nav id="sidebar" role="navigation">
|
|
|
|
<div class="sidebar">
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
<a class="brand" href="../index.html">
|
|
<img src="../images/logo.png" alt="OpenIddict" class="logomark">
|
|
<span class="brand-title">OpenIddict</span>
|
|
</a>
|
|
<div id="navbar">
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
<div class="sidebar-item-separator"></div>
|
|
|
|
|
|
<div id="sidetoggle">
|
|
<div id="sidetoc"></div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="footer">
|
|
|
|
<span>Generated by <strong>DocFX</strong></span>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="main-panel">
|
|
|
|
<div role="main" class="hide-when-search">
|
|
|
|
|
|
<div class="subnav navbar navbar-default">
|
|
<div class="container hide-when-search" id="breadcrumb">
|
|
<ul class="breadcrumb">
|
|
<li></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<article class="content wrap" id="_content" data-uid="">
|
|
<h1 id="contributing-a-new-web-provider">Contributing a new Web provider</h1>
|
|
|
|
<p>As part of the OpenIddict 4.0 effort, <a href="https://kevinchalet.com/2022/02/25/introducing-the-openiddict-client/">a new client stack has been added to OpenIddict</a>.
|
|
To simplify integrating with social and enterprise providers that offer OAuth 2.0 and OpenID Connect services, a companion package
|
|
(named <code>OpenIddict.Client.WebIntegration</code>) has been added to the client stack. While it shares some similarities with the
|
|
<a href="https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/issues">existing aspnet-contrib OAuth 2.0 providers</a>, <strong>there are actually important technical differences</strong>:</p>
|
|
<ul>
|
|
<li><p>Unlike the ASP.NET Core OAuth 2.0 base handler by the aspnet-contrib providers, <strong>the OpenIddict client is a dual-protocol OAuth 2.0 + OpenID Connect stack</strong>,
|
|
which means it can support both protocols <em>while</em> enforcing all the security checks required by these protocols.</p>
|
|
</li>
|
|
<li><p>While the aspnet-contrib providers only work on ASP.NET Core, <strong>the OpenIddict providers can not only be used in ASP.NET Core
|
|
and OWIN/ASP.NET 4.x applications, but also in Windows and Linux desktop applications</strong> without requiring any platform-specific code.</p>
|
|
</li>
|
|
<li><p>Unlike the aspnet-contrib providers, <strong>the source code needed to materialize the OpenIddict web providers is created dynamically</strong> using
|
|
<a href="https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview">Roslyn Source Generators</a> and
|
|
<a href="https://github.com/openiddict/openiddict-core/blob/dev/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml">a XML file that includes all the supported providers</a>
|
|
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.</p>
|
|
</li>
|
|
<li><p>To guarantee interoperability and make the best security choices, <strong>the OpenIddict client heavily relies on server configuration metadata</strong>, 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.</p>
|
|
</li>
|
|
</ul>
|
|
<p>Due to these differences, <strong>contributing a new provider to the OpenIddict stack is quite different from adding an aspnet-contrib provider</strong>:</p>
|
|
<h2 id="add-a-new-provider-node-for-the-new-provider">Add a new <code><Provider></code> node for the new provider</h2>
|
|
<p>To add a new OpenIddict web provider, <strong>the first step is to add a new <code><Provider></code> node</strong> to the <a href="https://github.com/openiddict/openiddict-core/blob/dev/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml">OpenIddictClientWebIntegrationProviders.xml</a> file. For instance:</p>
|
|
<pre><code class="lang-xml"><Provider Name="Zendesk" Documentation="https://developer.zendesk.com/documentation/live-chat/getting-started/auth/">
|
|
</Provider>
|
|
</code></pre><p>If available, a link to the official documentation MUST be added. If multiple languages are available, the following order SHOULD be used:</p>
|
|
<ul>
|
|
<li>English</li>
|
|
<li>French</li>
|
|
<li>Spanish</li>
|
|
<li>Any other language</li>
|
|
</ul>
|
|
<div class="WARNING"><h5>Warning</h5><p>The added provider MUST be placed in the XML file such that the alphabetical order is respected.</p>
|
|
</div>
|
|
<h2 id="add-an-environment-node-per-supported-environment">Add an <code><Environment></code> node per supported environment</h2>
|
|
<p><strong>The second step is to determine whether the service offers multiple environments</strong> (e.g Production, Testing or Development).</p>
|
|
<ul>
|
|
<li>If the provider supports multiple environments, multiple <code><Environment></code> nodes - one per environment - MUST be added under <code><Provider></code>:</li>
|
|
</ul>
|
|
<pre><code class="lang-xml"><Provider Name="Salesforce">
|
|
<Environment Name="Production" />
|
|
|
|
<Environment Name="Development" />
|
|
</Provider>
|
|
</code></pre><div class="WARNING"><h5>Warning</h5><p>When specifying multiple environments, the production environment MUST always appear first.</p>
|
|
</div>
|
|
<ul>
|
|
<li>If the provider doesn't support multiple environment, a single <code><Environment></code> MUST be added (the <code>Name</code> attribute SHOULD be omitted):</li>
|
|
</ul>
|
|
<pre><code class="lang-xml"><Provider Name="Google">
|
|
<Environment />
|
|
</Provider>
|
|
</code></pre><h2 id="add-the-appropriate-configuration-for-each-environment">Add the appropriate configuration for each environment</h2>
|
|
<p><strong>The third step is the most complicated one: adding the appropriate configuration for each of the added environments</strong>.</p>
|
|
<p>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 <code>https://provider/.well-known/openid-configuration</code>: if you get a valid JSON document from this endpoint, the server
|
|
supports OpenID Connect/OAuth 2.0 server metadata.</p>
|
|
<ul>
|
|
<li>If the server supports OpenID Connect/OAuth 2.0 server metadata, add an <code>Issuer</code> attribute to <code><Environment></code> corresponding to the provider address
|
|
without the <code>/.well-known/openid-configuration</code> part. For instance, Google exposes its discovery document at <code>https://accounts.google.com/.well-known/openid-configuration</code>
|
|
so the correct issuer to use is <code>https://accounts.google.com/</code>:</li>
|
|
</ul>
|
|
<pre><code class="lang-xml"><Provider Name="Google">
|
|
<Environment Issuer="https://accounts.google.com/" />
|
|
</Provider>
|
|
</code></pre><ul>
|
|
<li>If the server doesn't support OpenID Connect/OAuth 2.0 server metadata, you MUST add an <code>Issuer</code> attribute (corresponding to either
|
|
the value given in the documentation or the base address of the server) <strong>and</strong> a <code><Configuration></code> node with the static configuration needed by
|
|
the OpenIddict client to communicate with the remote authorization server. For instance:</li>
|
|
</ul>
|
|
<pre><code class="lang-xml"><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">
|
|
<GrantType Value="authorization_code" />
|
|
<GrantType Value="refresh_token" />
|
|
</Configuration>
|
|
</Environment>
|
|
</Provider>
|
|
</code></pre><div class="NOTE"><h5>Note</h5><p>If the provider doesn't support <code>grant_type=refresh_token</code> and only supports the authorization code flow
|
|
(typically with non-expiring access tokens), the <code><GrantType></code> nodes MUST be removed for clarity,
|
|
as the authorization code flow is always considered supported by default if no <code><GrantType></code> is present:</p>
|
|
<pre><code class="lang-xml"><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>
|
|
</code></pre></div>
|
|
<div class="CAUTION"><h5>Caution</h5><p>If the provider doesn't support server metadata but is known to support Proof Key for Code Exchange (PKCE), a <code><CodeChallengeMethod></code> node MUST
|
|
be added under <code><Configuration></code> to ensure the OpenIddict client will send appropriate <code>code_challenge</code>/<code>code_challenge_method</code> parameters:</p>
|
|
<pre><code class="lang-xml"><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>
|
|
</code></pre></div>
|
|
<div class="NOTE"><h5>Note</h5><p>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 URIs, a <code><Setting></code> node MUST be added under <code><Provider></code> to
|
|
store the tenant name. Once added, the URIs can include a placeholder of the same name:</p>
|
|
<pre><code class="lang-xml"><Provider Name="Zendesk">
|
|
<!--
|
|
Note: Zendesk is a multitenant provider that relies on subdomains to identify instances.
|
|
As such, the following URIs 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 PropertyName="Tenant" ParameterName="tenant" Type="String" Required="true"
|
|
Description="The tenant used to identify the Zendesk instance" />
|
|
</Provider>
|
|
</code></pre></div>
|
|
<h2 id="test-the-generated-provider">Test the generated provider</h2>
|
|
<p>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 <code>OpenIddict.Sandbox.AspNetCore.Client</code> sandbox:</p>
|
|
<ul>
|
|
<li>Update <code>Startup.cs</code> to register your new provider:</li>
|
|
</ul>
|
|
<pre><code class="lang-csharp">// Register the Web providers integrations.
|
|
options.UseWebProviders()
|
|
// ... other providers...
|
|
.Use[provider name](options =>
|
|
{
|
|
options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ");
|
|
options.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS");
|
|
options.SetRedirectUri("callback/login/[provider name]");
|
|
});
|
|
</code></pre><ul>
|
|
<li>Update <code>AuthenticationController.cs</code> to allow triggering challenges pointing to the new provider:</li>
|
|
</ul>
|
|
<pre><code class="lang-csharp">// 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) &&
|
|
// ... other providers...
|
|
!string.Equals(provider, [provider name], StringComparison.Ordinal))
|
|
{
|
|
return BadRequest();
|
|
}
|
|
</code></pre><ul>
|
|
<li>Update <code>Index.cshtml</code> under <code>Views\Home</code> to include a login button for the new provider:</li>
|
|
</ul>
|
|
<pre><code class="lang-html"><button class="btn btn-lg btn-success" type="submit" name="provider" value="[provider name]">
|
|
Sign in using [provider name]
|
|
</button>
|
|
</code></pre><div class="NOTE"><h5>Note</h5><p>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.</p>
|
|
</div>
|
|
<h2 id="if-necessary-add-the-necessary-workarounds-for-the-provider-to-work-correctly">If necessary, add the necessary workarounds for the provider to work correctly</h2>
|
|
<p>If an error occurs during the authentication process, the provider MAY require one or multiple workarounds for the integration to work correctly:</p>
|
|
<ul>
|
|
<li>The provider MAY require sending the client credentials as part of the <code>Authorization</code> header using basic authentication (i.e <code>client_secret_basic</code>).
|
|
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 <code><TokenEndpointAuthMethod></code>:</li>
|
|
</ul>
|
|
<pre><code class="lang-xml"><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>
|
|
</code></pre><ul>
|
|
<li>The provider MAY require sending one or multiple default or required scopes. If so, the default/required scopes MUST be added to the <code><Environment></code> node:</li>
|
|
</ul>
|
|
<pre><code class="lang-xml"><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>
|
|
</code></pre><ul>
|
|
<li>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 <code>FormatNonStandardScopeParameter</code> event handler present in
|
|
<a href="https://github.com/openiddict/openiddict-core/blob/dev/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs">OpenIddictClientWebIntegrationHandlers.cs</a> to use the correct separator required by the provider.</li>
|
|
</ul>
|
|
<pre><code class="lang-csharp">/// <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.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):
|
|
Providers.Reddit => string.Join(",", context.Scopes),
|
|
|
|
_ => context.Request.Scope
|
|
};
|
|
|
|
return default;
|
|
}
|
|
}
|
|
</code></pre><div class="NOTE"><h5>Note</h5><p>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
|
|
<a href="https://github.com/openiddict/openiddict-core/issues"><code>openiddict-core</code></a> repository to get help.</p>
|
|
</div>
|
|
<h2 id="send-a-pull-request-against-the-openiddict-core-repository">Send a pull request against the <code>openiddict-core</code> repository</h2>
|
|
<p>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
|
|
<a href="https://github.com/openiddict/openiddict-core/issues"><code>openiddict-repo</code></a> and ship with the already supported providers as part of the next update.</p>
|
|
</article>
|
|
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
|
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
|
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
|
|
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js"></script>
|
|
<script type="text/javascript" src="../styles/jquery.twbsPagination.js"></script>
|
|
<script type="text/javascript" src="../styles/url.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/anchor-js/anchor.min.js"></script>
|
|
<script type="text/javascript" src="../styles/docfx.js"></script>
|
|
<script type="text/javascript" src="../styles/main.js"></script>
|
|
|
|
</body>
|
|
|
|
</html>
|