From ceae11a0b7ca4ccc754a8fdfb85774a4889a93e6 Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Tue, 2 Jun 2026 21:30:07 +0300 Subject: [PATCH] Handle empty authorities in device authorization consent The main issue is inconsistent behavior. A `device_authorization` request without a `scope` is always accepted, but then, during the `consent` phase, if `authorities` is empty, we always get an `access_denied` error. However, would not it make sense to continue processing when both `scope` and `authorities` are empty? The `scope` parameter itself is optional according to [RFC-8628](https://datatracker.ietf.org/doc/html/rfc8628#section-3.1). There is also a note attached to it that refers to [Section 3.3 in RFC-6749](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). It states that if `scope` is empty, then we must either handle it with a default value or throw an error. Closes: gh-19238 Ref: gh-19256 Signed-off-by: Andrey Litvitski --- ...uthorizationConsentAuthenticationProvider.java | 15 +++++++++------ ...izationConsentAuthenticationProviderTests.java | 10 +++++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProvider.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProvider.java index 2d3ee05d887..8a1c924286c 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProvider.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProvider.java @@ -50,6 +50,7 @@ * used in the OAuth 2.0 Device Authorization Grant. * * @author Steve Riesenberg + * @author Andrey Litvitski * @since 7.0 * @see OAuth2DeviceAuthorizationConsentAuthenticationToken * @see OAuth2AuthorizationConsent @@ -183,7 +184,7 @@ public Authentication authenticate(Authentication authentication) throws Authent OAuth2Authorization.Token userCodeToken = authorization.getToken(OAuth2UserCode.class); Assert.notNull(userCodeToken, "userCode cannot be null"); - if (authorities.isEmpty()) { + if (authorities.isEmpty() && !requestedScopes.isEmpty()) { // Authorization consent denied (or revoked) if (currentAuthorizationConsent != null) { this.authorizationConsentService.remove(currentAuthorizationConsent); @@ -203,11 +204,13 @@ public Authentication authenticate(Authentication authentication) throws Authent throw createException(OAuth2ErrorCodes.ACCESS_DENIED, OAuth2ParameterNames.CLIENT_ID); } - OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build(); - if (currentAuthorizationConsent == null || !authorizationConsent.equals(currentAuthorizationConsent)) { - this.authorizationConsentService.save(authorizationConsent); - if (this.logger.isTraceEnabled()) { - this.logger.trace("Saved authorization consent"); + if (!authorities.isEmpty()) { + OAuth2AuthorizationConsent authorizationConsent = authorizationConsentBuilder.build(); + if (currentAuthorizationConsent == null || !authorizationConsent.equals(currentAuthorizationConsent)) { + this.authorizationConsentService.save(authorizationConsent); + if (this.logger.isTraceEnabled()) { + this.logger.trace("Saved authorization consent"); + } } } diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProviderTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProviderTests.java index 27d0e6376d2..2dc01094eb7 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProviderTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceAuthorizationConsentAuthenticationProviderTests.java @@ -63,6 +63,7 @@ * Tests for {@link OAuth2DeviceAuthorizationConsentAuthenticationProvider}. * * @author Steve Riesenberg + * @author Andrey Litvitski */ public class OAuth2DeviceAuthorizationConsentAuthenticationProviderTests { @@ -274,9 +275,12 @@ public void authenticateWhenRequestedScopesNotAuthorizedThenThrowOAuth2Authentic @Test public void authenticateWhenAuthoritiesIsEmptyThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - RegisteredClient registeredClient2 = TestRegisteredClients.registeredClient().scopes(Set::clear).build(); - OAuth2Authorization authorization = createAuthorization(registeredClient2); - Authentication authentication = createAuthentication(registeredClient2); + OAuth2Authorization authorization = createAuthorization(registeredClient); + TestingAuthenticationToken principal = new TestingAuthenticationToken("principal", null, + Collections.emptyList()); + Authentication authentication = new OAuth2DeviceAuthorizationConsentAuthenticationToken(AUTHORIZATION_URI, + registeredClient.getClientId(), principal, USER_CODE, STATE, Collections.emptySet(), + Collections.emptyMap()); given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization); given(this.registeredClientRepository.findByClientId(anyString())).willReturn(registeredClient); // @formatter:off