diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java index 115cbd5bb..c9b36b7c7 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -134,9 +134,30 @@ public Authentication authenticate(Authentication authentication) throws Authent throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); } + if (deviceCode.isInvalidated() && !userCode.isInvalidated()) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); + } + // In https://www.rfc-editor.org/rfc/rfc8628.html#section-3.5, // the following error codes are defined: + // expired_token + // The "device_code" has expired, and the device authorization + // session has concluded. The client MAY commence a new device + // authorization request but SHOULD wait for user interaction before + // restarting to avoid unnecessary polling. + if (deviceCode.isExpired()) { + // Invalidate the device code + authorization = OAuth2Authorization.from(authorization).invalidate(deviceCode.getToken()).build(); + this.authorizationService.save(authorization); + if (this.logger.isWarnEnabled()) { + this.logger.warn(LogMessage.format("Invalidated device code used by registered client '%s'", + authorization.getRegisteredClientId())); + } + OAuth2Error error = new OAuth2Error(EXPIRED_TOKEN, null, DEVICE_ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + // authorization_pending // The authorization request is still pending as the end user hasn't // yet completed the user-interaction steps (Section 3.3). The @@ -165,23 +186,6 @@ public Authentication authenticate(Authentication authentication) throws Authent throw new OAuth2AuthenticationException(error); } - // expired_token - // The "device_code" has expired, and the device authorization - // session has concluded. The client MAY commence a new device - // authorization request but SHOULD wait for user interaction before - // restarting to avoid unnecessary polling. - if (deviceCode.isExpired()) { - // Invalidate the device code - authorization = OAuth2Authorization.from(authorization).invalidate(deviceCode.getToken()).build(); - this.authorizationService.save(authorization); - if (this.logger.isWarnEnabled()) { - this.logger.warn(LogMessage.format("Invalidated device code used by registered client '%s'", - authorization.getRegisteredClientId())); - } - OAuth2Error error = new OAuth2Error(EXPIRED_TOKEN, null, DEVICE_ERROR_URI); - throw new OAuth2AuthenticationException(error); - } - if (this.logger.isTraceEnabled()) { this.logger.trace("Validated device token request parameters"); } diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProviderTests.java index 0c37bb16f..0a457ad8b 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceCodeAuthenticationProviderTests.java @@ -191,6 +191,7 @@ public void authenticateWhenUserCodeIsNotInvalidatedThenThrowOAuth2Authenticatio RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); Authentication authentication = createAuthentication(registeredClient); OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) + .token(createDeviceCode()) .token(createUserCode()) .build(); given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization); @@ -209,7 +210,7 @@ public void authenticateWhenUserCodeIsNotInvalidatedThenThrowOAuth2Authenticatio } @Test - public void authenticateWhenDeviceCodeIsInvalidatedThenThrowOAuth2AuthenticationException() { + public void authenticateWhenDeviceCodeAndUserCodeAreInvalidatedThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); Authentication authentication = createAuthentication(registeredClient); OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) @@ -231,13 +232,36 @@ public void authenticateWhenDeviceCodeIsInvalidatedThenThrowOAuth2Authentication verifyNoInteractions(this.tokenGenerator); } + @Test + public void authenticateWhenDeviceCodeIsInvalidatedThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + Authentication authentication = createAuthentication(registeredClient); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) + .token(createDeviceCode(), withInvalidated()) + .token(createUserCode()) + .build(); + given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization); + // @formatter:off + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> this.authenticationProvider.authenticate(authentication)) + .extracting(OAuth2AuthenticationException::getError) + .extracting(OAuth2Error::getErrorCode) + .isEqualTo(OAuth2ErrorCodes.INVALID_GRANT); + // @formatter:on + + verify(this.authorizationService).findByToken(DEVICE_CODE, + OAuth2DeviceCodeAuthenticationProvider.DEVICE_CODE_TOKEN_TYPE); + verifyNoMoreInteractions(this.authorizationService); + verifyNoInteractions(this.tokenGenerator); + } + @Test public void authenticateWhenDeviceCodeIsExpiredThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); Authentication authentication = createAuthentication(registeredClient); OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) .token(createExpiredDeviceCode()) - .token(createUserCode(), withInvalidated()) + .token(createUserCode()) .build(); given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization); // @formatter:off