Merge branch '7.0.x' into main
This commit is contained in:
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OAuth2AuthenticationContext} that holds an
|
||||
* {@link OAuth2ClientRegistrationAuthenticationToken} and additional information and is
|
||||
* used when validating the OAuth 2.0 Client Registration Request parameters.
|
||||
*
|
||||
* @author addcontent
|
||||
* @since 7.0.5
|
||||
* @see OAuth2AuthenticationContext
|
||||
* @see OAuth2ClientRegistrationAuthenticationToken
|
||||
* @see OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OAuth2ClientRegistrationAuthenticationContext implements OAuth2AuthenticationContext {
|
||||
|
||||
private final Map<Object, Object> context;
|
||||
|
||||
private OAuth2ClientRegistrationAuthenticationContext(Map<Object, Object> context) {
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided
|
||||
* {@link OAuth2ClientRegistrationAuthenticationToken}.
|
||||
* @param authentication the {@link OAuth2ClientRegistrationAuthenticationToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(OAuth2ClientRegistrationAuthenticationToken authentication) {
|
||||
return new Builder(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OAuth2ClientRegistrationAuthenticationContext}.
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<OAuth2ClientRegistrationAuthenticationContext, Builder> {
|
||||
|
||||
private Builder(OAuth2ClientRegistrationAuthenticationToken authentication) {
|
||||
super(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OAuth2ClientRegistrationAuthenticationContext}.
|
||||
* @return the {@link OAuth2ClientRegistrationAuthenticationContext}
|
||||
*/
|
||||
@Override
|
||||
public OAuth2ClientRegistrationAuthenticationContext build() {
|
||||
return new OAuth2ClientRegistrationAuthenticationContext(getContext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+29
-37
@@ -16,13 +16,12 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -37,12 +36,10 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
||||
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.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
@@ -51,7 +48,6 @@ import org.springframework.security.oauth2.server.authorization.converter.OAuth2
|
||||
import org.springframework.security.oauth2.server.authorization.converter.RegisteredClientOAuth2ClientRegistrationConverter;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -69,8 +65,6 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public final class OAuth2ClientRegistrationAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7591#section-3.2.2";
|
||||
|
||||
private static final String DEFAULT_CLIENT_REGISTRATION_AUTHORIZED_SCOPE = "client.create";
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
@@ -87,6 +81,8 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
||||
|
||||
private boolean openRegistrationAllowed;
|
||||
|
||||
private Consumer<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2ClientRegistrationAuthenticationProvider} using the
|
||||
* provided parameters.
|
||||
@@ -101,6 +97,7 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
||||
this.clientRegistrationConverter = new RegisteredClientOAuth2ClientRegistrationConverter();
|
||||
this.registeredClientConverter = new OAuth2ClientRegistrationRegisteredClientConverter();
|
||||
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
this.authenticationValidator = new OAuth2ClientRegistrationAuthenticationValidator();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -200,15 +197,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}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> 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<OAuth2ClientRegistrationAuthenticationContext> authenticationValidator) {
|
||||
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
|
||||
this.authenticationValidator = authenticationValidator;
|
||||
}
|
||||
|
||||
private OAuth2ClientRegistrationAuthenticationToken registerClient(
|
||||
OAuth2ClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
||||
@Nullable OAuth2Authorization authorization) {
|
||||
|
||||
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||
if (!isValidRedirectUris((redirectUris != null) ? redirectUris : Collections.emptyList())) {
|
||||
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");
|
||||
@@ -284,29 +301,4 @@ public final class OAuth2ClientRegistrationAuthenticationProvider implements Aut
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidRedirectUris(List<String> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+244
@@ -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).
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* Each validated field is backed by two public constants:
|
||||
* <ul>
|
||||
* <li>{@code DEFAULT_*_VALIDATOR} — strict validation that rejects unsafe values. This is
|
||||
* the default behavior and may reject input that was previously accepted.</li>
|
||||
* <li>{@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.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author addcontent
|
||||
* @since 7.0.5
|
||||
* @see OAuth2ClientRegistrationAuthenticationContext
|
||||
* @see OAuth2ClientRegistrationAuthenticationToken
|
||||
* @see OAuth2ClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OAuth2ClientRegistrationAuthenticationValidator
|
||||
implements Consumer<OAuth2ClientRegistrationAuthenticationContext> {
|
||||
|
||||
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<OAuth2ClientRegistrationAuthenticationContext> 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<OAuth2ClientRegistrationAuthenticationContext> 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<OAuth2ClientRegistrationAuthenticationContext> 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<OAuth2ClientRegistrationAuthenticationContext> 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<OAuth2ClientRegistrationAuthenticationContext> 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<OAuth2ClientRegistrationAuthenticationContext> SIMPLE_SCOPE_VALIDATOR = OAuth2ClientRegistrationAuthenticationValidator::validateScopeSimple;
|
||||
|
||||
private final Consumer<OAuth2ClientRegistrationAuthenticationContext> 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<String> 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<String> 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<String> 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);
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -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<Object, Object> context;
|
||||
|
||||
private OidcClientRegistrationAuthenticationContext(Map<Object, Object> context) {
|
||||
this.context = Collections.unmodifiableMap(new HashMap<>(context));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
@Override
|
||||
public <V> V get(Object key) {
|
||||
return hasKey(key) ? (V) this.context.get(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasKey(Object key) {
|
||||
Assert.notNull(key, "key cannot be null");
|
||||
return this.context.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Builder} with the provided
|
||||
* {@link OidcClientRegistrationAuthenticationToken}.
|
||||
* @param authentication the {@link OidcClientRegistrationAuthenticationToken}
|
||||
* @return the {@link Builder}
|
||||
*/
|
||||
public static Builder with(OidcClientRegistrationAuthenticationToken authentication) {
|
||||
return new Builder(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link OidcClientRegistrationAuthenticationContext}.
|
||||
*/
|
||||
public static final class Builder extends AbstractBuilder<OidcClientRegistrationAuthenticationContext, Builder> {
|
||||
|
||||
private Builder(OidcClientRegistrationAuthenticationToken authentication) {
|
||||
super(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link OidcClientRegistrationAuthenticationContext}.
|
||||
* @return the {@link OidcClientRegistrationAuthenticationContext}
|
||||
*/
|
||||
@Override
|
||||
public OidcClientRegistrationAuthenticationContext build() {
|
||||
return new OidcClientRegistrationAuthenticationContext(getContext());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+29
-30
@@ -16,14 +16,12 @@
|
||||
|
||||
package org.springframework.security.oauth2.server.authorization.oidc.authentication;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -61,7 +59,6 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -103,6 +100,8 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
private Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OidcClientRegistrationAuthenticationProvider} using the
|
||||
* provided parameters.
|
||||
@@ -122,6 +121,7 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
this.clientRegistrationConverter = new RegisteredClientOidcClientRegistrationConverter();
|
||||
this.registeredClientConverter = new OidcClientRegistrationRegisteredClientConverter();
|
||||
this.passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||
this.authenticationValidator = new OidcClientRegistrationAuthenticationValidator();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -208,6 +208,27 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@code Consumer} providing access to the
|
||||
* {@link OidcClientRegistrationAuthenticationContext} and is responsible for
|
||||
* validating specific OpenID Connect 1.0 Client Registration Request parameters
|
||||
* associated in the {@link OidcClientRegistrationAuthenticationToken}. The default
|
||||
* authentication validator is {@link OidcClientRegistrationAuthenticationValidator}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The authentication validator MUST throw
|
||||
* {@link OAuth2AuthenticationException} if validation fails.
|
||||
* @param authenticationValidator the {@code Consumer} providing access to the
|
||||
* {@link OidcClientRegistrationAuthenticationContext} and is responsible for
|
||||
* validating specific OpenID Connect 1.0 Client Registration Request parameters
|
||||
* @since 7.0.5
|
||||
*/
|
||||
public void setAuthenticationValidator(
|
||||
Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator) {
|
||||
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
|
||||
this.authenticationValidator = authenticationValidator;
|
||||
}
|
||||
|
||||
private OidcClientRegistrationAuthenticationToken registerClient(
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication,
|
||||
OAuth2Authorization authorization) {
|
||||
@@ -222,12 +243,10 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
List<String> postLogoutRedirectUris = (clientRegistrationRequest.getPostLogoutRedirectUris() != null)
|
||||
? clientRegistrationRequest.getPostLogoutRedirectUris() : Collections.emptyList();
|
||||
if (!isValidRedirectUris(postLogoutRedirectUris)) {
|
||||
throwInvalidClientRegistration("invalid_client_metadata",
|
||||
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||
}
|
||||
OidcClientRegistrationAuthenticationContext authenticationContext = OidcClientRegistrationAuthenticationContext
|
||||
.with(clientRegistrationAuthentication)
|
||||
.build();
|
||||
this.authenticationValidator.accept(authenticationContext);
|
||||
|
||||
if (!isValidTokenEndpointAuthenticationMethod(clientRegistrationRequest)) {
|
||||
throwInvalidClientRegistration("invalid_client_metadata",
|
||||
@@ -364,26 +383,6 @@ public final class OidcClientRegistrationAuthenticationProvider implements Authe
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidRedirectUris(List<String> 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 boolean isValidTokenEndpointAuthenticationMethod(OidcClientRegistration clientRegistration) {
|
||||
String authenticationMethod = clientRegistration.getTokenEndpointAuthenticationMethod();
|
||||
String authenticationSigningAlgorithm = clientRegistration.getTokenEndpointAuthenticationSigningAlgorithm();
|
||||
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* 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.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.oidc.OidcClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A {@code Consumer} providing access to the
|
||||
* {@link OidcClientRegistrationAuthenticationContext} containing an
|
||||
* {@link OidcClientRegistrationAuthenticationToken} and is the default
|
||||
* {@link OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
* authentication validator} used for validating specific OpenID Connect 1.0 Dynamic
|
||||
* Client Registration Request parameters.
|
||||
*
|
||||
* <p>
|
||||
* The default implementation validates {@link OidcClientRegistration#getRedirectUris()
|
||||
* redirect_uris}, {@link OidcClientRegistration#getPostLogoutRedirectUris()
|
||||
* post_logout_redirect_uris}, {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}, and
|
||||
* {@link OidcClientRegistration#getScopes() scope}. If validation fails, an
|
||||
* {@link OAuth2AuthenticationException} is thrown.
|
||||
*
|
||||
* <p>
|
||||
* Each validated field is backed by two public constants:
|
||||
* <ul>
|
||||
* <li>{@code DEFAULT_*_VALIDATOR} — strict validation that rejects unsafe values. This is
|
||||
* the default behavior and may reject input that was previously accepted.</li>
|
||||
* <li>{@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.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author addcontent
|
||||
* @since 7.0.5
|
||||
* @see OidcClientRegistrationAuthenticationContext
|
||||
* @see OidcClientRegistrationAuthenticationToken
|
||||
* @see OidcClientRegistrationAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OidcClientRegistrationAuthenticationValidator
|
||||
implements Consumer<OidcClientRegistrationAuthenticationContext> {
|
||||
|
||||
private static final String ERROR_URI = "https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationError";
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(OidcClientRegistrationAuthenticationValidator.class);
|
||||
|
||||
/**
|
||||
* The default validator for {@link OidcClientRegistration#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<OidcClientRegistrationAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateRedirectUris;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OidcClientRegistration#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<OidcClientRegistrationAuthenticationContext> SIMPLE_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateRedirectUrisSimple;
|
||||
|
||||
/**
|
||||
* The default validator for {@link OidcClientRegistration#getPostLogoutRedirectUris()
|
||||
* post_logout_redirect_uris}. Applies the same rules as
|
||||
* {@link #DEFAULT_REDIRECT_URI_VALIDATOR}.
|
||||
*/
|
||||
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validatePostLogoutRedirectUris;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OidcClientRegistration#getPostLogoutRedirectUris()
|
||||
* post_logout_redirect_uris} that preserves prior behavior (fragment-only check). Use
|
||||
* only when backward compatibility is required; values that enable XSS attacks on the
|
||||
* authorization server origin may be accepted.
|
||||
*/
|
||||
public static final Consumer<OidcClientRegistrationAuthenticationContext> SIMPLE_POST_LOGOUT_REDIRECT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validatePostLogoutRedirectUrisSimple;
|
||||
|
||||
/**
|
||||
* The default validator for {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}.
|
||||
* Rejects URIs that do not use the {@code https} scheme.
|
||||
*/
|
||||
public static final Consumer<OidcClientRegistrationAuthenticationContext> DEFAULT_JWK_SET_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateJwkSetUri;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OidcClientRegistration#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<OidcClientRegistrationAuthenticationContext> SIMPLE_JWK_SET_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateJwkSetUriSimple;
|
||||
|
||||
/**
|
||||
* The default validator for {@link OidcClientRegistration#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<OidcClientRegistrationAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateScope;
|
||||
|
||||
/**
|
||||
* The simple validator for {@link OidcClientRegistration#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<OidcClientRegistrationAuthenticationContext> SIMPLE_SCOPE_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateScopeSimple;
|
||||
|
||||
private final Consumer<OidcClientRegistrationAuthenticationContext> authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||
.andThen(DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(DEFAULT_SCOPE_VALIDATOR);
|
||||
|
||||
@Override
|
||||
public void accept(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
this.authenticationValidator.accept(authenticationContext);
|
||||
}
|
||||
|
||||
private static void validateRedirectUris(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||
validateRedirectUrisStrict(redirectUris, OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
private static void validatePostLogoutRedirectUris(
|
||||
OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> postLogoutRedirectUris = clientRegistrationAuthentication.getClientRegistration()
|
||||
.getPostLogoutRedirectUris();
|
||||
validateRedirectUrisStrict(postLogoutRedirectUris, "invalid_client_metadata",
|
||||
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||
}
|
||||
|
||||
private static void validateRedirectUrisStrict(List<String> redirectUris, String errorCode, String fieldName) {
|
||||
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: %s is not parseable ('%s')", fieldName, redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
return;
|
||||
}
|
||||
if (parsed.getFragment() != null) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(LogMessage.format("Invalid request: %s contains a fragment ('%s')", fieldName,
|
||||
redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
}
|
||||
String scheme = parsed.getScheme();
|
||||
if (scheme == null) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(LogMessage.format("Invalid request: %s has no scheme ('%s')", fieldName, redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
}
|
||||
if (isUnsafeScheme(scheme)) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(
|
||||
LogMessage.format("Invalid request: %s uses unsafe scheme ('%s')", fieldName, redirectUri));
|
||||
}
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateRedirectUrisSimple(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> redirectUris = clientRegistrationAuthentication.getClientRegistration().getRedirectUris();
|
||||
validateRedirectUrisFragmentOnly(redirectUris, OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
private static void validatePostLogoutRedirectUrisSimple(
|
||||
OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> postLogoutRedirectUris = clientRegistrationAuthentication.getClientRegistration()
|
||||
.getPostLogoutRedirectUris();
|
||||
validateRedirectUrisFragmentOnly(postLogoutRedirectUris, "invalid_client_metadata",
|
||||
OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||
}
|
||||
|
||||
private static void validateRedirectUrisFragmentOnly(List<String> redirectUris, String errorCode,
|
||||
String fieldName) {
|
||||
if (CollectionUtils.isEmpty(redirectUris)) {
|
||||
return;
|
||||
}
|
||||
for (String redirectUri : redirectUris) {
|
||||
try {
|
||||
URI parsed = new URI(redirectUri);
|
||||
if (parsed.getFragment() != null) {
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
}
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throwInvalidClientRegistration(errorCode, fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateJwkSetUri(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken 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", OidcClientMetadataClaimNames.JWKS_URI);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateJwkSetUriSimple(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
// No validation. Preserves prior behavior.
|
||||
}
|
||||
|
||||
private static void validateScope(OidcClientRegistrationAuthenticationContext authenticationContext) {
|
||||
OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
List<String> 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, OidcClientMetadataClaimNames.SCOPE);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateScopeSimple(OidcClientRegistrationAuthenticationContext 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);
|
||||
}
|
||||
|
||||
}
|
||||
+9
@@ -360,6 +360,11 @@ public class OAuth2ClientRegistrationAuthenticationProviderTests {
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
|
||||
this.authenticationProvider
|
||||
.setAuthenticationValidator(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
@@ -412,6 +417,10 @@ public class OAuth2ClientRegistrationAuthenticationProviderTests {
|
||||
@Test
|
||||
public void authenticateWhenOpenRegistrationThenReturnClientRegistration() {
|
||||
this.authenticationProvider.setOpenRegistrationAllowed(true);
|
||||
this.authenticationProvider
|
||||
.setAuthenticationValidator(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||
|
||||
// @formatter:off
|
||||
OAuth2ClientRegistration clientRegistration = OAuth2ClientRegistration.builder()
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2ClientRegistration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2ClientRegistrationAuthenticationValidator}.
|
||||
*
|
||||
* @author addcontent
|
||||
*/
|
||||
public class OAuth2ClientRegistrationAuthenticationValidatorTests {
|
||||
|
||||
private final OAuth2ClientRegistrationAuthenticationValidator validator = new OAuth2ClientRegistrationAuthenticationValidator();
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenProtocolRelativeThenRejected() {
|
||||
assertRejected(context("//client.example.com/path", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
|
||||
assertRejected(context("javascript:alert(document.cookie)", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenDataSchemeThenRejected() {
|
||||
assertRejected(context("data:text/html,<h1>content</h1>", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenVbscriptSchemeThenRejected() {
|
||||
assertRejected(context("vbscript:msgbox(\"content\")", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenFragmentThenRejected() {
|
||||
assertRejected(context("https://client.example.com/cb#fragment", null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OAuth2ClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenHttpsThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenCustomSchemeForNativeAppThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator.accept(context("myapp://callback", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenHttpLoopbackThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator.accept(context("http://127.0.0.1:8080", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJwkSetUriValidatorWhenHttpThenRejected() {
|
||||
assertRejected(context("https://client.example.com", "http://169.254.169.254/keys"), "invalid_client_metadata",
|
||||
OAuth2ClientMetadataClaimNames.JWKS_URI);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJwkSetUriValidatorWhenHttpsThenAccepted() {
|
||||
assertThatNoException().isThrownBy(
|
||||
() -> this.validator.accept(context("https://client.example.com", "https://client.example.com/jwks")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJwkSetUriValidatorWhenAbsentThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultScopeValidatorWhenNonEmptyThenRejected() {
|
||||
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
|
||||
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
|
||||
OAuth2ClientRegistration.builder()
|
||||
.redirectUri("https://client.example.com")
|
||||
.scope("write")
|
||||
.build()))
|
||||
.build();
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||
.extracting(OAuth2AuthenticationException::getError)
|
||||
.satisfies((error) -> assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultScopeValidatorWhenEmptyThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator.accept(context("https://client.example.com", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRedirectUriValidatorWhenProtocolRelativeThenAccepted() {
|
||||
OAuth2ClientRegistrationAuthenticationContext context = context("//client.example.com/path", null);
|
||||
assertThatNoException().isThrownBy(
|
||||
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRedirectUriValidatorWhenJavascriptThenAccepted() {
|
||||
OAuth2ClientRegistrationAuthenticationContext context = context("javascript:alert(document.cookie)", null);
|
||||
assertThatNoException().isThrownBy(
|
||||
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleJwkSetUriValidatorWhenHttpThenAccepted() {
|
||||
OAuth2ClientRegistrationAuthenticationContext context = context("https://client.example.com",
|
||||
"http://169.254.169.254/keys");
|
||||
assertThatNoException().isThrownBy(
|
||||
() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_JWK_SET_URI_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleScopeValidatorWhenNonEmptyThenAccepted() {
|
||||
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
|
||||
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
|
||||
OAuth2ClientRegistration.builder()
|
||||
.redirectUri("https://client.example.com")
|
||||
.scope("write")
|
||||
.build()))
|
||||
.build();
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void composedValidatorWhenDefaultUrisAndSimpleScopeThenAcceptsLegitimateRequest() {
|
||||
Consumer<OAuth2ClientRegistrationAuthenticationContext> composed = OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OAuth2ClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR);
|
||||
OAuth2ClientRegistrationAuthenticationContext context = OAuth2ClientRegistrationAuthenticationContext
|
||||
.with(new OAuth2ClientRegistrationAuthenticationToken(null,
|
||||
OAuth2ClientRegistration.builder()
|
||||
.redirectUri("https://client.example.com")
|
||||
.jwkSetUrl("https://client.example.com/jwks")
|
||||
.scope("openid")
|
||||
.scope("profile")
|
||||
.build()))
|
||||
.build();
|
||||
assertThatNoException().isThrownBy(() -> composed.accept(context));
|
||||
}
|
||||
|
||||
private static OAuth2ClientRegistrationAuthenticationContext context(String redirectUri, String jwkSetUrl) {
|
||||
OAuth2ClientRegistration.Builder builder = OAuth2ClientRegistration.builder();
|
||||
if (redirectUri != null) {
|
||||
builder.redirectUri(redirectUri);
|
||||
}
|
||||
if (jwkSetUrl != null) {
|
||||
builder.jwkSetUrl(jwkSetUrl);
|
||||
}
|
||||
return OAuth2ClientRegistrationAuthenticationContext
|
||||
.with(new OAuth2ClientRegistrationAuthenticationToken(null, builder.build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private void assertRejected(OAuth2ClientRegistrationAuthenticationContext context, String errorCode,
|
||||
String fieldName) {
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||
.extracting(OAuth2AuthenticationException::getError)
|
||||
.satisfies((error) -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(errorCode);
|
||||
assertThat(error.getDescription()).contains(fieldName);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+15
@@ -561,6 +561,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
|
||||
@Test
|
||||
public void authenticateWhenTokenEndpointAuthenticationSigningAlgorithmNotProvidedThenDefaults() {
|
||||
this.authenticationProvider
|
||||
.setAuthenticationValidator(OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
@@ -612,6 +617,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
|
||||
@Test
|
||||
public void authenticateWhenRegistrationAccessTokenNotGeneratedThenThrowOAuth2AuthenticationException() {
|
||||
this.authenticationProvider
|
||||
.setAuthenticationValidator(OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
@@ -653,6 +663,11 @@ public class OidcClientRegistrationAuthenticationProviderTests {
|
||||
|
||||
@Test
|
||||
public void authenticateWhenValidAccessTokenThenReturnClientRegistration() {
|
||||
this.authenticationProvider
|
||||
.setAuthenticationValidator(OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR));
|
||||
Jwt jwt = createJwtClientRegistration();
|
||||
OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaim(OAuth2ParameterNames.SCOPE));
|
||||
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientMetadataClaimNames;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.OidcClientRegistration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link OidcClientRegistrationAuthenticationValidator}.
|
||||
*
|
||||
* @author addcontent
|
||||
*/
|
||||
public class OidcClientRegistrationAuthenticationValidatorTests {
|
||||
|
||||
private final OidcClientRegistrationAuthenticationValidator validator = new OidcClientRegistrationAuthenticationValidator();
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenProtocolRelativeThenRejected() {
|
||||
assertRejected(context("//client.example.com/path", null, null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
|
||||
assertRejected(context("javascript:alert(document.cookie)", null, null), OAuth2ErrorCodes.INVALID_REDIRECT_URI,
|
||||
OidcClientMetadataClaimNames.REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultRedirectUriValidatorWhenHttpsThenAccepted() {
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> this.validator.accept(context("https://client.example.com", null, null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPostLogoutRedirectUriValidatorWhenJavascriptSchemeThenRejected() {
|
||||
assertRejected(context("https://client.example.com", "javascript:alert(document.cookie)", null),
|
||||
"invalid_client_metadata", OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPostLogoutRedirectUriValidatorWhenProtocolRelativeThenRejected() {
|
||||
assertRejected(context("https://client.example.com", "//client.example.com/post-logout", null),
|
||||
"invalid_client_metadata", OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultPostLogoutRedirectUriValidatorWhenHttpsThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator
|
||||
.accept(context("https://client.example.com", "https://client.example.com/post-logout", null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJwkSetUriValidatorWhenHttpThenRejected() {
|
||||
assertRejected(context("https://client.example.com", null, "http://169.254.169.254/keys"),
|
||||
"invalid_client_metadata", OidcClientMetadataClaimNames.JWKS_URI);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultJwkSetUriValidatorWhenHttpsThenAccepted() {
|
||||
assertThatNoException().isThrownBy(() -> this.validator
|
||||
.accept(context("https://client.example.com", null, "https://client.example.com/jwks")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultScopeValidatorWhenNonEmptyThenRejected() {
|
||||
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
|
||||
.with(new OidcClientRegistrationAuthenticationToken(principal(),
|
||||
OidcClientRegistration.builder().redirectUri("https://client.example.com").scope("write").build()))
|
||||
.build();
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||
.extracting(OAuth2AuthenticationException::getError)
|
||||
.satisfies((error) -> assertThat(error.getErrorCode()).isEqualTo(OAuth2ErrorCodes.INVALID_SCOPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleRedirectUriValidatorWhenJavascriptThenAccepted() {
|
||||
OidcClientRegistrationAuthenticationContext context = context("javascript:alert(document.cookie)", null, null);
|
||||
assertThatNoException().isThrownBy(
|
||||
() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_REDIRECT_URI_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simplePostLogoutRedirectUriValidatorWhenJavascriptThenAccepted() {
|
||||
OidcClientRegistrationAuthenticationContext context = context("https://client.example.com",
|
||||
"javascript:alert(document.cookie)", null);
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_POST_LOGOUT_REDIRECT_URI_VALIDATOR
|
||||
.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleJwkSetUriValidatorWhenHttpThenAccepted() {
|
||||
OidcClientRegistrationAuthenticationContext ctx = context("https://client.example.com", null,
|
||||
"http://169.254.169.254/keys");
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_JWK_SET_URI_VALIDATOR.accept(ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleScopeValidatorWhenNonEmptyThenAccepted() {
|
||||
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
|
||||
.with(new OidcClientRegistrationAuthenticationToken(principal(),
|
||||
OidcClientRegistration.builder().redirectUri("https://client.example.com").scope("write").build()))
|
||||
.build();
|
||||
assertThatNoException()
|
||||
.isThrownBy(() -> OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR.accept(context));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void composedValidatorWhenDefaultUrisAndSimpleScopeThenAcceptsLegitimateRequest() {
|
||||
Consumer<OidcClientRegistrationAuthenticationContext> composed = OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR)
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR)
|
||||
.andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR);
|
||||
OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext
|
||||
.with(new OidcClientRegistrationAuthenticationToken(principal(),
|
||||
OidcClientRegistration.builder()
|
||||
.redirectUri("https://client.example.com")
|
||||
.postLogoutRedirectUri("https://client.example.com/post-logout")
|
||||
.jwkSetUrl("https://client.example.com/jwks")
|
||||
.scope("openid")
|
||||
.scope("profile")
|
||||
.build()))
|
||||
.build();
|
||||
assertThatNoException().isThrownBy(() -> composed.accept(context));
|
||||
}
|
||||
|
||||
private static Authentication principal() {
|
||||
TestingAuthenticationToken principal = new TestingAuthenticationToken("user", "password");
|
||||
principal.setAuthenticated(true);
|
||||
return principal;
|
||||
}
|
||||
|
||||
private static OidcClientRegistrationAuthenticationContext context(String redirectUri, String postLogoutRedirectUri,
|
||||
String jwkSetUrl) {
|
||||
OidcClientRegistration.Builder builder = OidcClientRegistration.builder();
|
||||
if (redirectUri != null) {
|
||||
builder.redirectUri(redirectUri);
|
||||
}
|
||||
if (postLogoutRedirectUri != null) {
|
||||
builder.postLogoutRedirectUri(postLogoutRedirectUri);
|
||||
}
|
||||
if (jwkSetUrl != null) {
|
||||
builder.jwkSetUrl(jwkSetUrl);
|
||||
}
|
||||
return OidcClientRegistrationAuthenticationContext
|
||||
.with(new OidcClientRegistrationAuthenticationToken(principal(), builder.build()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private void assertRejected(OidcClientRegistrationAuthenticationContext context, String errorCode,
|
||||
String fieldName) {
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context))
|
||||
.extracting(OAuth2AuthenticationException::getError)
|
||||
.satisfies((error) -> {
|
||||
assertThat(error.getErrorCode()).isEqualTo(errorCode);
|
||||
assertThat(error.getDescription()).contains(fieldName);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+13
-2
@@ -235,7 +235,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
Object jwksUri = configuration.get("jwks_uri");
|
||||
Assert.notNull(jwksUri, "The public JWK Set URI must not be null");
|
||||
return jwksUri.toString();
|
||||
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
|
||||
}, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
|
||||
.validator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,6 +305,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
|
||||
private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer;
|
||||
|
||||
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
|
||||
|
||||
private JwkSetUriJwtDecoderBuilder(String jwkSetUri) {
|
||||
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
|
||||
this.jwkSetUri = (rest) -> jwkSetUri;
|
||||
@@ -444,6 +447,12 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
|
||||
Assert.notNull(validator, "validator cannot be null");
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) {
|
||||
if (this.signatureAlgorithms.isEmpty()) {
|
||||
return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource);
|
||||
@@ -482,7 +491,9 @@ public final class NimbusJwtDecoder implements JwtDecoder {
|
||||
* @return the configured {@link NimbusJwtDecoder}
|
||||
*/
|
||||
public NimbusJwtDecoder build() {
|
||||
return new NimbusJwtDecoder(processor());
|
||||
NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor());
|
||||
decoder.setJwtValidator(this.validator);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
private static final class SpringJWKSource<C extends SecurityContext> implements JWKSetSource<C> {
|
||||
|
||||
+12
-2
@@ -244,7 +244,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
Assert.notNull(jwksUri, "The public JWK Set URI must not be null");
|
||||
return Mono.just(jwksUri.toString());
|
||||
}),
|
||||
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms);
|
||||
ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms)
|
||||
.validator(JwtValidators.createDefaultWithIssuer(issuer));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,6 +336,8 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
|
||||
private BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer;
|
||||
|
||||
private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault();
|
||||
|
||||
private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) {
|
||||
Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty");
|
||||
this.jwkSetUri = (web) -> Mono.just(jwkSetUri);
|
||||
@@ -459,6 +462,11 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriReactiveJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) {
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
JwkSetUriReactiveJwtDecoderBuilder jwtProcessorCustomizer(
|
||||
BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer) {
|
||||
Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null");
|
||||
@@ -471,7 +479,9 @@ public final class NimbusReactiveJwtDecoder implements ReactiveJwtDecoder {
|
||||
* @return the configured {@link NimbusReactiveJwtDecoder}
|
||||
*/
|
||||
public NimbusReactiveJwtDecoder build() {
|
||||
return new NimbusReactiveJwtDecoder(processor());
|
||||
NimbusReactiveJwtDecoder decoder = new NimbusReactiveJwtDecoder(processor());
|
||||
decoder.setJwtValidator(this.validator);
|
||||
return decoder;
|
||||
}
|
||||
|
||||
Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector(ReactiveRemoteJWKSource source) {
|
||||
|
||||
+16
-1
@@ -332,7 +332,10 @@ public class NimbusJwtDecoderTests {
|
||||
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
|
||||
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
|
||||
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer)
|
||||
.restOperations(restOperations)
|
||||
.build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
|
||||
Jwt jwt = jwtDecoder.decode(SIGNED_JWT);
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
}
|
||||
@@ -350,6 +353,18 @@ public class NimbusJwtDecoderTests {
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
|
||||
String issuer = "https://example.org/wrong-issuer";
|
||||
RestOperations restOperations = mock(RestOperations.class);
|
||||
given(restOperations.exchange(any(RequestEntity.class), any(ParameterizedTypeReference.class)))
|
||||
.willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK));
|
||||
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
|
||||
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
|
||||
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build();
|
||||
assertThatExceptionOfType(JwtValidationException.class).isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withJwkSetUriWhenNullOrEmptyThenThrowsException() {
|
||||
// @formatter:off
|
||||
|
||||
+22
-2
@@ -617,11 +617,31 @@ public class NimbusReactiveJwtDecoderTests {
|
||||
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
|
||||
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
|
||||
given(spec.retrieve()).willReturn(responseSpec);
|
||||
NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
|
||||
.webClient(webClient)
|
||||
.build();
|
||||
jwtDecoder.setJwtValidator(JwtValidators.createDefault());
|
||||
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() {
|
||||
String issuer = "https://example.org/wrong-issuer";
|
||||
WebClient real = WebClient.builder().build();
|
||||
WebClient.RequestHeadersUriSpec spec = spy(real.get());
|
||||
WebClient webClient = spy(WebClient.class);
|
||||
given(webClient.get()).willReturn(spec);
|
||||
WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);
|
||||
given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet));
|
||||
given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class)))
|
||||
.willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks")));
|
||||
given(spec.retrieve()).willReturn(responseSpec);
|
||||
ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer)
|
||||
.webClient(webClient)
|
||||
.build();
|
||||
Jwt jwt = jwtDecoder.decode(this.messageReadToken).block();
|
||||
assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull();
|
||||
assertThatExceptionOfType(JwtValidationException.class)
|
||||
.isThrownBy(() -> jwtDecoder.decode(this.messageReadToken).block());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user