# 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 (the `Name` 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 at `https://accounts.google.com/.well-known/openid-configuration` so the correct issuer to use is `https://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` under `Views\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.e `client_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.