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 115cbd5b..ebd9644a 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. @@ -137,6 +137,25 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat // 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()) { + if (!deviceCode.isInvalidated()) { + // 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 +184,6 @@ public final class OAuth2DeviceCodeAuthenticationProvider implements Authenticat 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 0c37bb16..5113f7c3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 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. @@ -191,6 +191,7 @@ public class OAuth2DeviceCodeAuthenticationProviderTests { 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 class OAuth2DeviceCodeAuthenticationProviderTests { } @Test - public void authenticateWhenDeviceCodeIsInvalidatedThenThrowOAuth2AuthenticationException() { + public void authenticateWhenDeviceCodeAndUserCodeAreInvalidatedThenThrowOAuth2AuthenticationException() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); Authentication authentication = createAuthentication(registeredClient); OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient) @@ -237,7 +238,7 @@ public class OAuth2DeviceCodeAuthenticationProviderTests { 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