Browse Source

Implement built-in audiences and resources indicators validation

pull/2340/head
Kévin Chalet 8 months ago
parent
commit
78ba0a3dec
  1. 4
      shared/OpenIddict.Extensions/OpenIddictHelpers.cs
  2. 2
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  3. 51
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  4. 144
      src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
  5. 2
      src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs
  6. 2
      src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs
  7. 4
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  8. 113
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  9. 4
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  10. 68
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  11. 416
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  12. 328
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  13. 35
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  14. 158
      test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs
  15. 246
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs
  16. 326
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  17. 4
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
  18. 203
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

4
shared/OpenIddict.Extensions/OpenIddictHelpers.cs

@ -406,7 +406,7 @@ internal static class OpenIddictHelpers
Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null)) Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(static pair => !string.IsNullOrEmpty(pair.Key)) .Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key) .GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues(pair.Select(parts => parts.Value).ToArray())); .ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]));
} }
/// <summary> /// <summary>
@ -430,7 +430,7 @@ internal static class OpenIddictHelpers
Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null)) Value: parts.Length > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null))
.Where(static pair => !string.IsNullOrEmpty(pair.Key)) .Where(static pair => !string.IsNullOrEmpty(pair.Key))
.GroupBy(static pair => pair.Key) .GroupBy(static pair => pair.Key)
.ToDictionary(static pair => pair.Key!, static pair => new StringValues(pair.Select(parts => parts.Value).ToArray())); .ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]));
} }
/// <summary> /// <summary>

2
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -423,9 +423,11 @@ public static class OpenIddictConstants
public static class Prefixes public static class Prefixes
{ {
public const string Audience = "aud:";
public const string Endpoint = "ept:"; public const string Endpoint = "ept:";
public const string GrantType = "gt:"; public const string GrantType = "gt:";
public const string ResponseType = "rst:"; public const string ResponseType = "rst:";
public const string Resource = "rsrc:";
public const string Scope = "scp:"; public const string Scope = "scp:";
} }

51
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1793,6 +1793,9 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID0494" xml:space="preserve"> <data name="ID0494" xml:space="preserve">
<value>The type of the actor token cannot be resolved from the authentication context.</value> <value>The type of the actor token cannot be resolved from the authentication context.</value>
</data> </data>
<data name="ID0495" xml:space="preserve">
<value>The '{0}' parameter cannot contain values that are not valid absolute URIs containing no fragment component.</value>
</data>
<data name="ID2000" xml:space="preserve"> <data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value> <value>The security token is missing.</value>
</data> </data>
@ -2360,6 +2363,24 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID2189" xml:space="preserve"> <data name="ID2189" xml:space="preserve">
<value>The specified actor token cannot be used by this client application.</value> <value>The specified actor token cannot be used by this client application.</value>
</data> </data>
<data name="ID2190" xml:space="preserve">
<value>One of the specified '{0}' parameters is invalid.</value>
</data>
<data name="ID2191" xml:space="preserve">
<value>This client application is not allowed to use the specified audience(s).</value>
</data>
<data name="ID2192" xml:space="preserve">
<value>This client application is not allowed to use the specified resource(s).</value>
</data>
<data name="ID2193" xml:space="preserve">
<value>The '{0}' parameter is not allowed in authorization requests.</value>
</data>
<data name="ID2194" xml:space="preserve">
<value>The '{0}' parameter is not allowed in pushed authorization requests.</value>
</data>
<data name="ID2195" xml:space="preserve">
<value>The '{0}' parameter is only allowed for OAuth 2.0 Token Exchange requests.</value>
</data>
<data name="ID4000" xml:space="preserve"> <data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value> <value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data> </data>
@ -3137,6 +3158,36 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6271" xml:space="preserve"> <data name="ID6271" xml:space="preserve">
<value>The token request was rejected because the actor token was issued to a different client or for another resource server.</value> <value>The token request was rejected because the actor token was issued to a different client or for another resource server.</value>
</data> </data>
<data name="ID6272" xml:space="preserve">
<value>The token request was rejected because invalid audiences were specified: {Audiences}.</value>
</data>
<data name="ID6273" xml:space="preserve">
<value>The token request was rejected because invalid resources were specified: {Resources}.</value>
</data>
<data name="ID6274" xml:space="preserve">
<value>The authorization request was rejected because invalid resources were specified: {Resources}.</value>
</data>
<data name="ID6275" xml:space="preserve">
<value>The pushed authorization request was rejected because invalid resources were specified: {Resources}.</value>
</data>
<data name="ID6276" xml:space="preserve">
<value>The token request was rejected because the application '{ClientId}' was not allowed to use the audience {Audience}.</value>
</data>
<data name="ID6277" xml:space="preserve">
<value>The token request was rejected because the application '{ClientId}' was not allowed to use the resource {Resource}.</value>
</data>
<data name="ID6278" xml:space="preserve">
<value>The authorization request was rejected because the application '{ClientId}' was not allowed to use the resource {Resource}.</value>
</data>
<data name="ID6279" xml:space="preserve">
<value>The pushed authorization request was rejected because the application '{ClientId}' was not allowed to use the resource {Resource}.</value>
</data>
<data name="ID6280" xml:space="preserve">
<value>The token request was rejected because the '{Parameter}' parameter wasn't a valid absolute URI: {RedirectUri}.</value>
</data>
<data name="ID6281" xml:space="preserve">
<value>The token request was rejected because the '{Parameter}' contained a URI fragment: {RedirectUri}.</value>
</data>
<data name="ID8000" xml:space="preserve"> <data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value> <value>https://documentation.openiddict.com/errors/{0}</value>
</data> </data>

144
src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs

@ -38,6 +38,35 @@ public static class OpenIddictExtensions
return GetValues(request.AcrValues, Separators.Space); return GetValues(request.AcrValues, Separators.Space);
} }
/// <summary>
/// Extracts the audiences from an <see cref="OpenIddictRequest"/>.
/// </summary>
/// <param name="request">The <see cref="OpenIddictRequest"/> instance.</param>
public static ImmutableArray<string> GetAudiences(this OpenIddictRequest request)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
if (request.Audiences is not { IsDefaultOrEmpty: false } audiences)
{
return [];
}
HashSet<string> set = [];
foreach (var audience in audiences)
{
if (!string.IsNullOrEmpty(audience))
{
set.Add(audience);
}
}
return [.. set];
}
/// <summary> /// <summary>
/// Extracts the prompt values from an <see cref="OpenIddictRequest"/>. /// Extracts the prompt values from an <see cref="OpenIddictRequest"/>.
/// </summary> /// </summary>
@ -52,6 +81,35 @@ public static class OpenIddictExtensions
return GetValues(request.Prompt, Separators.Space); return GetValues(request.Prompt, Separators.Space);
} }
/// <summary>
/// Extracts the resources from an <see cref="OpenIddictRequest"/>.
/// </summary>
/// <param name="request">The <see cref="OpenIddictRequest"/> instance.</param>
public static ImmutableArray<string> GetResources(this OpenIddictRequest request)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
if (request.Resources is not { IsDefaultOrEmpty: false } resources)
{
return [];
}
HashSet<string> set = [];
foreach (var resource in resources)
{
if (!string.IsNullOrEmpty(resource))
{
set.Add(resource);
}
}
return [.. set];
}
/// <summary> /// <summary>
/// Extracts the response types from an <see cref="OpenIddictRequest"/>. /// Extracts the response types from an <see cref="OpenIddictRequest"/>.
/// </summary> /// </summary>
@ -94,30 +152,100 @@ public static class OpenIddictExtensions
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{ {
throw new ArgumentException(SR.GetResourceString(SR.ID0177), nameof(value)); throw new ArgumentException(SR.FormatID0366(nameof(value)), nameof(value));
} }
return HasValue(request.AcrValues, value, Separators.Space); return HasValue(request.AcrValues, value, Separators.Space);
} }
/// <summary>
/// Determines whether the requested audiences contains the specified value.
/// </summary>
/// <param name="request">The <see cref="OpenIddictRequest"/> instance.</param>
/// <param name="audience">The value to look for in the parameters.</param>
public static bool HasAudience(this OpenIddictRequest request, string audience)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
if (string.IsNullOrEmpty(audience))
{
throw new ArgumentException(SR.FormatID0366(nameof(audience)), nameof(audience));
}
var audiences = request.Audiences;
if (audiences is null or [])
{
return false;
}
for (var index = 0; index < audiences.Value.Length; index++)
{
if (audiences.Value[index] is { Length: > 0 } value &&
string.Equals(value, audience, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
/// <summary> /// <summary>
/// Determines whether the requested prompt contains the specified value. /// Determines whether the requested prompt contains the specified value.
/// </summary> /// </summary>
/// <param name="request">The <see cref="OpenIddictRequest"/> instance.</param> /// <param name="request">The <see cref="OpenIddictRequest"/> instance.</param>
/// <param name="prompt">The component to look for in the parameter.</param> /// <param name="value">The component to look for in the parameter.</param>
public static bool HasPromptValue(this OpenIddictRequest request, string prompt) public static bool HasPromptValue(this OpenIddictRequest request, string value)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(SR.FormatID0366(nameof(value)), nameof(value));
}
return HasValue(request.Prompt, value, Separators.Space);
}
/// <summary>
/// Determines whether the requested resources contains the specified value.
/// </summary>
/// <param name="request">The <see cref="OpenIddictRequest"/> instance.</param>
/// <param name="resource">The value to look for in the parameters.</param>
public static bool HasResource(this OpenIddictRequest request, string resource)
{ {
if (request is null) if (request is null)
{ {
throw new ArgumentNullException(nameof(request)); throw new ArgumentNullException(nameof(request));
} }
if (string.IsNullOrEmpty(prompt)) if (string.IsNullOrEmpty(resource))
{
throw new ArgumentException(SR.FormatID0366(nameof(resource)), nameof(resource));
}
var resources = request.Resources;
if (resources is null or [])
{
return false;
}
for (var index = 0; index < resources.Value.Length; index++)
{ {
throw new ArgumentException(SR.GetResourceString(SR.ID0178), nameof(prompt)); if (resources.Value[index] is { Length: > 0 } value &&
string.Equals(value, resource, StringComparison.Ordinal))
{
return true;
}
} }
return HasValue(request.Prompt, prompt, Separators.Space); return false;
} }
/// <summary> /// <summary>
@ -134,7 +262,7 @@ public static class OpenIddictExtensions
if (string.IsNullOrEmpty(type)) if (string.IsNullOrEmpty(type))
{ {
throw new ArgumentException(SR.GetResourceString(SR.ID0179), nameof(type)); throw new ArgumentException(SR.FormatID0366(nameof(type)), nameof(type));
} }
return HasValue(request.ResponseType, type, Separators.Space); return HasValue(request.ResponseType, type, Separators.Space);
@ -154,7 +282,7 @@ public static class OpenIddictExtensions
if (string.IsNullOrEmpty(scope)) if (string.IsNullOrEmpty(scope))
{ {
throw new ArgumentException(SR.GetResourceString(SR.ID0180), nameof(scope)); throw new ArgumentException(SR.FormatID0366(nameof(scope)), nameof(scope));
} }
return HasValue(request.Scope, scope, Separators.Space); return HasValue(request.Scope, scope, Separators.Space);

2
src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs

@ -182,7 +182,7 @@ public class OpenIddictMessage
// parameters with the same name to represent a multi-valued parameter. // parameters with the same name to represent a multi-valued parameter.
AddParameter(parameter.Key, parameter.Value switch AddParameter(parameter.Key, parameter.Value switch
{ {
null or [] => default, null or [] => default,
[string value] => new OpenIddictParameter(value), [string value] => new OpenIddictParameter(value),
[..] values => new OpenIddictParameter(values) [..] values => new OpenIddictParameter(values)
}); });

2
src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs

@ -1165,7 +1165,7 @@ public readonly struct OpenIddictParameter : IEquatable<OpenIddictParameter>
null or JsonElement { ValueKind: JsonValueKind.Null or JsonValueKind.Undefined } => null, null or JsonElement { ValueKind: JsonValueKind.Null or JsonValueKind.Undefined } => null,
// When the parameter is an array of strings, return a StringValues instance wrapping the cloned array. // When the parameter is an array of strings, return a StringValues instance wrapping the cloned array.
string?[] value => new StringValues(value.ToArray().ToArray()), string?[] value => new StringValues([.. value]),
// When the parameter is a string value, return a StringValues instance with a single entry. // When the parameter is a string value, return a StringValues instance with a single entry.
string value => new StringValues(value), string value => new StringValues(value),

4
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -1144,7 +1144,7 @@ public sealed class OpenIddictClientBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetPostLogoutRedirectionEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetPostLogoutRedirectionEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1197,7 +1197,7 @@ public sealed class OpenIddictClientBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetRedirectionEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetRedirectionEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>

113
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -1113,7 +1113,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetAuthorizationEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetAuthorizationEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1162,7 +1162,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetConfigurationEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetConfigurationEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1211,7 +1211,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetDeviceAuthorizationEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetDeviceAuthorizationEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1260,7 +1260,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetEndSessionEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetEndSessionEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1309,7 +1309,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetIntrospectionEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetIntrospectionEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1358,7 +1358,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetJsonWebKeySetEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetJsonWebKeySetEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1407,7 +1407,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetPushedAuthorizationEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetPushedAuthorizationEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1456,7 +1456,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetRevocationEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetRevocationEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1505,7 +1505,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetTokenEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetTokenEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1554,7 +1554,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetUserInfoEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetUserInfoEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1603,7 +1603,7 @@ public sealed class OpenIddictServerBuilder
throw new ArgumentNullException(nameof(uris)); throw new ArgumentNullException(nameof(uris));
} }
return SetEndUserVerificationEndpointUris(uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute)).ToArray()); return SetEndUserVerificationEndpointUris([.. uris.Select(uri => new Uri(uri, UriKind.RelativeOrAbsolute))]);
} }
/// <summary> /// <summary>
@ -1646,6 +1646,14 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder DisableAccessTokenEncryption() public OpenIddictServerBuilder DisableAccessTokenEncryption()
=> Configure(options => options.DisableAccessTokenEncryption = true); => Configure(options => options.DisableAccessTokenEncryption = true);
/// <summary>
/// Allows processing authorization and token requests that specify audiences that
/// have not been registered using <see cref="RegisterAudiences(string[])"/>.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder DisableAudienceValidation()
=> Configure(options => options.DisableAudienceValidation = true);
/// <summary> /// <summary>
/// Disables authorization storage so that ad-hoc authorizations are /// Disables authorization storage so that ad-hoc authorizations are
/// not created when an authorization code or refresh token is issued /// not created when an authorization code or refresh token is issued
@ -1656,6 +1664,14 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder DisableAuthorizationStorage() public OpenIddictServerBuilder DisableAuthorizationStorage()
=> Configure(options => options.DisableAuthorizationStorage = true); => Configure(options => options.DisableAuthorizationStorage = true);
/// <summary>
/// Allows processing authorization and token requests that specify resources that
/// have not been registered using <see cref="RegisterResources(string[])"/>.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder DisableResourceValidation()
=> Configure(options => options.DisableResourceValidation = true);
/// <summary> /// <summary>
/// Configures OpenIddict to disable rolling refresh tokens so /// Configures OpenIddict to disable rolling refresh tokens so
/// that refresh tokens used in a token request are not marked /// that refresh tokens used in a token request are not marked
@ -1707,6 +1723,13 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder EnableDegradedMode() public OpenIddictServerBuilder EnableDegradedMode()
=> Configure(options => options.EnableDegradedMode = true); => Configure(options => options.EnableDegradedMode = true);
/// <summary>
/// Disables audience permissions enforcement. Calling this method is NOT recommended.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder IgnoreAudiencePermissions()
=> Configure(options => options.IgnoreAudiencePermissions = true);
/// <summary> /// <summary>
/// Disables endpoint permissions enforcement. Calling this method is NOT recommended. /// Disables endpoint permissions enforcement. Calling this method is NOT recommended.
/// </summary> /// </summary>
@ -1721,6 +1744,13 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder IgnoreGrantTypePermissions() public OpenIddictServerBuilder IgnoreGrantTypePermissions()
=> Configure(options => options.IgnoreGrantTypePermissions = true); => Configure(options => options.IgnoreGrantTypePermissions = true);
/// <summary>
/// Disables resource permissions enforcement. Calling this method is NOT recommended.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder IgnoreResourcePermissions()
=> Configure(options => options.IgnoreResourcePermissions = true);
/// <summary> /// <summary>
/// Disables response type permissions enforcement. Calling this method is NOT recommended. /// Disables response type permissions enforcement. Calling this method is NOT recommended.
/// </summary> /// </summary>
@ -1735,6 +1765,27 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder IgnoreScopePermissions() public OpenIddictServerBuilder IgnoreScopePermissions()
=> Configure(options => options.IgnoreScopePermissions = true); => Configure(options => options.IgnoreScopePermissions = true);
/// <summary>
/// Registers the specified audiences as supported audiences
/// (exclusively used with the OAuth 2.0 Token Exchange flow).
/// </summary>
/// <param name="audiences">The supported audiences.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder RegisterAudiences(params string[] audiences)
{
if (audiences is null)
{
throw new ArgumentNullException(nameof(audiences));
}
if (Array.Exists(audiences, string.IsNullOrEmpty))
{
throw new ArgumentException(SR.FormatID0457(nameof(audiences)), nameof(audiences));
}
return Configure(options => options.Audiences.UnionWith(audiences));
}
/// <summary> /// <summary>
/// Registers the specified claims as supported claims so /// Registers the specified claims as supported claims so
/// they can be returned as part of the discovery document. /// they can be returned as part of the discovery document.
@ -1777,6 +1828,46 @@ public sealed class OpenIddictServerBuilder
return Configure(options => options.PromptValues.UnionWith(values)); return Configure(options => options.PromptValues.UnionWith(values));
} }
/// <summary>
/// Registers the specified resources as supported resources (typically used
/// with the OAuth 2.0 Token Exchange flow and with authorization or pushed
/// authorization requests that include one or more resource indicators).
/// </summary>
/// <param name="resources">The supported resources.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder RegisterResources(params string[] resources)
{
if (resources is null)
{
throw new ArgumentNullException(nameof(resources));
}
return RegisterResources([.. resources.Select(resource => new Uri(resource, UriKind.Absolute))]);
}
/// <summary>
/// Registers the specified resources as supported resources (typically used
/// with the OAuth 2.0 Token Exchange flow and with authorization or pushed
/// authorization requests that include one or more resource indicators).
/// </summary>
/// <param name="resources">The supported resources.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder RegisterResources(params Uri[] resources)
{
if (resources is null)
{
throw new ArgumentNullException(nameof(resources));
}
if (Array.Exists(resources, static resource => OpenIddictHelpers.IsImplicitFileUri(resource) ||
!string.IsNullOrEmpty(resource.Fragment)))
{
throw new ArgumentException(SR.FormatID0495(nameof(resources)), nameof(resources));
}
return Configure(options => options.Resources.UnionWith(resources));
}
/// <summary> /// <summary>
/// Registers the specified scopes as supported scopes so /// Registers the specified scopes as supported scopes so
/// they can be returned as part of the discovery document. /// they can be returned as part of the discovery document.

4
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -43,6 +43,8 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireAccessTokenGenerated>(); builder.Services.TryAddSingleton<RequireAccessTokenGenerated>();
builder.Services.TryAddSingleton<RequireAccessTokenValidated>(); builder.Services.TryAddSingleton<RequireAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireActorTokenValidated>(); builder.Services.TryAddSingleton<RequireActorTokenValidated>();
builder.Services.TryAddSingleton<RequireAudiencePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireAudienceValidationEnabled>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeGenerated>(); builder.Services.TryAddSingleton<RequireAuthorizationCodeGenerated>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeValidated>(); builder.Services.TryAddSingleton<RequireAuthorizationCodeValidated>();
builder.Services.TryAddSingleton<RequireAuthorizationIdResolved>(); builder.Services.TryAddSingleton<RequireAuthorizationIdResolved>();
@ -77,6 +79,8 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireRequestTokenGenerated>(); builder.Services.TryAddSingleton<RequireRequestTokenGenerated>();
builder.Services.TryAddSingleton<RequireRequestTokenPrincipal>(); builder.Services.TryAddSingleton<RequireRequestTokenPrincipal>();
builder.Services.TryAddSingleton<RequireRequestTokenValidated>(); builder.Services.TryAddSingleton<RequireRequestTokenValidated>();
builder.Services.TryAddSingleton<RequireResourcePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireResourceValidationEnabled>();
builder.Services.TryAddSingleton<RequireResponseTypePermissionsEnabled>(); builder.Services.TryAddSingleton<RequireResponseTypePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireRevocationRequest>(); builder.Services.TryAddSingleton<RequireRevocationRequest>();
builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>(); builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>();

68
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -62,6 +62,40 @@ public static class OpenIddictServerHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if audience permissions were disabled.
/// </summary>
public sealed class RequireAudiencePermissionsEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.Options.IgnoreAudiencePermissions);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if audience validation was not enabled.
/// </summary>
public sealed class RequireAudienceValidationEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.Options.DisableAudienceValidation);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if no authorization code is generated. /// Represents a filter that excludes the associated handlers if no authorization code is generated.
/// </summary> /// </summary>
@ -640,6 +674,40 @@ public static class OpenIddictServerHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if resource permissions were disabled.
/// </summary>
public sealed class RequireResourcePermissionsEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.Options.IgnoreResourcePermissions);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if resource validation was not enabled.
/// </summary>
public sealed class RequireResourceValidationEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.Options.DisableResourceValidation);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if response type permissions were disabled. /// Represents a filter that excludes the associated handlers if response type permissions were disabled.
/// </summary> /// </summary>

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

@ -44,16 +44,20 @@ public static partial class OpenIddictServerHandlers
ValidateResponseTypeParameter.Descriptor, ValidateResponseTypeParameter.Descriptor,
ValidateResponseModeParameter.Descriptor, ValidateResponseModeParameter.Descriptor,
ValidateScopeParameter.Descriptor, ValidateScopeParameter.Descriptor,
ValidateAudienceParameter.Descriptor,
ValidateResourceParameter.Descriptor,
ValidateNonceParameter.Descriptor, ValidateNonceParameter.Descriptor,
ValidatePromptParameter.Descriptor, ValidatePromptParameter.Descriptor,
ValidateProofKeyForCodeExchangeParameters.Descriptor, ValidateProofKeyForCodeExchangeParameters.Descriptor,
ValidateResponseType.Descriptor, ValidateResponseType.Descriptor,
ValidateClientRedirectUri.Descriptor, ValidateClientRedirectUri.Descriptor,
ValidateScopes.Descriptor, ValidateScopes.Descriptor,
ValidateResources.Descriptor,
ValidateEndpointPermissions.Descriptor, ValidateEndpointPermissions.Descriptor,
ValidateGrantTypePermissions.Descriptor, ValidateGrantTypePermissions.Descriptor,
ValidateResponseTypePermissions.Descriptor, ValidateResponseTypePermissions.Descriptor,
ValidateScopePermissions.Descriptor, ValidateScopePermissions.Descriptor,
ValidateResourcePermissions.Descriptor,
ValidatePushedAuthorizationRequestsRequirement.Descriptor, ValidatePushedAuthorizationRequestsRequirement.Descriptor,
ValidateProofKeyForCodeExchangeRequirement.Descriptor, ValidateProofKeyForCodeExchangeRequirement.Descriptor,
ValidateAuthorizedParty.Descriptor, ValidateAuthorizedParty.Descriptor,
@ -92,6 +96,8 @@ public static partial class OpenIddictServerHandlers
ValidatePushedResponseTypeParameter.Descriptor, ValidatePushedResponseTypeParameter.Descriptor,
ValidatePushedResponseModeParameter.Descriptor, ValidatePushedResponseModeParameter.Descriptor,
ValidatePushedScopeParameter.Descriptor, ValidatePushedScopeParameter.Descriptor,
ValidatePushedAudienceParameter.Descriptor,
ValidatePushedResourceParameter.Descriptor,
ValidatePushedNonceParameter.Descriptor, ValidatePushedNonceParameter.Descriptor,
ValidatePushedPromptParameter.Descriptor, ValidatePushedPromptParameter.Descriptor,
ValidatePushedProofKeyForCodeExchangeParameters.Descriptor, ValidatePushedProofKeyForCodeExchangeParameters.Descriptor,
@ -99,10 +105,12 @@ public static partial class OpenIddictServerHandlers
ValidatePushedResponseType.Descriptor, ValidatePushedResponseType.Descriptor,
ValidatePushedClientRedirectUri.Descriptor, ValidatePushedClientRedirectUri.Descriptor,
ValidatePushedScopes.Descriptor, ValidatePushedScopes.Descriptor,
ValidatePushedResources.Descriptor,
ValidatePushedEndpointPermissions.Descriptor, ValidatePushedEndpointPermissions.Descriptor,
ValidatePushedGrantTypePermissions.Descriptor, ValidatePushedGrantTypePermissions.Descriptor,
ValidatePushedResponseTypePermissions.Descriptor, ValidatePushedResponseTypePermissions.Descriptor,
ValidatePushedScopePermissions.Descriptor, ValidatePushedScopePermissions.Descriptor,
ValidatePushedResourcePermissions.Descriptor,
ValidatePushedProofKeyForCodeExchangeRequirement.Descriptor, ValidatePushedProofKeyForCodeExchangeRequirement.Descriptor,
ValidatePushedAuthorizedParty.Descriptor, ValidatePushedAuthorizedParty.Descriptor,
@ -1055,6 +1063,103 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for rejecting authorization requests that specify an audience parameter.
/// </summary>
public sealed class ValidateAudienceParameter : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.UseSingletonHandler<ValidateAudienceParameter>()
.SetOrder(ValidateScopeParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Prevent audiences parameters from being attached to authorization requests, as the
// standard "audience" parameter can only be used in OAuth 2.0 Token Exchange requests.
if (context.Request.Audiences is not (null or []))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2193(Parameters.Audience),
uri: SR.FormatID8000(SR.ID2193));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting authorization requests that don't specify a valid resource parameter.
/// </summary>
public sealed class ValidateResourceParameter : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.UseSingletonHandler<ValidateResourceParameter>()
.SetOrder(ValidateAudienceParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
foreach (var resource in context.Request.GetResources())
{
// Note: resource indicators MUST be valid URIs.
//
// For more information, see https://datatracker.ietf.org/doc/html/rfc8707#name-resource-parameter.
if (!Uri.TryCreate(resource, UriKind.Absolute, out Uri? uri) || OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Logger.LogInformation(6034, SR.GetResourceString(SR.ID6034), Parameters.Resource, resource);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2030(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2030));
return default;
}
// Note: resource indicators MUST NOT contain a fragment.
if (!string.IsNullOrEmpty(uri.Fragment))
{
context.Logger.LogInformation(6035, SR.GetResourceString(SR.ID6035), Parameters.Resource, resource);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2031(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2031));
return default;
}
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authorization requests that don't specify a nonce. /// Contains the logic responsible for rejecting authorization requests that don't specify a nonce.
/// </summary> /// </summary>
@ -1066,7 +1171,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.UseSingletonHandler<ValidateNonceParameter>() .UseSingletonHandler<ValidateNonceParameter>()
.SetOrder(ValidateScopeParameter.Descriptor.Order + 1_000) .SetOrder(ValidateResourceParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -1524,6 +1629,50 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for rejecting authorization requests that use unregistered resources.
/// </summary>
public sealed class ValidateResources : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.AddFilter<RequireResourceValidationEnabled>()
.UseSingletonHandler<ValidateResources>()
.SetOrder(ValidateScopes.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If at least one resource was not recognized, return an error.
var resources = context.Request.GetResources().ToHashSet(StringComparer.Ordinal);
resources.ExceptWith(context.Options.Resources.Select(static resource => resource.AbsoluteUri));
if (resources.Count is not 0)
{
context.Logger.LogInformation(6275, SR.GetResourceString(SR.ID6274), resources);
context.Reject(
error: Errors.InvalidTarget,
description: SR.FormatID2190(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2190));
return default;
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authorization requests made by unauthorized applications. /// Contains the logic responsible for rejecting authorization requests made by unauthorized applications.
/// Note: this handler is not used when the degraded mode is enabled or when endpoint permissions are disabled. /// Note: this handler is not used when the degraded mode is enabled or when endpoint permissions are disabled.
@ -1545,7 +1694,7 @@ public static partial class OpenIddictServerHandlers
.AddFilter<RequireEndpointPermissionsEnabled>() .AddFilter<RequireEndpointPermissionsEnabled>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateEndpointPermissions>() .UseScopedHandler<ValidateEndpointPermissions>()
.SetOrder(ValidateScopes.Descriptor.Order + 1_000) .SetOrder(ValidateResources.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -1816,6 +1965,63 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for rejecting authorization requests made by
/// applications that haven't been granted the appropriate audience permissions.
/// Note: this handler is not used when the degraded mode is enabled or when audience permissions are disabled.
/// </summary>
public sealed class ValidateResourcePermissions : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
public ValidateResourcePermissions() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public ValidateResourcePermissions(IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.AddFilter<RequireResourcePermissionsEnabled>()
.AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateResourcePermissions>()
.SetOrder(ValidateScopePermissions.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
foreach (var resource in context.Request.GetResources())
{
// Reject the request if the application is not allowed to use the iterated resource.
if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Resource + resource))
{
context.Logger.LogInformation(6281, SR.GetResourceString(SR.ID6278), context.ClientId, resource);
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2192),
uri: SR.FormatID8000(SR.ID2192));
return;
}
}
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authorization requests made by /// Contains the logic responsible for rejecting authorization requests made by
/// applications for which pushed authorization requests (PAR) are enforced. /// applications for which pushed authorization requests (PAR) are enforced.
@ -1837,7 +2043,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidatePushedAuthorizationRequestsRequirement>() .UseScopedHandler<ValidatePushedAuthorizationRequestsRequirement>()
.SetOrder(ValidateScopePermissions.Descriptor.Order + 1_000) .SetOrder(ValidateResourcePermissions.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -2951,6 +3157,103 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for rejecting pushed authorization requests that specify an audience parameter.
/// </summary>
public sealed class ValidatePushedAudienceParameter : IOpenIddictServerHandler<ValidatePushedAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidatePushedAuthorizationRequestContext>()
.UseSingletonHandler<ValidatePushedAudienceParameter>()
.SetOrder(ValidatePushedScopeParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Prevent audiences parameters from being attached to pushed authorization requests, as the
// standard "audience" parameter can only be used in OAuth 2.0 Token Exchange requests.
if (context.Request.Audiences is not (null or []))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2194(Parameters.Audience),
uri: SR.FormatID8000(SR.ID2194));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting pushed authorization requests that don't specify a valid resource parameter.
/// </summary>
public sealed class ValidatePushedResourceParameter : IOpenIddictServerHandler<ValidatePushedAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidatePushedAuthorizationRequestContext>()
.UseSingletonHandler<ValidatePushedResourceParameter>()
.SetOrder(ValidatePushedAudienceParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
foreach (var resource in context.Request.GetResources())
{
// Note: resource indicators MUST be valid URIs.
//
// For more information, see https://datatracker.ietf.org/doc/html/rfc8707#name-resource-parameter.
if (!Uri.TryCreate(resource, UriKind.Absolute, out Uri? uri) || OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Logger.LogInformation(6241, SR.GetResourceString(SR.ID6241), Parameters.Resource, resource);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2030(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2030));
return default;
}
// Note: resource indicators MUST NOT contain a fragment.
if (!string.IsNullOrEmpty(uri.Fragment))
{
context.Logger.LogInformation(6242, SR.GetResourceString(SR.ID6242), Parameters.Resource, resource);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2031(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2031));
return default;
}
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting pushed authorization requests that don't specify a nonce. /// Contains the logic responsible for rejecting pushed authorization requests that don't specify a nonce.
/// </summary> /// </summary>
@ -2962,7 +3265,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidatePushedAuthorizationRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidatePushedAuthorizationRequestContext>()
.UseSingletonHandler<ValidatePushedNonceParameter>() .UseSingletonHandler<ValidatePushedNonceParameter>()
.SetOrder(ValidatePushedScopeParameter.Descriptor.Order + 1_000) .SetOrder(ValidatePushedResourceParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -3481,6 +3784,50 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for rejecting pushed authorization requests that use unregistered resources.
/// </summary>
public sealed class ValidatePushedResources : IOpenIddictServerHandler<ValidatePushedAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidatePushedAuthorizationRequestContext>()
.AddFilter<RequireResourceValidationEnabled>()
.UseSingletonHandler<ValidatePushedResources>()
.SetOrder(ValidatePushedScopes.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If at least one resource was not recognized, return an error.
var resources = context.Request.GetResources().ToHashSet(StringComparer.Ordinal);
resources.ExceptWith(context.Options.Resources.Select(static resource => resource.AbsoluteUri));
if (resources.Count is not 0)
{
context.Logger.LogInformation(6275, SR.GetResourceString(SR.ID6275), resources);
context.Reject(
error: Errors.InvalidTarget,
description: SR.FormatID2190(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2190));
return default;
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting pushed authorization requests made by unauthorized applications. /// Contains the logic responsible for rejecting pushed authorization requests made by unauthorized applications.
/// Note: this handler is not used when the degraded mode is enabled or when endpoint permissions are disabled. /// Note: this handler is not used when the degraded mode is enabled or when endpoint permissions are disabled.
@ -3502,7 +3849,7 @@ public static partial class OpenIddictServerHandlers
.AddFilter<RequireEndpointPermissionsEnabled>() .AddFilter<RequireEndpointPermissionsEnabled>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidatePushedEndpointPermissions>() .UseScopedHandler<ValidatePushedEndpointPermissions>()
.SetOrder(ValidatePushedScopes.Descriptor.Order + 1_000) .SetOrder(ValidatePushedResources.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -3773,6 +4120,63 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for rejecting pushed authorization requests made by
/// applications that haven't been granted the appropriate audience permissions.
/// Note: this handler is not used when the degraded mode is enabled or when audience permissions are disabled.
/// </summary>
public sealed class ValidatePushedResourcePermissions : IOpenIddictServerHandler<ValidatePushedAuthorizationRequestContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
public ValidatePushedResourcePermissions() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public ValidatePushedResourcePermissions(IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidatePushedAuthorizationRequestContext>()
.AddFilter<RequireResourcePermissionsEnabled>()
.AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidatePushedResourcePermissions>()
.SetOrder(ValidatePushedScopePermissions.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ValidatePushedAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
foreach (var resource in context.Request.GetResources())
{
// Reject the request if the application is not allowed to use the iterated resource.
if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Resource + resource))
{
context.Logger.LogInformation(6283, SR.GetResourceString(SR.ID6279), context.ClientId, resource);
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2192),
uri: SR.FormatID8000(SR.ID2192));
return;
}
}
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting pushed authorization requests made by /// Contains the logic responsible for rejecting pushed authorization requests made by
/// applications for which proof key for code exchange (PKCE) was enforced. /// applications for which proof key for code exchange (PKCE) was enforced.
@ -3794,7 +4198,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidatePushedAuthorizationRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidatePushedAuthorizationRequestContext>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidatePushedProofKeyForCodeExchangeRequirement>() .UseScopedHandler<ValidatePushedProofKeyForCodeExchangeRequirement>()
.SetOrder(ValidatePushedScopePermissions.Descriptor.Order + 1_000) .SetOrder(ValidatePushedResourcePermissions.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();

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

@ -47,11 +47,17 @@ public static partial class OpenIddictServerHandlers
ValidateResourceOwnerCredentialsParameters.Descriptor, ValidateResourceOwnerCredentialsParameters.Descriptor,
ValidateProofKeyForCodeExchangeParameters.Descriptor, ValidateProofKeyForCodeExchangeParameters.Descriptor,
ValidateScopeParameter.Descriptor, ValidateScopeParameter.Descriptor,
ValidateAudienceParameter.Descriptor,
ValidateResourceParameter.Descriptor,
ValidateScopes.Descriptor, ValidateScopes.Descriptor,
ValidateAudiences.Descriptor,
ValidateResources.Descriptor,
ValidateAuthentication.Descriptor, ValidateAuthentication.Descriptor,
ValidateEndpointPermissions.Descriptor, ValidateEndpointPermissions.Descriptor,
ValidateGrantTypePermissions.Descriptor, ValidateGrantTypePermissions.Descriptor,
ValidateScopePermissions.Descriptor, ValidateScopePermissions.Descriptor,
ValidateAudiencePermissions.Descriptor,
ValidateResourcePermissions.Descriptor,
ValidateProofKeyForCodeExchangeRequirement.Descriptor, ValidateProofKeyForCodeExchangeRequirement.Descriptor,
ValidateAuthorizedParty.Descriptor, ValidateAuthorizedParty.Descriptor,
ValidateRedirectUri.Descriptor, ValidateRedirectUri.Descriptor,
@ -946,7 +952,111 @@ public static partial class OpenIddictServerHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authorization requests that use unregistered scopes. /// Contains the logic responsible for rejecting token requests that specify an audience parameter in an invalid context.
/// </summary>
public sealed class ValidateAudienceParameter : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseSingletonHandler<ValidateAudienceParameter>()
.SetOrder(ValidateScopeParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Request.Audiences is null or [])
{
return default;
}
// Prevent audiences parameters from being attached to token requests that don't use the
// OAuth 2.0 Token Exchange grant type, unless the specified grant type is a custom value.
if (context.Request.IsAuthorizationCodeGrantType() || context.Request.IsClientCredentialsGrantType() ||
context.Request.IsDeviceCodeGrantType() || context.Request.IsImplicitFlow() ||
context.Request.IsPasswordGrantType() || context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2195(Parameters.Audience),
uri: SR.FormatID8000(SR.ID2195));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting token requests that don't specify a valid resource parameter.
/// </summary>
public sealed class ValidateResourceParameter : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseSingletonHandler<ValidateResourceParameter>()
.SetOrder(ValidateAudienceParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
foreach (var resource in context.Request.GetResources())
{
// Note: resource indicators MUST be valid URIs.
//
// For more information, see https://datatracker.ietf.org/doc/html/rfc8707#name-resource-parameter.
if (!Uri.TryCreate(resource, UriKind.Absolute, out Uri? uri) || OpenIddictHelpers.IsImplicitFileUri(uri))
{
context.Logger.LogInformation(6284, SR.GetResourceString(SR.ID6280), Parameters.Resource, resource);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2030(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2030));
return default;
}
// Note: resource indicators MUST NOT contain a fragment.
if (!string.IsNullOrEmpty(uri.Fragment))
{
context.Logger.LogInformation(6285, SR.GetResourceString(SR.ID6281), Parameters.Resource, resource);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2031(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2031));
return default;
}
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting token requests that use unregistered scopes.
/// Note: this handler partially works with the degraded mode but is not used when scope validation is disabled. /// Note: this handler partially works with the degraded mode but is not used when scope validation is disabled.
/// </summary> /// </summary>
public sealed class ValidateScopes : IOpenIddictServerHandler<ValidateTokenRequestContext> public sealed class ValidateScopes : IOpenIddictServerHandler<ValidateTokenRequestContext>
@ -973,7 +1083,7 @@ public static partial class OpenIddictServerHandlers
new ValidateScopes(provider.GetService<IOpenIddictScopeManager>() ?? new ValidateScopes(provider.GetService<IOpenIddictScopeManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016))); throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
}) })
.SetOrder(ValidateScopeParameter.Descriptor.Order + 1_000) .SetOrder(ValidateResourceParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -1024,6 +1134,94 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for rejecting token requests that use unregistered audiences.
/// </summary>
public sealed class ValidateAudiences : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.AddFilter<RequireAudienceValidationEnabled>()
.UseSingletonHandler<ValidateAudiences>()
.SetOrder(ValidateScopes.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If at least one audience was not recognized, return an error.
var audiences = context.Request.GetAudiences().ToHashSet(StringComparer.Ordinal);
audiences.ExceptWith(context.Options.Audiences);
if (audiences.Count is not 0)
{
context.Logger.LogInformation(6272, SR.GetResourceString(SR.ID6272), audiences);
context.Reject(
error: Errors.InvalidTarget,
description: SR.FormatID2190(Parameters.Audience),
uri: SR.FormatID8000(SR.ID2190));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting token requests that use unregistered resources.
/// </summary>
public sealed class ValidateResources : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.AddFilter<RequireResourceValidationEnabled>()
.UseSingletonHandler<ValidateResources>()
.SetOrder(ValidateAudiences.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// If at least one resource was not recognized, return an error.
var resources = context.Request.GetResources().ToHashSet(StringComparer.Ordinal);
resources.ExceptWith(context.Options.Resources.Select(static resource => resource.AbsoluteUri));
if (resources.Count is not 0)
{
context.Logger.LogInformation(6273, SR.GetResourceString(SR.ID6273), resources);
context.Reject(
error: Errors.InvalidTarget,
description: SR.FormatID2190(Parameters.Resource),
uri: SR.FormatID8000(SR.ID2190));
return default;
}
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for applying the authentication logic to token requests. /// Contains the logic responsible for applying the authentication logic to token requests.
/// </summary> /// </summary>
@ -1040,7 +1238,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; } public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseScopedHandler<ValidateAuthentication>() .UseScopedHandler<ValidateAuthentication>()
.SetOrder(ValidateScopes.Descriptor.Order + 1_000) .SetOrder(ValidateResources.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();
@ -1215,9 +1413,9 @@ public static partial class OpenIddictServerHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting token requests made by applications /// Contains the logic responsible for rejecting token requests made by
/// that haven't been granted the appropriate grant type permission. /// applications that haven't been granted the appropriate scope permissions.
/// Note: this handler is not used when the degraded mode is enabled. /// Note: this handler is not used when the degraded mode is enabled or when scope permissions are disabled.
/// </summary> /// </summary>
public sealed class ValidateScopePermissions : IOpenIddictServerHandler<ValidateTokenRequestContext> public sealed class ValidateScopePermissions : IOpenIddictServerHandler<ValidateTokenRequestContext>
{ {
@ -1279,6 +1477,122 @@ public static partial class OpenIddictServerHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for rejecting token requests made by
/// applications that haven't been granted the appropriate audience permissions.
/// Note: this handler is not used when the degraded mode is enabled or when audience permissions are disabled.
/// </summary>
public sealed class ValidateAudiencePermissions : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
public ValidateAudiencePermissions() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public ValidateAudiencePermissions(IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.AddFilter<RequireClientIdParameter>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireAudiencePermissionsEnabled>()
.UseScopedHandler<ValidateAudiencePermissions>()
.SetOrder(ValidateGrantTypePermissions.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ValidateTokenRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
foreach (var audience in context.Request.GetAudiences())
{
// Reject the request if the application is not allowed to use the iterated audience.
if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Audience + audience))
{
context.Logger.LogInformation(6278, SR.GetResourceString(SR.ID6276), context.ClientId, audience);
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2191),
uri: SR.FormatID8000(SR.ID2191));
return;
}
}
}
}
/// <summary>
/// Contains the logic responsible for rejecting token requests made by
/// applications that haven't been granted the appropriate resource permissions.
/// Note: this handler is not used when the degraded mode is enabled or when resource permissions are disabled.
/// </summary>
public sealed class ValidateResourcePermissions : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
public ValidateResourcePermissions() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
public ValidateResourcePermissions(IOpenIddictApplicationManager applicationManager)
=> _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.AddFilter<RequireClientIdParameter>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireResourcePermissionsEnabled>()
.UseScopedHandler<ValidateResourcePermissions>()
.SetOrder(ValidateAudiencePermissions.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ValidateTokenRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0032));
foreach (var resource in context.Request.GetResources())
{
// Reject the request if the application is not allowed to use the iterated resource.
if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Resource + resource))
{
context.Logger.LogInformation(6279, SR.GetResourceString(SR.ID6277), context.ClientId, resource);
context.Reject(
error: Errors.InvalidRequest,
description: SR.GetResourceString(SR.ID2192),
uri: SR.FormatID8000(SR.ID2192));
return;
}
}
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting token requests made by /// Contains the logic responsible for rejecting token requests made by
/// applications for which proof key for code exchange (PKCE) was enforced. /// applications for which proof key for code exchange (PKCE) was enforced.
@ -1301,7 +1615,7 @@ public static partial class OpenIddictServerHandlers
.AddFilter<RequireClientIdParameter>() .AddFilter<RequireClientIdParameter>()
.AddFilter<RequireDegradedModeDisabled>() .AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateProofKeyForCodeExchangeRequirement>() .UseScopedHandler<ValidateProofKeyForCodeExchangeRequirement>()
.SetOrder(ValidateScopePermissions.Descriptor.Order + 1_000) .SetOrder(ValidateResourcePermissions.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();

35
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -356,6 +356,16 @@ public sealed class OpenIddictServerOptions
/// </remarks> /// </remarks>
public bool DisableTokenStorage { get; set; } public bool DisableTokenStorage { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether audience validation is disabled.
/// </summary>
public bool DisableAudienceValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether resource validation is disabled.
/// </summary>
public bool DisableResourceValidation { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether scope validation is disabled. /// Gets or sets a boolean indicating whether scope validation is disabled.
/// </summary> /// </summary>
@ -386,6 +396,12 @@ public sealed class OpenIddictServerOptions
OpenIddictConstants.TokenTypeIdentifiers.RefreshToken OpenIddictConstants.TokenTypeIdentifiers.RefreshToken
}; };
/// <summary>
/// Gets the OAuth 2.0 audiences enabled for this application
/// (exclusively used with the OAuth 2.0 Token Exchange flow).
/// </summary>
public HashSet<string> Audiences { get; } = new(StringComparer.Ordinal);
/// <summary> /// <summary>
/// Gets the OAuth 2.0 client assertion types enabled for this application. /// Gets the OAuth 2.0 client assertion types enabled for this application.
/// </summary> /// </summary>
@ -461,6 +477,13 @@ public sealed class OpenIddictServerOptions
/// </summary> /// </summary>
public bool RequirePushedAuthorizationRequests { get; set; } public bool RequirePushedAuthorizationRequests { get; set; }
/// <summary>
/// Gets the OAuth 2.0 resources enabled for this application (typically used
/// with the OAuth 2.0 Token Exchange flow and with authorization or pushed
/// authorization requests that include one or more resource indicators).
/// </summary>
public HashSet<Uri> Resources { get; } = [];
/// <summary> /// <summary>
/// Gets the OAuth 2.0/OpenID Connect response types enabled for this application. /// Gets the OAuth 2.0/OpenID Connect response types enabled for this application.
/// </summary> /// </summary>
@ -508,6 +531,12 @@ public sealed class OpenIddictServerOptions
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public string DefaultRequestedTokenType { get; set; } = TokenTypeIdentifiers.AccessToken; public string DefaultRequestedTokenType { get; set; } = TokenTypeIdentifiers.AccessToken;
/// <summary>
/// Gets or sets a boolean indicating whether audience permissions should be ignored.
/// Setting this property to <see langword="true"/> is NOT recommended.
/// </summary>
public bool IgnoreAudiencePermissions { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether endpoint permissions should be ignored. /// Gets or sets a boolean indicating whether endpoint permissions should be ignored.
/// Setting this property to <see langword="true"/> is NOT recommended. /// Setting this property to <see langword="true"/> is NOT recommended.
@ -520,6 +549,12 @@ public sealed class OpenIddictServerOptions
/// </summary> /// </summary>
public bool IgnoreGrantTypePermissions { get; set; } public bool IgnoreGrantTypePermissions { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether resource permissions should be ignored.
/// Setting this property to <see langword="true"/> is NOT recommended.
/// </summary>
public bool IgnoreResourcePermissions { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether response type permissions should be ignored. /// Gets or sets a boolean indicating whether response type permissions should be ignored.
/// Setting this property to <see langword="true"/> is NOT recommended. /// Setting this property to <see langword="true"/> is NOT recommended.

158
test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs

@ -49,6 +49,31 @@ public class OpenIddictExtensionsTests
Assert.Equal(values, request.GetAcrValues()); Assert.Equal(values, request.GetAcrValues());
} }
[Fact]
public void GetAudiences_ThrowsAnExceptionForNullRequest()
{
// Arrange
var request = (OpenIddictRequest) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => request.GetAudiences());
Assert.Equal("request", exception.ParamName);
}
[Fact]
public void GetAudiences_ReturnsExpectedAudiences()
{
// Arrange
var request = new OpenIddictRequest
{
Audiences = ["Contoso", null, string.Empty, "Fabrikam", "Fabrikam"]
};
// Act and assert
Assert.Equal<IEnumerable<string>>(["Contoso", "Fabrikam"], request.GetAudiences());
}
[Fact] [Fact]
public void GetPromptValues_ThrowsAnExceptionForNullRequest() public void GetPromptValues_ThrowsAnExceptionForNullRequest()
{ {
@ -85,6 +110,31 @@ public class OpenIddictExtensionsTests
Assert.Equal(values, request.GetPromptValues()); Assert.Equal(values, request.GetPromptValues());
} }
[Fact]
public void GetResources_ThrowsAnExceptionForNullRequest()
{
// Arrange
var request = (OpenIddictRequest) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => request.GetResources());
Assert.Equal("request", exception.ParamName);
}
[Fact]
public void GetResources_ReturnsExpectedResources()
{
// Arrange
var request = new OpenIddictRequest
{
Resources = ["urn:contoso", null, string.Empty, "urn:fabrikam", "urn:fabrikam"]
};
// Act and assert
Assert.Equal<IEnumerable<string>>(["urn:contoso", "urn:fabrikam"], request.GetResources());
}
[Fact] [Fact]
public void GetResponseTypes_ThrowsAnExceptionForNullRequest() public void GetResponseTypes_ThrowsAnExceptionForNullRequest()
{ {
@ -180,7 +230,7 @@ public class OpenIddictExtensionsTests
var exception = Assert.Throws<ArgumentException>(() => request.HasAcrValue(value!)); var exception = Assert.Throws<ArgumentException>(() => request.HasAcrValue(value!));
Assert.Equal("value", exception.ParamName); Assert.Equal("value", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0177), exception.Message); Assert.StartsWith(SR.FormatID0366("value"), exception.Message);
} }
[Theory] [Theory]
@ -217,6 +267,51 @@ public class OpenIddictExtensionsTests
Assert.Equal(result, request.HasAcrValue("mod-mf")); Assert.Equal(result, request.HasAcrValue("mod-mf"));
} }
[Fact]
public void HasAudience_ThrowsAnExceptionForNullRequest()
{
// Arrange
var request = (OpenIddictRequest) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() =>
{
request.HasAudience("Contoso");
});
Assert.Equal("request", exception.ParamName);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void HasAudience_ThrowsAnExceptionForNullOrEmptyAudience(string? resource)
{
// Arrange
var request = new OpenIddictRequest();
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => request.HasAudience(resource!));
Assert.Equal("audience", exception.ParamName);
Assert.StartsWith(SR.FormatID0366("audience"), exception.Message);
}
[Fact]
public void HasAudience_ReturnsExpectedResult()
{
// Arrange
var request = new OpenIddictRequest
{
Audiences = ["Contoso", null, string.Empty, "Fabrikam", "Fabrikam"]
};
// Act and assert
Assert.True(request.HasAudience("Contoso"));
Assert.True(request.HasAudience("Fabrikam"));
Assert.False(request.HasAudience("Northwind"));
}
[Fact] [Fact]
public void HasPromptValue_ThrowsAnExceptionForNullRequest() public void HasPromptValue_ThrowsAnExceptionForNullRequest()
{ {
@ -235,16 +330,16 @@ public class OpenIddictExtensionsTests
[Theory] [Theory]
[InlineData(null)] [InlineData(null)]
[InlineData("")] [InlineData("")]
public void HasPromptValue_ThrowsAnExceptionForNullOrEmptyPrompt(string? prompt) public void HasPromptValue_ThrowsAnExceptionForNullOrEmptyPrompt(string? value)
{ {
// Arrange // Arrange
var request = new OpenIddictRequest(); var request = new OpenIddictRequest();
// Act and assert // Act and assert
var exception = Assert.Throws<ArgumentException>(() => request.HasPromptValue(prompt!)); var exception = Assert.Throws<ArgumentException>(() => request.HasPromptValue(value!));
Assert.Equal("prompt", exception.ParamName); Assert.Equal("value", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0178), exception.Message); Assert.StartsWith(SR.FormatID0366("value"), exception.Message);
} }
[Theory] [Theory]
@ -269,12 +364,12 @@ public class OpenIddictExtensionsTests
[InlineData("LOGIN CONSENT SELECT_ACCOUNT ", false)] [InlineData("LOGIN CONSENT SELECT_ACCOUNT ", false)]
[InlineData("LOGIN", false)] [InlineData("LOGIN", false)]
[InlineData("LOGIN SELECT_ACCOUNT", false)] [InlineData("LOGIN SELECT_ACCOUNT", false)]
public void HasPromptValue_ReturnsExpectedResult(string? prompt, bool result) public void HasPromptValue_ReturnsExpectedResult(string? value, bool result)
{ {
// Arrange // Arrange
var request = new OpenIddictRequest var request = new OpenIddictRequest
{ {
Prompt = prompt Prompt = value
}; };
// Act and assert // Act and assert
@ -308,7 +403,7 @@ public class OpenIddictExtensionsTests
var exception = Assert.Throws<ArgumentException>(() => request.HasResponseType(type!)); var exception = Assert.Throws<ArgumentException>(() => request.HasResponseType(type!));
Assert.Equal("type", exception.ParamName); Assert.Equal("type", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0179), exception.Message); Assert.StartsWith(SR.FormatID0366("type"), exception.Message);
} }
[Theory] [Theory]
@ -345,6 +440,51 @@ public class OpenIddictExtensionsTests
Assert.Equal(result, request.HasResponseType(ResponseTypes.Code)); Assert.Equal(result, request.HasResponseType(ResponseTypes.Code));
} }
[Fact]
public void HasResource_ThrowsAnExceptionForNullRequest()
{
// Arrange
var request = (OpenIddictRequest) null!;
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() =>
{
request.HasResource("urn:contoso");
});
Assert.Equal("request", exception.ParamName);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void HasResource_ThrowsAnExceptionForNullOrEmptyResource(string? resource)
{
// Arrange
var request = new OpenIddictRequest();
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => request.HasResource(resource!));
Assert.Equal("resource", exception.ParamName);
Assert.StartsWith(SR.FormatID0366("resource"), exception.Message);
}
[Fact]
public void HasResource_ReturnsExpectedResult()
{
// Arrange
var request = new OpenIddictRequest
{
Resources = ["urn:contoso", null, string.Empty, "urn:fabrikam", "urn:fabrikam"]
};
// Act and assert
Assert.True(request.HasResource("urn:contoso"));
Assert.True(request.HasResource("urn:fabrikam"));
Assert.False(request.HasResource("urn:northwind"));
}
[Fact] [Fact]
public void HasScope_ThrowsAnExceptionForNullRequest() public void HasScope_ThrowsAnExceptionForNullRequest()
{ {
@ -372,7 +512,7 @@ public class OpenIddictExtensionsTests
var exception = Assert.Throws<ArgumentException>(() => request.HasScope(scope!)); var exception = Assert.Throws<ArgumentException>(() => request.HasScope(scope!));
Assert.Equal("scope", exception.ParamName); Assert.Equal("scope", exception.ParamName);
Assert.StartsWith(SR.GetResourceString(SR.ID0180), exception.Message); Assert.StartsWith(SR.FormatID0366("scope"), exception.Message);
} }
[Theory] [Theory]

246
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs

@ -415,6 +415,57 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.FormatID8000(SR.ID2033), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2033), response.ErrorUri);
} }
[Fact]
public async Task ValidateAuthorizationRequest_ForbiddenAudienceCausesAnError()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
Audiences = ["Contoso"],
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
ResponseType = "code id_token token",
Scope = Scopes.OpenId
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2193(Parameters.Audience), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2193), response.ErrorUri);
}
[Theory]
[InlineData("fabrikam", SR.ID2030)]
[InlineData("/path", SR.ID2030)]
[InlineData("/tmp/file.xml", SR.ID2030)]
[InlineData("C:\\tmp\\file.xml", SR.ID2030)]
[InlineData("http://www.fabrikam.com/path#param=value", SR.ID2031)]
[InlineData("urn:fabrikam#param", SR.ID2031)]
public async Task ValidateAuthorizationRequest_InvalidResourceCausesAnError(string resource, string message)
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
Resources = [resource],
ResponseType = ResponseTypes.Code
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(string.Format(SR.GetResourceString(message), Parameters.Resource), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(message), response.ErrorUri);
}
[Theory] [Theory]
[InlineData("code id_token")] [InlineData("code id_token")]
[InlineData("code id_token token")] [InlineData("code id_token token")]
@ -1017,6 +1068,82 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.FormatID8000(SR.ID2035), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2035), response.ErrorUri);
} }
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenUnregisteredResourceIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.Services.AddSingleton(CreateApplicationManager(mock =>
{
var application = new OpenIddictApplication();
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
Resources = ["urn:unregistered_resource"],
ResponseType = ResponseTypes.Code,
});
// Assert
Assert.Equal(Errors.InvalidTarget, response.Error);
Assert.Equal(SR.FormatID2190(Parameters.Resource), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2190), response.ErrorUri);
}
[Fact]
public async Task ValidateAuthorizationRequest_RequestIsValidatedWhenResourceRegisteredInOptionsIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.RegisterResources("urn:registered_resource");
options.AddEventHandler<HandleAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
}));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest
{
ClientId = "Fabrikam",
Nonce = "n-0S6_WzA2Mj",
RedirectUri = "http://www.fabrikam.com/path",
Resources = ["urn:registered_resource"],
ResponseType = ResponseTypes.Token
});
// Assert
Assert.Null(response.Error);
Assert.Null(response.ErrorDescription);
Assert.Null(response.ErrorUri);
Assert.NotNull(response.AccessToken);
}
[Fact] [Fact]
public async Task ValidateAuthorizationRequest_UnknownResponseModeParameterIsRejected() public async Task ValidateAuthorizationRequest_UnknownResponseModeParameterIsRejected()
{ {
@ -3775,6 +3902,73 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.FormatID8000(SR.ID2035), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2035), response.ErrorUri);
} }
[Fact]
public async Task ValidatePushedAuthorizationRequest_RequestIsRejectedWhenUnregisteredResourceIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.Services.AddSingleton(CreateApplicationManager(mock =>
{
var application = new OpenIddictApplication();
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}));
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/par", new OpenIddictRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
Resources = ["urn:registered_resource"],
ResponseType = ResponseTypes.Code
});
// Assert
Assert.Equal(Errors.InvalidTarget, response.Error);
Assert.Equal(SR.FormatID2190(Parameters.Resource), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2190), response.ErrorUri);
}
[Fact]
public async Task ValidatePushedAuthorizationRequest_RequestIsValidatedWhenResourceRegisteredInOptionsIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.RegisterResources("urn:registered_resource");
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/par", new OpenIddictRequest
{
ClientId = "Fabrikam",
Nonce = "n-0S6_WzA2Mj",
RedirectUri = "http://www.fabrikam.com/path",
Resources = ["urn:registered_resource"],
ResponseType = ResponseTypes.Token
});
// Assert
Assert.Null(response.Error);
Assert.Null(response.ErrorDescription);
Assert.Null(response.ErrorUri);
Assert.NotNull(response.RequestUri);
}
[Fact] [Fact]
public async Task ValidatePushedAuthorizationRequest_UnknownResponseModeParameterIsRejected() public async Task ValidatePushedAuthorizationRequest_UnknownResponseModeParameterIsRejected()
{ {
@ -4557,6 +4751,58 @@ public abstract partial class OpenIddictServerIntegrationTests
Permissions.Prefixes.Scope + Scopes.Email, It.IsAny<CancellationToken>()), Times.Once()); Permissions.Prefixes.Scope + Scopes.Email, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact]
public async Task ValidatePushedAuthorizationRequest_RequestIsRejectedWhenResourcePermissionIsNotGranted()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Resource + "urn:contoso", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Resource + "urn:fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.Services.AddSingleton(manager);
options.RegisterResources("urn:contoso", "urn:fabrikam");
options.Configure(options => options.IgnoreResourcePermissions = false);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/par", new OpenIddictRequest
{
ClientId = "Fabrikam",
RedirectUri = "http://www.fabrikam.com/path",
Resources = ["urn:contoso", "urn:fabrikam"],
ResponseType = ResponseTypes.Code
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2192), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2192), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Resource + "urn:contoso", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Resource + "urn:fabrikam", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact] [Fact]
public async Task ValidatePushedAuthorizationRequest_RequestIsRejectedWhenCodeChallengeIsMissingWithPkceFeatureEnforced() public async Task ValidatePushedAuthorizationRequest_RequestIsRejectedWhenCodeChallengeIsMissingWithPkceFeatureEnforced()
{ {

326
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

@ -547,6 +547,56 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.FormatID8000(SR.ID2074), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2074), response.ErrorUri);
} }
[Fact]
public async Task ValidateTokenRequest_ForbiddenAudienceCausesAnError()
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
Audiences = ["Contoso"],
ClientId = "Fabrikam",
DeviceCode = "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
GrantType = GrantTypes.DeviceCode
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.FormatID2195(Parameters.Audience), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2195), response.ErrorUri);
}
[Theory]
[InlineData("fabrikam", SR.ID2030)]
[InlineData("/path", SR.ID2030)]
[InlineData("/tmp/file.xml", SR.ID2030)]
[InlineData("C:\\tmp\\file.xml", SR.ID2030)]
[InlineData("http://www.fabrikam.com/path#param=value", SR.ID2031)]
[InlineData("urn:fabrikam#param", SR.ID2031)]
public async Task ValidateTokenRequest_InvalidResourceCausesAnError(string resource, string message)
{
// Arrange
await using var server = await CreateServerAsync(options => options.EnableDegradedMode());
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.TokenExchange,
Resources = [resource],
SubjectToken = "accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC",
SubjectTokenType = TokenTypeIdentifiers.AccessToken
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(string.Format(SR.GetResourceString(message), Parameters.Resource), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(message), response.ErrorUri);
}
[Fact] [Fact]
public async Task ValidateTokenRequest_InvalidAuthorizationCodeCausesAnError() public async Task ValidateTokenRequest_InvalidAuthorizationCodeCausesAnError()
{ {
@ -2130,6 +2180,140 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.NotNull(response.AccessToken); Assert.NotNull(response.AccessToken);
} }
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenUnregisteredAudienceIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync();
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
Audiences = ["unregistered_audience"],
GrantType = GrantTypes.TokenExchange,
SubjectToken = "8xLOxBtZp8",
SubjectTokenType = TokenTypeIdentifiers.RefreshToken
});
// Assert
Assert.Equal(Errors.InvalidTarget, response.Error);
Assert.Equal(SR.FormatID2190(Parameters.Audience), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2190), response.ErrorUri);
}
[Fact]
public async Task ValidateTokenRequest_RequestIsValidatedWhenAudienceRegisteredInOptionsIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.RegisterAudiences("registered_audience");
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeIdentifiers.RefreshToken)
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
Audiences = ["registered_audience"],
GrantType = GrantTypes.TokenExchange,
SubjectToken = "8xLOxBtZp8",
SubjectTokenType = TokenTypeIdentifiers.RefreshToken
});
// Assert
Assert.Null(response.Error);
Assert.Null(response.ErrorDescription);
Assert.Null(response.ErrorUri);
Assert.NotNull(response.AccessToken);
}
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenUnregisteredResourceIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync();
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.TokenExchange,
Resources = ["urn:unregistered_resource"],
SubjectToken = "8xLOxBtZp8",
SubjectTokenType = TokenTypeIdentifiers.RefreshToken
});
// Assert
Assert.Equal(Errors.InvalidTarget, response.Error);
Assert.Equal(SR.FormatID2190(Parameters.Resource), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2190), response.ErrorUri);
}
[Fact]
public async Task ValidateTokenRequest_RequestIsValidatedWhenResourceRegisteredInOptionsIsSpecified()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.EnableDegradedMode();
options.RegisterResources("urn:registered_resource");
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeIdentifiers.RefreshToken)
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
GrantType = GrantTypes.TokenExchange,
Resources = ["urn:registered_resource"],
SubjectToken = "8xLOxBtZp8",
SubjectTokenType = TokenTypeIdentifiers.RefreshToken
});
// Assert
Assert.Null(response.Error);
Assert.Null(response.ErrorDescription);
Assert.Null(response.ErrorUri);
Assert.NotNull(response.AccessToken);
}
[Fact] [Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenClientAssertionIsSpecifiedWithoutType() public async Task ValidateTokenRequest_RequestIsRejectedWhenClientAssertionIsSpecifiedWithoutType()
{ {
@ -2679,6 +2863,148 @@ public abstract partial class OpenIddictServerIntegrationTests
Permissions.Prefixes.Scope + Scopes.Email, It.IsAny<CancellationToken>()), Times.Once()); Permissions.Prefixes.Scope + Scopes.Email, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenAudiencePermissionIsNotGranted()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Audience + "Contoso", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Audience + "Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeIdentifiers.RefreshToken)
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
options.RegisterAudiences("Contoso", "Fabrikam");
options.Configure(options => options.IgnoreAudiencePermissions = false);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
GrantType = GrantTypes.TokenExchange,
Audiences = ["Contoso", "Fabrikam"],
SubjectToken = "8xLOxBtZp8",
SubjectTokenType = TokenTypeIdentifiers.RefreshToken
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2191), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2191), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Audience + "Contoso", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Audience + "Fabrikam", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenResourcePermissionIsNotGranted()
{
// Arrange
var application = new OpenIddictApplication();
var manager = CreateApplicationManager(mock =>
{
mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Resource + "urn:contoso", It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
mock.Setup(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Resource + "urn:fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
});
await using var server = await CreateServerAsync(options =>
{
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("8xLOxBtZp8", context.Token);
Assert.Equal([TokenTypeIdentifiers.RefreshToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetTokenType(TokenTypeIdentifiers.RefreshToken)
.SetClaim(Claims.Subject, "Bob le Bricoleur");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
options.Services.AddSingleton(manager);
options.RegisterResources("urn:contoso", "urn:fabrikam");
options.Configure(options => options.IgnoreResourcePermissions = false);
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{
ClientId = "Fabrikam",
GrantType = GrantTypes.TokenExchange,
Resources = ["urn:contoso", "urn:fabrikam"],
SubjectToken = "8xLOxBtZp8",
SubjectTokenType = TokenTypeIdentifiers.RefreshToken
});
// Assert
Assert.Equal(Errors.InvalidRequest, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2192), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2192), response.ErrorUri);
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Resource + "urn:contoso", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application,
Permissions.Prefixes.Resource + "urn:fabrikam", It.IsAny<CancellationToken>()), Times.Once());
}
[Fact] [Fact]
public async Task ValidateTokenRequest_RequestIsRejectedWhenCodeVerifierIsMissingWithPkceFeatureEnforced() public async Task ValidateTokenRequest_RequestIsRejectedWhenCodeVerifierIsMissingWithPkceFeatureEnforced()
{ {

4
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

@ -4123,8 +4123,10 @@ public abstract partial class OpenIddictServerIntegrationTests
options.AcceptAnonymousClients(); options.AcceptAnonymousClients();
// Disable permission enforcement by default. // Disable permission enforcement by default.
options.IgnoreEndpointPermissions() options.IgnoreAudiencePermissions()
.IgnoreEndpointPermissions()
.IgnoreGrantTypePermissions() .IgnoreGrantTypePermissions()
.IgnoreResourcePermissions()
.IgnoreResponseTypePermissions() .IgnoreResponseTypePermissions()
.IgnoreScopePermissions(); .IgnoreScopePermissions();

203
test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

@ -670,6 +670,22 @@ public class OpenIddictServerBuilderTests
Assert.True(options.DisableAccessTokenEncryption); Assert.True(options.DisableAccessTokenEncryption);
} }
[Fact]
public void DisableAudienceValidation_AudienceValidationIsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableAudienceValidation();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableAudienceValidation);
}
[Fact] [Fact]
public void DisableAuthorizationStorage_AuthorizationStorageIsDisabled() public void DisableAuthorizationStorage_AuthorizationStorageIsDisabled()
{ {
@ -686,6 +702,22 @@ public class OpenIddictServerBuilderTests
Assert.True(options.DisableAuthorizationStorage); Assert.True(options.DisableAuthorizationStorage);
} }
[Fact]
public void DisableResourceValidation_ResourceValidationIsDisabled()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.DisableResourceValidation();
var options = GetOptions(services);
// Assert
Assert.True(options.DisableResourceValidation);
}
[Fact] [Fact]
public void DisableRollingRefreshTokens_RollingRefreshTokensAreDisabled() public void DisableRollingRefreshTokens_RollingRefreshTokensAreDisabled()
{ {
@ -750,6 +782,86 @@ public class OpenIddictServerBuilderTests
Assert.True(options.DisableTokenStorage); Assert.True(options.DisableTokenStorage);
} }
[Fact]
public void IgnoreAudiencePermissions_AudiencePermissionsAreIgnored()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.IgnoreAudiencePermissions();
var options = GetOptions(services);
// Assert
Assert.True(options.IgnoreAudiencePermissions);
}
[Fact]
public void IgnoreGrantTypePermissions_GrantTypePermissionsAreIgnored()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.IgnoreGrantTypePermissions();
var options = GetOptions(services);
// Assert
Assert.True(options.IgnoreGrantTypePermissions);
}
[Fact]
public void IgnoreScopePermissions_ScopePermissionsAreIgnored()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.IgnoreScopePermissions();
var options = GetOptions(services);
// Assert
Assert.True(options.IgnoreScopePermissions);
}
[Fact]
public void IgnoreResourcePermissions_ResourcePermissionsAreIgnored()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.IgnoreResourcePermissions();
var options = GetOptions(services);
// Assert
Assert.True(options.IgnoreResourcePermissions);
}
[Fact]
public void IgnoreResponseTypePermissions_ResponseTypePermissionsAreIgnored()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.IgnoreResponseTypePermissions();
var options = GetOptions(services);
// Assert
Assert.True(options.IgnoreResponseTypePermissions);
}
[Fact] [Fact]
public void RequireProofKeyForCodeExchange_PkceIsEnforced() public void RequireProofKeyForCodeExchange_PkceIsEnforced()
{ {
@ -2032,6 +2144,51 @@ public class OpenIddictServerBuilderTests
Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Issuer); Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Issuer);
} }
[Fact]
public void RegisterAudiences_AudiencesAreAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.RegisterAudiences("custom_audience_1", "custom_audience_2");
var options = GetOptions(services);
// Assert
Assert.Contains("custom_audience_1", options.Audiences);
Assert.Contains("custom_audience_2", options.Audiences);
}
[Fact]
public void RegisterAudiences_ThrowsAnExceptionForNullAudiences()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.RegisterAudiences(audiences: null!));
Assert.Equal("audiences", exception.ParamName);
}
[Theory]
[InlineData(null)]
[InlineData("")]
public void RegisterAudiences_ThrowsAnExceptionForNullOrEmptyAudience(string? audience)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.RegisterAudiences([audience!]));
Assert.Equal("audiences", exception.ParamName);
Assert.Contains(SR.FormatID0457("audiences"), exception.Message);
}
[Fact] [Fact]
public void RegisterClaims_ClaimsAreAdded() public void RegisterClaims_ClaimsAreAdded()
{ {
@ -2122,6 +2279,52 @@ public class OpenIddictServerBuilderTests
Assert.Contains(SR.FormatID0457("values"), exception.Message); Assert.Contains(SR.FormatID0457("values"), exception.Message);
} }
[Fact]
public void RegisterResources_ResourcesAreAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.RegisterResources("urn:custom_resource_1", "urn:custom_resource_2");
var options = GetOptions(services);
// Assert
Assert.Contains(new Uri("urn:custom_resource_1", UriKind.Absolute), options.Resources);
Assert.Contains(new Uri("urn:custom_resource_2", UriKind.Absolute), options.Resources);
}
[Fact]
public void RegisterResources_ThrowsAnExceptionForNullResources()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentNullException>(() => builder.RegisterResources((Uri[]) null!));
Assert.Equal("resources", exception.ParamName);
}
[Theory]
[InlineData("C:\\tmp\\file.xml")]
[InlineData("http://www.fabrikam.com/path#param=value")]
[InlineData("urn:fabrikam#param")]
public void RegisterResources_ThrowsAnExceptionForInvalidResources(string? resource)
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act and assert
var exception = Assert.Throws<ArgumentException>(() => builder.RegisterResources([resource!]));
Assert.Equal("resources", exception.ParamName);
Assert.Contains(SR.FormatID0495("resources"), exception.Message);
}
[Fact] [Fact]
public void RegisterScopes_ScopesAreAdded() public void RegisterScopes_ScopesAreAdded()
{ {

Loading…
Cancel
Save