Use OAuth2TokenGenerator for OAuth2AuthorizationCode

Closes gh-639
This commit is contained in:
Joe Grandja
2022-02-25 06:25:16 -05:00
parent cdb48f510e
commit a661e1cdb7
2 changed files with 130 additions and 14 deletions

View File

@@ -28,6 +28,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
@@ -46,12 +47,16 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
@@ -72,10 +77,9 @@ import org.springframework.web.util.UriComponentsBuilder;
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
*/
public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
private static final StringKeyGenerator DEFAULT_AUTHORIZATION_CODE_GENERATOR =
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
new Base64StringKeyGenerator(Base64.getUrlEncoder());
private static final Function<String, OAuth2AuthenticationValidator> DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER =
@@ -83,7 +87,11 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
private final RegisteredClientRepository registeredClientRepository;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2AuthorizationConsentService authorizationConsentService;
private Supplier<String> authorizationCodeGenerator = DEFAULT_AUTHORIZATION_CODE_GENERATOR::generateKey;
@Deprecated
private Supplier<String> authorizationCodeSupplier;
private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
private Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER;
private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
@@ -122,9 +130,22 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
/**
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}.
*
* @deprecated Use {@link #setAuthorizationCodeGenerator(OAuth2TokenGenerator)} instead
* @param authorizationCodeGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}
*/
@Deprecated
public void setAuthorizationCodeGenerator(Supplier<String> authorizationCodeGenerator) {
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
this.authorizationCodeSupplier = authorizationCodeGenerator;
}
/**
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
*
* @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
* @since 0.2.3
*/
public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
this.authorizationCodeGenerator = authorizationCodeGenerator;
}
@@ -258,7 +279,22 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
.build();
}
OAuth2AuthorizationCode authorizationCode = generateAuthorizationCode();
OAuth2AuthorizationCode authorizationCode;
if (this.authorizationCodeSupplier != null) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
authorizationCode = new OAuth2AuthorizationCode(this.authorizationCodeSupplier.get(), issuedAt, expiresAt);
} else {
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
authorizationCodeRequestAuthentication, registeredClient, null, authorizationRequest.getScopes());
authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
if (authorizationCode == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the authorization code.", ERROR_URI);
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
}
}
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
.token(authorizationCode)
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes())
@@ -286,12 +322,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName);
}
private OAuth2AuthorizationCode generateAuthorizationCode() {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.get(), issuedAt, expiresAt);
}
private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
@@ -383,7 +413,21 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
this.authorizationConsentService.save(authorizationConsent);
}
OAuth2AuthorizationCode authorizationCode = generateAuthorizationCode();
OAuth2AuthorizationCode authorizationCode;
if (this.authorizationCodeSupplier != null) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
authorizationCode = new OAuth2AuthorizationCode(this.authorizationCodeSupplier.get(), issuedAt, expiresAt);
} else {
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
authorizationCodeRequestAuthentication, registeredClient, authorization, authorizedScopes);
authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
if (authorizationCode == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the authorization code.", ERROR_URI);
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
}
}
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
.token(authorizationCode)
@@ -424,6 +468,28 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
}
private static OAuth2TokenContext createAuthorizationCodeTokenContext(
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient, OAuth2Authorization authorization, Set<String> authorizedScopes) {
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal((Authentication) authorizationCodeRequestAuthentication.getPrincipal())
.providerContext(ProviderContextHolder.getProviderContext())
.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
.authorizedScopes(authorizedScopes)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrant(authorizationCodeRequestAuthentication);
// @formatter:on
if (authorization != null) {
tokenContextBuilder.authorization(authorization);
}
return tokenContextBuilder.build();
}
private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,
OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
@@ -522,7 +588,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
private static void throwError(String errorCode, String parameterName,
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
throwError(errorCode, parameterName, "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1",
throwError(errorCode, parameterName, ERROR_URI,
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
}
@@ -580,6 +646,24 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
}
private static class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
private final StringKeyGenerator authorizationCodeGenerator =
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
@Nullable
@Override
public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
if (context.getTokenType() == null ||
!OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
return null;
}
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
}
}
private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
@Override

View File

@@ -46,11 +46,15 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -86,6 +90,8 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
this.registeredClientRepository, this.authorizationService, this.authorizationConsentService);
this.principal = new TestingAuthenticationToken("principalName", "password");
this.principal.setAuthenticated(true);
ProviderSettings providerSettings = ProviderSettings.builder().issuer("https://provider.com").build();
ProviderContextHolder.setProviderContext(new ProviderContext(providerSettings, null));
}
@Test
@@ -119,7 +125,10 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
@Test
public void setAuthorizationCodeGeneratorWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationCodeGenerator(null))
assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationCodeGenerator((Supplier<String>) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizationCodeGenerator cannot be null");
assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationCodeGenerator((OAuth2TokenGenerator<OAuth2AuthorizationCode>) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizationCodeGenerator cannot be null");
}
@@ -533,6 +542,29 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
assertThat(authenticationResult.getAuthorizationCode().getTokenValue()).isEqualTo(authorizationCodeGenerator.get());
}
@Test
public void authenticateWhenAuthorizationCodeNotGeneratedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
.thenReturn(registeredClient);
@SuppressWarnings("unchecked")
OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = mock(OAuth2TokenGenerator.class);
this.authenticationProvider.setAuthorizationCodeGenerator(authorizationCodeGenerator);
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
authorizationCodeRequestAuthentication(registeredClient, this.principal)
.build();
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.extracting(ex -> ((OAuth2AuthorizationCodeRequestAuthenticationException) ex).getError())
.satisfies(error -> {
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.SERVER_ERROR);
assertThat(error.getDescription()).contains("The token generator failed to generate the authorization code.");
});
}
@Test
public void authenticateWhenCustomAuthenticationValidatorResolverThenUsed() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();