Skip to content

Commit b37d4dd

Browse files
committed
Describe error message when redirect_uri contains localhost
Closes gh-680
1 parent 4199ab0 commit b37d4dd

File tree

2 files changed

+98
-73
lines changed

2 files changed

+98
-73
lines changed

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

Lines changed: 94 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -475,69 +475,6 @@ private static boolean requireAuthorizationConsent(RegisteredClient registeredCl
475475
return true;
476476
}
477477

478-
private static boolean isValidRedirectUri(String requestedRedirectUri, RegisteredClient registeredClient) {
479-
UriComponents requestedRedirect;
480-
try {
481-
requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
482-
if (requestedRedirect.getFragment() != null) {
483-
return false;
484-
}
485-
} catch (Exception ex) {
486-
return false;
487-
}
488-
489-
String requestedRedirectHost = requestedRedirect.getHost();
490-
if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
491-
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7.1
492-
// While redirect URIs using localhost (i.e.,
493-
// "http://localhost:{port}/{path}") function similarly to loopback IP
494-
// redirects described in Section 10.3.3, the use of "localhost" is NOT RECOMMENDED.
495-
return false;
496-
}
497-
if (!isLoopbackAddress(requestedRedirectHost)) {
498-
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7
499-
// When comparing client redirect URIs against pre-registered URIs,
500-
// authorization servers MUST utilize exact string matching.
501-
return registeredClient.getRedirectUris().contains(requestedRedirectUri);
502-
}
503-
504-
// As per https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-10.3.3
505-
// The authorization server MUST allow any port to be specified at the
506-
// time of the request for loopback IP redirect URIs, to accommodate
507-
// clients that obtain an available ephemeral port from the operating
508-
// system at the time of the request.
509-
for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
510-
UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
511-
registeredRedirect.port(requestedRedirect.getPort());
512-
if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
513-
return true;
514-
}
515-
}
516-
return false;
517-
}
518-
519-
private static boolean isLoopbackAddress(String host) {
520-
// IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1"
521-
if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) {
522-
return true;
523-
}
524-
// IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255
525-
String[] ipv4Octets = host.split("\\.");
526-
if (ipv4Octets.length != 4) {
527-
return false;
528-
}
529-
try {
530-
int[] address = new int[ipv4Octets.length];
531-
for (int i=0; i < ipv4Octets.length; i++) {
532-
address[i] = Integer.parseInt(ipv4Octets[i]);
533-
}
534-
return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 &&
535-
address[2] <= 255 && address[3] >= 1 && address[3] <= 255;
536-
} catch (NumberFormatException ex) {
537-
return false;
538-
}
539-
}
540-
541478
private static boolean isPrincipalAuthenticated(Authentication principal) {
542479
return principal != null &&
543480
!AnonymousAuthenticationToken.class.isAssignableFrom(principal.getClass()) &&
@@ -560,9 +497,16 @@ private static void throwError(String errorCode, String parameterName,
560497
private static void throwError(String errorCode, String parameterName, String errorUri,
561498
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
562499
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
500+
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
501+
throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient, authorizationRequest);
502+
}
503+
504+
private static void throwError(OAuth2Error error, String parameterName,
505+
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
506+
RegisteredClient registeredClient, OAuth2AuthorizationRequest authorizationRequest) {
563507

564508
boolean redirectOnError = true;
565-
if (errorCode.equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
509+
if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) &&
566510
(parameterName.equals(OAuth2ParameterNames.CLIENT_ID) ||
567511
parameterName.equals(OAuth2ParameterNames.REDIRECT_URI) ||
568512
parameterName.equals(OAuth2ParameterNames.STATE))) {
@@ -587,7 +531,6 @@ private static void throwError(String errorCode, String parameterName, String er
587531
authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated());
588532
}
589533

590-
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
591534
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult);
592535
}
593536

@@ -637,16 +580,95 @@ public void validate(OAuth2AuthenticationContext authenticationContext) {
637580
authenticationContext.getAuthentication();
638581
RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class);
639582

640-
if (StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) {
641-
if (!isValidRedirectUri(authorizationCodeRequestAuthentication.getRedirectUri(), registeredClient)) {
583+
String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri();
584+
585+
if (StringUtils.hasText(requestedRedirectUri)) {
586+
// ***** redirect_uri is available in authorization request
587+
588+
UriComponents requestedRedirect = null;
589+
try {
590+
requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build();
591+
} catch (Exception ex) { }
592+
if (requestedRedirect == null || requestedRedirect.getFragment() != null) {
642593
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
643594
authorizationCodeRequestAuthentication, registeredClient);
644595
}
645-
} else if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
646-
registeredClient.getRedirectUris().size() != 1) {
647-
// redirect_uri is REQUIRED for OpenID Connect
648-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
649-
authorizationCodeRequestAuthentication, registeredClient);
596+
597+
String requestedRedirectHost = requestedRedirect.getHost();
598+
if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
599+
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1
600+
// While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}")
601+
// function similarly to loopback IP redirects described in Section 10.3.3,
602+
// the use of "localhost" is NOT RECOMMENDED.
603+
OAuth2Error error = new OAuth2Error(
604+
OAuth2ErrorCodes.INVALID_REQUEST,
605+
"localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " +
606+
"Use the IP literal (127.0.0.1) instead.",
607+
"https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1");
608+
throwError(error, OAuth2ParameterNames.REDIRECT_URI,
609+
authorizationCodeRequestAuthentication, registeredClient, null);
610+
}
611+
612+
if (!isLoopbackAddress(requestedRedirectHost)) {
613+
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7
614+
// When comparing client redirect URIs against pre-registered URIs,
615+
// authorization servers MUST utilize exact string matching.
616+
if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) {
617+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
618+
authorizationCodeRequestAuthentication, registeredClient);
619+
}
620+
} else {
621+
// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-10.3.3
622+
// The authorization server MUST allow any port to be specified at the
623+
// time of the request for loopback IP redirect URIs, to accommodate
624+
// clients that obtain an available ephemeral port from the operating
625+
// system at the time of the request.
626+
boolean validRedirectUri = false;
627+
for (String registeredRedirectUri : registeredClient.getRedirectUris()) {
628+
UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri);
629+
registeredRedirect.port(requestedRedirect.getPort());
630+
if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) {
631+
validRedirectUri = true;
632+
break;
633+
}
634+
}
635+
if (!validRedirectUri) {
636+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
637+
authorizationCodeRequestAuthentication, registeredClient);
638+
}
639+
}
640+
641+
} else {
642+
// ***** redirect_uri is NOT available in authorization request
643+
644+
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) ||
645+
registeredClient.getRedirectUris().size() != 1) {
646+
// redirect_uri is REQUIRED for OpenID Connect
647+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
648+
authorizationCodeRequestAuthentication, registeredClient);
649+
}
650+
}
651+
}
652+
653+
private static boolean isLoopbackAddress(String host) {
654+
// IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1"
655+
if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) {
656+
return true;
657+
}
658+
// IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255
659+
String[] ipv4Octets = host.split("\\.");
660+
if (ipv4Octets.length != 4) {
661+
return false;
662+
}
663+
try {
664+
int[] address = new int[ipv4Octets.length];
665+
for (int i=0; i < ipv4Octets.length; i++) {
666+
address[i] = Integer.parseInt(ipv4Octets[i]);
667+
}
668+
return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 &&
669+
address[2] <= 255 && address[3] >= 1 && address[3] <= 255;
670+
} catch (NumberFormatException ex) {
671+
return false;
650672
}
651673
}
652674

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,10 @@ public void authenticateWhenRedirectUriLocalhostThenThrowOAuth2AuthorizationCode
207207
.satisfies(ex ->
208208
assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
209209
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)
210-
);
210+
)
211+
.extracting(ex -> ((OAuth2AuthorizationCodeRequestAuthenticationException) ex).getError())
212+
.satisfies(error ->
213+
assertThat(error.getDescription()).isEqualTo("localhost is not allowed for the redirect_uri (https://localhost:5000). Use the IP literal (127.0.0.1) instead."));
211214
}
212215

213216
@Test

0 commit comments

Comments
 (0)