Browse Source
Closes gh-210 Signed-off-by: Joe Grandja <10884212+jgrandja@users.noreply.github.com>pull/1927/head
24 changed files with 2481 additions and 214 deletions
@ -0,0 +1,135 @@
@@ -0,0 +1,135 @@
|
||||
/* |
||||
* Copyright 2020-2025 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.HashSet; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* An {@link Authentication} base implementation for the OAuth 2.0 Authorization Request |
||||
* used in the Authorization Code Grant. |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 1.5 |
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationToken |
||||
* @see OAuth2PushedAuthorizationRequestAuthenticationToken |
||||
*/ |
||||
abstract class AbstractOAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken { |
||||
|
||||
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; |
||||
|
||||
private final String authorizationUri; |
||||
|
||||
private final String clientId; |
||||
|
||||
private final Authentication principal; |
||||
|
||||
private final String redirectUri; |
||||
|
||||
private final String state; |
||||
|
||||
private final Set<String> scopes; |
||||
|
||||
private final Map<String, Object> additionalParameters; |
||||
|
||||
protected AbstractOAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId, |
||||
Authentication principal, @Nullable String redirectUri, @Nullable String state, |
||||
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) { |
||||
super(Collections.emptyList()); |
||||
Assert.hasText(authorizationUri, "authorizationUri cannot be empty"); |
||||
Assert.hasText(clientId, "clientId cannot be empty"); |
||||
Assert.notNull(principal, "principal cannot be null"); |
||||
this.authorizationUri = authorizationUri; |
||||
this.clientId = clientId; |
||||
this.principal = principal; |
||||
this.redirectUri = redirectUri; |
||||
this.state = state; |
||||
this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet()); |
||||
this.additionalParameters = Collections.unmodifiableMap( |
||||
(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap()); |
||||
} |
||||
|
||||
@Override |
||||
public Object getPrincipal() { |
||||
return this.principal; |
||||
} |
||||
|
||||
@Override |
||||
public Object getCredentials() { |
||||
return ""; |
||||
} |
||||
|
||||
/** |
||||
* Returns the authorization URI. |
||||
* @return the authorization URI |
||||
*/ |
||||
public String getAuthorizationUri() { |
||||
return this.authorizationUri; |
||||
} |
||||
|
||||
/** |
||||
* Returns the client identifier. |
||||
* @return the client identifier |
||||
*/ |
||||
public String getClientId() { |
||||
return this.clientId; |
||||
} |
||||
|
||||
/** |
||||
* Returns the redirect uri. |
||||
* @return the redirect uri |
||||
*/ |
||||
@Nullable |
||||
public String getRedirectUri() { |
||||
return this.redirectUri; |
||||
} |
||||
|
||||
/** |
||||
* Returns the state. |
||||
* @return the state |
||||
*/ |
||||
@Nullable |
||||
public String getState() { |
||||
return this.state; |
||||
} |
||||
|
||||
/** |
||||
* Returns the requested (or authorized) scope(s). |
||||
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not |
||||
* available |
||||
*/ |
||||
public Set<String> getScopes() { |
||||
return this.scopes; |
||||
} |
||||
|
||||
/** |
||||
* Returns the additional parameters. |
||||
* @return the additional parameters, or an empty {@code Map} if not available |
||||
*/ |
||||
public Map<String, Object> getAdditionalParameters() { |
||||
return this.additionalParameters; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,184 @@
@@ -0,0 +1,184 @@
|
||||
/* |
||||
* Copyright 2020-2025 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.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType; |
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; |
||||
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.client.RegisteredClient; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Pushed Authorization |
||||
* Request used in the Authorization Code Grant. |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 1.5 |
||||
* @see OAuth2PushedAuthorizationRequestAuthenticationToken |
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationToken |
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationValidator |
||||
* @see OAuth2AuthorizationService |
||||
* @see <a target="_blank" href= |
||||
* "https://datatracker.ietf.org/doc/html/rfc9126#section-2.1">Section 2.1 Pushed |
||||
* Authorization Request</a> |
||||
* @see <a target="_blank" href= |
||||
* "https://datatracker.ietf.org/doc/html/rfc9126#section-2.2">Section 2.2 Pushed |
||||
* Authorization Response</a> |
||||
*/ |
||||
public final class OAuth2PushedAuthorizationRequestAuthenticationProvider implements AuthenticationProvider { |
||||
|
||||
private final Log logger = LogFactory.getLog(getClass()); |
||||
|
||||
private final OAuth2AuthorizationService authorizationService; |
||||
|
||||
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = new OAuth2AuthorizationCodeRequestAuthenticationValidator(); |
||||
|
||||
/** |
||||
* Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationProvider} using |
||||
* the provided parameters. |
||||
* @param authorizationService the authorization service |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestAuthenticationProvider(OAuth2AuthorizationService authorizationService) { |
||||
Assert.notNull(authorizationService, "authorizationService cannot be null"); |
||||
this.authorizationService = authorizationService; |
||||
} |
||||
|
||||
@Override |
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException { |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthentication = (OAuth2PushedAuthorizationRequestAuthenticationToken) authentication; |
||||
|
||||
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils |
||||
.getAuthenticatedClientElseThrowInvalidClient(pushedAuthorizationRequestAuthentication); |
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); |
||||
|
||||
if (this.logger.isTraceEnabled()) { |
||||
this.logger.trace("Retrieved registered client"); |
||||
} |
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = OAuth2AuthorizationCodeRequestAuthenticationContext |
||||
.with(toAuthorizationCodeRequestAuthentication(pushedAuthorizationRequestAuthentication)) |
||||
.registeredClient(registeredClient) |
||||
.build(); |
||||
|
||||
// grant_type
|
||||
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_AUTHORIZATION_GRANT_TYPE_VALIDATOR |
||||
.accept(authenticationContext); |
||||
|
||||
// redirect_uri and scope
|
||||
this.authenticationValidator.accept(authenticationContext); |
||||
|
||||
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
|
||||
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_CODE_CHALLENGE_VALIDATOR |
||||
.accept(authenticationContext); |
||||
|
||||
// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
|
||||
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_PROMPT_VALIDATOR.accept(authenticationContext); |
||||
|
||||
if (this.logger.isTraceEnabled()) { |
||||
this.logger.trace("Validated pushed authorization request parameters"); |
||||
} |
||||
|
||||
// @formatter:off
|
||||
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode() |
||||
.authorizationUri(pushedAuthorizationRequestAuthentication.getAuthorizationUri()) |
||||
.clientId(registeredClient.getClientId()) |
||||
.redirectUri(pushedAuthorizationRequestAuthentication.getRedirectUri()) |
||||
.scopes(pushedAuthorizationRequestAuthentication.getScopes()) |
||||
.state(pushedAuthorizationRequestAuthentication.getState()) |
||||
.additionalParameters(pushedAuthorizationRequestAuthentication.getAdditionalParameters()) |
||||
.build(); |
||||
// @formatter:on
|
||||
|
||||
OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri |
||||
.create(); |
||||
|
||||
if (this.logger.isTraceEnabled()) { |
||||
this.logger.trace("Generated pushed authorization request uri"); |
||||
} |
||||
|
||||
// @formatter:off
|
||||
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient) |
||||
.principalName(clientPrincipal.getName()) |
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) |
||||
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest) |
||||
.attribute(OAuth2ParameterNames.STATE, pushedAuthorizationRequestUri.getState()) |
||||
.build(); |
||||
// @formatter:on
|
||||
this.authorizationService.save(authorization); |
||||
|
||||
if (this.logger.isTraceEnabled()) { |
||||
this.logger.trace("Saved authorization"); |
||||
} |
||||
|
||||
if (this.logger.isTraceEnabled()) { |
||||
this.logger.trace("Authenticated pushed authorization request"); |
||||
} |
||||
|
||||
return new OAuth2PushedAuthorizationRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(), |
||||
authorizationRequest.getClientId(), clientPrincipal, pushedAuthorizationRequestUri.getRequestUri(), |
||||
pushedAuthorizationRequestUri.getExpiresAt(), authorizationRequest.getRedirectUri(), |
||||
authorizationRequest.getState(), authorizationRequest.getScopes()); |
||||
} |
||||
|
||||
@Override |
||||
public boolean supports(Class<?> authentication) { |
||||
return OAuth2PushedAuthorizationRequestAuthenticationToken.class.isAssignableFrom(authentication); |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@code Consumer} providing access to the |
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for |
||||
* validating specific OAuth 2.0 Pushed Authorization Request parameters associated in |
||||
* the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. The default |
||||
* authentication validator is |
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}. |
||||
* |
||||
* <p> |
||||
* <b>NOTE:</b> The authentication validator MUST throw |
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails. |
||||
* @param authenticationValidator the {@code Consumer} providing access to the |
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for |
||||
* validating specific OAuth 2.0 Pushed Authorization Request parameters |
||||
*/ |
||||
public void setAuthenticationValidator( |
||||
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) { |
||||
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null"); |
||||
this.authenticationValidator = authenticationValidator; |
||||
} |
||||
|
||||
private static OAuth2AuthorizationCodeRequestAuthenticationToken toAuthorizationCodeRequestAuthentication( |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationCodeRequestAuthentication) { |
||||
return new OAuth2AuthorizationCodeRequestAuthenticationToken( |
||||
pushedAuthorizationCodeRequestAuthentication.getAuthorizationUri(), |
||||
pushedAuthorizationCodeRequestAuthentication.getClientId(), |
||||
(Authentication) pushedAuthorizationCodeRequestAuthentication.getPrincipal(), |
||||
pushedAuthorizationCodeRequestAuthentication.getRedirectUri(), |
||||
pushedAuthorizationCodeRequestAuthentication.getState(), |
||||
pushedAuthorizationCodeRequestAuthentication.getScopes(), |
||||
pushedAuthorizationCodeRequestAuthentication.getAdditionalParameters()); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
/* |
||||
* Copyright 2020-2025 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.time.Instant; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
|
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* An {@link Authentication} implementation for the OAuth 2.0 Pushed Authorization Request |
||||
* used in the Authorization Code Grant. |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 1.5 |
||||
* @see OAuth2PushedAuthorizationRequestAuthenticationProvider |
||||
*/ |
||||
public class OAuth2PushedAuthorizationRequestAuthenticationToken |
||||
extends AbstractOAuth2AuthorizationCodeRequestAuthenticationToken { |
||||
|
||||
private final String requestUri; |
||||
|
||||
private final Instant requestUriExpiresAt; |
||||
|
||||
/** |
||||
* Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationToken} using the |
||||
* provided parameters. |
||||
* @param authorizationUri the authorization URI |
||||
* @param clientId the client identifier |
||||
* @param principal the authenticated client principal |
||||
* @param redirectUri the redirect uri |
||||
* @param state the state |
||||
* @param scopes the requested scope(s) |
||||
* @param additionalParameters the additional parameters |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestAuthenticationToken(String authorizationUri, String clientId, |
||||
Authentication principal, @Nullable String redirectUri, @Nullable String state, |
||||
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) { |
||||
super(authorizationUri, clientId, principal, redirectUri, state, scopes, additionalParameters); |
||||
this.requestUri = null; |
||||
this.requestUriExpiresAt = null; |
||||
} |
||||
|
||||
/** |
||||
* Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationToken} using the |
||||
* provided parameters. |
||||
* @param authorizationUri the authorization URI |
||||
* @param clientId the client identifier |
||||
* @param principal the authenticated client principal |
||||
* @param requestUri the request URI corresponding to the authorization request posted |
||||
* @param requestUriExpiresAt the expiration time on or after which the |
||||
* {@code requestUri} MUST NOT be accepted |
||||
* @param redirectUri the redirect uri |
||||
* @param state the state |
||||
* @param scopes the authorized scope(s) |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestAuthenticationToken(String authorizationUri, String clientId, |
||||
Authentication principal, String requestUri, Instant requestUriExpiresAt, @Nullable String redirectUri, |
||||
@Nullable String state, @Nullable Set<String> scopes) { |
||||
super(authorizationUri, clientId, principal, redirectUri, state, scopes, null); |
||||
Assert.hasText(requestUri, "requestUri cannot be empty"); |
||||
Assert.notNull(requestUriExpiresAt, "requestUriExpiresAt cannot be null"); |
||||
this.requestUri = requestUri; |
||||
this.requestUriExpiresAt = requestUriExpiresAt; |
||||
setAuthenticated(true); |
||||
} |
||||
|
||||
@Nullable |
||||
public String getRequestUri() { |
||||
return this.requestUri; |
||||
} |
||||
|
||||
@Nullable |
||||
public Instant getRequestUriExpiresAt() { |
||||
return this.requestUriExpiresAt; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
/* |
||||
* Copyright 2020-2025 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.time.Instant; |
||||
import java.util.Base64; |
||||
|
||||
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; |
||||
import org.springframework.security.crypto.keygen.StringKeyGenerator; |
||||
|
||||
/** |
||||
* @author Joe Grandja |
||||
* @since 1.5 |
||||
*/ |
||||
final class OAuth2PushedAuthorizationRequestUri { |
||||
|
||||
private static final String REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:"; |
||||
|
||||
private static final String REQUEST_URI_DELIMITER = "___"; |
||||
|
||||
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator( |
||||
Base64.getUrlEncoder()); |
||||
|
||||
private String requestUri; |
||||
|
||||
private String state; |
||||
|
||||
private Instant expiresAt; |
||||
|
||||
static OAuth2PushedAuthorizationRequestUri create() { |
||||
String state = DEFAULT_STATE_GENERATOR.generateKey(); |
||||
Instant expiresAt = Instant.now().plusSeconds(30); |
||||
OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = new OAuth2PushedAuthorizationRequestUri(); |
||||
pushedAuthorizationRequestUri.requestUri = REQUEST_URI_PREFIX + state + REQUEST_URI_DELIMITER |
||||
+ expiresAt.toEpochMilli(); |
||||
pushedAuthorizationRequestUri.state = state + REQUEST_URI_DELIMITER + expiresAt.toEpochMilli(); |
||||
pushedAuthorizationRequestUri.expiresAt = expiresAt; |
||||
return pushedAuthorizationRequestUri; |
||||
} |
||||
|
||||
static OAuth2PushedAuthorizationRequestUri parse(String requestUri) { |
||||
int stateStartIndex = REQUEST_URI_PREFIX.length(); |
||||
int expiresAtStartIndex = requestUri.indexOf(REQUEST_URI_DELIMITER) + REQUEST_URI_DELIMITER.length(); |
||||
OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = new OAuth2PushedAuthorizationRequestUri(); |
||||
pushedAuthorizationRequestUri.requestUri = requestUri; |
||||
pushedAuthorizationRequestUri.state = requestUri.substring(stateStartIndex); |
||||
pushedAuthorizationRequestUri.expiresAt = Instant |
||||
.ofEpochMilli(Long.parseLong(requestUri.substring(expiresAtStartIndex))); |
||||
return pushedAuthorizationRequestUri; |
||||
} |
||||
|
||||
String getRequestUri() { |
||||
return this.requestUri; |
||||
} |
||||
|
||||
String getState() { |
||||
return this.state; |
||||
} |
||||
|
||||
Instant getExpiresAt() { |
||||
return this.expiresAt; |
||||
} |
||||
|
||||
private OAuth2PushedAuthorizationRequestUri() { |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
/* |
||||
* Copyright 2020-2025 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; |
||||
|
||||
/** |
||||
* The values defined for the "prompt" parameter for the OpenID Connect 1.0 Authentication |
||||
* Request. |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 1.5 |
||||
*/ |
||||
final class OidcPrompt { |
||||
|
||||
static final String NONE = "none"; |
||||
|
||||
static final String LOGIN = "login"; |
||||
|
||||
static final String CONSENT = "consent"; |
||||
|
||||
static final String SELECT_ACCOUNT = "select_account"; |
||||
|
||||
private OidcPrompt() { |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,265 @@
@@ -0,0 +1,265 @@
|
||||
/* |
||||
* Copyright 2020-2025 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.config.annotation.web.configurers; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.function.Consumer; |
||||
|
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
|
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.security.authentication.AuthenticationManager; |
||||
import org.springframework.security.authentication.AuthenticationProvider; |
||||
import org.springframework.security.config.annotation.ObjectPostProcessor; |
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||
import org.springframework.security.oauth2.core.OAuth2Error; |
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext; |
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException; |
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationValidator; |
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationProvider; |
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken; |
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; |
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2PushedAuthorizationRequestEndpointFilter; |
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter; |
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter; |
||||
import org.springframework.security.web.authentication.AuthenticationConverter; |
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; |
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter; |
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Configurer for the OAuth 2.0 Pushed Authorization Request Endpoint. |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 1.5 |
||||
* @see OAuth2AuthorizationServerConfigurer#pushedAuthorizationRequestEndpoint |
||||
* @see OAuth2PushedAuthorizationRequestEndpointFilter |
||||
*/ |
||||
public final class OAuth2PushedAuthorizationRequestEndpointConfigurer extends AbstractOAuth2Configurer { |
||||
|
||||
private RequestMatcher requestMatcher; |
||||
|
||||
private final List<AuthenticationConverter> pushedAuthorizationRequestConverters = new ArrayList<>(); |
||||
|
||||
private Consumer<List<AuthenticationConverter>> pushedAuthorizationRequestConvertersConsumer = ( |
||||
authorizationRequestConverters) -> { |
||||
}; |
||||
|
||||
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>(); |
||||
|
||||
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> { |
||||
}; |
||||
|
||||
private AuthenticationSuccessHandler pushedAuthorizationResponseHandler; |
||||
|
||||
private AuthenticationFailureHandler errorResponseHandler; |
||||
|
||||
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationCodeRequestAuthenticationValidator; |
||||
|
||||
/** |
||||
* Restrict for internal use only. |
||||
* @param objectPostProcessor an {@code ObjectPostProcessor} |
||||
*/ |
||||
OAuth2PushedAuthorizationRequestEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) { |
||||
super(objectPostProcessor); |
||||
} |
||||
|
||||
/** |
||||
* Adds an {@link AuthenticationConverter} used when attempting to extract a Pushed |
||||
* Authorization Request from {@link HttpServletRequest} to an instance of |
||||
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} used for authenticating |
||||
* the request. |
||||
* @param pushedAuthorizationRequestConverter an {@link AuthenticationConverter} used |
||||
* when attempting to extract a Pushed Authorization Request from |
||||
* {@link HttpServletRequest} |
||||
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further |
||||
* configuration |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestConverter( |
||||
AuthenticationConverter pushedAuthorizationRequestConverter) { |
||||
Assert.notNull(pushedAuthorizationRequestConverter, "pushedAuthorizationRequestConverter cannot be null"); |
||||
this.pushedAuthorizationRequestConverters.add(pushedAuthorizationRequestConverter); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@code Consumer} providing access to the {@code List} of default and |
||||
* (optionally) added |
||||
* {@link #pushedAuthorizationRequestConverter(AuthenticationConverter) |
||||
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a |
||||
* specific {@link AuthenticationConverter}. |
||||
* @param pushedAuthorizationRequestConvertersConsumer the {@code Consumer} providing |
||||
* access to the {@code List} of default and (optionally) added |
||||
* {@link AuthenticationConverter}'s |
||||
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further |
||||
* configuration |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestConverters( |
||||
Consumer<List<AuthenticationConverter>> pushedAuthorizationRequestConvertersConsumer) { |
||||
Assert.notNull(pushedAuthorizationRequestConvertersConsumer, |
||||
"pushedAuthorizationRequestConvertersConsumer cannot be null"); |
||||
this.pushedAuthorizationRequestConvertersConsumer = pushedAuthorizationRequestConvertersConsumer; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Adds an {@link AuthenticationProvider} used for authenticating an |
||||
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken}. |
||||
* @param authenticationProvider an {@link AuthenticationProvider} used for |
||||
* authenticating an {@link OAuth2PushedAuthorizationRequestAuthenticationToken} |
||||
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further |
||||
* configuration |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestEndpointConfigurer authenticationProvider( |
||||
AuthenticationProvider authenticationProvider) { |
||||
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null"); |
||||
this.authenticationProviders.add(authenticationProvider); |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@code Consumer} providing access to the {@code List} of default and |
||||
* (optionally) added {@link #authenticationProvider(AuthenticationProvider) |
||||
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a |
||||
* specific {@link AuthenticationProvider}. |
||||
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the |
||||
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s |
||||
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further |
||||
* configuration |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestEndpointConfigurer authenticationProviders( |
||||
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) { |
||||
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null"); |
||||
this.authenticationProvidersConsumer = authenticationProvidersConsumer; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling an |
||||
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} and returning the |
||||
* Pushed Authorization Response. |
||||
* @param pushedAuthorizationResponseHandler the {@link AuthenticationSuccessHandler} |
||||
* used for handling an {@link OAuth2PushedAuthorizationRequestAuthenticationToken} |
||||
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further |
||||
* configuration |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationResponseHandler( |
||||
AuthenticationSuccessHandler pushedAuthorizationResponseHandler) { |
||||
this.pushedAuthorizationResponseHandler = pushedAuthorizationResponseHandler; |
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link AuthenticationFailureHandler} used for handling an |
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationException} and returning the |
||||
* {@link OAuth2Error Error Response}. |
||||
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for |
||||
* handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException} |
||||
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further |
||||
* configuration |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestEndpointConfigurer errorResponseHandler( |
||||
AuthenticationFailureHandler errorResponseHandler) { |
||||
this.errorResponseHandler = errorResponseHandler; |
||||
return this; |
||||
} |
||||
|
||||
void addAuthorizationCodeRequestAuthenticationValidator( |
||||
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) { |
||||
this.authorizationCodeRequestAuthenticationValidator = (this.authorizationCodeRequestAuthenticationValidator == null) |
||||
? authenticationValidator |
||||
: this.authorizationCodeRequestAuthenticationValidator.andThen(authenticationValidator); |
||||
} |
||||
|
||||
@Override |
||||
void init(HttpSecurity httpSecurity) { |
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils |
||||
.getAuthorizationServerSettings(httpSecurity); |
||||
String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed() |
||||
? OAuth2ConfigurerUtils |
||||
.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint()) |
||||
: authorizationServerSettings.getPushedAuthorizationRequestEndpoint(); |
||||
this.requestMatcher = new AntPathRequestMatcher(pushedAuthorizationRequestEndpointUri, HttpMethod.POST.name()); |
||||
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity); |
||||
if (!this.authenticationProviders.isEmpty()) { |
||||
authenticationProviders.addAll(0, this.authenticationProviders); |
||||
} |
||||
this.authenticationProvidersConsumer.accept(authenticationProviders); |
||||
authenticationProviders.forEach( |
||||
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider))); |
||||
} |
||||
|
||||
@Override |
||||
void configure(HttpSecurity httpSecurity) { |
||||
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class); |
||||
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils |
||||
.getAuthorizationServerSettings(httpSecurity); |
||||
String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed() |
||||
? OAuth2ConfigurerUtils |
||||
.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint()) |
||||
: authorizationServerSettings.getPushedAuthorizationRequestEndpoint(); |
||||
OAuth2PushedAuthorizationRequestEndpointFilter pushedAuthorizationRequestEndpointFilter = new OAuth2PushedAuthorizationRequestEndpointFilter( |
||||
authenticationManager, pushedAuthorizationRequestEndpointUri); |
||||
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters(); |
||||
if (!this.pushedAuthorizationRequestConverters.isEmpty()) { |
||||
authenticationConverters.addAll(0, this.pushedAuthorizationRequestConverters); |
||||
} |
||||
this.pushedAuthorizationRequestConvertersConsumer.accept(authenticationConverters); |
||||
pushedAuthorizationRequestEndpointFilter |
||||
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters)); |
||||
if (this.pushedAuthorizationResponseHandler != null) { |
||||
pushedAuthorizationRequestEndpointFilter |
||||
.setAuthenticationSuccessHandler(this.pushedAuthorizationResponseHandler); |
||||
} |
||||
if (this.errorResponseHandler != null) { |
||||
pushedAuthorizationRequestEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler); |
||||
} |
||||
httpSecurity.addFilterAfter(postProcess(pushedAuthorizationRequestEndpointFilter), AuthorizationFilter.class); |
||||
} |
||||
|
||||
@Override |
||||
RequestMatcher getRequestMatcher() { |
||||
return this.requestMatcher; |
||||
} |
||||
|
||||
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() { |
||||
List<AuthenticationConverter> authenticationConverters = new ArrayList<>(); |
||||
|
||||
authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter()); |
||||
|
||||
return authenticationConverters; |
||||
} |
||||
|
||||
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) { |
||||
List<AuthenticationProvider> authenticationProviders = new ArrayList<>(); |
||||
|
||||
OAuth2PushedAuthorizationRequestAuthenticationProvider pushedAuthorizationRequestAuthenticationProvider = new OAuth2PushedAuthorizationRequestAuthenticationProvider( |
||||
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity)); |
||||
if (this.authorizationCodeRequestAuthenticationValidator != null) { |
||||
pushedAuthorizationRequestAuthenticationProvider |
||||
.setAuthenticationValidator(new OAuth2AuthorizationCodeRequestAuthenticationValidator() |
||||
.andThen(this.authorizationCodeRequestAuthenticationValidator)); |
||||
} |
||||
authenticationProviders.add(pushedAuthorizationRequestAuthenticationProvider); |
||||
|
||||
return authenticationProviders; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
/* |
||||
* Copyright 2020-2025 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.web; |
||||
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter; |
||||
import org.springframework.http.converter.HttpMessageConverter; |
||||
import org.springframework.http.converter.json.GsonHttpMessageConverter; |
||||
import org.springframework.http.converter.json.JsonbHttpMessageConverter; |
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; |
||||
import org.springframework.util.ClassUtils; |
||||
|
||||
/** |
||||
* Utility methods for {@link HttpMessageConverter}'s. |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 1.5 |
||||
*/ |
||||
final class HttpMessageConverters { |
||||
|
||||
private static final boolean jackson2Present; |
||||
|
||||
private static final boolean gsonPresent; |
||||
|
||||
private static final boolean jsonbPresent; |
||||
|
||||
static { |
||||
ClassLoader classLoader = HttpMessageConverters.class.getClassLoader(); |
||||
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) |
||||
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); |
||||
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); |
||||
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); |
||||
} |
||||
|
||||
private HttpMessageConverters() { |
||||
} |
||||
|
||||
static GenericHttpMessageConverter<Object> getJsonMessageConverter() { |
||||
if (jackson2Present) { |
||||
return new MappingJackson2HttpMessageConverter(); |
||||
} |
||||
if (gsonPresent) { |
||||
return new GsonHttpMessageConverter(); |
||||
} |
||||
if (jsonbPresent) { |
||||
return new JsonbHttpMessageConverter(); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,223 @@
@@ -0,0 +1,223 @@
|
||||
/* |
||||
* Copyright 2020-2025 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.web; |
||||
|
||||
import java.io.IOException; |
||||
import java.time.Instant; |
||||
import java.time.temporal.ChronoUnit; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
|
||||
import jakarta.servlet.FilterChain; |
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
|
||||
import org.springframework.core.ParameterizedTypeReference; |
||||
import org.springframework.core.log.LogMessage; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.GenericHttpMessageConverter; |
||||
import org.springframework.http.server.ServletServerHttpResponse; |
||||
import org.springframework.security.authentication.AbstractAuthenticationToken; |
||||
import org.springframework.security.authentication.AuthenticationDetailsSource; |
||||
import org.springframework.security.authentication.AuthenticationManager; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; |
||||
import org.springframework.security.oauth2.core.OAuth2Error; |
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; |
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationProvider; |
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken; |
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter; |
||||
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler; |
||||
import org.springframework.security.web.authentication.AuthenticationConverter; |
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; |
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; |
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
||||
import org.springframework.security.web.util.matcher.RequestMatcher; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.filter.OncePerRequestFilter; |
||||
|
||||
/** |
||||
* A {@code Filter} for the OAuth 2.0 Pushed Authorization Request endpoint, which handles |
||||
* the processing of the OAuth 2.0 Pushed Authorization Request. |
||||
* |
||||
* @author Joe Grandja |
||||
* @since 1.5 |
||||
* @see AuthenticationManager |
||||
* @see OAuth2PushedAuthorizationRequestAuthenticationProvider |
||||
* @see <a target="_blank" href= |
||||
* "https://datatracker.ietf.org/doc/html/rfc9126#name-pushed-authorization-reques">Section |
||||
* 2. Pushed Authorization Request Endpoint</a> |
||||
* @see <a target="_blank" href= |
||||
* "https://datatracker.ietf.org/doc/html/rfc9126#section-2.1">Section 2.1 Pushed |
||||
* Authorization Request</a> |
||||
* @see <a target="_blank" href= |
||||
* "https://datatracker.ietf.org/doc/html/rfc9126#section-2.2">Section 2.2 Pushed |
||||
* Authorization Response</a> |
||||
*/ |
||||
public final class OAuth2PushedAuthorizationRequestEndpointFilter extends OncePerRequestFilter { |
||||
|
||||
/** |
||||
* The default endpoint {@code URI} for pushed authorization requests. |
||||
*/ |
||||
private static final String DEFAULT_PUSHED_AUTHORIZATION_REQUEST_ENDPOINT_URI = "/oauth2/par"; |
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() { |
||||
}; |
||||
|
||||
private static final GenericHttpMessageConverter<Object> JSON_MESSAGE_CONVERTER = HttpMessageConverters |
||||
.getJsonMessageConverter(); |
||||
|
||||
private final AuthenticationManager authenticationManager; |
||||
|
||||
private final RequestMatcher pushedAuthorizationRequestEndpointMatcher; |
||||
|
||||
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); |
||||
|
||||
private AuthenticationConverter authenticationConverter; |
||||
|
||||
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendPushedAuthorizationResponse; |
||||
|
||||
private AuthenticationFailureHandler authenticationFailureHandler = new OAuth2ErrorAuthenticationFailureHandler(); |
||||
|
||||
/** |
||||
* Constructs an {@code OAuth2PushedAuthorizationRequestEndpointFilter} using the |
||||
* provided parameters. |
||||
* @param authenticationManager the authentication manager |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestEndpointFilter(AuthenticationManager authenticationManager) { |
||||
this(authenticationManager, DEFAULT_PUSHED_AUTHORIZATION_REQUEST_ENDPOINT_URI); |
||||
} |
||||
|
||||
/** |
||||
* Constructs an {@code OAuth2PushedAuthorizationRequestEndpointFilter} using the |
||||
* provided parameters. |
||||
* @param authenticationManager the authentication manager |
||||
* @param pushedAuthorizationRequestEndpointUri the endpoint {@code URI} for pushed |
||||
* authorization requests |
||||
*/ |
||||
public OAuth2PushedAuthorizationRequestEndpointFilter(AuthenticationManager authenticationManager, |
||||
String pushedAuthorizationRequestEndpointUri) { |
||||
Assert.notNull(authenticationManager, "authenticationManager cannot be null"); |
||||
Assert.hasText(pushedAuthorizationRequestEndpointUri, "pushedAuthorizationRequestEndpointUri cannot be empty"); |
||||
this.authenticationManager = authenticationManager; |
||||
this.pushedAuthorizationRequestEndpointMatcher = new AntPathRequestMatcher( |
||||
pushedAuthorizationRequestEndpointUri, HttpMethod.POST.name()); |
||||
this.authenticationConverter = new OAuth2AuthorizationCodeRequestAuthenticationConverter(); |
||||
} |
||||
|
||||
@Override |
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
||||
throws ServletException, IOException { |
||||
|
||||
if (!this.pushedAuthorizationRequestEndpointMatcher.matches(request)) { |
||||
filterChain.doFilter(request, response); |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
Authentication pushedAuthorizationRequestAuthentication = this.authenticationConverter.convert(request); |
||||
if (pushedAuthorizationRequestAuthentication instanceof AbstractAuthenticationToken) { |
||||
((AbstractAuthenticationToken) pushedAuthorizationRequestAuthentication) |
||||
.setDetails(this.authenticationDetailsSource.buildDetails(request)); |
||||
} |
||||
Authentication pushedAuthorizationRequestAuthenticationResult = this.authenticationManager |
||||
.authenticate(pushedAuthorizationRequestAuthentication); |
||||
|
||||
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, |
||||
pushedAuthorizationRequestAuthenticationResult); |
||||
|
||||
} |
||||
catch (OAuth2AuthenticationException ex) { |
||||
if (this.logger.isTraceEnabled()) { |
||||
this.logger.trace(LogMessage.format("Pushed authorization request failed: %s", ex.getError()), ex); |
||||
} |
||||
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link AuthenticationDetailsSource} used for building an authentication |
||||
* details instance from {@link HttpServletRequest}. |
||||
* @param authenticationDetailsSource the {@link AuthenticationDetailsSource} used for |
||||
* building an authentication details instance from {@link HttpServletRequest} |
||||
*/ |
||||
public void setAuthenticationDetailsSource( |
||||
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { |
||||
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null"); |
||||
this.authenticationDetailsSource = authenticationDetailsSource; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link AuthenticationConverter} used when attempting to extract a Pushed |
||||
* Authorization Request from {@link HttpServletRequest} to an instance of |
||||
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} used for authenticating |
||||
* the request. |
||||
* @param authenticationConverter the {@link AuthenticationConverter} used when |
||||
* attempting to extract a Pushed Authorization Request from |
||||
* {@link HttpServletRequest} |
||||
*/ |
||||
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { |
||||
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); |
||||
this.authenticationConverter = authenticationConverter; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link AuthenticationSuccessHandler} used for handling an |
||||
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} and returning the |
||||
* Pushed Authorization Response. |
||||
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used |
||||
* for handling an {@link OAuth2PushedAuthorizationRequestAuthenticationToken} |
||||
*/ |
||||
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) { |
||||
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null"); |
||||
this.authenticationSuccessHandler = authenticationSuccessHandler; |
||||
} |
||||
|
||||
/** |
||||
* Sets the {@link AuthenticationFailureHandler} used for handling an |
||||
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error |
||||
* Response}. |
||||
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used |
||||
* for handling an {@link OAuth2AuthenticationException} |
||||
*/ |
||||
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) { |
||||
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null"); |
||||
this.authenticationFailureHandler = authenticationFailureHandler; |
||||
} |
||||
|
||||
private void sendPushedAuthorizationResponse(HttpServletRequest request, HttpServletResponse response, |
||||
Authentication authentication) throws IOException { |
||||
|
||||
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthentication = (OAuth2PushedAuthorizationRequestAuthenticationToken) authentication; |
||||
|
||||
Map<String, Object> pushedAuthorizationResponse = new LinkedHashMap<>(); |
||||
pushedAuthorizationResponse.put("request_uri", pushedAuthorizationRequestAuthentication.getRequestUri()); |
||||
long expiresIn = ChronoUnit.SECONDS.between(Instant.now(), |
||||
pushedAuthorizationRequestAuthentication.getRequestUriExpiresAt()); |
||||
pushedAuthorizationResponse.put(OAuth2ParameterNames.EXPIRES_IN, expiresIn); |
||||
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response); |
||||
httpResponse.setStatusCode(HttpStatus.CREATED); |
||||
|
||||
JSON_MESSAGE_CONVERTER.write(pushedAuthorizationResponse, STRING_OBJECT_MAP.getType(), |
||||
MediaType.APPLICATION_JSON, httpResponse); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,422 @@
@@ -0,0 +1,422 @@
|
||||
/* |
||||
* Copyright 2020-2025 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.Set; |
||||
import java.util.function.Consumer; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.ArgumentCaptor; |
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType; |
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod; |
||||
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.OAuth2AuthorizationRequest; |
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; |
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; |
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; |
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes; |
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; |
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; |
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; |
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; |
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.verify; |
||||
|
||||
/** |
||||
* Tests for {@link OAuth2PushedAuthorizationRequestAuthenticationProvider}. |
||||
* |
||||
* @author Joe Grandja |
||||
*/ |
||||
public class OAuth2PushedAuthorizationRequestAuthenticationProviderTests { |
||||
|
||||
private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/par"; |
||||
|
||||
private static final String STATE = "state"; |
||||
|
||||
private OAuth2AuthorizationService authorizationService; |
||||
|
||||
private OAuth2PushedAuthorizationRequestAuthenticationProvider authenticationProvider; |
||||
|
||||
@BeforeEach |
||||
public void setUp() { |
||||
this.authorizationService = mock(OAuth2AuthorizationService.class); |
||||
this.authenticationProvider = new OAuth2PushedAuthorizationRequestAuthenticationProvider( |
||||
this.authorizationService); |
||||
} |
||||
|
||||
@Test |
||||
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> new OAuth2PushedAuthorizationRequestAuthenticationProvider(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("authorizationService cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void supportsWhenTypeOAuth2PushedAuthorizationRequestAuthenticationTokenThenReturnTrue() { |
||||
assertThat(this.authenticationProvider.supports(OAuth2PushedAuthorizationRequestAuthenticationToken.class)) |
||||
.isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void setAuthenticationValidatorWhenNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidator(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("authenticationValidator cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenClientNotAuthenticatedThenThrowOAuth2AuthenticationException() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[1]; |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken( |
||||
registeredClient.getClientId(), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null, null); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE, |
||||
registeredClient.getScopes(), null); |
||||
// @formatter:off
|
||||
assertThatExceptionOfType(OAuth2AuthenticationException.class) |
||||
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.extracting(OAuth2AuthenticationException::getError) |
||||
.extracting(OAuth2Error::getErrorCode) |
||||
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT); |
||||
// @formatter:on
|
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenClientNotAuthorizedToRequestCodeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient() |
||||
.authorizationGrantTypes(Set::clear) |
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) |
||||
.build(); |
||||
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[1]; |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, null, |
||||
registeredClient.getScopes(), null); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID, |
||||
authentication.getRedirectUri())); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenInvalidRedirectUriHostThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https:///invalid", STATE, |
||||
registeredClient.getScopes(), null); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenInvalidRedirectUriFragmentThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://example.com#fragment", |
||||
STATE, registeredClient.getScopes(), null); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenUnregisteredRedirectUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://invalid-example.com", |
||||
STATE, registeredClient.getScopes(), null); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenRedirectUriIPv4LoopbackAndDifferentPortThenReturnPushedAuthorizationResponse() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient() |
||||
.redirectUri("https://127.0.0.1:8080") |
||||
.build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://127.0.0.1:5000", STATE, |
||||
registeredClient.getScopes(), null); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider |
||||
.authenticate(authentication); |
||||
assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenRedirectUriIPv6LoopbackAndDifferentPortThenReturnPushedAuthorizationResponse() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient() |
||||
.redirectUri("https://[::1]:8080") |
||||
.build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://[::1]:5000", STATE, |
||||
registeredClient.getScopes(), null); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider |
||||
.authenticate(authentication); |
||||
assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenMissingRedirectUriAndMultipleRegisteredThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient() |
||||
.redirectUri("https://example2.com") |
||||
.build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, null, STATE, |
||||
registeredClient.getScopes(), null); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenAuthenticationRequestMissingRedirectUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
// redirect_uri is REQUIRED for OpenID Connect requests
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, null, STATE, |
||||
registeredClient.getScopes(), null); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null)); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenInvalidScopeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2]; |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE, |
||||
Collections.singleton("invalid-scope"), null); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, authentication.getRedirectUri())); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenPkceRequiredAndMissingCodeChallengeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient() |
||||
.clientSettings(ClientSettings.builder().requireProofKey(true).build()) |
||||
.build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2]; |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE, |
||||
registeredClient.getScopes(), null); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, |
||||
authentication.getRedirectUri())); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenPkceUnsupportedCodeChallengeMethodThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2]; |
||||
Map<String, Object> additionalParameters = new HashMap<>(); |
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge"); |
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported"); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE, |
||||
registeredClient.getScopes(), additionalParameters); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, |
||||
authentication.getRedirectUri())); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenPkceMissingCodeChallengeMethodThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2]; |
||||
Map<String, Object> additionalParameters = new HashMap<>(); |
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge"); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE, |
||||
registeredClient.getScopes(), additionalParameters); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, |
||||
authentication.getRedirectUri())); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenAuthenticationRequestWithPromptNoneLoginThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException( |
||||
"none login"); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenAuthenticationRequestWithPromptNoneConsentThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException( |
||||
"none consent"); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenAuthenticationRequestWithPromptNoneSelectAccountThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() { |
||||
assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException( |
||||
"none select_account"); |
||||
} |
||||
|
||||
private void assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException( |
||||
String prompt) { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2]; |
||||
Map<String, Object> additionalParameters = new HashMap<>(); |
||||
additionalParameters.put("prompt", prompt); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE, |
||||
registeredClient.getScopes(), additionalParameters); |
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication)) |
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class) |
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authentication.getRedirectUri())); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenPushedAuthorizationRequestValidThenReturnPushedAuthorizationResponse() { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[0]; |
||||
Map<String, Object> additionalParameters = new HashMap<>(); |
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge"); |
||||
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE, |
||||
registeredClient.getScopes(), additionalParameters); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider |
||||
.authenticate(authentication); |
||||
assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult); |
||||
} |
||||
|
||||
@Test |
||||
public void authenticateWhenCustomAuthenticationValidatorThenUsed() { |
||||
@SuppressWarnings("unchecked") |
||||
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = mock(Consumer.class); |
||||
this.authenticationProvider.setAuthenticationValidator(authenticationValidator); |
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient, |
||||
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret()); |
||||
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2]; |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE, |
||||
registeredClient.getScopes(), null); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider |
||||
.authenticate(authentication); |
||||
assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult); |
||||
verify(authenticationValidator).accept(any()); |
||||
} |
||||
|
||||
private void assertPushedAuthorizationResponse(RegisteredClient registeredClient, |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authentication, |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult) { |
||||
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class); |
||||
verify(this.authorizationService).save(authorizationCaptor.capture()); |
||||
OAuth2Authorization authorization = authorizationCaptor.getValue(); |
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization |
||||
.getAttribute(OAuth2AuthorizationRequest.class.getName()); |
||||
assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); |
||||
assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE); |
||||
assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(authentication.getAuthorizationUri()); |
||||
assertThat(authorizationRequest.getClientId()).isEqualTo(registeredClient.getClientId()); |
||||
assertThat(authorizationRequest.getRedirectUri()).isEqualTo(authentication.getRedirectUri()); |
||||
assertThat(authorizationRequest.getScopes()).isEqualTo(authentication.getScopes()); |
||||
assertThat(authorizationRequest.getState()).isEqualTo(authentication.getState()); |
||||
assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(authentication.getAdditionalParameters()); |
||||
|
||||
assertThat(authorization.getRegisteredClientId()).isEqualTo(registeredClient.getId()); |
||||
assertThat(authorization.getPrincipalName()).isEqualTo(authentication.getName()); |
||||
assertThat(authorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); |
||||
assertThat(authorization.<String>getAttribute(OAuth2ParameterNames.STATE)).isNotNull(); |
||||
|
||||
assertThat(authenticationResult.getClientId()).isEqualTo(authorizationRequest.getClientId()); |
||||
assertThat(authenticationResult.getPrincipal()).isEqualTo(authentication.getPrincipal()); |
||||
assertThat(authenticationResult.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri()); |
||||
assertThat(authenticationResult.getRedirectUri()).isEqualTo(authorizationRequest.getRedirectUri()); |
||||
assertThat(authenticationResult.getScopes()).isEqualTo(authorizationRequest.getScopes()); |
||||
assertThat(authenticationResult.getState()).isEqualTo(authorizationRequest.getState()); |
||||
assertThat(authenticationResult.getRequestUri()).isNotNull(); |
||||
assertThat(authenticationResult.getRequestUriExpiresAt()).isNotNull(); |
||||
assertThat(authenticationResult.isAuthenticated()).isTrue(); |
||||
} |
||||
|
||||
private static void assertAuthenticationException( |
||||
OAuth2AuthorizationCodeRequestAuthenticationException authenticationException, String errorCode, |
||||
String parameterName, String redirectUri) { |
||||
|
||||
OAuth2Error error = authenticationException.getError(); |
||||
assertThat(error.getErrorCode()).isEqualTo(errorCode); |
||||
assertThat(error.getDescription()).contains(parameterName); |
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationException |
||||
.getAuthorizationCodeRequestAuthentication(); |
||||
assertThat(authorizationCodeRequestAuthentication.getRedirectUri()).isEqualTo(redirectUri); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,490 @@
@@ -0,0 +1,490 @@
|
||||
/* |
||||
* Copyright 2020-2025 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.web; |
||||
|
||||
import java.time.Instant; |
||||
import java.time.temporal.ChronoUnit; |
||||
import java.util.Map; |
||||
import java.util.function.Consumer; |
||||
|
||||
import jakarta.servlet.FilterChain; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
import org.assertj.core.api.InstanceOfAssertFactories; |
||||
import org.junit.jupiter.api.AfterEach; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.ArgumentCaptor; |
||||
|
||||
import org.springframework.core.ParameterizedTypeReference; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.converter.GenericHttpMessageConverter; |
||||
import org.springframework.http.converter.HttpMessageConverter; |
||||
import org.springframework.mock.http.client.MockClientHttpResponse; |
||||
import org.springframework.mock.web.MockHttpServletRequest; |
||||
import org.springframework.mock.web.MockHttpServletResponse; |
||||
import org.springframework.security.authentication.AuthenticationDetailsSource; |
||||
import org.springframework.security.authentication.AuthenticationManager; |
||||
import org.springframework.security.authentication.TestingAuthenticationToken; |
||||
import org.springframework.security.core.context.SecurityContext; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
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.OAuth2AuthorizationResponseType; |
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; |
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames; |
||||
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter; |
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes; |
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken; |
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; |
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; |
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; |
||||
import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext; |
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; |
||||
import org.springframework.security.web.authentication.AuthenticationConverter; |
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler; |
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; |
||||
import org.springframework.security.web.authentication.WebAuthenticationDetails; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.ArgumentMatchers.same; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.verify; |
||||
import static org.mockito.Mockito.verifyNoInteractions; |
||||
|
||||
/** |
||||
* Tests for {@link OAuth2PushedAuthorizationRequestEndpointFilter}. |
||||
* |
||||
* @author Joe Grandja |
||||
*/ |
||||
public class OAuth2PushedAuthorizationRequestEndpointFilterTests { |
||||
|
||||
private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/par"; |
||||
|
||||
private static final String STATE = "state"; |
||||
|
||||
private static final String REMOTE_ADDRESS = "remote-address"; |
||||
|
||||
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter(); |
||||
|
||||
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters |
||||
.getJsonMessageConverter(); |
||||
|
||||
private AuthenticationManager authenticationManager; |
||||
|
||||
private OAuth2PushedAuthorizationRequestEndpointFilter filter; |
||||
|
||||
private TestingAuthenticationToken clientPrincipal; |
||||
|
||||
@BeforeEach |
||||
public void setUp() { |
||||
this.authenticationManager = mock(AuthenticationManager.class); |
||||
this.filter = new OAuth2PushedAuthorizationRequestEndpointFilter(this.authenticationManager); |
||||
this.clientPrincipal = new TestingAuthenticationToken("client-id", "client-secret"); |
||||
this.clientPrincipal.setAuthenticated(true); |
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); |
||||
securityContext.setAuthentication(this.clientPrincipal); |
||||
SecurityContextHolder.setContext(securityContext); |
||||
AuthorizationServerContextHolder |
||||
.setContext(new TestAuthorizationServerContext(AuthorizationServerSettings.builder().build(), null)); |
||||
} |
||||
|
||||
@AfterEach |
||||
public void cleanup() { |
||||
SecurityContextHolder.clearContext(); |
||||
AuthorizationServerContextHolder.resetContext(); |
||||
} |
||||
|
||||
@Test |
||||
public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> new OAuth2PushedAuthorizationRequestEndpointFilter(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("authenticationManager cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void constructorWhenPushedAuthorizationRequestEndpointUriNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> new OAuth2PushedAuthorizationRequestEndpointFilter(this.authenticationManager, null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("pushedAuthorizationRequestEndpointUri cannot be empty"); |
||||
} |
||||
|
||||
@Test |
||||
public void setAuthenticationDetailsSourceWhenNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> this.filter.setAuthenticationDetailsSource(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("authenticationDetailsSource cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> this.filter.setAuthenticationConverter(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("authenticationConverter cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void setAuthenticationSuccessHandlerWhenNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> this.filter.setAuthenticationSuccessHandler(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("authenticationSuccessHandler cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void setAuthenticationFailureHandlerWhenNullThenThrowIllegalArgumentException() { |
||||
assertThatThrownBy(() -> this.filter.setAuthenticationFailureHandler(null)) |
||||
.isInstanceOf(IllegalArgumentException.class) |
||||
.hasMessage("authenticationFailureHandler cannot be null"); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenNotPushedAuthorizationRequestThenNotProcessed() throws Exception { |
||||
String requestUri = "/path"; |
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); |
||||
request.setServletPath(requestUri); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
FilterChain filterChain = mock(FilterChain.class); |
||||
|
||||
this.filter.doFilter(request, response, filterChain); |
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestIncludesRequestUriThenInvalidRequestError() throws Exception { |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError( |
||||
TestRegisteredClients.registeredClient().build(), "request_uri", OAuth2ErrorCodes.INVALID_REQUEST, |
||||
(request) -> request.addParameter("request_uri", "request_uri")); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestMultipleResponseTypeThenInvalidRequestError() throws Exception { |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError( |
||||
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.RESPONSE_TYPE, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, |
||||
(request) -> request.addParameter(OAuth2ParameterNames.RESPONSE_TYPE, "id_token")); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestInvalidResponseTypeThenUnsupportedResponseTypeError() |
||||
throws Exception { |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError( |
||||
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.RESPONSE_TYPE, |
||||
OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, |
||||
(request) -> request.setParameter(OAuth2ParameterNames.RESPONSE_TYPE, "id_token")); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestMissingClientIdThenInvalidRequestError() throws Exception { |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError( |
||||
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.CLIENT_ID, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, (request) -> request.removeParameter(OAuth2ParameterNames.CLIENT_ID)); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestMultipleClientIdThenInvalidRequestError() throws Exception { |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError( |
||||
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.CLIENT_ID, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, |
||||
(request) -> request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2")); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestMultipleRedirectUriThenInvalidRequestError() throws Exception { |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError( |
||||
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.REDIRECT_URI, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, |
||||
(request) -> request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "https://example2.com")); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestMultipleScopeThenInvalidRequestError() throws Exception { |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError( |
||||
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.SCOPE, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, |
||||
(request) -> request.addParameter(OAuth2ParameterNames.SCOPE, "scope2")); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestMultipleStateThenInvalidRequestError() throws Exception { |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError( |
||||
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.STATE, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, |
||||
(request) -> request.addParameter(OAuth2ParameterNames.STATE, "state2")); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestMultipleCodeChallengeThenInvalidRequestError() throws Exception { |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError( |
||||
TestRegisteredClients.registeredClient().build(), PkceParameterNames.CODE_CHALLENGE, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, (request) -> { |
||||
request.addParameter(PkceParameterNames.CODE_CHALLENGE, "code-challenge"); |
||||
request.addParameter(PkceParameterNames.CODE_CHALLENGE, "another-code-challenge"); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestMultipleCodeChallengeMethodThenInvalidRequestError() |
||||
throws Exception { |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError( |
||||
TestRegisteredClients.registeredClient().build(), PkceParameterNames.CODE_CHALLENGE_METHOD, |
||||
OAuth2ErrorCodes.INVALID_REQUEST, (request) -> { |
||||
request.addParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); |
||||
request.addParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthenticationRequestMultiplePromptThenInvalidRequestError() throws Exception { |
||||
// Setup OpenID Connect request
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scopes((scopes) -> { |
||||
scopes.clear(); |
||||
scopes.add(OidcScopes.OPENID); |
||||
}).build(); |
||||
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(registeredClient, "prompt", |
||||
OAuth2ErrorCodes.INVALID_REQUEST, (request) -> { |
||||
request.addParameter("prompt", "none"); |
||||
request.addParameter("prompt", "login"); |
||||
}); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestAuthenticationExceptionThenErrorResponse() throws Exception { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "error description", "error uri"); |
||||
given(this.authenticationManager.authenticate(any())).willThrow(new OAuth2AuthenticationException(error)); |
||||
|
||||
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
FilterChain filterChain = mock(FilterChain.class); |
||||
|
||||
this.filter.doFilter(request, response, filterChain); |
||||
|
||||
verify(this.authenticationManager).authenticate(any()); |
||||
verifyNoInteractions(filterChain); |
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); |
||||
OAuth2Error errorResponse = readError(response); |
||||
assertThat(errorResponse.getErrorCode()).isEqualTo(error.getErrorCode()); |
||||
assertThat(errorResponse.getDescription()).isEqualTo(error.getDescription()); |
||||
assertThat(errorResponse.getUri()).isEqualTo(error.getUri()); |
||||
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.clientPrincipal); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, "request_uri", |
||||
Instant.now().plusSeconds(30), registeredClient.getRedirectUris().iterator().next(), STATE, |
||||
registeredClient.getScopes()); |
||||
|
||||
AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class); |
||||
given(authenticationConverter.convert(any())).willReturn(pushedAuthorizationRequestAuthenticationResult); |
||||
this.filter.setAuthenticationConverter(authenticationConverter); |
||||
|
||||
given(this.authenticationManager.authenticate(any())) |
||||
.willReturn(pushedAuthorizationRequestAuthenticationResult); |
||||
|
||||
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
FilterChain filterChain = mock(FilterChain.class); |
||||
|
||||
this.filter.doFilter(request, response, filterChain); |
||||
|
||||
verify(authenticationConverter).convert(any()); |
||||
verify(this.authenticationManager).authenticate(any()); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, "request_uri", |
||||
Instant.now().plusSeconds(30), registeredClient.getRedirectUris().iterator().next(), STATE, |
||||
registeredClient.getScopes()); |
||||
given(this.authenticationManager.authenticate(any())) |
||||
.willReturn(pushedAuthorizationRequestAuthenticationResult); |
||||
|
||||
AuthenticationSuccessHandler authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class); |
||||
this.filter.setAuthenticationSuccessHandler(authenticationSuccessHandler); |
||||
|
||||
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
FilterChain filterChain = mock(FilterChain.class); |
||||
|
||||
this.filter.doFilter(request, response, filterChain); |
||||
|
||||
verify(this.authenticationManager).authenticate(any()); |
||||
verifyNoInteractions(filterChain); |
||||
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), |
||||
same(pushedAuthorizationRequestAuthenticationResult)); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri"); |
||||
OAuth2AuthenticationException authenticationException = new OAuth2AuthenticationException(error); |
||||
given(this.authenticationManager.authenticate(any())).willThrow(authenticationException); |
||||
|
||||
AuthenticationFailureHandler authenticationFailureHandler = mock(AuthenticationFailureHandler.class); |
||||
this.filter.setAuthenticationFailureHandler(authenticationFailureHandler); |
||||
|
||||
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
FilterChain filterChain = mock(FilterChain.class); |
||||
|
||||
this.filter.doFilter(request, response, filterChain); |
||||
|
||||
verify(this.authenticationManager).authenticate(any()); |
||||
verifyNoInteractions(filterChain); |
||||
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), same(authenticationException)); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient); |
||||
|
||||
AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource = mock( |
||||
AuthenticationDetailsSource.class); |
||||
WebAuthenticationDetails webAuthenticationDetails = new WebAuthenticationDetails(request); |
||||
given(authenticationDetailsSource.buildDetails(request)).willReturn(webAuthenticationDetails); |
||||
this.filter.setAuthenticationDetailsSource(authenticationDetailsSource); |
||||
|
||||
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, "request_uri", |
||||
Instant.now().plusSeconds(30), registeredClient.getRedirectUris().iterator().next(), STATE, |
||||
registeredClient.getScopes()); |
||||
|
||||
given(this.authenticationManager.authenticate(any())) |
||||
.willReturn(pushedAuthorizationRequestAuthenticationResult); |
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
FilterChain filterChain = mock(FilterChain.class); |
||||
|
||||
this.filter.doFilter(request, response, filterChain); |
||||
|
||||
verify(authenticationDetailsSource).buildDetails(any()); |
||||
verify(this.authenticationManager).authenticate(any()); |
||||
} |
||||
|
||||
@Test |
||||
public void doFilterWhenPushedAuthorizationRequestAuthenticatedThenPushedAuthorizationResponse() throws Exception { |
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build(); |
||||
String requestUri = "request_uri"; |
||||
Instant requestUriExpiresAt = Instant.now().plusSeconds(30); |
||||
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken( |
||||
AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, requestUri, |
||||
requestUriExpiresAt, registeredClient.getRedirectUris().iterator().next(), STATE, |
||||
registeredClient.getScopes()); |
||||
given(this.authenticationManager.authenticate(any())) |
||||
.willReturn(pushedAuthorizationRequestAuthenticationResult); |
||||
|
||||
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient); |
||||
request.addParameter("custom-param", "custom-value-1", "custom-value-2"); |
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
FilterChain filterChain = mock(FilterChain.class); |
||||
|
||||
this.filter.doFilter(request, response, filterChain); |
||||
|
||||
ArgumentCaptor<OAuth2PushedAuthorizationRequestAuthenticationToken> pushedAuthorizationRequestAuthenticationCaptor = ArgumentCaptor |
||||
.forClass(OAuth2PushedAuthorizationRequestAuthenticationToken.class); |
||||
verify(this.authenticationManager).authenticate(pushedAuthorizationRequestAuthenticationCaptor.capture()); |
||||
verifyNoInteractions(filterChain); |
||||
|
||||
assertThat(pushedAuthorizationRequestAuthenticationCaptor.getValue().getDetails()) |
||||
.asInstanceOf(InstanceOfAssertFactories.type(WebAuthenticationDetails.class)) |
||||
.extracting(WebAuthenticationDetails::getRemoteAddress) |
||||
.isEqualTo(REMOTE_ADDRESS); |
||||
|
||||
// Assert that multi-valued request parameters are preserved
|
||||
assertThat(pushedAuthorizationRequestAuthenticationCaptor.getValue().getAdditionalParameters()) |
||||
.extracting((params) -> params.get("custom-param")) |
||||
.asInstanceOf(InstanceOfAssertFactories.type(String[].class)) |
||||
.isEqualTo(new String[] { "custom-value-1", "custom-value-2" }); |
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value()); |
||||
Map<String, Object> responseParameters = readPushedAuthorizationResponse(response); |
||||
assertThat(responseParameters.get("request_uri")).isEqualTo(requestUri); |
||||
assertThat(responseParameters.get("expires_in")) |
||||
.isEqualTo((int) ChronoUnit.SECONDS.between(Instant.now(), requestUriExpiresAt)); |
||||
} |
||||
|
||||
private void doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(RegisteredClient registeredClient, |
||||
String parameterName, String errorCode, Consumer<MockHttpServletRequest> requestConsumer) throws Exception { |
||||
|
||||
doFilterWhenRequestInvalidParameterThenError(createPushedAuthorizationRequest(registeredClient), parameterName, |
||||
errorCode, requestConsumer); |
||||
} |
||||
|
||||
private void doFilterWhenRequestInvalidParameterThenError(MockHttpServletRequest request, String parameterName, |
||||
String errorCode, Consumer<MockHttpServletRequest> requestConsumer) throws Exception { |
||||
|
||||
requestConsumer.accept(request); |
||||
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||
FilterChain filterChain = mock(FilterChain.class); |
||||
|
||||
this.filter.doFilter(request, response, filterChain); |
||||
|
||||
verifyNoInteractions(filterChain); |
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); |
||||
OAuth2Error error = readError(response); |
||||
assertThat(error.getErrorCode()).isEqualTo(errorCode); |
||||
assertThat(error.getDescription()).isEqualTo("OAuth 2.0 Parameter: " + parameterName); |
||||
} |
||||
|
||||
private static MockHttpServletRequest createPushedAuthorizationRequest(RegisteredClient registeredClient) { |
||||
String requestUri = AuthorizationServerContextHolder.getContext() |
||||
.getAuthorizationServerSettings() |
||||
.getPushedAuthorizationRequestEndpoint(); |
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri); |
||||
request.setServletPath(requestUri); |
||||
request.setRemoteAddr(REMOTE_ADDRESS); |
||||
|
||||
request.addParameter(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue()); |
||||
request.addParameter(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()); |
||||
request.addParameter(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next()); |
||||
request.addParameter(OAuth2ParameterNames.SCOPE, |
||||
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " ")); |
||||
request.addParameter(OAuth2ParameterNames.STATE, "state"); |
||||
|
||||
return request; |
||||
} |
||||
|
||||
private OAuth2Error readError(MockHttpServletResponse response) throws Exception { |
||||
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(), |
||||
HttpStatus.valueOf(response.getStatus())); |
||||
return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private Map<String, Object> readPushedAuthorizationResponse(MockHttpServletResponse response) throws Exception { |
||||
final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() { |
||||
}; |
||||
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(), |
||||
HttpStatus.valueOf(response.getStatus())); |
||||
return (Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, httpResponse); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue