|
28 | 28 | import java.util.function.Function;
|
29 | 29 | import java.util.function.Supplier;
|
30 | 30 |
|
| 31 | +import org.springframework.lang.Nullable; |
31 | 32 | import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
32 | 33 | import org.springframework.security.authentication.AuthenticationProvider;
|
33 | 34 | import org.springframework.security.core.Authentication;
|
|
46 | 47 | import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
47 | 48 | import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
48 | 49 | import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
| 50 | +import org.springframework.security.oauth2.server.authorization.DefaultOAuth2TokenContext; |
49 | 51 | import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
50 | 52 | import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
51 | 53 | import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
52 | 54 | 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; |
53 | 57 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
54 | 58 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
| 59 | +import org.springframework.security.oauth2.server.authorization.context.ProviderContextHolder; |
55 | 60 | import org.springframework.util.Assert;
|
56 | 61 | import org.springframework.util.StringUtils;
|
57 | 62 | import org.springframework.web.util.UriComponents;
|
|
72 | 77 | * @see <a target="_blank" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Request</a>
|
73 | 78 | */
|
74 | 79 | 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"; |
76 | 81 | 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); |
79 | 83 | private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
|
80 | 84 | new Base64StringKeyGenerator(Base64.getUrlEncoder());
|
81 | 85 | private static final Function<String, OAuth2AuthenticationValidator> DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER =
|
82 | 86 | createDefaultAuthenticationValidatorResolver();
|
83 | 87 | private final RegisteredClientRepository registeredClientRepository;
|
84 | 88 | private final OAuth2AuthorizationService authorizationService;
|
85 | 89 | 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(); |
87 | 95 | private Function<String, OAuth2AuthenticationValidator> authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER;
|
88 | 96 | private Consumer<OAuth2AuthorizationConsentAuthenticationContext> authorizationConsentCustomizer;
|
89 | 97 |
|
@@ -122,9 +130,22 @@ public boolean supports(Class<?> authentication) {
|
122 | 130 | /**
|
123 | 131 | * Sets the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}.
|
124 | 132 | *
|
| 133 | + * @deprecated Use {@link #setAuthorizationCodeGenerator(OAuth2TokenGenerator)} instead |
125 | 134 | * @param authorizationCodeGenerator the {@code Supplier<String>} that generates the value for the {@link OAuth2AuthorizationCode}
|
126 | 135 | */
|
| 136 | + @Deprecated |
127 | 137 | 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) { |
128 | 149 | Assert.notNull(authorizationCodeGenerator, "authorizationCodeGenerator cannot be null");
|
129 | 150 | this.authorizationCodeGenerator = authorizationCodeGenerator;
|
130 | 151 | }
|
@@ -258,7 +279,22 @@ private Authentication authenticateAuthorizationRequest(Authentication authentic
|
258 | 279 | .build();
|
259 | 280 | }
|
260 | 281 |
|
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 | + |
262 | 298 | OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
|
263 | 299 | .token(authorizationCode)
|
264 | 300 | .attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizationRequest.getScopes())
|
@@ -286,12 +322,6 @@ private OAuth2AuthenticationValidator resolveAuthenticationValidator(String para
|
286 | 322 | DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName);
|
287 | 323 | }
|
288 | 324 |
|
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 |
| - |
295 | 325 | private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException {
|
296 | 326 | OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
|
297 | 327 | (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
@@ -383,7 +413,21 @@ private Authentication authenticateAuthorizationConsent(Authentication authentic
|
383 | 413 | this.authorizationConsentService.save(authorizationConsent);
|
384 | 414 | }
|
385 | 415 |
|
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 | + } |
387 | 431 |
|
388 | 432 | OAuth2Authorization updatedAuthorization = OAuth2Authorization.from(authorization)
|
389 | 433 | .token(authorizationCode)
|
@@ -424,6 +468,28 @@ private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient
|
424 | 468 | .attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest);
|
425 | 469 | }
|
426 | 470 |
|
| 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 | + |
427 | 493 | private static boolean requireAuthorizationConsent(RegisteredClient registeredClient,
|
428 | 494 | OAuth2AuthorizationRequest authorizationRequest, OAuth2AuthorizationConsent authorizationConsent) {
|
429 | 495 |
|
@@ -522,7 +588,7 @@ private static void throwError(String errorCode, String parameterName,
|
522 | 588 | private static void throwError(String errorCode, String parameterName,
|
523 | 589 | OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
524 | 590 | 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, |
526 | 592 | authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
|
527 | 593 | }
|
528 | 594 |
|
@@ -580,6 +646,24 @@ private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OA
|
580 | 646 | .authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode());
|
581 | 647 | }
|
582 | 648 |
|
| 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 | + |
583 | 667 | private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
|
584 | 668 |
|
585 | 669 | @Override
|
|
0 commit comments