Skip to content

Commit a661e1c

Browse files
committed
Use OAuth2TokenGenerator for OAuth2AuthorizationCode
Closes spring-projectsgh-639
1 parent cdb48f5 commit a661e1c

File tree

2 files changed

+130
-14
lines changed

2 files changed

+130
-14
lines changed

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

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.function.Function;
2929
import java.util.function.Supplier;
3030

31+
import org.springframework.lang.Nullable;
3132
import org.springframework.security.authentication.AnonymousAuthenticationToken;
3233
import org.springframework.security.authentication.AuthenticationProvider;
3334
import org.springframework.security.core.Authentication;
@@ -46,12 +47,16 @@
4647
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
4748
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
4849
import org.springframework.security.oauth2.core.oidc.OidcScopes;
50+
import org.springframework.security.oauth2.server.authorization.DefaultOAuth2TokenContext;
4951
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
5052
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
5153
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
5254
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
55+
import org.springframework.security.oauth2.server.authorization.OAuth2TokenContext;
56+
import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator;
5357
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
5458
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
59+
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
5560
import org.springframework.util.Assert;
5661
import org.springframework.util.StringUtils;
5762
import org.springframework.web.util.UriComponents;
@@ -72,18 +77,21 @@
7277
* @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
7378
*/
7479
public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {
75-
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
80+
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
7681
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
77-
private static final StringKeyGenerator DEFAULT_AUTHORIZATION_CODE_GENERATOR =
78-
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
82+
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
7983
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
8084
new Base64StringKeyGenerator(Base64.getUrlEncoder());
8185
private static final Function<String, OAuth2AuthenticationValidator> DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER =
8286
createDefaultAuthenticationValidatorResolver();
8387
private final RegisteredClientRepository registeredClientRepository;
8488
private final OAuth2AuthorizationService authorizationService;
8589
private final OAuth2AuthorizationConsentService authorizationConsentService;
86-
private Supplier<String> authorizationCodeGenerator = DEFAULT_AUTHORIZATION_CODE_GENERATOR::generateKey;
90+
91+
@Deprecated
92+
private Supplier<String> authorizationCodeSupplier;
93+
94+
private OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator();
8795
private Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER;
8896
private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
8997

@@ -122,9 +130,22 @@ public boolean supports(Class<?> authentication) {
122130
/**
123131
* Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}.
124132
*
133+
* @deprecated Use {@link #setAuthorizationCodeGenerator(OAuth2TokenGenerator)} instead
125134
* @param authorizationCodeGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}
126135
*/
136+
@Deprecated
127137
public void setAuthorizationCodeGenerator(Supplier<String> authorizationCodeGenerator) {
138+
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
139+
this.authorizationCodeSupplier = authorizationCodeGenerator;
140+
}
141+
142+
/**
143+
* Sets the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}.
144+
*
145+
* @param authorizationCodeGenerator the {@link OAuth2TokenGenerator} that generates the {@link OAuth2AuthorizationCode}
146+
* @since 0.2.3
147+
*/
148+
public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator) {
128149
Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
129150
this.authorizationCodeGenerator = authorizationCodeGenerator;
130151
}
@@ -258,7 +279,22 @@ private Authentication authenticateAuthorizationRequest(Authentication authentic
258279
.build();
259280
}
260281

261-
OAuth2AuthorizationCode authorizationCode = generateAuthorizationCode();
282+
OAuth2AuthorizationCode authorizationCode;
283+
if (this.authorizationCodeSupplier != null) {
284+
Instant issuedAt = Instant.now();
285+
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
286+
authorizationCode = new OAuth2AuthorizationCode(this.authorizationCodeSupplier.get(), issuedAt, expiresAt);
287+
} else {
288+
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
289+
authorizationCodeRequestAuthentication, registeredClient, null, authorizationRequest.getScopes());
290+
authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
291+
if (authorizationCode == null) {
292+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
293+
"The token generator failed to generate the authorization code.", ERROR_URI);
294+
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
295+
}
296+
}
297+
262298
OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
263299
.token(authorizationCode)
264300
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes())
@@ -286,12 +322,6 @@ private OAuth2AuthenticationValidator resolveAuthenticationValidator(String para
286322
DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName);
287323
}
288324

289-
private OAuth2AuthorizationCode generateAuthorizationCode() {
290-
Instant issuedAt = Instant.now();
291-
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
292-
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.get(), issuedAt, expiresAt);
293-
}
294-
295325
private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
296326
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
297327
(OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
@@ -383,7 +413,21 @@ private Authentication authenticateAuthorizationConsent(Authentication authentic
383413
this.authorizationConsentService.save(authorizationConsent);
384414
}
385415

386-
OAuth2AuthorizationCode authorizationCode = generateAuthorizationCode();
416+
OAuth2AuthorizationCode authorizationCode;
417+
if (this.authorizationCodeSupplier != null) {
418+
Instant issuedAt = Instant.now();
419+
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
420+
authorizationCode = new OAuth2AuthorizationCode(this.authorizationCodeSupplier.get(), issuedAt, expiresAt);
421+
} else {
422+
OAuth2TokenContext tokenContext = createAuthorizationCodeTokenContext(
423+
authorizationCodeRequestAuthentication, registeredClient, authorization, authorizedScopes);
424+
authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);
425+
if (authorizationCode == null) {
426+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
427+
"The token generator failed to generate the authorization code.", ERROR_URI);
428+
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, null);
429+
}
430+
}
387431

388432
OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
389433
.token(authorizationCode)
@@ -424,6 +468,28 @@ private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient
424468
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
425469
}
426470

471+
private static OAuth2TokenContext createAuthorizationCodeTokenContext(
472+
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
473+
RegisteredClient registeredClient, OAuth2Authorization authorization, Set<String> authorizedScopes) {
474+
475+
// @formatter:off
476+
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
477+
.registeredClient(registeredClient)
478+
.principal((Authentication) authorizationCodeRequestAuthentication.getPrincipal())
479+
.providerContext(ProviderContextHolder.getProviderContext())
480+
.tokenType(new OAuth2TokenType(OAuth2ParameterNames.CODE))
481+
.authorizedScopes(authorizedScopes)
482+
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
483+
.authorizationGrant(authorizationCodeRequestAuthentication);
484+
// @formatter:on
485+
486+
if (authorization != null) {
487+
tokenContextBuilder.authorization(authorization);
488+
}
489+
490+
return tokenContextBuilder.build();
491+
}
492+
427493
private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,
428494
OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
429495

@@ -522,7 +588,7 @@ private static void throwError(String errorCode, String parameterName,
522588
private static void throwError(String errorCode, String parameterName,
523589
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
524590
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
525-
throwError(errorCode, parameterName, "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1",
591+
throwError(errorCode, parameterName, ERROR_URI,
526592
authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
527593
}
528594

@@ -580,6 +646,24 @@ private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OA
580646
.authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
581647
}
582648

649+
private static class OAuth2AuthorizationCodeGenerator implements OAuth2TokenGenerator<OAuth2AuthorizationCode> {
650+
private final StringKeyGenerator authorizationCodeGenerator =
651+
new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
652+
653+
@Nullable
654+
@Override
655+
public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
656+
if (context.getTokenType() == null ||
657+
!OAuth2ParameterNames.CODE.equals(context.getTokenType().getValue())) {
658+
return null;
659+
}
660+
Instant issuedAt = Instant.now();
661+
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES); // TODO Allow configuration for authorization code time-to-live
662+
return new OAuth2AuthorizationCode(this.authorizationCodeGenerator.generateKey(), issuedAt, expiresAt);
663+
}
664+
665+
}
666+
583667
private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
584668

585669
@Override

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,15 @@
4646
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
4747
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
4848
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
49+
import org.springframework.security.oauth2.server.authorization.OAuth2TokenGenerator;
4950
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
5051
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
5152
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
5253
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
5354
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
55+
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
56+
import org.springframework.security.oauth2.server.authorization.context.ProviderContext;
57+
import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder;
5458

5559
import static org.assertj.core.api.Assertions.assertThat;
5660
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -86,6 +90,8 @@ public void setUp() {
8690
this.registeredClientRepository, this.authorizationService, this.authorizationConsentService);
8791
this.principal = new TestingAuthenticationToken("principalName", "password");
8892
this.principal.setAuthenticated(true);
93+
ProviderSettings providerSettings = ProviderSettings.builder().issuer("https://provider.com").build();
94+
ProviderContextHolder.setProviderContext(new ProviderContext(providerSettings, null));
8995
}
9096

9197
@Test
@@ -119,7 +125,10 @@ public void supportsWhenTypeOAuth2AuthorizationCodeRequestAuthenticationTokenThe
119125

120126
@Test
121127
public void setAuthorizationCodeGeneratorWhenNullThenThrowIllegalArgumentException() {
122-
assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationCodeGenerator(null))
128+
assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationCodeGenerator((Supplier<String>) null))
129+
.isInstanceOf(IllegalArgumentException.class)
130+
.hasMessage("authorizationCodeGenerator cannot be null");
131+
assertThatThrownBy(() -> this.authenticationProvider.setAuthorizationCodeGenerator((OAuth2TokenGenerator<OAuth2AuthorizationCode>) null))
123132
.isInstanceOf(IllegalArgumentException.class)
124133
.hasMessage("authorizationCodeGenerator cannot be null");
125134
}
@@ -533,6 +542,29 @@ public String get() {
533542
assertThat(authenticationResult.getAuthorizationCode().getTokenValue()).isEqualTo(authorizationCodeGenerator.get());
534543
}
535544

545+
@Test
546+
public void authenticateWhenAuthorizationCodeNotGeneratedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
547+
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
548+
when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
549+
.thenReturn(registeredClient);
550+
551+
@SuppressWarnings("unchecked")
552+
OAuth2TokenGenerator<OAuth2AuthorizationCode> authorizationCodeGenerator = mock(OAuth2TokenGenerator.class);
553+
this.authenticationProvider.setAuthorizationCodeGenerator(authorizationCodeGenerator);
554+
555+
OAuth2AuthorizationCodeRequestAuthenticationToken authentication =
556+
authorizationCodeRequestAuthentication(registeredClient, this.principal)
557+
.build();
558+
559+
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
560+
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
561+
.extracting(ex -> ((OAuth2AuthorizationCodeRequestAuthenticationException) ex).getError())
562+
.satisfies(error -> {
563+
assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.SERVER_ERROR);
564+
assertThat(error.getDescription()).contains("The token generator failed to generate the authorization code.");
565+
});
566+
}
567+
536568
@Test
537569
public void authenticateWhenCustomAuthenticationValidatorResolverThenUsed() {
538570
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();

0 commit comments

Comments
 (0)