diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java index 062a6426..9350d713 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java @@ -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 Section 4.1.1 Authorization Request */ 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 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 authorizationCodeGenerator = DEFAULT_AUTHORIZATION_CODE_GENERATOR::generateKey; + + @Deprecated + private Supplier authorizationCodeSupplier; + + private OAuth2TokenGenerator authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator(); private Function authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER; private Consumer authorizationConsentCustomizer; @@ -122,9 +130,22 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen /** * Sets the {@code Supplier} that generates the value for the {@link OAuth2AuthorizationCode}. * + * @deprecated Use {@link #setAuthorizationCodeGenerator(OAuth2TokenGenerator)} instead * @param authorizationCodeGenerator the {@code Supplier} that generates the value for the {@link OAuth2AuthorizationCode} */ + @Deprecated public void setAuthorizationCodeGenerator(Supplier 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 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 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 { + 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 diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java index 2691400b..9b31e6ff 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java @@ -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) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("authorizationCodeGenerator cannot be null"); + assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationCodeGenerator((OAuth2TokenGenerator) 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 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();