diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProvider.java index b5e0ff41a..231765195 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProvider.java @@ -109,6 +109,21 @@ public Authentication authenticate(Authentication authentication) throws Authent this.logger.trace("Retrieved authorization with user code"); } + OAuth2Authorization.Token userCode = authorization.getToken(OAuth2UserCode.class); + if (userCode.isInvalidated()) { + if (this.logger.isTraceEnabled()) { + this.logger.trace("User code is invalided"); + } + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); + } + + if (userCode.isExpired()) { + if (this.logger.isTraceEnabled()) { + this.logger.trace("User code is expired"); + } + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT); + } + Authentication principal = (Authentication) deviceVerificationAuthentication.getPrincipal(); if (!isPrincipalAuthenticated(principal)) { if (this.logger.isTraceEnabled()) { @@ -161,7 +176,6 @@ public Authentication authenticate(Authentication authentication) throws Authent requestedScopes, currentAuthorizedScopes); } - OAuth2Authorization.Token userCode = authorization.getToken(OAuth2UserCode.class); // @formatter:off authorization = OAuth2Authorization.from(authorization) .principalName(principal.getName()) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProviderTests.java index a0f3d12bd..d30f71841 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProviderTests.java @@ -20,6 +20,7 @@ import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; @@ -145,10 +146,72 @@ public void authenticateWhenAuthorizationNotFoundThenThrowOAuth2AuthenticationEx verifyNoInteractions(this.registeredClientRepository, this.authorizationConsentService); } + @Test + public void authenticateWhenUserCodeIsInvalidedThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + // @formatter:off + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient) + .token(createDeviceCode()) + .token(createUserCode(), withInvalidated()) + .attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes()) + .build(); + // @formatter:on + given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization); + Authentication authentication = createAuthentication(); + // @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(USER_CODE, + OAuth2DeviceVerificationAuthenticationProvider.USER_CODE_TOKEN_TYPE); + verifyNoMoreInteractions(this.authorizationService); + verifyNoInteractions(this.registeredClientRepository, this.authorizationConsentService); + } + + @Test + public void authenticateWhenUserCodeIsExpiredThenThrowOAuth2AuthenticationException() { + RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); + // @formatter:off + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient) + // Device code would also be expired but not relevant for this test + .token(createDeviceCode()) + .token(createExpiredUserCode()) + .attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes()) + .build(); + // @formatter:on + given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization); + Authentication authentication = createAuthentication(); + // @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(USER_CODE, + OAuth2DeviceVerificationAuthenticationProvider.USER_CODE_TOKEN_TYPE); + verifyNoMoreInteractions(this.authorizationService); + verifyNoInteractions(this.registeredClientRepository, this.authorizationConsentService); + } + @Test public void authenticateWhenPrincipalNotAuthenticatedThenReturnUnauthenticated() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); - OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build(); + // @formatter:off + OAuth2Authorization authorization = TestOAuth2Authorizations + .authorization(registeredClient) + .token(createDeviceCode()) + .token(createUserCode()) + .attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes()) + .build(); + // @formatter:on TestingAuthenticationToken principal = new TestingAuthenticationToken("user", null); Authentication authentication = new OAuth2DeviceVerificationAuthenticationToken(principal, USER_CODE, Collections.emptyMap()); @@ -331,6 +394,15 @@ private static OAuth2UserCode createUserCode() { return new OAuth2UserCode(USER_CODE, issuedAt, issuedAt.plus(30, ChronoUnit.MINUTES)); } + private static OAuth2UserCode createExpiredUserCode() { + Instant issuedAt = Instant.now().minus(45, ChronoUnit.MINUTES); + return new OAuth2UserCode(USER_CODE, issuedAt, issuedAt.plus(30, ChronoUnit.MINUTES)); + } + + private static Consumer> withInvalidated() { + return (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true); + } + private static Function, Boolean> isInvalidated() { return (token) -> token.getMetadata(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME); }