Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

823 lines
37 KiB

/*
* 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.
*/
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Security.Claims;
using System.Text.Json;
using OpenIddict.Extensions;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class OpenIddictClientWebIntegrationHandlers
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Authentication processing:
*/
HandleNonStandardFrontchannelErrorResponse.Descriptor,
OverrideTokenEndpoint.Descriptor,
AttachNonStandardClientAssertionTokenClaims.Descriptor,
AttachTokenRequestNonStandardClientCredentials.Descriptor,
AdjustRedirectUriInTokenRequest.Descriptor,
OverrideValidatedBackchannelTokens.Descriptor,
DisableBackchannelIdentityTokenNonceValidation.Descriptor,
AttachAdditionalUserinfoRequestParameters.Descriptor,
PopulateUserinfoTokenPrincipalFromTokenResponse.Descriptor,
/*
* Challenge processing:
*/
OverrideAuthorizationEndpoint.Descriptor,
OverrideResponseMode.Descriptor,
FormatNonStandardScopeParameter.Descriptor,
IncludeStateParameterInRedirectUri.Descriptor,
AttachAdditionalChallengeParameters.Descriptor)
.AddRange(Discovery.DefaultHandlers)
.AddRange(Exchange.DefaultHandlers)
.AddRange(Protection.DefaultHandlers)
.AddRange(Userinfo.DefaultHandlers);
/// <summary>
/// Contains the logic responsible for handling non-standard
/// authorization errors for the providers that require it.
/// </summary>
public sealed class HandleNonStandardFrontchannelErrorResponse : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<HandleNonStandardFrontchannelErrorResponse>()
.SetOrder(HandleFrontchannelErrorResponse.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: some providers are known to return non-standard errors.
// To normalize the set of errors handled by the OpenIddict client,
// the non-standard errors are mapped to their standard equivalent.
//
// Errors that are not handled here will be automatically handled
// by the standard handler present in the core OpenIddict client.
if (context.Registration.ProviderName is Providers.Deezer)
{
// Note: Deezer uses a custom "error_reason" parameter instead of the
// standard "error" parameter defined by the OAuth 2.0 specification.
//
// See https://developers.deezer.com/api/oauth for more information.
var error = (string?) context.Request["error_reason"];
if (string.Equals(error, "user_denied", StringComparison.Ordinal))
{
context.Reject(
error: Errors.AccessDenied,
description: SR.GetResourceString(SR.ID2149),
uri: SR.FormatID8000(SR.ID2149));
return default;
}
}
else if (context.Registration.ProviderName is Providers.LinkedIn)
{
var error = (string?) context.Request[Parameters.Error];
if (string.Equals(error, "user_cancelled_authorize", StringComparison.Ordinal) ||
string.Equals(error, "user_cancelled_login", StringComparison.Ordinal))
{
context.Reject(
error: Errors.AccessDenied,
description: SR.GetResourceString(SR.ID2149),
uri: SR.FormatID8000(SR.ID2149));
return default;
}
}
else if (context.Registration.ProviderName is Providers.Mixcloud)
{
var error = (string?) context.Request[Parameters.Error];
if (string.Equals(error, "user_denied", StringComparison.Ordinal))
{
context.Reject(
error: Errors.AccessDenied,
description: SR.GetResourceString(SR.ID2149),
uri: SR.FormatID8000(SR.ID2149));
return default;
}
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for overriding the address
/// of the token endpoint for the providers that require it.
/// </summary>
public sealed class OverrideTokenEndpoint : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<OverrideTokenEndpoint>()
.SetOrder(ResolveTokenEndpoint.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.TokenEndpoint = context.Registration.ProviderName switch
{
// Trovo uses a different token endpoint for the refresh token grant.
//
// For more information, see
// https://developer.trovo.live/docs/APIs.html#_4-3-refresh-access-token.
Providers.Trovo when context.GrantType is GrantTypes.RefreshToken
=> new Uri("https://open-api.trovo.live/openplatform/refreshtoken", UriKind.Absolute),
_ => context.TokenEndpoint
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for amending the client
/// assertion methods for the providers that require it.
/// </summary>
public sealed class AttachNonStandardClientAssertionTokenClaims : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionTokenGenerated>()
.UseSingletonHandler<AttachNonStandardClientAssertionTokenClaims>()
.SetOrder(PrepareClientAssertionTokenPrincipal.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.ClientAssertionTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// For client assertions to be considered valid by the Apple ID authentication service,
// the team identifier associated with the developer account MUST be used as the issuer
// and the static "https://appleid.apple.com" URL MUST be used as the token audience.
//
// For more information about the custom client authentication method implemented by Apple,
// see https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens.
if (context.Registration.ProviderName is Providers.Apple)
{
var options = context.Registration.GetAppleOptions();
context.ClientAssertionTokenPrincipal.SetClaim(Claims.Private.Issuer, options.TeamId);
context.ClientAssertionTokenPrincipal.SetAudiences("https://appleid.apple.com");
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching custom client credentials
/// parameters to the token request for the providers that require it.
/// </summary>
public sealed class AttachTokenRequestNonStandardClientCredentials : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenRequest>()
.UseSingletonHandler<AttachTokenRequestNonStandardClientCredentials>()
.SetOrder(AttachTokenRequestClientCredentials.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.TokenRequest is not null, SR.GetResourceString(SR.ID4008));
// Apple implements a non-standard client authentication method for the token endpoint
// that is inspired by the standard private_key_jwt method but doesn't use the standard
// client_assertion/client_assertion_type parameters. Instead, the client assertion
// must be sent as a "dynamic" client secret using client_secret_post. Since the logic
// is the same as private_key_jwt, the configuration is amended to assume Apple supports
// private_key_jwt and an event handler is responsible for populating the client_secret
// parameter using the client assertion token once it has been generated by OpenIddict.
if (context.Registration.ProviderName is Providers.Apple)
{
context.TokenRequest.ClientSecret = context.TokenRequest.ClientAssertion;
context.TokenRequest.ClientAssertion = null;
context.TokenRequest.ClientAssertionType = null;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching custom client credentials
/// parameters to the token request for the providers that require it.
/// </summary>
public sealed class AdjustRedirectUriInTokenRequest : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenRequest>()
.UseSingletonHandler<AdjustRedirectUriInTokenRequest>()
.SetOrder(AttachTokenRequestClientCredentials.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.TokenRequest is not null, SR.GetResourceString(SR.ID4008));
if (context.TokenRequest.RedirectUri is null)
{
return default;
}
// Note: some providers don't support the "state" parameter, don't flow
// it correctly or don't include it in errored authorization responses.
//
// Since OpenIddict requires flowing the state token in every circumstance
// (for security reasons), the state token is appended to the "redirect_uri"
// instead of being sent as a standard OAuth 2.0 authorization request parameter.
//
// Note: for token requests to use the actual redirect_uri that was sent as part
// of the authorization requests, the value persisted in the state token principal
// MUST be replaced to include the state token received by the redirection endpoint.
context.TokenRequest.RedirectUri = context.Registration.ProviderName switch
{
Providers.Deezer or
Providers.Mixcloud => OpenIddictHelpers.AddQueryStringParameter(
uri: new Uri(context.TokenRequest.RedirectUri, UriKind.Absolute),
name: Parameters.State,
value: context.StateToken).AbsoluteUri,
_ => context.TokenRequest.RedirectUri
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for overriding the set
/// of required tokens for the providers that require it.
/// </summary>
public sealed class OverrideValidatedBackchannelTokens : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<OverrideValidatedBackchannelTokens>()
.SetOrder(EvaluateValidatedBackchannelTokens.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
(context.ExtractBackchannelIdentityToken,
context.RequireBackchannelIdentityToken,
context.ValidateBackchannelIdentityToken) = context.Registration.ProviderName switch
{
// While PayPal claims the OpenID Connect flavor of the code flow is supported,
// their implementation doesn't return an id_token from the token endpoint.
Providers.PayPal => (false, false, false),
_ => (context.ExtractBackchannelIdentityToken,
context.RequireBackchannelIdentityToken,
context.ValidateBackchannelIdentityToken)
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for disabling the backchannel
/// identity token nonce validation for the providers that require it.
/// </summary>
public sealed class DisableBackchannelIdentityTokenNonceValidation : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<DisableBackchannelIdentityTokenNonceValidation>()
.SetOrder(ValidateBackchannelIdentityTokenNonce.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: despite implementing OpenID Connect, some providers are known to implement the
// specification incorrectly and either don't support the "nonce" authorization request
// parameter, don't include it in the issued identity tokens or flow an unexpected value.
//
// Despite being an important security feature, nonce validation is explicitly disabled
// for the providers that are known to cause errors when nonce validation is enforced.
context.DisableBackchannelIdentityTokenNonceValidation = context.Registration.ProviderName switch
{
// These providers don't include the nonce in their identity tokens:
Providers.Asana or Providers.Dropbox => true,
_ => context.DisableBackchannelIdentityTokenNonceValidation
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching additional parameters
/// to the userinfo request for the providers that require it.
/// </summary>
public sealed class AttachAdditionalUserinfoRequestParameters : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireUserinfoRequest>()
.UseSingletonHandler<AttachAdditionalUserinfoRequestParameters>()
.SetOrder(AttachUserinfoRequestParameters.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.UserinfoRequest is not null, SR.GetResourceString(SR.ID4008));
// By default, LinkedIn returns all the basic fields except the profile image.
// To retrieve the profile image, a projection parameter must be sent with
// all the parameters that should be returned from the userinfo endpoint.
if (context.Registration.ProviderName is Providers.LinkedIn)
{
var options = context.Registration.GetLinkedInOptions();
context.UserinfoRequest["projection"] = string.Concat("(", string.Join(",", options.Fields), ")");
}
// Patreon limits the number of fields returned by the userinfo endpoint
// but allows returning additional information using special parameters that
// determine what fields will be returned as part of the userinfo response.
else if (context.Registration.ProviderName is Providers.Patreon)
{
var options = context.Registration.GetPatreonOptions();
context.UserinfoRequest["fields[user]"] = string.Join(",", options.UserFields);
}
// StackOverflow requires sending an application key and a site parameter
// containing the name of the site from which the user profile is retrieved.
else if (context.Registration.ProviderName is Providers.StackExchange)
{
var options = context.Registration.GetStackExchangeOptions();
context.UserinfoRequest["key"] = options.ApplicationKey;
context.UserinfoRequest["site"] = options.Site;
}
// Trakt allows retrieving additional user details via the "extended" parameter.
else if (context.Registration.ProviderName is Providers.Trakt)
{
context.UserinfoRequest["extended"] = "full";
}
// Twitter limits the number of fields returned by the userinfo endpoint
// but allows returning additional information using special parameters that
// determine what fields will be returned as part of the userinfo response.
else if (context.Registration.ProviderName is Providers.Twitter)
{
var options = context.Registration.GetTwitterOptions();
context.UserinfoRequest["expansions"] = string.Join(",", options.Expansions);
context.UserinfoRequest["tweet.fields"] = string.Join(",", options.TweetFields);
context.UserinfoRequest["user.fields"] = string.Join(",", options.UserFields);
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for creating a userinfo token principal from the custom
/// parameters returned in the token response for the providers that require it.
/// </summary>
public sealed class PopulateUserinfoTokenPrincipalFromTokenResponse : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenRequest>()
.UseSingletonHandler<PopulateUserinfoTokenPrincipalFromTokenResponse>()
.SetOrder(ValidateUserinfoToken.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
// Don't overwrite the userinfo token principal if one was already set.
if (context.UserinfoTokenPrincipal is not null)
{
return default;
}
// Some providers don't provide an OAuth 2.0/OpenID Connect userinfo endpoint but
// return the user information using custom/non-standard token response parameters.
// To work around that, this handler is responsible for extracting these parameters
// from the token response and creating a userinfo token principal containing them.
if (context.Registration.ProviderName is Providers.StripeConnect)
{
var identity = new ClaimsIdentity(
context.Registration.TokenValidationParameters.AuthenticationType,
context.Registration.TokenValidationParameters.NameClaimType,
context.Registration.TokenValidationParameters.RoleClaimType);
var issuer = context.Configuration.Issuer!.AbsoluteUri;
foreach (var parameter in context.TokenResponse.GetParameters())
{
switch (context.Registration.ProviderName)
{
// For Stripe, only include "livemode" and the parameters that are prefixed with "stripe_":
case Providers.StripeConnect when
!string.Equals(parameter.Key, "livemode", StringComparison.OrdinalIgnoreCase) &&
!parameter.Key.StartsWith("stripe_", StringComparison.OrdinalIgnoreCase):
continue;
}
// Note: in the typical case, the response parameters should be deserialized from a
// JSON response and thus natively stored as System.Text.Json.JsonElement instances.
//
// In the rare cases where the underlying value wouldn't be a JsonElement instance
// (e.g when custom parameters are manually added to the response), the static
// conversion operator would take care of converting the underlying value to a
// JsonElement instance using the same value type as the original parameter value.
switch ((JsonElement) parameter.Value)
{
// Top-level claims represented as arrays are split and mapped to multiple CLR claims
// to match the logic implemented by IdentityModel for JWT token deserialization.
case { ValueKind: JsonValueKind.Array } value:
identity.AddClaims(parameter.Key, value, issuer);
break;
case { ValueKind: _ } value:
identity.AddClaim(parameter.Key, value, issuer);
break;
}
}
context.UserinfoTokenPrincipal = new ClaimsPrincipal(identity);
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for overriding the address of
/// the authorization endpoint for the providers that require it.
/// </summary>
public sealed class OverrideAuthorizationEndpoint : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<OverrideAuthorizationEndpoint>()
.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.AuthorizationEndpoint = context.Registration.ProviderName switch
{
// Stripe uses a different authorization endpoint for express accounts.
//
// The type of account can be defined globally (via the Stripe options) or
// per authentication demand by adding a specific authentication property.
// If the authentication property is present, the global option is ignored.
//
// For more information, see
// https://stripe.com/docs/connect/oauth-reference?locale=en-us#get-authorize.
Providers.StripeConnect when context.Properties.TryGetValue(".stripe_account_type", out string? type) &&
string.Equals(type, "express", StringComparison.OrdinalIgnoreCase)
=> new Uri("https://connect.stripe.com/express/oauth/authorize", UriKind.Absolute),
Providers.StripeConnect when context.Registration.GetStripeConnectOptions() is { AccountType: string type } &&
string.Equals(type, "express", StringComparison.OrdinalIgnoreCase)
=> new Uri("https://connect.stripe.com/express/oauth/authorize", UriKind.Absolute),
_ => context.AuthorizationEndpoint
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for overriding the response mode for the providers that require it.
/// </summary>
public sealed class OverrideResponseMode : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<OverrideResponseMode>()
// Note: this handler MUST be invoked after the scopes have been attached to the
// context to support overriding the response mode based on the requested scopes.
.SetOrder(AttachScopes.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.ResponseMode = context.Registration.ProviderName switch
{
// Note: Apple requires using form_post when the "email" or "name" scopes are requested.
Providers.Apple when context.Scopes.Contains(Scopes.Email) || context.Scopes.Contains("name")
=> ResponseModes.FormPost,
_ => context.ResponseMode
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for overriding the standard "scope"
/// parameter for providers that are known to use a non-standard format.
/// </summary>
public sealed 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.Deezer => string.Join(",", context.Scopes),
// The following providers are known to use plus-separated scopes instead of
// the standard format (that requires using a space as the scope separator):
Providers.Trovo => string.Join("+", context.Scopes),
_ => context.Request.Scope
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for persisting the state parameter in the redirect URI for
/// providers that don't support it but allow arbitrary dynamic parameters in redirect_uri.
/// </summary>
public sealed class IncludeStateParameterInRedirectUri : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<IncludeStateParameterInRedirectUri>()
.SetOrder(FormatNonStandardScopeParameter.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.RedirectUri is null)
{
return default;
}
// Note: some providers don't support the "state" parameter, don't flow
// it correctly or don't include it in errored authorization responses.
//
// Since OpenIddict requires flowing the state token in every circumstance
// (for security reasons), the state token is appended to the "redirect_uri"
// instead of being sent as a standard OAuth 2.0 authorization request parameter.
//
// Note: this workaround only works for providers that allow dynamic
// redirection URIs and implement a relaxed validation policy logic.
(context.Request.RedirectUri, context.Request.State) = context.Registration.ProviderName switch
{
Providers.Deezer or
Providers.Mixcloud => (OpenIddictHelpers.AddQueryStringParameter(
uri: new Uri(context.RedirectUri, UriKind.Absolute),
name: Parameters.State,
value: context.Request.State).AbsoluteUri, null),
_ => (context.Request.RedirectUri, context.Request.State)
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching additional parameters
/// to the authorization request for the providers that require it.
/// </summary>
public sealed class AttachAdditionalChallengeParameters : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachAdditionalChallengeParameters>()
.SetOrder(AttachChallengeParameters.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Active Directory Federation Services allows sending a custom "resource"
// parameter to define what API resources the access token will give access to.
if (context.Registration.ProviderName is Providers.ActiveDirectoryFederationServices)
{
var options = context.Registration.GetActiveDirectoryFederationServicesOptions();
context.Request["resource"] = options.Resource;
}
// By default, Google doesn't return a refresh token but allows sending an "access_type"
// parameter to retrieve one (but it is only returned during the first authorization dance).
if (context.Registration.ProviderName is Providers.Google)
{
var options = context.Registration.GetGoogleOptions();
context.Request["access_type"] = options.AccessType;
}
// Pro Santé Connect's specification requires sending an acr_values parameter containing
// the desired level of authentication (currently, only "eidas1" is supported). For more
// information, see https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000045551195.
else if (context.Registration.ProviderName is Providers.ProSantéConnect)
{
var options = context.Registration.GetProSantéConnectOptions();
context.Request.AcrValues = options.AuthenticationLevel;
}
// By default, Reddit doesn't return a refresh token but
// allows sending a "duration" parameter to retrieve one.
else if (context.Registration.ProviderName is Providers.Reddit)
{
var options = context.Registration.GetRedditOptions();
context.Request["duration"] = options.Duration;
}
// Slack allows sending an optional "team" parameter to simplify the login process.
else if (context.Registration.ProviderName is Providers.Slack)
{
var options = context.Registration.GetSlackOptions();
context.Request["team"] = options.Team;
}
return default;
}
}
}