Browse Source

Introduce Allow*Flow() methods in the client stack to control what grant types/response types/response modes/code challenge methods are enabled

pull/1590/head
Kévin Chalet 4 years ago
parent
commit
d7582544b8
  1. 1
      Directory.Build.targets
  2. 3
      sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs
  3. 3
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  4. 5
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
  5. 3
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
  6. 15
      shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs
  7. 31
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  8. 121
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  9. 63
      src/OpenIddict.Client/OpenIddictClientConfiguration.cs
  10. 395
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  11. 24
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  12. 24
      src/OpenIddict.Client/OpenIddictClientRegistration.cs
  13. 6
      src/OpenIddict.Client/OpenIddictClientService.cs
  14. 6
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  15. 5
      src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs
  16. 5
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs
  17. 81
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  18. 22
      src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
  19. 4
      src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs
  20. 2
      src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs
  21. 6
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  22. 3
      src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
  23. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  24. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  25. 5
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  26. 8
      src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs

1
Directory.Build.targets

@ -43,6 +43,7 @@
<DefineConstants>$(DefineConstants);SUPPORTS_EPHEMERAL_KEY_SETS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_RSA_KEY_CREATION_WITH_SPECIFIED_SIZE</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_TOHASHSET_LINQ_EXTENSION</DefineConstants>
</PropertyGroup>
<PropertyGroup

3
sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs

@ -82,6 +82,9 @@ namespace OpenIddict.Sandbox.AspNet.Client
options.SetPostLogoutRedirectionEndpointUris(
"/callback/logout/local");
// Note: this sample uses the code flow, but you can enable the other flows if necessary.
options.AllowAuthorizationCodeFlow();
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()

3
sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs

@ -159,6 +159,9 @@ namespace OpenIddict.Sandbox.AspNet.Server
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris("/callback/login/github");
// Note: this sample uses the code flow, but you can enable the other flows if necessary.
options.AllowAuthorizationCodeFlow();
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()

5
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs

@ -91,6 +91,9 @@ public class Startup
options.SetPostLogoutRedirectionEndpointUris(
"/callback/logout/local");
// Note: this sample uses the code flow, but you can enable the other flows if necessary.
options.AllowAuthorizationCodeFlow();
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()
@ -119,7 +122,7 @@ public class Startup
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" },
RedirectUri = new Uri("https://localhost:44381/callback/login/local", UriKind.Absolute),
PostLogoutRedirectUri = new Uri("https://localhost:44381/callback/logout/local", UriKind.Absolute),
PostLogoutRedirectUri = new Uri("https://localhost:44381/callback/logout/local", UriKind.Absolute)
});
// Register the Web providers integrations.

3
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs

@ -76,6 +76,9 @@ public class Startup
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.SetRedirectionEndpointUris("/callback/login/github");
// Note: this sample uses the code flow, but you can enable the other flows if necessary.
options.AllowAuthorizationCodeFlow();
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
options.AddDevelopmentEncryptionCertificate()

15
shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs

@ -78,6 +78,19 @@ internal static class OpenIddictHelpers
}
}
#if !SUPPORTS_TOHASHSET_LINQ_EXTENSION
/// <summary>
/// Creates a new <see cref="HashSet{T}"/> instance and imports the elements present in the specified source.
/// </summary>
/// <typeparam name="TSource">The type of the elements present in the collection.</typeparam>
/// <param name="source">The source collection.</param>
/// <param name="comparer">The comparer to use.</param>
/// <returns>A new <see cref="HashSet{T}"/> instance and imports the elements present in the specified source.</returns>
/// <exception cref="ArgumentNullException">The <paramref name="source"/> is <see langword="null"/>.</exception>
public static HashSet<TSource> ToHashSet<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource>? comparer)
=> new(source ?? throw new ArgumentNullException(nameof(source)), comparer);
#endif
/// <summary>
/// Adds a query string parameter to the specified <see cref="Uri"/>.
/// </summary>
@ -212,7 +225,7 @@ internal static class OpenIddictHelpers
// a user identity, a fake one containing an "unauthenticated" identity (i.e with its
// AuthenticationType property deliberately left to null) is used to allow the host
// to return a "successful" authentication result for these delegation-only scenarios.
if (!principals.Any(principal => principal?.Identity is ClaimsIdentity { IsAuthenticated: true }))
if (!Array.Exists(principals, static principal => principal?.Identity is ClaimsIdentity { IsAuthenticated: true }))
{
return new ClaimsPrincipal(new ClaimsIdentity());
}

31
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1151,13 +1151,13 @@ To validate tokens received by custom API endpoints, the OpenIddict validation h
<value>The specified grant type is not supported.</value>
</data>
<data name="ID0297" xml:space="preserve">
<value>A common grant type supported by both the client and the server couldn't be negotiated automatically. If the error persists, consider specifying a list of allowed grant types in the client registration and ensure the supported grant types listed in the authorization server configuration are appropriate.</value>
<value>A common grant type supported by both the client and the server couldn't be negotiated automatically. Ensure at least one common flow is enabled in the client options. If the error persists, consider specifying a list of allowed grant types in the client registration and ensure the supported grant types listed in the authorization server configuration are appropriate.</value>
</data>
<data name="ID0298" xml:space="preserve">
<value>A common response type combination supported by both the client and the server couldn't be negotiated automatically. If the error persists, consider specifying a list of allowed response type combinations in the client registration and ensure the supported response type combinations listed in the authorization server configuration are appropriate.</value>
<value>A common response type combination supported by both the client and the server couldn't be negotiated automatically. Ensure at least one common flow is enabled in the client options. If the error persists, consider specifying a list of allowed response type combinations in the client registration and ensure the supported response type combinations listed in the authorization server configuration are appropriate.</value>
</data>
<data name="ID0299" xml:space="preserve">
<value>A common response mode supported by both the client and the server couldn't be negotiated automatically. If the error persists, consider specifying a list of allowed response modes in the client registration and ensure the supported response modes listed in the authorization server configuration are appropriate.</value>
<value>A common response mode supported by both the client and the server couldn't be negotiated automatically. Ensure at least one common flow is enabled in the client options. If the error persists, consider specifying a list of allowed response modes in the client registration and ensure the supported response modes listed in the authorization server configuration are appropriate.</value>
</data>
<data name="ID0300" xml:space="preserve">
<value>A redirection URI must be specified in the client registration options when using OpenID Connect.</value>
@ -1189,7 +1189,7 @@ To apply redirection responses, create a class implementing 'IOpenIddictClientHa
<value>The specified list of valid token types is not valid.</value>
</data>
<data name="ID0309" xml:space="preserve">
<value>A grant type must be specified when triggering authentication demands from endpoints that are not managed by the OpenIddict client stack.</value>
<value>A grant type must be specified when triggering authentication demands from endpoints that are not managed by the OpenIddict client stack. This error may also indicate that the redirection endpoint was not correctly enabled in the OpenIddict client options.</value>
</data>
<data name="ID0310" xml:space="preserve">
<value>The specified grant type ({0}) is not currently supported for authentication demands.</value>
@ -1358,6 +1358,29 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID0355" xml:space="preserve">
<value>No issuer was specified in the authentication context.</value>
</data>
<data name="ID0356" xml:space="preserve">
<value>The redirection endpoint must be enabled to use the authorization code and implicit flows.</value>
</data>
<data name="ID0357" xml:space="preserve">
<value>At least one encryption key must be registered in the OpenIddict client options when using interactive login or logout flows.
Consider registering a certificate using 'services.AddOpenIddict().AddClient().AddEncryptionCertificate()' or 'services.AddOpenIddict().AddClient().AddDevelopmentEncryptionCertificate()' or call 'services.AddOpenIddict().AddClient().AddEphemeralEncryptionKey()' to use an ephemeral key.</value>
</data>
<data name="ID0358" xml:space="preserve">
<value>At least one signing key must be registered in the OpenIddict client options when enabling using interactive login or logout flows.
Consider registering a certificate using 'services.AddOpenIddict().AddClient().AddSigningCertificate()' or 'services.AddOpenIddict().AddClient().AddDevelopmentSigningCertificate()' or call 'services.AddOpenIddict().AddClient().AddEphemeralSigningKey()' to use an ephemeral key.</value>
</data>
<data name="ID0359" xml:space="preserve">
<value>The specified grant type ({0}) has not been enabled in the OpenIddict client options.</value>
</data>
<data name="ID0360" xml:space="preserve">
<value>No grant type enabled in the client options could be found in the list of grant types allowed by the client registration, which typically indicates an invalid configuration. Ensure the 'OpenIddictClientRegistration.GrantTypes' collection contain at least one of the grant types enabled in the client options or leave it empty to allow OpenIddict to negotiate all the enabled grant types.</value>
</data>
<data name="ID0361" xml:space="preserve">
<value>No response type enabled in the client options could be found in the list of response types allowed by the client registration, which typically indicates an invalid configuration. Ensure the 'OpenIddictClientRegistration.ResponseTypes' collection contain at least one of the response types enabled in the client options or leave it empty to allow OpenIddict to negotiate all the enabled response types.</value>
</data>
<data name="ID0362" xml:space="preserve">
<value>No response mode enabled in the client options could be found in the list of response modes allowed by the client registration, which typically indicates an invalid configuration. Ensure the 'OpenIddictClientRegistration.ResponseModes' collection contain at least one of the response modes enabled in the client options or leave it empty to allow OpenIddict to negotiate all the enabled response modes.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>

121
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -208,7 +208,7 @@ public sealed class OpenIddictClientBuilder
.OfType<X509Certificate2>()
.ToList();
if (!certificates.Any(certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now))
if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now))
{
#if SUPPORTS_CERTIFICATE_GENERATION
using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048);
@ -321,7 +321,8 @@ public sealed class OpenIddictClientBuilder
if (certificate.Version >= 3)
{
var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Any(extension => extension.KeyUsages.HasFlag(X509KeyUsageFlags.KeyEncipherment)))
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
extension.KeyUsages.HasFlag(X509KeyUsageFlags.KeyEncipherment)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0060));
}
@ -570,7 +571,7 @@ public sealed class OpenIddictClientBuilder
.OfType<X509Certificate2>()
.ToList();
if (!certificates.Any(certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now))
if (!certificates.Exists(static certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now))
{
#if SUPPORTS_CERTIFICATE_GENERATION
using var algorithm = OpenIddictHelpers.CreateRsaKey(size: 2048);
@ -712,7 +713,8 @@ public sealed class OpenIddictClientBuilder
if (certificate.Version >= 3)
{
var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Any(extension => extension.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature)))
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
extension.KeyUsages.HasFlag(X509KeyUsageFlags.DigitalSignature)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0070));
}
@ -886,6 +888,109 @@ public sealed class OpenIddictClientBuilder
public OpenIddictClientBuilder DisableTokenStorage()
=> Configure(options => options.DisableTokenStorage = true);
/// <summary>
/// Enables authorization code flow support. For more information
/// about this specific OAuth 2.0/OpenID Connect flow, visit
/// https://tools.ietf.org/html/rfc6749#section-4.1 and
/// http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth.
/// </summary>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
public OpenIddictClientBuilder AllowAuthorizationCodeFlow()
=> Configure(options =>
{
options.CodeChallengeMethods.Add(CodeChallengeMethods.Sha256);
options.GrantTypes.Add(GrantTypes.AuthorizationCode);
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
options.ResponseModes.Add(ResponseModes.Query);
options.ResponseTypes.Add(ResponseTypes.Code);
});
/// <summary>
/// Enables client credentials flow support. For more information about this
/// specific OAuth 2.0 flow, visit https://tools.ietf.org/html/rfc6749#section-4.4.
/// </summary>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
public OpenIddictClientBuilder AllowClientCredentialsFlow()
=> Configure(options => options.GrantTypes.Add(GrantTypes.ClientCredentials));
/// <summary>
/// Enables hybrid flow support. For more information
/// about this specific OpenID Connect flow, visit
/// http://openid.net/specs/openid-connect-core-1_0.html#HybridFlowAuth.
/// </summary>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
public OpenIddictClientBuilder AllowHybridFlow()
=> Configure(options =>
{
options.CodeChallengeMethods.Add(CodeChallengeMethods.Sha256);
options.GrantTypes.Add(GrantTypes.AuthorizationCode);
options.GrantTypes.Add(GrantTypes.Implicit);
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token);
options.ResponseTypes.Add(ResponseTypes.Code + ' ' + ResponseTypes.Token);
});
/// <summary>
/// Enables implicit flow support. For more information
/// about this specific OAuth 2.0/OpenID Connect flow, visit
/// https://tools.ietf.org/html/rfc6749#section-4.2 and
/// http://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth.
/// </summary>
/// <remarks>
/// The implicit flow is not recommended for new applications and should
/// only be enabled when maintaining backward compatibility is important.
/// </remarks>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
public OpenIddictClientBuilder AllowImplicitFlow()
=> Configure(options =>
{
options.GrantTypes.Add(GrantTypes.Implicit);
options.ResponseModes.Add(ResponseModes.FormPost);
options.ResponseModes.Add(ResponseModes.Fragment);
options.ResponseTypes.Add(ResponseTypes.IdToken);
options.ResponseTypes.Add(ResponseTypes.IdToken + ' ' + ResponseTypes.Token);
// Note: response_type=token is not considered secure enough as it allows malicious
// actors to inject access tokens that were initially issued to a different client.
// As such, while OpenIddict-based servers allow using response_type=token for backward
// compatibility with legacy clients, OpenIddict-based clients are deliberately not
// allowed to negotiate the unsafe and OAuth 2.0-only response_type=token flow.
//
// For more information, see https://datatracker.ietf.org/doc/html/rfc6749#section-10.16 and
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-2.1.2.
});
/// <summary>
/// Enables password flow support. For more information about this specific
/// OAuth 2.0 flow, visit https://tools.ietf.org/html/rfc6749#section-4.3.
/// </summary>
/// <remarks>
/// The password flow is not recommended for new applications and should
/// only be enabled when maintaining backward compatibility is important.
/// </remarks>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
public OpenIddictClientBuilder AllowPasswordFlow()
=> Configure(options => options.GrantTypes.Add(GrantTypes.Password));
/// <summary>
/// Enables refresh token flow support. For more information about this
/// specific OAuth 2.0 flow, visit https://tools.ietf.org/html/rfc6749#section-6.
/// </summary>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
public OpenIddictClientBuilder AllowRefreshTokenFlow()
=> Configure(options => options.GrantTypes.Add(GrantTypes.RefreshToken));
/// <summary>
/// Sets the relative or absolute URLs associated to the redirection endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
@ -928,12 +1033,12 @@ public sealed class OpenIddictClientBuilder
throw new ArgumentNullException(nameof(addresses));
}
if (addresses.Any(address => !address.IsWellFormedOriginalString()))
if (Array.Exists(addresses, static address => !address.IsWellFormedOriginalString()))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(addresses));
}
if (addresses.Any(address => address.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase)))
if (Array.Exists(addresses, static address => address.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException(SR.FormatID0081("~"), nameof(addresses));
}
@ -975,12 +1080,12 @@ public sealed class OpenIddictClientBuilder
throw new ArgumentNullException(nameof(addresses));
}
if (addresses.Any(address => !address.IsWellFormedOriginalString()))
if (Array.Exists(addresses, static address => !address.IsWellFormedOriginalString()))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0072), nameof(addresses));
}
if (addresses.Any(address => address.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase)))
if (Array.Exists(addresses, static address => address.OriginalString.StartsWith("~", StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException(SR.FormatID0081("~"), nameof(addresses));
}

63
src/OpenIddict.Client/OpenIddictClientConfiguration.cs

@ -9,6 +9,7 @@ using System.Diagnostics;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
namespace OpenIddict.Client;
@ -63,8 +64,8 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
else
{
if (!options.Handlers.Any(descriptor => descriptor.ContextType == typeof(ApplyConfigurationRequestContext)) ||
!options.Handlers.Any(descriptor => descriptor.ContextType == typeof(ApplyCryptographyRequestContext)))
if (!options.Handlers.Exists(static descriptor => descriptor.ContextType == typeof(ApplyConfigurationRequestContext)) ||
!options.Handlers.Exists(static descriptor => descriptor.ContextType == typeof(ApplyCryptographyRequestContext)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0313));
}
@ -97,6 +98,64 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
}
}
// Ensure at least one flow has been enabled.
if (options.GrantTypes.Count is 0)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0076));
}
var addresses = options.RedirectionEndpointUris.Distinct()
.Concat(options.PostLogoutRedirectionEndpointUris.Distinct())
.ToList();
// Ensure endpoint addresses are unique across endpoints.
if (addresses.Count != addresses.Distinct().Count())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0285));
}
// Ensure the redirection endpoint has been enabled when the authorization code or implicit grants are supported.
if (options.RedirectionEndpointUris.Count is 0 && (options.GrantTypes.Contains(GrantTypes.AuthorizationCode) ||
options.GrantTypes.Contains(GrantTypes.Implicit)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0356));
}
// Ensure the grant types/response types configuration is consistent.
foreach (var type in options.ResponseTypes)
{
var types = type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToHashSet(StringComparer.Ordinal);
if (types.Contains(ResponseTypes.Code) && !options.GrantTypes.Contains(GrantTypes.AuthorizationCode))
{
throw new InvalidOperationException(SR.FormatID0281(ResponseTypes.Code));
}
if (types.Contains(ResponseTypes.IdToken) && !options.GrantTypes.Contains(GrantTypes.Implicit))
{
throw new InvalidOperationException(SR.FormatID0282(ResponseTypes.IdToken));
}
if (types.Contains(ResponseTypes.Token) && !options.GrantTypes.Contains(GrantTypes.Implicit))
{
throw new InvalidOperationException(SR.FormatID0282(ResponseTypes.Token));
}
}
// When the redirection or post-logout redirection endpoint has been enabled, ensure signing
// and encryption credentials have been provided as they are required to protect state tokens.
if (options.RedirectionEndpointUris.Count is not 0 || options.PostLogoutRedirectionEndpointUris.Count is not 0)
{
if (options.EncryptionCredentials.Count is 0)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0357));
}
if (options.SigningCredentials.Count is 0)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0358));
}
}
// Ensure issuers are not used in multiple client registrations.
if (options.Registrations.Count != options.Registrations.Select(registration => registration.Issuer)
.Distinct()

395
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -167,6 +167,17 @@ public static partial class OpenIddictClientHandlers
{
case OpenIddictClientEndpointType.Redirection:
case OpenIddictClientEndpointType.PostLogoutRedirection:
// Ensure signing/and encryption credentials are present as they are required to protect state tokens.
if (context.Options.EncryptionCredentials.Count is 0)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0357));
}
if (context.Options.SigningCredentials.Count is 0)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0358));
}
break;
case OpenIddictClientEndpointType.Unknown:
@ -182,6 +193,11 @@ public static partial class OpenIddictClientHandlers
throw new InvalidOperationException(SR.FormatID0310(context.GrantType));
}
if (!context.Options.GrantTypes.Contains(context.GrantType))
{
throw new InvalidOperationException(SR.FormatID0359(context.GrantType));
}
if (context.GrantType is GrantTypes.Password)
{
if (string.IsNullOrEmpty(context.Username))
@ -1313,11 +1329,7 @@ public static partial class OpenIddictClientHandlers
{
Key: Claims.Audience or Claims.AuthenticationMethodReference,
Value: List<Claim> values
} => values.Count switch
{
1 => values[0].ValueType is ClaimValueTypes.String,
_ => values.All(value => value.ValueType is ClaimValueTypes.String)
},
} => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
// The following JWT claims MUST be represented as unique numeric dates.
{
@ -2615,11 +2627,7 @@ public static partial class OpenIddictClientHandlers
{
Key: Claims.Audience or Claims.AuthenticationMethodReference,
Value: List<Claim> values
} => values.Count switch
{
1 => values[0].ValueType is ClaimValueTypes.String,
_ => values.All(value => value.ValueType is ClaimValueTypes.String)
},
} => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
// The following JWT claims MUST be represented as unique numeric dates.
{
@ -3539,11 +3547,30 @@ public static partial class OpenIddictClientHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0006));
}
// If an explicit grant type was specified, ensure it is supported by OpenIddict.
if (!string.IsNullOrEmpty(context.GrantType) &&
context.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.Implicit))
// If an explicit grant type was specified, ensure it is
// supported by OpenIddict and enabled in the client options.
if (!string.IsNullOrEmpty(context.GrantType))
{
if (context.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.Implicit))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0296));
}
if (!context.Options.GrantTypes.Contains(context.GrantType))
{
throw new InvalidOperationException(SR.FormatID0359(context.GrantType));
}
}
// Ensure signing/and encryption credentials are present as they are required to protect state tokens.
if (context.Options.EncryptionCredentials.Count is 0)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0296));
throw new InvalidOperationException(SR.GetResourceString(SR.ID0357));
}
if (context.Options.SigningCredentials.Count is 0)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0358));
}
// If a provider name was specified, resolve the corresponding issuer.
@ -3657,40 +3684,39 @@ public static partial class OpenIddictClientHandlers
// See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
// and https://datatracker.ietf.org/doc/html/rfc8414#section-2 for more information.
context.GrantType = (context.Registration.GrantTypes, context.Configuration.GrantTypesSupported) switch
context.GrantType = (
// Note: if grant types are explicitly listed in the client registration, only use
// the grant types that are both listed and enabled in the global client options.
// Otherwise, always default to the grant types that have been enabled globally.
SupportedClientGrantTypes: context.Registration.GrantTypes.Count switch
{
0 => context.Options.GrantTypes as ICollection<string>,
_ => context.Options.GrantTypes.Intersect(context.Registration.GrantTypes, StringComparer.Ordinal).ToList()
},
SupportedServerGrantTypes: context.Configuration.GrantTypesSupported) switch
{
// If neither the client nor the server specify a list of grant types,
// use the authorization code grant, as it's always supported by default.
({ Count: 0 }, { Count: 0 }) => GrantTypes.AuthorizationCode,
// If the list of grant types supported by the client is empty, abort the challenge operation.
({ Count: 0 }, { Count: _ }) => throw new InvalidOperationException(SR.GetResourceString(SR.ID0360)),
// If the client supports the code grant and the server doesn't specify a list of
// grant types, use the authorization code grant, as it's always supported by default.
({ Count: > 0 } client, { Count: 0 }) when client.Contains(GrantTypes.AuthorizationCode)
// If both the client and the server support the code grant, prefer it over the implicit grant.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(GrantTypes.AuthorizationCode) && server.Contains(GrantTypes.AuthorizationCode)
=> GrantTypes.AuthorizationCode,
// If the client supports the code grant and the server doesn't specify a list of
// grant types, use the authorization code grant, as it's always supported by default.
({ Count: 0 }, { Count: > 0 } server) when server.Contains(GrantTypes.AuthorizationCode)
({ Count: > 0 } client, { Count: 0 }) when client.Contains(GrantTypes.AuthorizationCode)
=> GrantTypes.AuthorizationCode,
// If both the client and the server support the code grant, prefer it over the implicit grant.
// If both the client and the server support the implicit grant, use it as a last chance option.
({ Count: > 0 } client, { Count: > 0 } server) when
server.Contains(GrantTypes.AuthorizationCode) && client.Contains(GrantTypes.AuthorizationCode)
=> GrantTypes.AuthorizationCode,
// If the client supports the implicit grant and the server doesn't specify a list
// of grant types, use the implicit code grant, as it's always supported by default.
({ Count: > 0 } client, { Count: 0 }) when client.Contains(GrantTypes.Implicit)
client.Contains(GrantTypes.Implicit) && server.Contains(GrantTypes.Implicit)
=> GrantTypes.Implicit,
// If the client supports the implicit grant and the server doesn't specify a list
// of grant types, use the implicit code grant, as it's always supported by default.
({ Count: 0 }, { Count: > 0 } server) when server.Contains(GrantTypes.Implicit)
=> GrantTypes.Implicit,
// If both the client and the server support the implicit grant, use it as a last chance option.
({ Count: > 0 } client, { Count: > 0 } server) when
server.Contains(GrantTypes.Implicit) && client.Contains(GrantTypes.Implicit)
({ Count: > 0 } client, { Count: 0 }) when client.Contains(GrantTypes.Implicit)
=> GrantTypes.Implicit,
// If no common grant type can be negotiated, abort the challenge operation.
@ -3806,12 +3832,40 @@ public static partial class OpenIddictClientHandlers
return default;
}
// Only attach a response type for the grant types known to support this mechanism.
if (context.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.Implicit))
{
return default;
}
context.ResponseType = (
context.GrantType,
context.Registration.ResponseTypes.Select(types =>
types.Split(Separators.Space).ToImmutableHashSet(StringComparer.Ordinal)).ToList(),
context.Configuration.ResponseTypesSupported.Select(types =>
types.Split(Separators.Space).ToImmutableHashSet(StringComparer.Ordinal)).ToList()) switch
NegotiatedGrantType: context.GrantType,
// Note: if response types are explicitly listed in the client registration, only use
// the response types that are both listed and enabled in the global client options.
// Otherwise, always default to the response types that have been enabled globally.
SupportedClientResponseTypes: context.Registration.ResponseTypes.Count switch
{
0 => context.Options.ResponseTypes.Select(types => types
.Split(Separators.Space, StringSplitOptions.None)
.ToHashSet(StringComparer.Ordinal))
.ToList(),
_ => context.Options.ResponseTypes.Select(types => types
.Split(Separators.Space, StringSplitOptions.None)
.ToHashSet(StringComparer.Ordinal))
.Where(types => context.Registration.ResponseTypes.Any(value => value
.Split(Separators.Space, StringSplitOptions.None)
.ToHashSet(StringComparer.Ordinal)
.SetEquals(types)))
.ToList()
},
SupportedServerResponseTypes: context.Configuration.ResponseTypesSupported
.Select(types => types
.Split(Separators.Space, StringSplitOptions.None)
.ToHashSet(StringComparer.Ordinal))
.ToList()) switch
{
// Note: the OAuth 2.0 provider metadata and OpenID Connect discovery specifications define
// the supported response types as a required property. Nevertheless, to ensure OpenIddict
@ -3834,121 +3888,95 @@ public static partial class OpenIddictClientHandlers
// concerns that code-based flows - that require a backchannel request - typically don't
// have when the client application (confidential or public) is executed on a server.
// If neither the client nor the server specify a list of response types,
// use "response_type=code", as it's the most commonly supported type.
(GrantTypes.AuthorizationCode, { Count: 0 }, { Count: 0 })
// If the list of response types supported by the client is empty, abort the challenge operation.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: 0 }, { Count: _ })
=> throw new InvalidOperationException(SR.GetResourceString(SR.ID0361)),
// If both the client and the server support "response_type=code", use it.
(GrantTypes.AuthorizationCode, { Count: > 0 } client, { Count: > 0 } server) when
client.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.Code)) &&
server.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.Code))
=> ResponseTypes.Code,
// If the client supports "response_type=code" and the server doesn't
// specify a list of response types, use "response_type=code".
// specify a list of response types, assume "response_type=code" is supported.
(GrantTypes.AuthorizationCode, { Count: > 0 } client, { Count: 0 }) when
client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code }))
=> ResponseTypes.Code,
// If the server supports "response_type=code" and the client doesn't
// specify a list of response types, use "response_type=code".
(GrantTypes.AuthorizationCode, { Count: 0 }, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code }))
client.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.Code))
=> ResponseTypes.Code,
// If both the client and the server support "response_type=code", use it.
(GrantTypes.AuthorizationCode, { Count: > 0 } client, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code })) &&
client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code }))
=> ResponseTypes.Code,
// If both the client and the server support "response_type=code id_token", use it.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when
client.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.IdToken)) &&
server.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.IdToken))
=> ResponseTypes.Code + ' ' + ResponseTypes.IdToken,
// If the client supports "response_type=code id_token" and the server doesn't
// specify a list of response types, use "response_type=code id_token".
// specify a list of response types, assume "response_type=code id_token" is supported.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: 0 }) when
client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken }))
client.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.IdToken))
=> ResponseTypes.Code + ' ' + ResponseTypes.IdToken,
// If the server supports "response_type=code id_token" and the client doesn't
// specify a list of response types, use "response_type=code id_token".
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: 0 }, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken }))
=> ResponseTypes.Code + ' ' + ResponseTypes.IdToken,
// If both the client and the server support "response_type=code id_token", use it.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken })) &&
client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken }))
=> ResponseTypes.Code + ' ' + ResponseTypes.IdToken,
// If neither the client nor the server specify a list of response types, use "response_type=id_token".
(GrantTypes.Implicit, { Count: 0 }, { Count: 0 })
// If both the client and the server support "response_type=id_token", use it.
(GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when
client.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.IdToken)) &&
server.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.IdToken))
=> ResponseTypes.IdToken,
// If the client supports "response_type=id_token" and the server doesn't
// specify a list of response types, use "response_type=id_token".
// specify a list of response types, assume "response_type=id_token" is supported.
(GrantTypes.Implicit, { Count: > 0 } client, { Count: 0 }) when
client.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken }))
=> ResponseTypes.IdToken,
// If the server supports "response_type=id_token" and the client doesn't
// specify a list of response types, use "response_type=id_token".
(GrantTypes.Implicit, { Count: 0 }, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken }))
client.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.IdToken))
=> ResponseTypes.IdToken,
// If both the client and the server support "response_type=id_token", use it.
(GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken })) &&
client.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken }))
=> ResponseTypes.IdToken,
// If both the client and the server support "response_type=code id_token token", use it.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when
client.Exists(static types => types.Count is 3 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.IdToken) &&
types.Contains(ResponseTypes.Token)) &&
server.Exists(static types => types.Count is 3 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.IdToken) &&
types.Contains(ResponseTypes.Token))
=> ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token,
// If the client supports "response_type=code id_token token" and the server doesn't
// specify a list of response types, use "response_type=code id_token token".
// specify a list of response types, assume "response_type=code id_token token" is supported.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: 0 }) when
client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken, ResponseTypes.Token }))
client.Exists(static types => types.Count is 3 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.IdToken) &&
types.Contains(ResponseTypes.Token))
=> ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token,
// If the server supports "response_type=code id_token token" and the client doesn't
// specify a list of response types, use "response_type=code id_token token".
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: 0 }, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken, ResponseTypes.Token }))
=> ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token,
// If both the client and the server support "response_type=code id_token token", use it.
// If both the client and the server support "response_type=code token", use it.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken, ResponseTypes.Token })) &&
client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.IdToken, ResponseTypes.Token }))
=> ResponseTypes.Code + ' ' + ResponseTypes.IdToken + ' ' + ResponseTypes.Token,
client.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.Token)) &&
server.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.Token))
=> ResponseTypes.Code + ' ' + ResponseTypes.Token,
// If the client supports "response_type=code token" and the server doesn't
// specify a list of response types, use "response_type=code token".
// specify a list of response types, assume "response_type=code token" is supported.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: 0 }) when
client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.Token }))
=> ResponseTypes.Code + ' ' + ResponseTypes.Token,
// If the server supports "response_type=code token" and the client doesn't
// specify a list of response types, use "response_type=code token".
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: 0 }, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.Token }))
client.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.Code) &&
types.Contains(ResponseTypes.Token))
=> ResponseTypes.Code + ' ' + ResponseTypes.Token,
// If both the client and the server support "response_type=code token", use it.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.Token })) &&
client.Any(static set => set.SetEquals(new[] { ResponseTypes.Code, ResponseTypes.Token }))
=> ResponseTypes.Code + ' ' + ResponseTypes.Token,
// If both the client and the server support "response_type=id_token token", use it.
(GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when
client.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.IdToken) &&
types.Contains(ResponseTypes.Token)) &&
server.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.IdToken) &&
types.Contains(ResponseTypes.Token))
=> ResponseTypes.IdToken + ' ' + ResponseTypes.Token,
// If the client supports "response_type=id_token token" and the server doesn't
// specify a list of response types, use "response_type=id_token token".
// specify a list of response types, assume "response_type=id_token token" is supported.
(GrantTypes.Implicit, { Count: > 0 } client, { Count: 0 }) when
client.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken, ResponseTypes.Token }))
=> ResponseTypes.IdToken + ' ' + ResponseTypes.Token,
// If the server supports "response_type=id_token token" and the client doesn't
// specify a list of response types, use "response_type=id_token token".
(GrantTypes.Implicit, { Count: 0 }, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken, ResponseTypes.Token }))
=> ResponseTypes.IdToken + ' ' + ResponseTypes.Token,
// If both the client and the server support "response_type=id_token token", use it.
(GrantTypes.Implicit, { Count: > 0 } client, { Count: > 0 } server) when
server.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken, ResponseTypes.Token })) &&
client.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken, ResponseTypes.Token }))
client.Exists(static types => types.Count is 2 && types.Contains(ResponseTypes.IdToken) &&
types.Contains(ResponseTypes.Token))
=> ResponseTypes.IdToken + ' ' + ResponseTypes.Token,
// Note: response_type=token is not considered secure enough as it allows malicious
@ -3961,7 +3989,7 @@ public static partial class OpenIddictClientHandlers
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-2.1.2.
// If no common response type can be negotiated, abort the challenge operation.
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0298))
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0298)),
};
return default;
@ -3997,6 +4025,12 @@ public static partial class OpenIddictClientHandlers
return default;
}
// Only attach a response mode for the grant types known to support this mechanism.
if (context.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.Implicit))
{
return default;
}
// Note: in most cases, the query response mode will be used as it offers the
// best compatibility and, unlike the form_post response mode, is compatible
// with SameSite=Lax cookies (as it uses GET requests for the callback stage).
@ -4014,30 +4048,26 @@ public static partial class OpenIddictClientHandlers
// can never be used with a response type containing id_token or token, as required by the OAuth 2.0
// multiple response types specification. To prevent invalid combinations from being sent to the
// remote server, the response types are taken into account when selecting the best response mode.
var types = context.ResponseType!.Split(Separators.Space).ToImmutableHashSet(StringComparer.Ordinal);
context.ResponseMode = (context.Registration.ResponseModes, context.Configuration.ResponseModesSupported) switch
var types = context.ResponseType?.Split(Separators.Space).ToHashSet(StringComparer.Ordinal);
if (types is not { Count: > 0 })
{
// If neither the client nor the server specify a list of response modes,
// use "response_mode=form_post" if the response types contain a value
// that prevents response_mode=query from being used (token/id_token).
({ Count: 0 }, { Count: 0 }) when
types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token)
=> ResponseModes.FormPost,
return default;
}
// If the client support response_mode=form_post and the server doesn't
// specify a list of response modes, use it if the response types contain
// a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
context.ResponseMode = (
// Note: if response modes are explicitly listed in the client registration, only use
// the response modes that are both listed and enabled in the global client options.
// Otherwise, always default to the response modes that have been enabled globally.
SupportedClientResponseModes: context.Registration.ResponseModes.Count switch
{
0 => context.Options.ResponseModes as ICollection<string>,
_ => context.Options.ResponseModes.Intersect(context.Registration.ResponseModes, StringComparer.Ordinal).ToList()
},
// If the server support response_mode=form_post and the server doesn't
// specify a list of response modes, use it if the response types contain
// a value that prevents response_mode=query from being used (token/id_token).
({ Count: 0 }, { Count: > 0 } server) when server.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
SupportedServerResponseModes: context.Configuration.ResponseModesSupported) switch
{
// If the list of response modes supported by the client is empty, abort the challenge operation.
({ Count: 0 }, { Count: _ }) => throw new InvalidOperationException(SR.GetResourceString(SR.ID0362)),
// If both the client and the server support response_mode=form_post, use it if the response
// types contain a value that prevents response_mode=query from being used (token/id_token).
@ -4046,24 +4076,27 @@ public static partial class OpenIddictClientHandlers
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
// If neither the client nor the server specify a list of response modes,
// use "response_mode=query" as a fallback as it's universally supported:
({ Count: 0 }, { Count: 0 }) => ResponseModes.Query,
// If the client support response_mode=form_post and the server doesn't specify a list
// of response modes, assume it is supported and use it if the response types contain
// a value that prevents response_mode=query from being used (token/id_token).
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.FormPost) &&
(types.Contains(ResponseTypes.IdToken) || types.Contains(ResponseTypes.Token))
=> ResponseModes.FormPost,
// If the client support response_mode=query and the server
// doesn't specify a list of response modes, use it:
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.Query)
// If both the client and the server support response_mode=query, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.Query) && server.Contains(ResponseModes.Query)
=> ResponseModes.Query,
// If the server support response_mode=query and the client
// doesn't specify a list of response modes, use it:
({ Count: 0 }, { Count: > 0 } server) when server.Contains(ResponseModes.Query)
// If the client support response_mode=query and the server doesn't
// specify a list of response modes, assume it is supported.
({ Count: > 0 } client, { Count: 0 }) when client.Contains(ResponseModes.Query)
=> ResponseModes.Query,
// If both the client and the server support response_mode=query, use it:
// If both the client and the server support response_mode=form_post, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(ResponseModes.Query) && server.Contains(ResponseModes.Query)
=> ResponseModes.Query,
client.Contains(ResponseModes.FormPost) && server.Contains(ResponseModes.FormPost)
=> ResponseModes.FormPost,
// If no common response mode can be negotiated, abort the challenge operation.
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0299))
@ -4278,34 +4311,39 @@ public static partial class OpenIddictClientHandlers
// Don't attach a code challenge method if no authorization code is requested as some implementations
// (like OpenIddict server) are known to eagerly block authorization requests that specify an invalid
// code_challenge/code_challenge_method/response_type combination (e.g response_type=id_token).
if (!context.ResponseType!.Split(Separators.Space).Contains(ResponseTypes.Code))
var types = context.ResponseType?.Split(Separators.Space);
if (types is not { Length: > 0 } || !types.Contains(ResponseTypes.Code))
{
return default;
}
context.CodeChallengeMethod ??= (
context.Registration.CodeChallengeMethods,
context.Configuration.CodeChallengeMethodsSupported) switch
// Note: if code challenge methods are explicitly listed in the client registration, only use
// the code challenge methods that are both listed and enabled in the global client options.
// Otherwise, always default to the code challenge methods that have been enabled globally.
SupportedClientCodeChallengeMethods: context.Registration.CodeChallengeMethods.Count switch
{
0 => context.Options.CodeChallengeMethods as ICollection<string>,
_ => context.Options.CodeChallengeMethods
.Intersect(context.Registration.CodeChallengeMethods, StringComparer.Ordinal)
.ToList(),
},
SupportedServerCodeChallengeMethods: context.Configuration.CodeChallengeMethodsSupported) switch
{
// If neither the client nor the server specify a list of code challenge methods, don't use PKCE.
({ Count: 0 }, { Count: 0 }) => null,
// If the list of code challenge methods supported by the
// client is empty, don't use Proof Key for Code Exchange.
({ Count: 0 }, { Count: _ }) => null,
// If the server doesn't specify a list of code challenge methods, don't use PKCE.
// If the server doesn't specify a list of code challenge methods,
// Proof Key for Code Exchange is assumed to be unsupported.
({ Count: > 0 }, { Count: 0 }) => null,
// If the client doesn't specify a list of code challenge methods but the server support S256, use it.
({ Count: 0 }, { Count: > 0 } server) when server.Contains(CodeChallengeMethods.Sha256)
=> CodeChallengeMethods.Sha256,
// If both the client and the server support S256, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(CodeChallengeMethods.Sha256) && server.Contains(CodeChallengeMethods.Sha256)
=> CodeChallengeMethods.Sha256,
// If the client doesn't specify a list of code challenge methods but the server support plain, use it.
({ Count: 0 }, { Count: > 0 } server) when server.Contains(CodeChallengeMethods.Plain)
=> CodeChallengeMethods.Plain,
// If both the client and the server support plain, use it.
({ Count: > 0 } client, { Count: > 0 } server) when
client.Contains(CodeChallengeMethods.Plain) && server.Contains(CodeChallengeMethods.Plain)
@ -4316,7 +4354,7 @@ public static partial class OpenIddictClientHandlers
// Note: while enforced by OAuth 2.1 under certain circumstances, PKCE is not a required feature for
// OAuth 2.0 and OpenID Connect (where features like nonce validation can serve similar purposes).
// As such, no error is returned at this stage is no common code challenge method could be inferred.
// As such, no error is returned at this stage if no common code challenge method could be inferred.
if (string.IsNullOrEmpty(context.CodeChallengeMethod))
{
return default;
@ -4691,6 +4729,17 @@ public static partial class OpenIddictClientHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0024));
}
// Ensure signing/and encryption credentials are present as they are required to protect state tokens.
if (context.Options.EncryptionCredentials.Count is 0)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0357));
}
if (context.Options.SigningCredentials.Count is 0)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0358));
}
// If a provider name was specified, resolve the corresponding issuer.
if (!string.IsNullOrEmpty(context.ProviderName))
{

24
src/OpenIddict.Client/OpenIddictClientOptions.cs

@ -4,6 +4,7 @@
* the license and the contributors participating to this project.
*/
using System.ComponentModel;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
@ -117,4 +118,27 @@ public sealed class OpenIddictClientOptions
/// as it prevents the tokens from being revoked (if needed).
/// </summary>
public bool DisableTokenStorage { get; set; }
/// <summary>
/// Gets the OAuth 2.0 code challenge methods enabled for this application.
/// By default, only the S256 method is allowed (if the code flow is enabled).
/// </summary>
public HashSet<string> CodeChallengeMethods { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the OAuth 2.0/OpenID Connect flows enabled for this application.
/// </summary>
public HashSet<string> GrantTypes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the OAuth 2.0/OpenID Connect response types enabled for this application.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public HashSet<string> ResponseTypes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the OAuth 2.0/OpenID Connect response modes enabled for this application.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public HashSet<string> ResponseModes { get; } = new(StringComparer.Ordinal);
}

24
src/OpenIddict.Client/OpenIddictClientRegistration.cs

@ -52,41 +52,41 @@ public sealed class OpenIddictClientRegistration
/// <summary>
/// Gets the code challenge methods allowed by the client instance.
/// If no value is explicitly set, the default code challenge methods are automatically used.
/// If no value is explicitly set, all the methods enabled in the client options can be used.
/// </summary>
/// <remarks>
/// The final code challenge method used in authorization requests is chosen by OpenIddict
/// based on the server configuration and the values registered in this property.
/// The final code challenge method used in authorization requests is chosen by OpenIddict based
/// on the client options, the server configuration and the values registered in this property.
/// </remarks>
public HashSet<string> CodeChallengeMethods { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the grant types allowed by the client instance.
/// If no value is explicitly set, the default grant types are automatically used.
/// If no value is explicitly set, all the modes enabled in the client options can be used.
/// </summary>
/// <remarks>
/// The final grant type used in authorization requests is chosen by OpenIddict
/// based on the server configuration and the values registered in this property.
/// The final grant type used in authorization requests is chosen by OpenIddict based on
/// the client options, the server configuration and the values registered in this property.
/// </remarks>
public HashSet<string> GrantTypes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the response type combinations allowed by the client instance.
/// If no value is explicitly set, the default response types are automatically used.
/// If no value is explicitly set, all the types enabled in the client options can be used.
/// </summary>
/// <remarks>
/// The final response type used in authorization requests is chosen by OpenIddict
/// based on the server configuration and the values registered in this property.
/// The final response type used in authorization requests is chosen by OpenIddict based on
/// the client options, the server configuration and the values registered in this property.
/// </remarks>
public HashSet<string> ResponseTypes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the response modes allowed by the client instance.
/// If no value is explicitly set, the default response modes are automatically used.
/// If no value is explicitly set, all the modes enabled in the client options can be used.
/// </summary>
/// <remarks>
/// The final response method used in authorization requests is chosen by OpenIddict
/// based on the server configuration and the values registered in this property.
/// The final response method used in authorization requests is chosen by OpenIddict based on
/// the client options, the server configuration and the values registered in this property.
/// </remarks>
public HashSet<string> ResponseModes { get; } = new(StringComparer.Ordinal);

6
src/OpenIddict.Client/OpenIddictClientService.cs

@ -45,7 +45,7 @@ public sealed class OpenIddictClientService
throw new ArgumentNullException(nameof(issuer));
}
if (scopes is not null && scopes.Any(string.IsNullOrEmpty))
if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes));
}
@ -163,7 +163,7 @@ public sealed class OpenIddictClientService
throw new ArgumentException(SR.GetResourceString(SR.ID0336), nameof(password));
}
if (scopes is not null && scopes.Any(string.IsNullOrEmpty))
if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes));
}
@ -277,7 +277,7 @@ public sealed class OpenIddictClientService
throw new ArgumentException(SR.GetResourceString(SR.ID0156), nameof(token));
}
if (scopes is not null && scopes.Any(string.IsNullOrEmpty))
if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes));
}

6
src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs

@ -12,6 +12,7 @@ using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
using ValidationException = OpenIddict.Abstractions.OpenIddictExceptions.ValidationException;
@ -839,8 +840,9 @@ public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthori
throw new ArgumentNullException(nameof(authorization));
}
return new HashSet<string>(await Store.GetScopesAsync(
authorization, cancellationToken), StringComparer.Ordinal).IsSupersetOf(scopes);
return (await Store.GetScopesAsync(authorization, cancellationToken))
.ToHashSet(StringComparer.Ordinal)
.IsSupersetOf(scopes);
}
/// <summary>

5
src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs

@ -15,6 +15,7 @@ using System.Text.Json;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using OpenIddict.EntityFramework.Models;
using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
namespace OpenIddict.EntityFramework;
@ -308,7 +309,9 @@ public class OpenIddictEntityFrameworkAuthorizationStore<TAuthorization, TApplic
await foreach (var authorization in authorizations)
{
if (new HashSet<string>(await GetScopesAsync(authorization, cancellationToken), StringComparer.Ordinal).IsSupersetOf(scopes))
if ((await GetScopesAsync(authorization, cancellationToken))
.ToHashSet(StringComparer.Ordinal)
.IsSupersetOf(scopes))
{
yield return authorization;
}

5
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs

@ -14,6 +14,7 @@ using System.Text.Json;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using OpenIddict.EntityFrameworkCore.Models;
using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
namespace OpenIddict.EntityFrameworkCore;
@ -367,7 +368,9 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore<TAuthorization, TAp
await foreach (var authorization in authorizations)
{
if (new HashSet<string>(await GetScopesAsync(authorization, cancellationToken), StringComparer.Ordinal).IsSupersetOf(scopes))
if ((await GetScopesAsync(authorization, cancellationToken))
.ToHashSet(StringComparer.Ordinal)
.IsSupersetOf(scopes))
{
yield return authorization;
}

81
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -8,6 +8,7 @@ using System.ComponentModel;
using System.Diagnostics;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
namespace OpenIddict.Server;
@ -114,7 +115,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
// Ensure the grant types/response types configuration is consistent.
foreach (var type in options.ResponseTypes)
{
var types = new HashSet<string>(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries), StringComparer.Ordinal);
var types = type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToHashSet(StringComparer.Ordinal);
if (types.Contains(ResponseTypes.Code) && !options.GrantTypes.Contains(GrantTypes.AuthorizationCode))
{
throw new InvalidOperationException(SR.FormatID0281(ResponseTypes.Code));
@ -142,20 +143,20 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0085));
}
if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey))
if (!options.SigningCredentials.Exists(static credentials => credentials.Key is AsymmetricSecurityKey))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0086));
}
// If all the registered encryption credentials are backed by a X.509 certificate, at least one of them must be valid.
if (options.EncryptionCredentials.All(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
if (options.EncryptionCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
(x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0087));
}
// If all the registered signing credentials are backed by a X.509 certificate, at least one of them must be valid.
if (options.SigningCredentials.All(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
if (options.SigningCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
(x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0088));
@ -166,58 +167,58 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
// If the degraded mode was enabled, ensure custom validation handlers
// have been registered for the endpoints that require manual validation.
if (options.AuthorizationEndpointUris.Count is not 0 && !options.Handlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateAuthorizationRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
if (options.AuthorizationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateAuthorizationRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0089));
}
if (options.DeviceEndpointUris.Count is not 0 && !options.Handlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateDeviceRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
if (options.DeviceEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateDeviceRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0090));
}
if (options.IntrospectionEndpointUris.Count is not 0 && !options.Handlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateIntrospectionRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
if (options.IntrospectionEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateIntrospectionRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0091));
}
if (options.LogoutEndpointUris.Count is not 0 && !options.Handlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateLogoutRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
if (options.LogoutEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateLogoutRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0092));
}
if (options.RevocationEndpointUris.Count is not 0 && !options.Handlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateRevocationRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
if (options.RevocationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateRevocationRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0093));
}
if (options.TokenEndpointUris.Count is not 0 && !options.Handlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateTokenRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
if (options.TokenEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateTokenRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0094));
}
if (options.VerificationEndpointUris.Count is not 0 && !options.Handlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateVerificationRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
if (options.VerificationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateVerificationRequestContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0095));
}
@ -227,18 +228,18 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
if (options.GrantTypes.Contains(GrantTypes.DeviceCode))
{
if (!options.Handlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateTokenContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
if (!options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(ValidateTokenContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0096));
}
if (!options.Handlers.Any(
descriptor => descriptor.ContextType == typeof(GenerateTokenContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
if (!options.Handlers.Exists(static descriptor =>
descriptor.ContextType == typeof(GenerateTokenContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0097));
}

22
src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs

@ -129,67 +129,67 @@ public static partial class OpenIddictServerEvents
/// <summary>
/// Gets the list of claims supported by the authorization server.
/// </summary>
public HashSet<string> Claims { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> Claims { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets a list of the code challenge methods
/// supported by the authorization server.
/// </summary>
public HashSet<string> CodeChallengeMethods { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> CodeChallengeMethods { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the list of grant types
/// supported by the authorization server.
/// </summary>
public HashSet<string> GrantTypes { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> GrantTypes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets a list of signing algorithms supported by the
/// authorization server for signing the identity tokens.
/// </summary>
public HashSet<string> IdTokenSigningAlgorithms { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> IdTokenSigningAlgorithms { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets a list of client authentication methods supported by
/// the introspection endpoint provided by the authorization server.
/// </summary>
public HashSet<string> IntrospectionEndpointAuthenticationMethods { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> IntrospectionEndpointAuthenticationMethods { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the list of response modes
/// supported by the authorization server.
/// </summary>
public HashSet<string> ResponseModes { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> ResponseModes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the list of response types
/// supported by the authorization server.
/// </summary>
public HashSet<string> ResponseTypes { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> ResponseTypes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets a list of client authentication methods supported by
/// the revocation endpoint provided by the authorization server.
/// </summary>
public HashSet<string> RevocationEndpointAuthenticationMethods { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> RevocationEndpointAuthenticationMethods { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the list of scope values
/// supported by the authorization server.
/// </summary>
public HashSet<string> Scopes { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> Scopes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the list of subject types
/// supported by the authorization server.
/// </summary>
public HashSet<string> SubjectTypes { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> SubjectTypes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets a list of client authentication methods supported by
/// the token endpoint provided by the authorization server.
/// </summary>
public HashSet<string> TokenEndpointAuthenticationMethods { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> TokenEndpointAuthenticationMethods { get; } = new(StringComparer.Ordinal);
}
/// <summary>

4
src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs

@ -106,7 +106,7 @@ public static partial class OpenIddictServerEvents
/// Gets the list of audiences returned to the caller
/// as part of the "aud" claim, if applicable.
/// </summary>
public HashSet<string> Audiences { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> Audiences { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the "client_id" claim returned to the caller, if applicable.
@ -135,7 +135,7 @@ public static partial class OpenIddictServerEvents
/// Gets the list of scopes returned to the caller
/// as part of the "scope" claim, if applicable.
/// </summary>
public HashSet<string> Scopes { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> Scopes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the "sub" claim

2
src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs

@ -107,7 +107,7 @@ public static partial class OpenIddictServerEvents
/// <summary>
/// Gets or sets the values used for the "aud" claim.
/// </summary>
public HashSet<string> Audiences { get; } = new HashSet<string>(StringComparer.Ordinal);
public HashSet<string> Audiences { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the value used for the "birthdate" claim.

6
src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

@ -643,7 +643,7 @@ public static partial class OpenIddictServerHandlers
}
// Reject requests that specify an unsupported response_type.
var types = new HashSet<string>(context.Request.GetResponseTypes(), StringComparer.Ordinal);
var types = context.Request.GetResponseTypes().ToHashSet(StringComparer.Ordinal);
if (!context.Options.ResponseTypes.Any(type =>
types.SetEquals(type.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries))))
{
@ -1232,7 +1232,7 @@ public static partial class OpenIddictServerHandlers
}
// If all the specified scopes are registered in the options, avoid making a database lookup.
var scopes = new HashSet<string>(context.Request.GetScopes(), StringComparer.Ordinal);
var scopes = context.Request.GetScopes().ToHashSet(StringComparer.Ordinal);
scopes.ExceptWith(context.Options.Scopes);
// Note: the remaining scopes are only checked if the degraded mode was not enabled,
@ -1488,7 +1488,7 @@ public static partial class OpenIddictServerHandlers
// Note: response types can be specified in any order. To ensure permissions are correctly
// checked even if the order differs from the one specified in the request, a HashSet is used.
var values = permission[prefix.Length..].Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries);
if (values.Length is not 0 && new HashSet<string>(values, StringComparer.Ordinal).SetEquals(types))
if (values.Length is not 0 && values.ToHashSet(StringComparer.Ordinal).SetEquals(types))
{
return true;
}

3
src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs

@ -10,6 +10,7 @@ using System.Security.Claims;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Extensions;
namespace OpenIddict.Server;
@ -446,7 +447,7 @@ public static partial class OpenIddictServerHandlers
}
// If all the specified scopes are registered in the options, avoid making a database lookup.
var scopes = new HashSet<string>(context.Request.GetScopes(), StringComparer.Ordinal);
var scopes = context.Request.GetScopes().ToHashSet(StringComparer.Ordinal);
scopes.ExceptWith(context.Options.Scopes);
// Note: the remaining scopes are only checked if the degraded mode was not enabled,

4
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -795,7 +795,7 @@ public static partial class OpenIddictServerHandlers
}
// If all the specified scopes are registered in the options, avoid making a database lookup.
var scopes = new HashSet<string>(context.Request.GetScopes(), StringComparer.Ordinal);
var scopes = context.Request.GetScopes().ToHashSet(StringComparer.Ordinal);
scopes.ExceptWith(context.Options.Scopes);
// Note: the remaining scopes are only checked if the degraded mode was not enabled,
@ -1657,7 +1657,7 @@ public static partial class OpenIddictServerHandlers
// When an explicit scope parameter has been included in the token request
// but was missing from the initial request, the request MUST be rejected.
// See http://tools.ietf.org/html/rfc6749#section-6 for more information.
var scopes = new HashSet<string>(context.Principal.GetScopes(), StringComparer.Ordinal);
var scopes = context.Principal.GetScopes().ToHashSet(StringComparer.Ordinal);
if (scopes.Count is 0)
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6094), Parameters.Scope);

2
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -2932,7 +2932,7 @@ public static partial class OpenIddictServerHandlers
// If the granted access token scopes differ from the requested scopes, return the granted scopes
// list as a parameter to inform the client application of the fact the scopes set will be reduced.
var scopes = new HashSet<string>(context.AccessTokenPrincipal.GetScopes(), StringComparer.Ordinal);
var scopes = context.AccessTokenPrincipal.GetScopes().ToHashSet(StringComparer.Ordinal);
if ((context.EndpointType is OpenIddictServerEndpointType.Token && context.Request.IsAuthorizationCodeGrantType()) ||
!scopes.SetEquals(context.Request.GetScopes()))
{

5
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -190,7 +190,8 @@ public sealed class OpenIddictValidationBuilder
if (certificate.Version >= 3)
{
var extensions = certificate.Extensions.OfType<X509KeyUsageExtension>().ToList();
if (extensions.Count is not 0 && !extensions.Any(extension => extension.KeyUsages.HasFlag(X509KeyUsageFlags.KeyEncipherment)))
if (extensions.Count is not 0 && !extensions.Exists(static extension =>
extension.KeyUsages.HasFlag(X509KeyUsageFlags.KeyEncipherment)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0060));
}
@ -355,7 +356,7 @@ public sealed class OpenIddictValidationBuilder
throw new ArgumentNullException(nameof(audiences));
}
if (audiences.Any(string.IsNullOrEmpty))
if (Array.Exists(audiences, string.IsNullOrEmpty))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0123), nameof(audiences));
}

8
src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs

@ -46,7 +46,7 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
if (options.ValidationType is OpenIddictValidationType.Introspection)
{
if (!options.Handlers.Any(descriptor => descriptor.ContextType == typeof(ApplyIntrospectionRequestContext)))
if (!options.Handlers.Exists(static descriptor => descriptor.ContextType == typeof(ApplyIntrospectionRequestContext)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0129));
}
@ -79,7 +79,7 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
// If all the registered encryption credentials are backed by a X.509 certificate, at least one of them must be valid.
if (options.EncryptionCredentials.Count is not 0 &&
options.EncryptionCredentials.All(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
options.EncryptionCredentials.TrueForAll(credentials => credentials.Key is X509SecurityKey x509SecurityKey &&
(x509SecurityKey.Certificate.NotBefore > DateTime.Now || x509SecurityKey.Certificate.NotAfter < DateTime.Now)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0087));
@ -95,8 +95,8 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
else
{
if (!options.Handlers.Any(descriptor => descriptor.ContextType == typeof(ApplyConfigurationRequestContext)) ||
!options.Handlers.Any(descriptor => descriptor.ContextType == typeof(ApplyCryptographyRequestContext)))
if (!options.Handlers.Exists(static descriptor => descriptor.ContextType == typeof(ApplyConfigurationRequestContext)) ||
!options.Handlers.Exists(static descriptor => descriptor.ContextType == typeof(ApplyCryptographyRequestContext)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0135));
}

Loading…
Cancel
Save