Skip to content

Commit ce528ee

Browse files
antoinelauzon-belljgrandja
authored andcommitted
Check user code expiry and invalidity
Closes gh-1977 Signed-off-by: Antoine Lauzon <[email protected]>
1 parent 84f9299 commit ce528ee

File tree

2 files changed

+91
-4
lines changed

2 files changed

+91
-4
lines changed

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProvider.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2023 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -109,6 +109,15 @@ public Authentication authenticate(Authentication authentication) throws Authent
109109
this.logger.trace("Retrieved authorization with user code");
110110
}
111111

112+
OAuth2Authorization.Token<OAuth2UserCode> userCode = authorization.getToken(OAuth2UserCode.class);
113+
if (!userCode.isActive()) {
114+
if (!userCode.isInvalidated()) {
115+
authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, userCode.getToken());
116+
this.authorizationService.save(authorization);
117+
}
118+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
119+
}
120+
112121
Authentication principal = (Authentication) deviceVerificationAuthentication.getPrincipal();
113122
if (!isPrincipalAuthenticated(principal)) {
114123
if (this.logger.isTraceEnabled()) {
@@ -161,7 +170,6 @@ public Authentication authenticate(Authentication authentication) throws Authent
161170
requestedScopes, currentAuthorizedScopes);
162171
}
163172

164-
OAuth2Authorization.Token<OAuth2UserCode> userCode = authorization.getToken(OAuth2UserCode.class);
165173
// @formatter:off
166174
authorization = OAuth2Authorization.from(authorization)
167175
.principalName(principal.getName())

oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2DeviceVerificationAuthenticationProviderTests.java

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2023 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import java.time.temporal.ChronoUnit;
2121
import java.util.Collections;
2222
import java.util.Map;
23+
import java.util.function.Consumer;
2324
import java.util.function.Function;
2425

2526
import org.junit.jupiter.api.BeforeEach;
@@ -145,10 +146,79 @@ public void authenticateWhenAuthorizationNotFoundThenThrowOAuth2AuthenticationEx
145146
verifyNoInteractions(this.registeredClientRepository, this.authorizationConsentService);
146147
}
147148

149+
@Test
150+
public void authenticateWhenUserCodeIsInvalidedThenThrowOAuth2AuthenticationException() {
151+
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
152+
// @formatter:off
153+
OAuth2Authorization authorization = TestOAuth2Authorizations
154+
.authorization(registeredClient)
155+
.token(createDeviceCode())
156+
.token(createUserCode(), withInvalidated())
157+
.attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
158+
.build();
159+
// @formatter:on
160+
given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization);
161+
Authentication authentication = createAuthentication();
162+
// @formatter:off
163+
assertThatExceptionOfType(OAuth2AuthenticationException.class)
164+
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
165+
.extracting(OAuth2AuthenticationException::getError)
166+
.extracting(OAuth2Error::getErrorCode)
167+
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
168+
// @formatter:on
169+
170+
verify(this.authorizationService).findByToken(USER_CODE,
171+
OAuth2DeviceVerificationAuthenticationProvider.USER_CODE_TOKEN_TYPE);
172+
verifyNoMoreInteractions(this.authorizationService);
173+
verifyNoInteractions(this.registeredClientRepository, this.authorizationConsentService);
174+
}
175+
176+
@Test
177+
public void authenticateWhenUserCodeIsExpiredButNotInvalidatedThenInvalidateUserCodeAndThrowOAuth2AuthenticationException() {
178+
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
179+
// @formatter:off
180+
OAuth2Authorization authorization = TestOAuth2Authorizations
181+
.authorization(registeredClient)
182+
// Device code would also be expired but not relevant for this test
183+
.token(createDeviceCode())
184+
.token(createExpiredUserCode())
185+
.attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
186+
.build();
187+
// @formatter:on
188+
given(this.authorizationService.findByToken(anyString(), any(OAuth2TokenType.class))).willReturn(authorization);
189+
Authentication authentication = createAuthentication();
190+
// @formatter:off
191+
assertThatExceptionOfType(OAuth2AuthenticationException.class)
192+
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
193+
.extracting(OAuth2AuthenticationException::getError)
194+
.extracting(OAuth2Error::getErrorCode)
195+
.isEqualTo(OAuth2ErrorCodes.INVALID_GRANT);
196+
// @formatter:on
197+
198+
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
199+
verify(this.authorizationService).findByToken(USER_CODE,
200+
OAuth2DeviceVerificationAuthenticationProvider.USER_CODE_TOKEN_TYPE);
201+
verify(this.authorizationService).save(authorizationCaptor.capture());
202+
verifyNoMoreInteractions(this.authorizationService);
203+
verifyNoInteractions(this.registeredClientRepository, this.authorizationConsentService);
204+
205+
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue();
206+
assertThat(updatedAuthorization.getToken(OAuth2UserCode.class))
207+
.extracting(isInvalidated())
208+
.isEqualTo(true);
209+
}
210+
148211
@Test
149212
public void authenticateWhenPrincipalNotAuthenticatedThenReturnUnauthenticated() {
150213
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
151-
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient).build();
214+
// @formatter:off
215+
OAuth2Authorization authorization = TestOAuth2Authorizations
216+
.authorization(registeredClient)
217+
.token(createDeviceCode())
218+
.token(createUserCode())
219+
.attribute(OAuth2ParameterNames.SCOPE, registeredClient.getScopes())
220+
.build();
221+
// @formatter:on
152222
TestingAuthenticationToken principal = new TestingAuthenticationToken("user", null);
153223
Authentication authentication = new OAuth2DeviceVerificationAuthenticationToken(principal, USER_CODE,
154224
Collections.emptyMap());
@@ -331,6 +401,15 @@ private static OAuth2UserCode createUserCode() {
331401
return new OAuth2UserCode(USER_CODE, issuedAt, issuedAt.plus(30, ChronoUnit.MINUTES));
332402
}
333403

404+
private static OAuth2UserCode createExpiredUserCode() {
405+
Instant issuedAt = Instant.now().minus(45, ChronoUnit.MINUTES);
406+
return new OAuth2UserCode(USER_CODE, issuedAt, issuedAt.plus(30, ChronoUnit.MINUTES));
407+
}
408+
409+
private static Consumer<Map<String, Object>> withInvalidated() {
410+
return (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true);
411+
}
412+
334413
private static Function<OAuth2Authorization.Token<? extends OAuth2Token>, Boolean> isInvalidated() {
335414
return (token) -> token.getMetadata(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME);
336415
}

0 commit comments

Comments
 (0)