Browse Source

Add Deezer support

pull/1551/head
Kévin Chalet 4 years ago
parent
commit
2f5a76636b
  1. 119
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs
  2. 12
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs
  3. 144
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  4. 16
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml

119
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs

@ -0,0 +1,119 @@
/*
* 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 OpenIddict.Extensions;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
public static partial class OpenIddictClientWebIntegrationHandlers
{
public static class Exchange
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Token request preparation:
*/
AttachNonStandardQueryStringParameters.Descriptor,
/*
* Token response extraction:
*/
MapNonStandardResponseParameters.Descriptor);
/// <summary>
/// Contains the logic responsible for attaching non-standard query string
/// parameters to the token request for the providers that require it.
/// </summary>
public class AttachNonStandardQueryStringParameters : IOpenIddictClientHandler<PrepareTokenRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareTokenRequestContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<AttachNonStandardQueryStringParameters>()
.SetOrder(AttachQueryStringParameters<PrepareTokenRequestContext>.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(PrepareTokenRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
// this may indicate that the request was incorrectly processed by another client stack.
var request = context.Transaction.GetHttpRequestMessage() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
if (request.RequestUri is null)
{
return default;
}
// By default, Deezer returns non-standard token responses formatted as formurl-encoded
// payloads and declared as "text/html" content but allows sending an "output" query string
// parameter containing "json" to get a response conforming to the OAuth 2.0 specification.
if (context.Registration.ProviderName is Providers.Deezer)
{
request.RequestUri = OpenIddictHelpers.AddQueryStringParameter(
request.RequestUri, name: "output", value: "json");
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching non-standard query string
/// parameters to the token request for the providers that require it.
/// </summary>
public class MapNonStandardResponseParameters : IOpenIddictClientHandler<ExtractTokenResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ExtractTokenResponseContext>()
.UseSingletonHandler<MapNonStandardResponseParameters>()
.SetOrder(int.MaxValue - 50_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ExtractTokenResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Response is null)
{
return default;
}
// Note: Deezer doesn't return a standard "expires_in" parameter
// but returns an equivalent "expires" integer parameter instead.
if (context.Registration.ProviderName is Providers.Deezer)
{
context.Response[Parameters.ExpiresIn] = context.Response["expires"];
context.Response["expires"] = null;
}
return default;
}
}
}
}

12
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs

@ -7,7 +7,6 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text.Json;
using static OpenIddict.Client.OpenIddictClientHandlers.Userinfo;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlers.Userinfo;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
@ -63,7 +62,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// using the Bearer authentication scheme. Some providers don't support this method
// and require sending the access token as part of the userinfo request payload.
if (context.Registration.ProviderName is Providers.StackExchange)
if (context.Registration.ProviderName is Providers.Deezer or Providers.StackExchange)
{
context.Request.AccessToken = request.Headers.Authorization?.Parameter;
request.Headers.Authorization = null;
@ -77,21 +76,20 @@ public static partial class OpenIddictClientWebIntegrationHandlers
/// Contains the logic responsible for extracting the userinfo response
/// from nested JSON nodes (e.g "data") for the providers that require it.
/// </summary>
public class UnwrapUserinfoResponse : IOpenIddictClientHandler<HandleUserinfoResponseContext>
public class UnwrapUserinfoResponse : IOpenIddictClientHandler<ExtractUserinfoResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleUserinfoResponseContext>()
.AddFilter<RequireHttpMetadataAddress>()
= OpenIddictClientHandlerDescriptor.CreateBuilder<ExtractUserinfoResponseContext>()
.UseSingletonHandler<UnwrapUserinfoResponse>()
.SetOrder(PopulateClaims.Descriptor.Order - 500)
.SetOrder(int.MaxValue - 50_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleUserinfoResponseContext context)
public ValueTask HandleAsync(ExtractUserinfoResponseContext context)
{
if (context is null)
{

144
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

@ -8,6 +8,7 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Security.Claims;
using OpenIddict.Extensions;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
@ -22,15 +23,18 @@ public static partial class OpenIddictClientWebIntegrationHandlers
HandleNonStandardFrontchannelErrorResponse.Descriptor,
AttachNonStandardClientAssertionTokenClaims.Descriptor,
AttachTokenRequestNonStandardClientCredentials.Descriptor,
AdjustRedirectUriInTokenRequest.Descriptor,
OverrideValidatedBackchannelTokens.Descriptor,
AttachAdditionalUserinfoRequestParameters.Descriptor,
/*
* Challenge processing:
*/
AttachNonDefaultResponseMode.Descriptor,
FormatNonStandardScopeParameter.Descriptor)
OverrideResponseMode.Descriptor,
FormatNonStandardScopeParameter.Descriptor,
IncludeStateParameterInRedirectUri.Descriptor)
.AddRange(Discovery.DefaultHandlers)
.AddRange(Exchange.DefaultHandlers)
.AddRange(Protection.DefaultHandlers)
.AddRange(Userinfo.DefaultHandlers);
@ -65,14 +69,27 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// 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.LinkedIn)
if (context.Registration.ProviderName is Providers.Deezer)
{
var error = (string?) context.Request[Parameters.Error];
if (string.IsNullOrEmpty(error))
// 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))
{
@ -178,6 +195,61 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
}
/// <summary>
/// Contains the logic responsible for attaching custom client credentials
/// parameters to the token request for the providers that require it.
/// </summary>
public 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.
if (context.Registration.ProviderName is Providers.Deezer)
{
context.TokenRequest.RedirectUri = OpenIddictHelpers.AddQueryStringParameter(
address: new Uri(context.TokenRequest.RedirectUri, UriKind.Absolute),
name: Parameters.State,
value: context.StateToken).AbsoluteUri;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for overriding the set
/// of required tokens for the providers that require it.
@ -283,9 +355,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
/// <summary>
/// Contains the logic responsible for attaching a specific response mode for providers that require it.
/// Contains the logic responsible for overriding response mode for providers that require it.
/// </summary>
public class AttachNonDefaultResponseMode : IOpenIddictClientHandler<ProcessChallengeContext>
public class OverrideResponseMode : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -293,7 +365,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachNonDefaultResponseMode>()
.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)
@ -350,7 +422,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
{
// 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),
Providers.Deezer or Providers.Reddit => string.Join(",", context.Scopes),
_ => context.Request.Scope
};
@ -358,4 +430,58 @@ public static partial class OpenIddictClientWebIntegrationHandlers
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 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.
if (context.Registration.ProviderName is Providers.Deezer)
{
context.Request.RedirectUri = OpenIddictHelpers.AddQueryStringParameter(
address: new Uri(context.RedirectUri, UriKind.Absolute),
name: Parameters.State,
value: context.Request.State).AbsoluteUri;
context.Request.State = null;
}
return default;
}
}
}

16
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml

@ -13,6 +13,22 @@
Description="The team ID associated with the developer account" />
</Provider>
<Provider Name="Deezer" Documentation="https://developers.deezer.com/api/oauth">
<!--
Note: the Deezer documentation describes an implementation with important deviations from the OAuth 2.0 standard,
including the use of many non-standard and custom parameters. Luckily, while the documentation hasn't been fixed
to reflect it, the Deezer implementation has been updated at some point to also support the standard parameters.
As such, the Deezer integration tries to use the standard parameters and only use the non-standard equivalents
when no other option exists (e.g an "output" query string parameter must be sent to get JSON token responses).
-->
<Environment Issuer="https://deezer.com/">
<Configuration AuthorizationEndpoint="https://connect.deezer.com/oauth/auth.php"
TokenEndpoint="https://connect.deezer.com/oauth/access_token.php"
UserinfoEndpoint="https://api.deezer.com/user/me" />
</Environment>
</Provider>
<Provider Name="GitHub" Documentation="https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps">
<Environment Issuer="https://github.com/">
<Configuration AuthorizationEndpoint="https://github.com/login/oauth/authorize"

Loading…
Cancel
Save