authenticationValidator;
+
/**
* Constructs an {@code OAuth2ClientRegistrationAuthenticationProvider} using the
* provided parameters.
@@ -99,6 +94,7 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
this.clientRegistrationConverter = new RegisteredClientOAuth2ClientRegistrationConverter();
this.registeredClientConverter = new OAuth2ClientRegistrationRegisteredClientConverter();
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
+ this.authenticationValidator = new OAuth2ClientRegistrationAuthenticationValidator();
}
@Override
@@ -197,14 +193,35 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
this.openRegistrationAllowed = openRegistrationAllowed;
}
+ /**
+ * Sets the {@code Consumer} providing access to the
+ * {@link OAuth2ClientRegistrationAuthenticationContext} and is responsible for
+ * validating specific OAuth 2.0 Client Registration Request parameters associated in
+ * the {@link OAuth2ClientRegistrationAuthenticationToken}. The default authentication
+ * validator is {@link OAuth2ClientRegistrationAuthenticationValidator}.
+ *
+ *
+ * NOTE: The authentication validator MUST throw
+ * {@link OAuth2AuthenticationException} if validation fails.
+ * @param authenticationValidator the {@code Consumer} providing access to the
+ * {@link OAuth2ClientRegistrationAuthenticationContext} and is responsible for
+ * validating specific OAuth 2.0 Client Registration Request parameters
+ * @since 7.0.5
+ */
+ public void setAuthenticationValidator(
+ Consumer authenticationValidator) {
+ Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
+ this.authenticationValidator = authenticationValidator;
+ }
+
private OAuth2ClientRegistrationAuthenticationToken registerClient(
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication,
OAuth2Authorization authorization) {
- if (!isValidRedirectUris(clientRegistrationAuthentication.getClientRegistration().getRedirectUris())) {
- throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
- OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
- }
+ OAuth2ClientRegistrationAuthenticationContext authenticationContext = OAuth2ClientRegistrationAuthenticationContext
+ .with(clientRegistrationAuthentication)
+ .build();
+ this.authenticationValidator.accept(authenticationContext);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated client registration request parameters");
@@ -277,29 +294,4 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
}
}
- private static boolean isValidRedirectUris(List redirectUris) {
- if (CollectionUtils.isEmpty(redirectUris)) {
- return true;
- }
-
- for (String redirectUri : redirectUris) {
- try {
- URI validRedirectUri = new URI(redirectUri);
- if (validRedirectUri.getFragment() != null) {
- return false;
- }
- }
- catch (URISyntaxException ex) {
- return false;
- }
- }
-
- return true;
- }
-
- private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
- OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
- throw new OAuth2AuthenticationException(error);
- }
-
}
diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationValidator.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationValidator.java
new file mode 100644
index 0000000000..2fb62f469a
--- /dev/null
+++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientRegistrationAuthenticationValidator.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2004-present 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.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.log.LogMessage;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
+import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * A {@code Consumer} providing access to the
+ * {@link OAuth2ClientRegistrationAuthenticationContext} containing an
+ * {@link OAuth2ClientRegistrationAuthenticationToken} and is the default
+ * {@link OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
+ * authentication validator} used for validating specific OAuth 2.0 Dynamic Client
+ * Registration Request parameters (RFC 7591).
+ *
+ *
+ * The default implementation validates {@link OAuth2ClientRegistration#getRedirectUris()
+ * redirect_uris}, {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}, and
+ * {@link OAuth2ClientRegistration#getScopes() scope}. If validation fails, an
+ * {@link OAuth2AuthenticationException} is thrown.
+ *
+ *
+ * Each validated field is backed by two public constants:
+ *
+ * - {@code DEFAULT_*_VALIDATOR} — strict validation that rejects unsafe values. This is
+ * the default behavior and may reject input that was previously accepted.
+ * - {@code SIMPLE_*_VALIDATOR} — lenient validation preserving the behavior from prior
+ * releases. Use only when strictly required for backward compatibility and with full
+ * understanding that it may accept values that enable attacks against the authorization
+ * server.
+ *
+ *
+ * @author addcontent
+ * @since 7.0.5
+ * @see OAuth2ClientRegistrationAuthenticationContext
+ * @see OAuth2ClientRegistrationAuthenticationToken
+ * @see OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
+ */
+public final class OAuth2ClientRegistrationAuthenticationValidator
+ implements Consumer {
+
+ private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2";
+
+ private static final Log LOGGER = LogFactory.getLog(OAuth2ClientRegistrationAuthenticationValidator.class);
+
+ /**
+ * The default validator for {@link OAuth2ClientRegistration#getRedirectUris()
+ * redirect_uris}. Rejects URIs that contain a fragment, have no scheme (e.g.
+ * protocol-relative {@code //host/path}), or use an unsafe scheme
+ * ({@code javascript}, {@code data}, {@code vbscript}).
+ */
+ public static final Consumer DEFAULT_REDIRECT_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateRedirectUris;
+
+ /**
+ * The simple validator for {@link OAuth2ClientRegistration#getRedirectUris()
+ * redirect_uris} that preserves prior behavior (fragment-only check). Use only when
+ * backward compatibility is required; values that enable open redirect and XSS
+ * attacks may be accepted.
+ */
+ public static final Consumer SIMPLE_REDIRECT_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateRedirectUrisSimple;
+
+ /**
+ * The default validator for {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}.
+ * Rejects URIs that do not use the {@code https} scheme.
+ */
+ public static final Consumer DEFAULT_JWK_SET_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateJwkSetUri;
+
+ /**
+ * The simple validator for {@link OAuth2ClientRegistration#getJwkSetUrl() jwks_uri}
+ * that preserves prior behavior (no validation). Use only when backward compatibility
+ * is required; values that enable SSRF attacks may be accepted.
+ */
+ public static final Consumer SIMPLE_JWK_SET_URI_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateJwkSetUriSimple;
+
+ /**
+ * The default validator for {@link OAuth2ClientRegistration#getScopes() scope}.
+ * Rejects any request that includes a non-empty scope value. Deployers that need to
+ * accept scopes during Dynamic Client Registration must configure their own validator
+ * (for example, by chaining on top of {@link #SIMPLE_SCOPE_VALIDATOR}).
+ */
+ public static final Consumer DEFAULT_SCOPE_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateScope;
+
+ /**
+ * The simple validator for {@link OAuth2ClientRegistration#getScopes() scope} that
+ * preserves prior behavior (accepts any scope). Use only when backward compatibility
+ * is required; values that enable arbitrary scope injection may be accepted.
+ */
+ public static final Consumer SIMPLE_SCOPE_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateScopeSimple;
+
+ private final Consumer authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR
+ .andThen(DEFAULT_JWK_SET_URI_VALIDATOR)
+ .andThen(DEFAULT_SCOPE_VALIDATOR);
+
+ @Override
+ public void accept(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
+ this.authenticationValidator.accept(authenticationContext);
+ }
+
+ private static void validateRedirectUris(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
+ OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
+ .getAuthentication();
+ List redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
+ if (CollectionUtils.isEmpty(redirectUris)) {
+ return;
+ }
+ for (String redirectUri : redirectUris) {
+ URI parsed;
+ try {
+ parsed = new URI(redirectUri);
+ }
+ catch (URISyntaxException ex) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER
+ .debug(LogMessage.format("Invalid request: redirect_uri is not parseable ('%s')", redirectUri));
+ }
+ throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
+ OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
+ return;
+ }
+ if (parsed.getFragment() != null) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ LogMessage.format("Invalid request: redirect_uri contains a fragment ('%s')", redirectUri));
+ }
+ throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
+ OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
+ }
+ String scheme = parsed.getScheme();
+ if (scheme == null) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(LogMessage.format("Invalid request: redirect_uri has no scheme ('%s')", redirectUri));
+ }
+ throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
+ OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
+ }
+ if (isUnsafeScheme(scheme)) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ LogMessage.format("Invalid request: redirect_uri uses unsafe scheme ('%s')", redirectUri));
+ }
+ throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
+ OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
+ }
+ }
+ }
+
+ private static void validateRedirectUrisSimple(
+ OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
+ OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
+ .getAuthentication();
+ List redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
+ if (CollectionUtils.isEmpty(redirectUris)) {
+ return;
+ }
+ for (String redirectUri : redirectUris) {
+ try {
+ URI parsed = new URI(redirectUri);
+ if (parsed.getFragment() != null) {
+ throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
+ OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
+ }
+ }
+ catch (URISyntaxException ex) {
+ throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_REDIRECT_URI,
+ OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
+ }
+ }
+ }
+
+ private static void validateJwkSetUri(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
+ OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
+ .getAuthentication();
+ URL jwkSetUrl = clientRegistrationAuthentication.getClientRegistration().getJwkSetUrl();
+ if (jwkSetUrl == null) {
+ return;
+ }
+ if (!"https".equalsIgnoreCase(jwkSetUrl.getProtocol())) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(LogMessage.format("Invalid request: jwks_uri does not use https ('%s')", jwkSetUrl));
+ }
+ throwInvalidClientRegistration("invalid_client_metadata", OAuth2ClientMetadataClaimNames.JWKS_URI);
+ }
+ }
+
+ private static void validateJwkSetUriSimple(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
+ // No validation. Preserves prior behavior.
+ }
+
+ private static void validateScope(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
+ OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
+ .getAuthentication();
+ List scopes = clientRegistrationAuthentication.getClientRegistration().getScopes();
+ if (!CollectionUtils.isEmpty(scopes)) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(LogMessage.format(
+ "Invalid request: scope must not be set during Dynamic Client Registration ('%s')", scopes));
+ }
+ throwInvalidClientRegistration(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ClientMetadataClaimNames.SCOPE);
+ }
+ }
+
+ private static void validateScopeSimple(OAuth2ClientRegistrationAuthenticationContext authenticationContext) {
+ // No validation. Preserves prior behavior.
+ }
+
+ private static boolean isUnsafeScheme(String scheme) {
+ return "javascript".equalsIgnoreCase(scheme) || "data".equalsIgnoreCase(scheme)
+ || "vbscript".equalsIgnoreCase(scheme);
+ }
+
+ private static void throwInvalidClientRegistration(String errorCode, String fieldName) {
+ OAuth2Error error = new OAuth2Error(errorCode, "Invalid Client Registration: " + fieldName, ERROR_URI);
+ throw new OAuth2AuthenticationException(error);
+ }
+
+}
diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationContext.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationContext.java
new file mode 100644
index 0000000000..7027924025
--- /dev/null
+++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationContext.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2004-present 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.oidc.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.authentication.OAuth2AuthenticationContext;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link OAuth2AuthenticationContext} that holds an
+ * {@link OidcClientRegistrationAuthenticationToken} and additional information and is
+ * used when validating the OpenID Connect 1.0 Client Registration Request parameters.
+ *
+ * @author addcontent
+ * @since 7.0.5
+ * @see OAuth2AuthenticationContext
+ * @see OidcClientRegistrationAuthenticationToken
+ * @see OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
+ */
+public final class OidcClientRegistrationAuthenticationContext implements OAuth2AuthenticationContext {
+
+ private final Map