Browse Source

Add built-in support for JSON Structured Syntax Suffixes and add Xero to the list of supported providers

pull/1727/head
Kévin Chalet 3 years ago
parent
commit
8b60481f55
  1. 12
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
  2. 93
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
  3. 12
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs
  4. 12
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
  5. 12
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs

12
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs

@ -680,7 +680,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
// If the returned Content-Type doesn't indicate the response has a JSON payload,
// ignore it and allow other handlers in the pipeline to process the HTTP response.
if (!string.Equals(response.Content.Headers.ContentType?.MediaType,
MediaTypes.Json, StringComparison.OrdinalIgnoreCase))
MediaTypes.Json, StringComparison.OrdinalIgnoreCase) &&
!HasJsonStructuredSyntaxSuffix(response.Content.Headers.ContentType))
{
return;
}
@ -708,6 +709,15 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
return;
}
static bool HasJsonStructuredSyntaxSuffix(MediaTypeHeaderValue? type) =>
// If the length of the media type is less than the expected number of characters needed
// to compose a JSON-derived type (i.e application/*+json), assume the content is not JSON.
type?.MediaType is { Length: >= 18 } &&
// JSON media types MUST always start with "application/".
type.MediaType.AsSpan(0, 12).Equals("application/".AsSpan(), StringComparison.OrdinalIgnoreCase) &&
// JSON media types MUST always end with "+json".
type.MediaType.AsSpan()[^5..].Equals("+json".AsSpan(), StringComparison.OrdinalIgnoreCase);
}
}

93
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs

@ -20,8 +20,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers
*/
AmendIssuer.Descriptor,
AmendGrantTypes.Descriptor,
AmendTokenEndpointClientAuthenticationMethods.Descriptor,
AmendCodeChallengeMethods.Descriptor,
AmendScopes.Descriptor,
AmendTokenEndpointClientAuthenticationMethods.Descriptor,
AmendEndpoints.Descriptor);
/// <summary>
@ -120,18 +121,18 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
/// <summary>
/// Contains the logic responsible for amending the client authentication
/// methods supported by the token endpoint for the providers that require it.
/// Contains the logic responsible for amending the supported
/// code challenge methods for the providers that require it.
/// </summary>
public sealed class AmendTokenEndpointClientAuthenticationMethods : IOpenIddictClientHandler<HandleConfigurationResponseContext>
public sealed class AmendCodeChallengeMethods : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<AmendTokenEndpointClientAuthenticationMethods>()
.SetOrder(ExtractTokenEndpointClientAuthenticationMethods.Descriptor.Order + 500)
.UseSingletonHandler<AmendCodeChallengeMethods>()
.SetOrder(ExtractCodeChallengeMethods.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -143,17 +144,14 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
// 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)
// Microsoft Account supports both the "plain" and "S256" code challenge methods but
// doesn't list them in the server configuration metadata. To ensure the OpenIddict
// client uses Proof Key for Code Exchange for the Microsoft provider, the 2 methods
// are manually added to the list of supported code challenge methods by this handler.
if (context.Registration.ProviderName is Providers.Microsoft)
{
context.Configuration.TokenEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.PrivateKeyJwt);
context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Plain);
context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Sha256);
}
return default;
@ -161,18 +159,17 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
/// <summary>
/// Contains the logic responsible for amending the supported
/// code challenge methods for the providers that require it.
/// Contains the logic responsible for amending the supported scopes for the providers that require it.
/// </summary>
public sealed class AmendCodeChallengeMethods : IOpenIddictClientHandler<HandleConfigurationResponseContext>
public sealed class AmendScopes : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<AmendCodeChallengeMethods>()
.SetOrder(ExtractCodeChallengeMethods.Descriptor.Order + 500)
.UseSingletonHandler<AmendScopes>()
.SetOrder(ExtractScopes.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -184,14 +181,54 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
// Microsoft Account supports both the "plain" and "S256" code challenge methods but
// doesn't list them in the server configuration metadata. To ensure the OpenIddict
// client uses Proof Key for Code Exchange for the Microsoft provider, the 2 methods
// are manually added to the list of supported code challenge methods by this handler.
if (context.Registration.ProviderName is Providers.Microsoft)
// While it is a recommended node, Xero doesn't include "scopes_supported" in its server
// configuration and thus is treated as an OAuth 2.0-only provider by the OpenIddict client.
//
// To avoid that, the "openid" scope is manually added to indicate OpenID Connect is supported.
if (context.Registration.ProviderName is Providers.Xero)
{
context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Plain);
context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Sha256);
context.Configuration.ScopesSupported.Add(Scopes.OpenId);
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for amending the client authentication
/// methods supported by the token endpoint for the providers that require it.
/// </summary>
public sealed class AmendTokenEndpointClientAuthenticationMethods : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<AmendTokenEndpointClientAuthenticationMethods>()
.SetOrder(ExtractTokenEndpointClientAuthenticationMethods.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// 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.Configuration.TokenEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.PrivateKeyJwt);
}
return default;

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

@ -211,13 +211,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers
response.Content.Headers.ContentType = context.Registration.ProviderName switch
{
Providers.Mixcloud or // Mixcloud returns JSON-formatted contents declared as "text/javascript".
Providers.Patreon or // Patreon returns JSON-formatted contents declared as "application/vnd.api+json".
Providers.Vimeo // Vimeo returns JSON-formatted contents declared as "application/vnd.vimeo.user+json".
=> new MediaTypeHeaderValue(MediaTypes.Json)
{
CharSet = Charsets.Utf8
},
// Mixcloud returns JSON-formatted contents declared as "text/javascript".
Providers.Mixcloud => new MediaTypeHeaderValue(MediaTypes.Json)
{
CharSet = Charsets.Utf8
},
_ => response.Content.Headers.ContentType
};

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

@ -824,6 +824,18 @@
</Environment>
</Provider>
<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
█▄▀█▀▄██ ▄▄▄██ ▄▄▀██ ▄▄▄ ██
███ ████ ▄▄▄██ ▀▀▄██ ███ ██
█▀▄█▄▀██ ▀▀▀██ ██ ██ ▀▀▀ ██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Xero" Documentation="https://developer.xero.com/documentation/xero-app-store/app-partner-guides/sign-in/">
<Environment Issuer="https://identity.xero.com/" />
</Provider>
<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ███ █ ▄▄▀██ ██ ██ ▄▄▄ ██ ▄▄▄ ██

12
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs

@ -679,7 +679,8 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
// If the returned Content-Type doesn't indicate the response has a JSON payload,
// ignore it and allow other handlers in the pipeline to process the HTTP response.
if (!string.Equals(response.Content.Headers.ContentType?.MediaType,
MediaTypes.Json, StringComparison.OrdinalIgnoreCase))
MediaTypes.Json, StringComparison.OrdinalIgnoreCase) &&
!HasJsonStructuredSyntaxSuffix(response.Content.Headers.ContentType))
{
return;
}
@ -707,6 +708,15 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
return;
}
static bool HasJsonStructuredSyntaxSuffix(MediaTypeHeaderValue? type) =>
// If the length of the media type is less than the expected number of characters needed
// to compose a JSON-derived type (i.e application/*+json), assume the content is not JSON.
type?.MediaType is { Length: >= 18 } &&
// JSON media types MUST always start with "application/".
type.MediaType.AsSpan(0, 12).Equals("application/".AsSpan(), StringComparison.OrdinalIgnoreCase) &&
// JSON media types MUST always end with "+json".
type.MediaType.AsSpan()[^5..].Equals("+json".AsSpan(), StringComparison.OrdinalIgnoreCase);
}
}

Loading…
Cancel
Save