diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
index 8d366a61..5323cf58 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
@@ -44,6 +44,7 @@ public static partial class OpenIddictServerHandlers
ValidateRefreshTokenParameter.Descriptor,
ValidateResourceOwnerCredentialsParameters.Descriptor,
ValidateProofKeyForCodeExchangeParameters.Descriptor,
+ ValidateScopeParameter.Descriptor,
ValidateScopes.Descriptor,
ValidateClientId.Descriptor,
ValidateClientType.Descriptor,
@@ -706,6 +707,53 @@ public static partial class OpenIddictServerHandlers
}
}
+ ///
+ /// Contains the logic responsible for rejecting token requests that specify an invalid scope parameter.
+ ///
+ public sealed class ValidateScopeParameter : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateResourceOwnerCredentialsParameters.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidateTokenRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Reject authorization code and device authorization code requests that contain a "scope" parameter.
+ //
+ // Note: using the "scope" parameter with grant_type=refresh_token is deliberately allowed
+ // by the specification and is typically used to retrieve an access token granting a more
+ // limited access than the scopes initially specified in the authorization request.
+ //
+ // For more information, see https://tools.ietf.org/html/rfc6749#section-6.
+ if (!string.IsNullOrEmpty(context.Request.Scope) && (context.Request.IsAuthorizationCodeGrantType() ||
+ context.Request.IsDeviceCodeGrantType()))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6094), Parameters.Scope);
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2074(Parameters.Scope),
+ uri: SR.FormatID8000(SR.ID2074));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible for rejecting authorization requests that use unregistered scopes.
/// Note: this handler partially works with the degraded mode but is not used when scope validation is disabled.
@@ -734,7 +782,7 @@ public static partial class OpenIddictServerHandlers
new ValidateScopes(provider.GetService() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
- .SetOrder(ValidateProofKeyForCodeExchangeParameters.Descriptor.Order + 1_000)
+ .SetOrder(ValidateScopeParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@@ -1576,8 +1624,8 @@ public static partial class OpenIddictServerHandlers
}
///
- /// Contains the logic responsible for rejecting token requests that specify scopes that
- /// were not initially granted by the resource owner during the authorization request.
+ /// Contains the logic responsible for rejecting refresh token requests that specify scopes
+ /// that were not initially granted by the resource owner during the authorization request.
///
public sealed class ValidateGrantedScopes : IOpenIddictServerHandler
{
@@ -1599,12 +1647,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
- if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType())
- {
- return default;
- }
-
- if (string.IsNullOrEmpty(context.Request.Scope))
+ if (string.IsNullOrEmpty(context.Request.Scope) || !context.Request.IsRefreshTokenGrantType())
{
return default;
}
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
index a33857fa..976d7ec4 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
@@ -339,6 +339,60 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.NotNull(response.AccessToken);
}
+ [Fact]
+ public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenScopeIsSent()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+ options.RegisterScopes(Scopes.Phone, Scopes.Profile);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ Code = "SplxlOBeZQQYbYS6WxSbIA",
+ GrantType = GrantTypes.AuthorizationCode,
+ Scope = "profile phone"
+ });
+
+ // Assert
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.FormatID2074(Parameters.Scope), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2074), response.ErrorUri);
+ }
+
+ [Fact]
+ public async Task ValidateTokenRequest_DeviceAuthorizationCodeCausesAnErrorWhenScopeIsSent()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+ options.RegisterScopes(Scopes.Phone, Scopes.Profile);
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ ClientId = "Fabrikam",
+ DeviceCode = "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
+ GrantType = GrantTypes.DeviceCode,
+ Scope = "profile phone"
+ });
+
+ // Assert
+ Assert.Equal(Errors.InvalidRequest, response.Error);
+ Assert.Equal(SR.FormatID2074(Parameters.Scope), response.ErrorDescription);
+ Assert.Equal(SR.FormatID8000(SR.ID2074), response.ErrorUri);
+ }
+
[Fact]
public async Task ValidateTokenRequest_InvalidAuthorizationCodeCausesAnError()
{
@@ -1037,98 +1091,6 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.NotNull(response.AccessToken);
}
- [Fact]
- public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenScopeIsUnexpected()
- {
- // Arrange
- await using var server = await CreateServerAsync(options =>
- {
- options.EnableDegradedMode();
- options.RegisterScopes(Scopes.Phone, Scopes.Profile);
-
- options.AddEventHandler(builder =>
- {
- builder.UseInlineHandler(context =>
- {
- Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token);
- Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes);
-
- context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
- .SetTokenType(TokenTypeHints.AuthorizationCode)
- .SetPresenters("Fabrikam")
- .SetScopes(Enumerable.Empty())
- .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
- {
- ClientId = "Fabrikam",
- Code = "SplxlOBeZQQYbYS6WxSbIA",
- GrantType = GrantTypes.AuthorizationCode,
- Scope = "profile phone"
- });
-
- // Assert
- Assert.Equal(Errors.InvalidGrant, response.Error);
- Assert.Equal(SR.FormatID2074(Parameters.Scope), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2074), response.ErrorUri);
- }
-
- [Fact]
- public async Task ValidateTokenRequest_AuthorizationCodeCausesAnErrorWhenScopeIsInvalid()
- {
- // Arrange
- await using var server = await CreateServerAsync(options =>
- {
- options.EnableDegradedMode();
- options.RegisterScopes(Scopes.Phone, Scopes.Profile);
-
- options.AddEventHandler(builder =>
- {
- builder.UseInlineHandler(context =>
- {
- Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token);
- Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes);
-
- context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
- .SetTokenType(TokenTypeHints.AuthorizationCode)
- .SetPresenters("Fabrikam")
- .SetScopes("profile", "email")
- .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
- {
- ClientId = "Fabrikam",
- Code = "SplxlOBeZQQYbYS6WxSbIA",
- GrantType = GrantTypes.AuthorizationCode,
- Scope = "profile phone"
- });
-
- // Assert
- Assert.Equal(Errors.InvalidGrant, response.Error);
- Assert.Equal(SR.FormatID2052(Parameters.Scope), response.ErrorDescription);
- Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri);
- }
-
[Fact]
public async Task ValidateTokenRequest_RefreshTokenCausesAnErrorWhenScopeIsUnexpected()
{