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() {