Skip to content

Commit b40b265

Browse files
committed
Merge branch '1.3.x' into 1.4.x
2 parents e5dbc3b + fe4b5ad commit b40b265

File tree

2 files changed

+99
-4
lines changed

2 files changed

+99
-4
lines changed

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 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.
@@ -22,6 +22,7 @@
2222
import org.apache.commons.logging.Log;
2323
import org.apache.commons.logging.LogFactory;
2424

25+
import org.springframework.core.log.LogMessage;
2526
import org.springframework.security.authentication.AnonymousAuthenticationToken;
2627
import org.springframework.security.authentication.AuthenticationProvider;
2728
import org.springframework.security.core.Authentication;
@@ -109,6 +110,19 @@ public Authentication authenticate(Authentication authentication) throws Authent
109110
this.logger.trace("Retrieved authorization with user code");
110111
}
111112

113+
OAuth2Authorization.Token<OAuth2UserCode> userCode = authorization.getToken(OAuth2UserCode.class);
114+
if (!userCode.isActive()) {
115+
if (!userCode.isInvalidated()) {
116+
authorization = OAuth2Authorization.from(authorization).invalidate(userCode.getToken()).build();
117+
this.authorizationService.save(authorization);
118+
if (this.logger.isWarnEnabled()) {
119+
this.logger.warn(LogMessage.format("Invalidated user code used by registered client '%s'",
120+
authorization.getRegisteredClientId()));
121+
}
122+
}
123+
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
124+
}
125+
112126
Authentication principal = (Authentication) deviceVerificationAuthentication.getPrincipal();
113127
if (!isPrincipalAuthenticated(principal)) {
114128
if (this.logger.isTraceEnabled()) {
@@ -161,7 +175,6 @@ public Authentication authenticate(Authentication authentication) throws Authent
161175
requestedScopes, currentAuthorizedScopes);
162176
}
163177

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

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

Lines changed: 84 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;
@@ -55,6 +56,7 @@
5556
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
5657
import static org.mockito.ArgumentMatchers.any;
5758
import static org.mockito.ArgumentMatchers.anyString;
59+
import static org.mockito.ArgumentMatchers.eq;
5860
import static org.mockito.BDDMockito.given;
5961
import static org.mockito.Mockito.mock;
6062
import static org.mockito.Mockito.verify;
@@ -145,10 +147,81 @@ public void authenticateWhenAuthorizationNotFoundThenThrowOAuth2AuthenticationEx
145147
verifyNoInteractions(this.registeredClientRepository, this.authorizationConsentService);
146148
}
147149

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

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

0 commit comments

Comments
 (0)