diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationValidator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationValidator.java deleted file mode 100644 index afcb2510..00000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthenticationValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.oauth2.server.authorization.authentication; - -import org.springframework.security.core.Authentication; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; - -/** - * Implementations of this interface are responsible for validating the attribute(s) - * of the {@link Authentication} associated to the {@link OAuth2AuthenticationContext}. - * - * @author Joe Grandja - * @since 0.2.0 - * @see OAuth2AuthenticationContext - */ -@FunctionalInterface -public interface OAuth2AuthenticationValidator { - - /** - * Validate the attribute(s) of the {@link Authentication}. - * - * @param authenticationContext the authentication context - * @throws OAuth2AuthenticationException if the attribute(s) of the {@code Authentication} is invalid - */ - void validate(OAuth2AuthenticationContext authenticationContext) throws OAuth2AuthenticationException; - -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java index 73d902f3..c158d940 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationContext.java @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.authentication; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import org.springframework.lang.Nullable; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; @@ -30,7 +31,8 @@ import org.springframework.util.Assert; * @author Joe Grandja * @since 0.4.0 * @see OAuth2AuthenticationContext - * @see OAuth2AuthorizationCodeRequestAuthenticationProvider + * @see OAuth2AuthorizationCodeRequestAuthenticationToken + * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer) */ public final class OAuth2AuthorizationCodeRequestAuthenticationContext implements OAuth2AuthenticationContext { private final Map context; 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 dadeb212..80c50097 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 @@ -19,12 +19,9 @@ import java.security.Principal; import java.time.Instant; import java.util.Base64; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Function; import org.springframework.lang.Nullable; import org.springframework.security.authentication.AnonymousAuthenticationToken; @@ -55,8 +52,6 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.web.util.UriComponents; -import org.springframework.web.util.UriComponentsBuilder; /** * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Request (and Consent) @@ -66,6 +61,7 @@ import org.springframework.web.util.UriComponentsBuilder; * @author Steve Riesenberg * @since 0.1.2 * @see OAuth2AuthorizationCodeRequestAuthenticationToken + * @see OAuth2AuthorizationCodeRequestAuthenticationValidator * @see OAuth2AuthorizationCodeAuthenticationProvider * @see RegisteredClientRepository * @see OAuth2AuthorizationService @@ -78,13 +74,12 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen 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 = - createDefaultAuthenticationValidatorResolver(); private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationService authorizationService; private final OAuth2AuthorizationConsentService authorizationConsentService; private OAuth2TokenGenerator authorizationCodeGenerator = new OAuth2AuthorizationCodeGenerator(); - private Function authenticationValidatorResolver = DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER; + private Consumer authenticationValidator = + new OAuth2AuthorizationCodeRequestAuthenticationValidator(); private Consumer authorizationConsentCustomizer; /** @@ -131,23 +126,20 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen } /** - * Sets the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter. + * Sets the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} + * and is responsible for validating specific OAuth 2.0 Authorization Request parameters + * associated in the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. + * The default authentication validator is {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}. * *

- * The following OAuth 2.0 Authorization Request parameters are supported: - *

    - *
  1. {@link OAuth2ParameterNames#REDIRECT_URI}
  2. - *
  3. {@link OAuth2ParameterNames#SCOPE}
  4. - *
+ * NOTE: The authentication validator MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails. * - *

- * NOTE: The resolved {@link OAuth2AuthenticationValidator} MUST throw {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails. - * - * @param authenticationValidatorResolver the resolver that resolves an {@link OAuth2AuthenticationValidator} from the provided OAuth 2.0 Authorization Request parameter + * @param authenticationValidator the {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for validating specific OAuth 2.0 Authorization Request parameters + * @since 0.4.0 */ - public void setAuthenticationValidatorResolver(Function authenticationValidatorResolver) { - Assert.notNull(authenticationValidatorResolver, "authenticationValidatorResolver cannot be null"); - this.authenticationValidatorResolver = authenticationValidatorResolver; + public void setAuthenticationValidator(Consumer authenticationValidator) { + Assert.notNull(authenticationValidator, "authenticationValidator cannot be null"); + this.authenticationValidator = authenticationValidator; } /** @@ -186,22 +178,17 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen authorizationCodeRequestAuthentication, null); } - OAuth2AuthenticationContext authenticationContext = + OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = OAuth2AuthorizationCodeRequestAuthenticationContext.with(authorizationCodeRequestAuthentication) .registeredClient(registeredClient) .build(); - - OAuth2AuthenticationValidator redirectUriValidator = resolveAuthenticationValidator(OAuth2ParameterNames.REDIRECT_URI); - redirectUriValidator.validate(authenticationContext); + this.authenticationValidator.accept(authenticationContext); if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) { throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID, authorizationCodeRequestAuthentication, registeredClient); } - OAuth2AuthenticationValidator scopeValidator = resolveAuthenticationValidator(OAuth2ParameterNames.SCOPE); - scopeValidator.validate(authenticationContext); - // code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE) String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get(PkceParameterNames.CODE_CHALLENGE); if (StringUtils.hasText(codeChallenge)) { @@ -284,13 +271,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen .build(); } - private OAuth2AuthenticationValidator resolveAuthenticationValidator(String parameterName) { - OAuth2AuthenticationValidator authenticationValidator = this.authenticationValidatorResolver.apply(parameterName); - return authenticationValidator != null ? - authenticationValidator : - DEFAULT_AUTHENTICATION_VALIDATOR_RESOLVER.apply(parameterName); - } - private Authentication authenticateAuthorizationConsent(Authentication authentication) throws AuthenticationException { OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication; @@ -414,13 +394,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen .build(); } - private static Function createDefaultAuthenticationValidatorResolver() { - Map authenticationValidators = new HashMap<>(); - authenticationValidators.put(OAuth2ParameterNames.REDIRECT_URI, new DefaultRedirectUriOAuth2AuthenticationValidator()); - authenticationValidators.put(OAuth2ParameterNames.SCOPE, new DefaultScopeOAuth2AuthenticationValidator()); - return authenticationValidators::get; - } - private static OAuth2Authorization.Builder authorizationBuilder(RegisteredClient registeredClient, Authentication principal, OAuth2AuthorizationRequest authorizationRequest) { return OAuth2Authorization.withRegisteredClient(registeredClient) @@ -505,7 +478,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen boolean redirectOnError = true; if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) && (parameterName.equals(OAuth2ParameterNames.CLIENT_ID) || - parameterName.equals(OAuth2ParameterNames.REDIRECT_URI) || parameterName.equals(OAuth2ParameterNames.STATE))) { redirectOnError = false; } @@ -520,14 +492,14 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen .redirectUri(redirectUri) .state(state) .build(); - authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated()); } else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) { authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication) .redirectUri(null) // Prevent redirects .build(); - authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated()); } + authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated()); + throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult); } @@ -569,124 +541,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen } - private static class DefaultRedirectUriOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator { - - @Override - public void validate(OAuth2AuthenticationContext authenticationContext) { - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - authenticationContext.getAuthentication(); - RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class); - - String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri(); - - if (StringUtils.hasText(requestedRedirectUri)) { - // ***** redirect_uri is available in authorization request - - UriComponents requestedRedirect = null; - try { - requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build(); - } catch (Exception ex) { } - if (requestedRedirect == null || requestedRedirect.getFragment() != null) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, - authorizationCodeRequestAuthentication, registeredClient); - } - - String requestedRedirectHost = requestedRedirect.getHost(); - if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) { - // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1 - // While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}") - // function similarly to loopback IP redirects described in Section 10.3.3, - // the use of "localhost" is NOT RECOMMENDED. - OAuth2Error error = new OAuth2Error( - OAuth2ErrorCodes.INVALID_REQUEST, - "localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " + - "Use the IP literal (127.0.0.1) instead.", - "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1"); - throwError(error, OAuth2ParameterNames.REDIRECT_URI, - authorizationCodeRequestAuthentication, registeredClient, null); - } - - if (!isLoopbackAddress(requestedRedirectHost)) { - // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7 - // When comparing client redirect URIs against pre-registered URIs, - // authorization servers MUST utilize exact string matching. - if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, - authorizationCodeRequestAuthentication, registeredClient); - } - } else { - // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-10.3.3 - // The authorization server MUST allow any port to be specified at the - // time of the request for loopback IP redirect URIs, to accommodate - // clients that obtain an available ephemeral port from the operating - // system at the time of the request. - boolean validRedirectUri = false; - for (String registeredRedirectUri : registeredClient.getRedirectUris()) { - UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri); - registeredRedirect.port(requestedRedirect.getPort()); - if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) { - validRedirectUri = true; - break; - } - } - if (!validRedirectUri) { - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, - authorizationCodeRequestAuthentication, registeredClient); - } - } - - } else { - // ***** redirect_uri is NOT available in authorization request - - if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) || - registeredClient.getRedirectUris().size() != 1) { - // redirect_uri is REQUIRED for OpenID Connect - throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, - authorizationCodeRequestAuthentication, registeredClient); - } - } - } - - private static boolean isLoopbackAddress(String host) { - // IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1" - if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) { - return true; - } - // IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255 - String[] ipv4Octets = host.split("\\."); - if (ipv4Octets.length != 4) { - return false; - } - try { - int[] address = new int[ipv4Octets.length]; - for (int i=0; i < ipv4Octets.length; i++) { - address[i] = Integer.parseInt(ipv4Octets[i]); - } - return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 && - address[2] <= 255 && address[3] >= 1 && address[3] <= 255; - } catch (NumberFormatException ex) { - return false; - } - } - - } - - private static class DefaultScopeOAuth2AuthenticationValidator implements OAuth2AuthenticationValidator { - - @Override - public void validate(OAuth2AuthenticationContext authenticationContext) { - OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = - authenticationContext.getAuthentication(); - RegisteredClient registeredClient = authenticationContext.get(RegisteredClient.class); - - Set requestedScopes = authorizationCodeRequestAuthentication.getScopes(); - Set allowedScopes = registeredClient.getScopes(); - if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) { - throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, - authorizationCodeRequestAuthentication, registeredClient); - } - } - - } - } diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java new file mode 100644 index 00000000..c19f6a97 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java @@ -0,0 +1,226 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.oauth2.server.authorization.authentication; + +import java.util.Set; +import java.util.function.Consumer; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * A {@code Consumer} providing access to the {@link OAuth2AuthorizationCodeRequestAuthenticationContext} + * containing an {@link OAuth2AuthorizationCodeRequestAuthenticationToken} + * and is the default {@link OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer) authentication validator} + * used for validating specific OAuth 2.0 Authorization Request parameters used in the Authorization Code Grant. + * + *

+ * The default implementation first validates {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()} + * and then {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}. + * If validation fails, an {@link OAuth2AuthorizationCodeRequestAuthenticationException} is thrown. + * + * @author Joe Grandja + * @since 0.4.0 + * @see OAuth2AuthorizationCodeRequestAuthenticationContext + * @see OAuth2AuthorizationCodeRequestAuthenticationToken + * @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer) + */ +public final class OAuth2AuthorizationCodeRequestAuthenticationValidator implements Consumer { + private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1"; + + /** + * The default validator for {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}. + */ + public static final Consumer DEFAULT_SCOPE_VALIDATOR = + OAuth2AuthorizationCodeRequestAuthenticationValidator::validateScope; + + /** + * The default validator for {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}. + */ + public static final Consumer DEFAULT_REDIRECT_URI_VALIDATOR = + OAuth2AuthorizationCodeRequestAuthenticationValidator::validateRedirectUri; + + private final Consumer authenticationValidator = + DEFAULT_REDIRECT_URI_VALIDATOR.andThen(DEFAULT_SCOPE_VALIDATOR); + + @Override + public void accept(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) { + this.authenticationValidator.accept(authenticationContext); + } + + private static void validateScope(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) { + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = + authenticationContext.getAuthentication(); + RegisteredClient registeredClient = authenticationContext.getRegisteredClient(); + + Set requestedScopes = authorizationCodeRequestAuthentication.getScopes(); + Set allowedScopes = registeredClient.getScopes(); + if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) { + throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, + authorizationCodeRequestAuthentication, registeredClient); + } + } + + private static void validateRedirectUri(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) { + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = + authenticationContext.getAuthentication(); + RegisteredClient registeredClient = authenticationContext.getRegisteredClient(); + + String requestedRedirectUri = authorizationCodeRequestAuthentication.getRedirectUri(); + + if (StringUtils.hasText(requestedRedirectUri)) { + // ***** redirect_uri is available in authorization request + + UriComponents requestedRedirect = null; + try { + requestedRedirect = UriComponentsBuilder.fromUriString(requestedRedirectUri).build(); + } catch (Exception ex) { } + if (requestedRedirect == null || requestedRedirect.getFragment() != null) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, + authorizationCodeRequestAuthentication, registeredClient); + } + + String requestedRedirectHost = requestedRedirect.getHost(); + if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) { + // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1 + // While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}") + // function similarly to loopback IP redirects described in Section 10.3.3, + // the use of "localhost" is NOT RECOMMENDED. + OAuth2Error error = new OAuth2Error( + OAuth2ErrorCodes.INVALID_REQUEST, + "localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " + + "Use the IP literal (127.0.0.1) instead.", + "https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7.1"); + throwError(error, OAuth2ParameterNames.REDIRECT_URI, + authorizationCodeRequestAuthentication, registeredClient); + } + + if (!isLoopbackAddress(requestedRedirectHost)) { + // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-9.7 + // When comparing client redirect URIs against pre-registered URIs, + // authorization servers MUST utilize exact string matching. + if (!registeredClient.getRedirectUris().contains(requestedRedirectUri)) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, + authorizationCodeRequestAuthentication, registeredClient); + } + } else { + // As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-01#section-10.3.3 + // The authorization server MUST allow any port to be specified at the + // time of the request for loopback IP redirect URIs, to accommodate + // clients that obtain an available ephemeral port from the operating + // system at the time of the request. + boolean validRedirectUri = false; + for (String registeredRedirectUri : registeredClient.getRedirectUris()) { + UriComponentsBuilder registeredRedirect = UriComponentsBuilder.fromUriString(registeredRedirectUri); + registeredRedirect.port(requestedRedirect.getPort()); + if (registeredRedirect.build().toString().equals(requestedRedirect.toString())) { + validRedirectUri = true; + break; + } + } + if (!validRedirectUri) { + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, + authorizationCodeRequestAuthentication, registeredClient); + } + } + + } else { + // ***** redirect_uri is NOT available in authorization request + + if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID) || + registeredClient.getRedirectUris().size() != 1) { + // redirect_uri is REQUIRED for OpenID Connect + throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, + authorizationCodeRequestAuthentication, registeredClient); + } + } + } + + private static boolean isLoopbackAddress(String host) { + // IPv6 loopback address should either be "0:0:0:0:0:0:0:1" or "::1" + if ("[0:0:0:0:0:0:0:1]".equals(host) || "[::1]".equals(host)) { + return true; + } + // IPv4 loopback address ranges from 127.0.0.1 to 127.255.255.255 + String[] ipv4Octets = host.split("\\."); + if (ipv4Octets.length != 4) { + return false; + } + try { + int[] address = new int[ipv4Octets.length]; + for (int i=0; i < ipv4Octets.length; i++) { + address[i] = Integer.parseInt(ipv4Octets[i]); + } + return address[0] == 127 && address[1] >= 0 && address[1] <= 255 && address[2] >= 0 && + address[2] <= 255 && address[3] >= 1 && address[3] <= 255; + } catch (NumberFormatException ex) { + return false; + } + } + + private static void throwError(String errorCode, String parameterName, + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication, + RegisteredClient registeredClient) { + OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI); + throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient); + } + + private static void throwError(OAuth2Error error, String parameterName, + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication, + RegisteredClient registeredClient) { + + boolean redirectOnError = true; + if (error.getErrorCode().equals(OAuth2ErrorCodes.INVALID_REQUEST) && + parameterName.equals(OAuth2ParameterNames.REDIRECT_URI)) { + redirectOnError = false; + } + + OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = authorizationCodeRequestAuthentication; + + if (redirectOnError && !StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) { + String redirectUri = registeredClient.getRedirectUris().iterator().next(); + authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication) + .redirectUri(redirectUri) + .build(); + } else if (!redirectOnError && StringUtils.hasText(authorizationCodeRequestAuthentication.getRedirectUri())) { + authorizationCodeRequestAuthenticationResult = from(authorizationCodeRequestAuthentication) + .redirectUri(null) // Prevent redirects + .build(); + } + + authorizationCodeRequestAuthenticationResult.setAuthenticated(authorizationCodeRequestAuthentication.isAuthenticated()); + + throw new OAuth2AuthorizationCodeRequestAuthenticationException(error, authorizationCodeRequestAuthenticationResult); + } + + private static OAuth2AuthorizationCodeRequestAuthenticationToken.Builder from(OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) { + return OAuth2AuthorizationCodeRequestAuthenticationToken.with(authorizationCodeRequestAuthentication.getClientId(), (Authentication) authorizationCodeRequestAuthentication.getPrincipal()) + .authorizationUri(authorizationCodeRequestAuthentication.getAuthorizationUri()) + .redirectUri(authorizationCodeRequestAuthentication.getRedirectUri()) + .scopes(authorizationCodeRequestAuthentication.getScopes()) + .state(authorizationCodeRequestAuthentication.getState()) + .additionalParameters(authorizationCodeRequestAuthentication.getAdditionalParameters()) + .authorizationCode(authorizationCodeRequestAuthentication.getAuthorizationCode()); + } + +} 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 f86d2283..acaa546d 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 @@ -22,7 +22,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import java.util.function.Function; import org.junit.Before; import org.junit.Test; @@ -60,7 +59,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -128,10 +126,10 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { } @Test - public void setAuthenticationValidatorResolverWhenNullThenThrowIllegalArgumentException() { - assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidatorResolver(null)) + public void setAuthenticationValidatorWhenNullThenThrowIllegalArgumentException() { + assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidator(null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("authenticationValidatorResolver cannot be null"); + .hasMessage("authenticationValidator cannot be null"); } @Test @@ -555,14 +553,14 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { } @Test - public void authenticateWhenCustomAuthenticationValidatorResolverThenUsed() { + public void authenticateWhenCustomAuthenticationValidatorThenUsed() { RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); when(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId()))) .thenReturn(registeredClient); @SuppressWarnings("unchecked") - Function authenticationValidatorResolver = mock(Function.class); - this.authenticationProvider.setAuthenticationValidatorResolver(authenticationValidatorResolver); + Consumer authenticationValidator = mock(Consumer.class); + this.authenticationProvider.setAuthenticationValidator(authenticationValidator); OAuth2AuthorizationCodeRequestAuthenticationToken authentication = authorizationCodeRequestAuthentication(registeredClient, this.principal) @@ -573,10 +571,7 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests { assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication, authenticationResult); - ArgumentCaptor parameterNameCaptor = ArgumentCaptor.forClass(String.class); - verify(authenticationValidatorResolver, times(2)).apply(parameterNameCaptor.capture()); - assertThat(parameterNameCaptor.getAllValues()).containsExactly( - OAuth2ParameterNames.REDIRECT_URI, OAuth2ParameterNames.SCOPE); + verify(authenticationValidator).accept(any()); } private void assertAuthorizationCodeRequestWithAuthorizationCodeResult(