19
19
import java .time .Instant ;
20
20
import java .util .Base64 ;
21
21
import java .util .Collections ;
22
- import java .util .HashMap ;
23
22
import java .util .HashSet ;
24
- import java .util .Map ;
25
23
import java .util .Set ;
26
24
import java .util .function .Consumer ;
27
- import java .util .function .Function ;
28
25
29
26
import org .springframework .lang .Nullable ;
30
27
import org .springframework .security .authentication .AnonymousAuthenticationToken ;
55
52
import org .springframework .security .oauth2 .server .authorization .token .OAuth2TokenGenerator ;
56
53
import org .springframework .util .Assert ;
57
54
import org .springframework .util .StringUtils ;
58
- import org .springframework .web .util .UriComponents ;
59
- import org .springframework .web .util .UriComponentsBuilder ;
60
55
61
56
/**
62
57
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent)
66
61
* @author Steve Riesenberg
67
62
* @since 0.1.2
68
63
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
64
+ * @see OAuth2AuthorizationCodeRequestAuthenticationValidator
69
65
* @see OAuth2AuthorizationCodeAuthenticationProvider
70
66
* @see RegisteredClientRepository
71
67
* @see OAuth2AuthorizationService
@@ -78,13 +74,12 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
78
74
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType (OAuth2ParameterNames .STATE );
79
75
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR =
80
76
new Base64StringKeyGenerator (Base64 .getUrlEncoder ());
81
- private static final Function <String , OAuth2AuthenticationValidator > DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER =
82
- createDefaultAuthenticationValidatorResolver ();
83
77
private final RegisteredClientRepository registeredClientRepository ;
84
78
private final OAuth2AuthorizationService authorizationService ;
85
79
private final OAuth2AuthorizationConsentService authorizationConsentService ;
86
80
private OAuth2TokenGenerator <OAuth2AuthorizationCode > authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator ();
87
- private Function <String , OAuth2AuthenticationValidator > authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER ;
81
+ private Consumer <OAuth2AuthorizationCodeRequestAuthenticationContext > authenticationValidator =
82
+ new OAuth2AuthorizationCodeRequestAuthenticationValidator ();
88
83
private Consumer <OAuth2AuthorizationConsentAuthenticationContext > authorizationConsentCustomizer ;
89
84
90
85
/**
@@ -131,23 +126,20 @@ public void setAuthorizationCodeGenerator(OAuth2TokenGenerator<OAuth2Authorizati
131
126
}
132
127
133
128
/**
134
- * Sets the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter.
129
+ * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext}
130
+ * and is responsible for validating specific OAuth 2.0 Authorization Request parameters
131
+ * associated in the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}.
132
+ * The default authentication validator is {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}.
135
133
*
136
134
* <p>
137
- * The following OAuth 2.0 Authorization Request parameters are supported:
138
- * <ol>
139
- * <li>{@link OAuth2ParameterNames#REDIRECT_URI}</li>
140
- * <li>{@link OAuth2ParameterNames#SCOPE}</li>
141
- * </ol>
135
+ * <b>NOTE:</b> The authentication validator MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
142
136
*
143
- * <p>
144
- * <b>NOTE:</b> The resolved {@link OAuth2AuthenticationValidator} MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
145
- *
146
- * @param authenticationValidatorResolver the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter
137
+ * @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for validating specific OAuth 2.0 Authorization Request parameters
138
+ * @since 0.4.0
147
139
*/
148
- public void setAuthenticationValidatorResolver ( Function < String , OAuth2AuthenticationValidator > authenticationValidatorResolver ) {
149
- Assert .notNull (authenticationValidatorResolver , "authenticationValidatorResolver cannot be null" );
150
- this .authenticationValidatorResolver = authenticationValidatorResolver ;
140
+ public void setAuthenticationValidator ( Consumer < OAuth2AuthorizationCodeRequestAuthenticationContext > authenticationValidator ) {
141
+ Assert .notNull (authenticationValidator , "authenticationValidator cannot be null" );
142
+ this .authenticationValidator = authenticationValidator ;
151
143
}
152
144
153
145
/**
@@ -186,22 +178,17 @@ private Authentication authenticateAuthorizationRequest(Authentication authentic
186
178
authorizationCodeRequestAuthentication , null );
187
179
}
188
180
189
- OAuth2AuthenticationContext authenticationContext =
181
+ OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext =
190
182
OAuth2AuthorizationCodeRequestAuthenticationContext .with (authorizationCodeRequestAuthentication )
191
183
.registeredClient (registeredClient )
192
184
.build ();
193
-
194
- OAuth2AuthenticationValidator redirectUriValidator = resolveAuthenticationValidator (OAuth2ParameterNames .REDIRECT_URI );
195
- redirectUriValidator .validate (authenticationContext );
185
+ this .authenticationValidator .accept (authenticationContext );
196
186
197
187
if (!registeredClient .getAuthorizationGrantTypes ().contains (AuthorizationGrantType .AUTHORIZATION_CODE )) {
198
188
throwError (OAuth2ErrorCodes .UNAUTHORIZED_CLIENT , OAuth2ParameterNames .CLIENT_ID ,
199
189
authorizationCodeRequestAuthentication , registeredClient );
200
190
}
201
191
202
- OAuth2AuthenticationValidator scopeValidator = resolveAuthenticationValidator (OAuth2ParameterNames .SCOPE );
203
- scopeValidator .validate (authenticationContext );
204
-
205
192
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
206
193
String codeChallenge = (String ) authorizationCodeRequestAuthentication .getAdditionalParameters ().get (PkceParameterNames .CODE_CHALLENGE );
207
194
if (StringUtils .hasText (codeChallenge )) {
@@ -284,13 +271,6 @@ private Authentication authenticateAuthorizationRequest(Authentication authentic
284
271
.build ();
285
272
}
286
273
287
- private OAuth2AuthenticationValidator resolveAuthenticationValidator (String parameterName ) {
288
- OAuth2AuthenticationValidator authenticationValidator = this .authenticationValidatorResolver .apply (parameterName );
289
- return authenticationValidator != null ?
290
- authenticationValidator :
291
- DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER .apply (parameterName );
292
- }
293
-
294
274
private Authentication authenticateAuthorizationConsent (Authentication authentication ) throws AuthenticationException {
295
275
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
296
276
(OAuth2AuthorizationCodeRequestAuthenticationToken ) authentication ;
@@ -414,13 +394,6 @@ private Authentication authenticateAuthorizationConsent(Authentication authentic
414
394
.build ();
415
395
}
416
396
417
- private static Function <String , OAuth2AuthenticationValidator > createDefaultAuthenticationValidatorResolver () {
418
- Map <String , OAuth2AuthenticationValidator > authenticationValidators = new HashMap <>();
419
- authenticationValidators .put (OAuth2ParameterNames .REDIRECT_URI , new DefaultRedirectUriOAuth2AuthenticationValidator ());
420
- authenticationValidators .put (OAuth2ParameterNames .SCOPE , new DefaultScopeOAuth2AuthenticationValidator ());
421
- return authenticationValidators ::get ;
422
- }
423
-
424
397
private static OAuth2Authorization .Builder authorizationBuilder (RegisteredClient registeredClient , Authentication principal ,
425
398
OAuth2AuthorizationRequest authorizationRequest ) {
426
399
return OAuth2Authorization .withRegisteredClient (registeredClient )
@@ -505,7 +478,6 @@ private static void throwError(OAuth2Error error, String parameterName,
505
478
boolean redirectOnError = true ;
506
479
if (error .getErrorCode ().equals (OAuth2ErrorCodes .INVALID_REQUEST ) &&
507
480
(parameterName .equals (OAuth2ParameterNames .CLIENT_ID ) ||
508
- parameterName .equals (OAuth2ParameterNames .REDIRECT_URI ) ||
509
481
parameterName .equals (OAuth2ParameterNames .STATE ))) {
510
482
redirectOnError = false ;
511
483
}
@@ -520,14 +492,14 @@ private static void throwError(OAuth2Error error, String parameterName,
520
492
.redirectUri (redirectUri )
521
493
.state (state )
522
494
.build ();
523
- authorizationCodeRequestAuthenticationResult .setAuthenticated (authorizationCodeRequestAuthentication .isAuthenticated ());
524
495
} else if (!redirectOnError && StringUtils .hasText (authorizationCodeRequestAuthentication .getRedirectUri ())) {
525
496
authorizationCodeRequestAuthenticationResult = from (authorizationCodeRequestAuthentication )
526
497
.redirectUri (null ) // Prevent redirects
527
498
.build ();
528
- authorizationCodeRequestAuthenticationResult .setAuthenticated (authorizationCodeRequestAuthentication .isAuthenticated ());
529
499
}
530
500
501
+ authorizationCodeRequestAuthenticationResult .setAuthenticated (authorizationCodeRequestAuthentication .isAuthenticated ());
502
+
531
503
throw new OAuth2AuthorizationCodeRequestAuthenticationException (error , authorizationCodeRequestAuthenticationResult );
532
504
}
533
505
@@ -569,124 +541,4 @@ public OAuth2AuthorizationCode generate(OAuth2TokenContext context) {
569
541
570
542
}
571
543
572
- private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
573
-
574
- @ Override
575
- public void validate (OAuth2AuthenticationContext authenticationContext ) {
576
- OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
577
- authenticationContext .getAuthentication ();
578
- RegisteredClient registeredClient = authenticationContext .get (RegisteredClient .class );
579
-
580
- String requestedRedirectUri = authorizationCodeRequestAuthentication .getRedirectUri ();
581
-
582
- if (StringUtils .hasText (requestedRedirectUri )) {
583
- // ***** redirect_uri is available in authorization request
584
-
585
- UriComponents requestedRedirect = null ;
586
- try {
587
- requestedRedirect = UriComponentsBuilder .fromUriString (requestedRedirectUri ).build ();
588
- } catch (Exception ex ) { }
589
- if (requestedRedirect == null || requestedRedirect .getFragment () != null ) {
590
- throwError (OAuth2ErrorCodes .INVALID_REQUEST , OAuth2ParameterNames .REDIRECT_URI ,
591
- authorizationCodeRequestAuthentication , registeredClient );
592
- }
593
-
594
- String requestedRedirectHost = requestedRedirect .getHost ();
595
- if (requestedRedirectHost == null || requestedRedirectHost .equals ("localhost" )) {
596
- // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1
597
- // While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}")
598
- // function similarly to loopback IP redirects described in Section 10.3.3,
599
- // the use of "localhost" is NOT RECOMMENDED.
600
- OAuth2Error error = new OAuth2Error (
601
- OAuth2ErrorCodes .INVALID_REQUEST ,
602
- "localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " +
603
- "Use the IP literal (127.0.0.1) instead." ,
604
- "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1" );
605
- throwError (error , OAuth2ParameterNames .REDIRECT_URI ,
606
- authorizationCodeRequestAuthentication , registeredClient , null );
607
- }
608
-
609
- if (!isLoopbackAddress (requestedRedirectHost )) {
610
- // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7
611
- // When comparing client redirect URIs against pre-registered URIs,
612
- // authorization servers MUST utilize exact string matching.
613
- if (!registeredClient .getRedirectUris ().contains (requestedRedirectUri )) {
614
- throwError (OAuth2ErrorCodes .INVALID_REQUEST , OAuth2ParameterNames .REDIRECT_URI ,
615
- authorizationCodeRequestAuthentication , registeredClient );
616
- }
617
- } else {
618
- // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-10.3.3
619
- // The authorization server MUST allow any port to be specified at the
620
- // time of the request for loopback IP redirect URIs, to accommodate
621
- // clients that obtain an available ephemeral port from the operating
622
- // system at the time of the request.
623
- boolean validRedirectUri = false ;
624
- for (String registeredRedirectUri : registeredClient .getRedirectUris ()) {
625
- UriComponentsBuilder registeredRedirect = UriComponentsBuilder .fromUriString (registeredRedirectUri );
626
- registeredRedirect .port (requestedRedirect .getPort ());
627
- if (registeredRedirect .build ().toString ().equals (requestedRedirect .toString ())) {
628
- validRedirectUri = true ;
629
- break ;
630
- }
631
- }
632
- if (!validRedirectUri ) {
633
- throwError (OAuth2ErrorCodes .INVALID_REQUEST , OAuth2ParameterNames .REDIRECT_URI ,
634
- authorizationCodeRequestAuthentication , registeredClient );
635
- }
636
- }
637
-
638
- } else {
639
- // ***** redirect_uri is NOT available in authorization request
640
-
641
- if (authorizationCodeRequestAuthentication .getScopes ().contains (OidcScopes .OPENID ) ||
642
- registeredClient .getRedirectUris ().size () != 1 ) {
643
- // redirect_uri is REQUIRED for OpenID Connect
644
- throwError (OAuth2ErrorCodes .INVALID_REQUEST , OAuth2ParameterNames .REDIRECT_URI ,
645
- authorizationCodeRequestAuthentication , registeredClient );
646
- }
647
- }
648
- }
649
-
650
- private static boolean isLoopbackAddress (String host ) {
651
- // IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1"
652
- if ("[0:0:0:0:0:0:0:1]" .equals (host ) || "[::1]" .equals (host )) {
653
- return true ;
654
- }
655
- // IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255
656
- String [] ipv4Octets = host .split ("\\ ." );
657
- if (ipv4Octets .length != 4 ) {
658
- return false ;
659
- }
660
- try {
661
- int [] address = new int [ipv4Octets .length ];
662
- for (int i =0 ; i < ipv4Octets .length ; i ++) {
663
- address [i ] = Integer .parseInt (ipv4Octets [i ]);
664
- }
665
- return address [0 ] == 127 && address [1 ] >= 0 && address [1 ] <= 255 && address [2 ] >= 0 &&
666
- address [2 ] <= 255 && address [3 ] >= 1 && address [3 ] <= 255 ;
667
- } catch (NumberFormatException ex ) {
668
- return false ;
669
- }
670
- }
671
-
672
- }
673
-
674
- private static class DefaultScopeOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator {
675
-
676
- @ Override
677
- public void validate (OAuth2AuthenticationContext authenticationContext ) {
678
- OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
679
- authenticationContext .getAuthentication ();
680
- RegisteredClient registeredClient = authenticationContext .get (RegisteredClient .class );
681
-
682
- Set <String > requestedScopes = authorizationCodeRequestAuthentication .getScopes ();
683
- Set <String > allowedScopes = registeredClient .getScopes ();
684
- if (!requestedScopes .isEmpty () && !allowedScopes .containsAll (requestedScopes )) {
685
- throwError (OAuth2ErrorCodes .INVALID_SCOPE , OAuth2ParameterNames .SCOPE ,
686
- authorizationCodeRequestAuthentication , registeredClient );
687
- }
688
- }
689
-
690
- }
691
-
692
544
}
0 commit comments