|
|
|
@ -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"); |
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
|
|
* you may not use this file except in compliance with the License. |
|
|
|
* you may not use this file except in compliance with the License. |
|
|
|
@ -20,6 +20,7 @@ import java.time.Instant; |
|
|
|
import java.time.temporal.ChronoUnit; |
|
|
|
import java.time.temporal.ChronoUnit; |
|
|
|
import java.util.Collections; |
|
|
|
import java.util.Collections; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Map; |
|
|
|
|
|
|
|
import java.util.function.Consumer; |
|
|
|
import java.util.function.Function; |
|
|
|
import java.util.function.Function; |
|
|
|
|
|
|
|
|
|
|
|
import org.junit.jupiter.api.BeforeEach; |
|
|
|
import org.junit.jupiter.api.BeforeEach; |
|
|
|
@ -55,6 +56,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
|
|
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
|
|
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
|
|
|
import static org.mockito.ArgumentMatchers.any; |
|
|
|
import static org.mockito.ArgumentMatchers.any; |
|
|
|
import static org.mockito.ArgumentMatchers.anyString; |
|
|
|
import static org.mockito.ArgumentMatchers.anyString; |
|
|
|
|
|
|
|
import static org.mockito.ArgumentMatchers.eq; |
|
|
|
import static org.mockito.BDDMockito.given; |
|
|
|
import static org.mockito.BDDMockito.given; |
|
|
|
import static org.mockito.Mockito.mock; |
|
|
|
import static org.mockito.Mockito.mock; |
|
|
|
import static org.mockito.Mockito.verify; |
|
|
|
import static org.mockito.Mockito.verify; |
|
|
|
@ -145,10 +147,81 @@ public class OAuth2DeviceVerificationAuthenticationProviderTests { |
|
|
|
verifyNoInteractions(this.registeredClientRepository, this.authorizationConsentService); |
|
|
|
verifyNoInteractions(this.registeredClientRepository, this.authorizationConsentService); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
|
|
|
public void authenticateWhenUserCodeIsInvalidatedThenThrowOAuth2AuthenticationException() { |
|
|
|
|
|
|
|
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(eq(USER_CODE), |
|
|
|
|
|
|
|
eq(OAuth2DeviceVerificationAuthenticationProvider.USER_CODE_TOKEN_TYPE))) |
|
|
|
|
|
|
|
.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 authenticateWhenUserCodeIsExpiredAndNotInvalidatedThenThrowOAuth2AuthenticationException() { |
|
|
|
|
|
|
|
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(eq(USER_CODE), |
|
|
|
|
|
|
|
eq(OAuth2DeviceVerificationAuthenticationProvider.USER_CODE_TOKEN_TYPE))) |
|
|
|
|
|
|
|
.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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); |
|
|
|
|
|
|
|
verify(this.authorizationService).findByToken(USER_CODE, |
|
|
|
|
|
|
|
OAuth2DeviceVerificationAuthenticationProvider.USER_CODE_TOKEN_TYPE); |
|
|
|
|
|
|
|
verify(this.authorizationService).save(authorizationCaptor.capture()); |
|
|
|
|
|
|
|
verifyNoMoreInteractions(this.authorizationService); |
|
|
|
|
|
|
|
verifyNoInteractions(this.registeredClientRepository, this.authorizationConsentService); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
OAuth2Authorization updatedAuthorization = authorizationCaptor.getValue(); |
|
|
|
|
|
|
|
assertThat(updatedAuthorization.getToken(OAuth2UserCode.class)).extracting(isInvalidated()).isEqualTo(true); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
@Test |
|
|
|
public void authenticateWhenPrincipalNotAuthenticatedThenReturnUnauthenticated() { |
|
|
|
public void authenticateWhenPrincipalNotAuthenticatedThenReturnUnauthenticated() { |
|
|
|
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
|
|
|
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); |
|
|
|
TestingAuthenticationToken principal = new TestingAuthenticationToken("user", null); |
|
|
|
Authentication authentication = new OAuth2DeviceVerificationAuthenticationToken(principal, USER_CODE, |
|
|
|
Authentication authentication = new OAuth2DeviceVerificationAuthenticationToken(principal, USER_CODE, |
|
|
|
Collections.emptyMap()); |
|
|
|
Collections.emptyMap()); |
|
|
|
@ -331,6 +404,15 @@ public class OAuth2DeviceVerificationAuthenticationProviderTests { |
|
|
|
return new OAuth2UserCode(USER_CODE, issuedAt, issuedAt.plus(30, ChronoUnit.MINUTES)); |
|
|
|
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<Map<String, Object>> withInvalidated() { |
|
|
|
|
|
|
|
return (metadata) -> metadata.put(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME, true); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static Function<OAuth2Authorization.Token<? extends OAuth2Token>, Boolean> isInvalidated() { |
|
|
|
private static Function<OAuth2Authorization.Token<? extends OAuth2Token>, Boolean> isInvalidated() { |
|
|
|
return (token) -> token.getMetadata(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME); |
|
|
|
return (token) -> token.getMetadata(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME); |
|
|
|
} |
|
|
|
} |
|
|
|
|