Add support for OAuth 2.0 Pushed Authorization Requests (PAR)
Closes gh-210 Signed-off-by: Joe Grandja <10884212+jgrandja@users.noreply.github.com>
This commit is contained in:
@@ -7,4 +7,5 @@
|
||||
<suppress files="SpringAuthorizationServerVersion\.java" checks="HideUtilityClassConstructor"/>
|
||||
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toLowerCaseWithoutLocale"/>
|
||||
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toUpperCaseWithoutLocale"/>
|
||||
<suppress files="AbstractOAuth2AuthorizationCodeRequestAuthenticationToken\.java" checks="SpringMethodVisibility"/>
|
||||
</suppressions>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -206,6 +206,8 @@ public final class JwtClientAssertionDecoderFactory implements JwtDecoderFactory
|
||||
authorizationServerSettings.getTokenIntrospectionEndpoint()));
|
||||
audience.add(asUrl(authorizationServerContext.getIssuer(),
|
||||
authorizationServerSettings.getTokenRevocationEndpoint()));
|
||||
audience.add(asUrl(authorizationServerContext.getIssuer(),
|
||||
authorizationServerSettings.getPushedAuthorizationRequestEndpoint()));
|
||||
return audience;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -27,7 +27,6 @@ import java.util.function.Predicate;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
@@ -39,7 +38,6 @@ 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.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.OAuth2AuthorizationCode;
|
||||
@@ -81,7 +79,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
|
||||
|
||||
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
|
||||
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
|
||||
|
||||
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(
|
||||
Base64.getUrlEncoder());
|
||||
@@ -122,6 +120,13 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
|
||||
|
||||
String requestUri = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
|
||||
.get("request_uri");
|
||||
if (StringUtils.hasText(requestUri)) {
|
||||
authorizationCodeRequestAuthentication = fromPushedAuthorizationRequest(
|
||||
authorizationCodeRequestAuthentication);
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository
|
||||
.findByClientId(authorizationCodeRequestAuthentication.getClientId());
|
||||
if (registeredClient == null) {
|
||||
@@ -136,47 +141,28 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
OAuth2AuthorizationCodeRequestAuthenticationContext.Builder authenticationContextBuilder = OAuth2AuthorizationCodeRequestAuthenticationContext
|
||||
.with(authorizationCodeRequestAuthentication)
|
||||
.registeredClient(registeredClient);
|
||||
this.authenticationValidator.accept(authenticationContextBuilder.build());
|
||||
OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = authenticationContextBuilder
|
||||
.build();
|
||||
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
||||
if (this.logger.isDebugEnabled()) {
|
||||
this.logger.debug(LogMessage.format(
|
||||
"Invalid request: requested grant_type is not allowed" + " for registered client '%s'",
|
||||
registeredClient.getId()));
|
||||
}
|
||||
throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
// 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)
|
||||
String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
|
||||
.get(PkceParameterNames.CODE_CHALLENGE);
|
||||
if (StringUtils.hasText(codeChallenge)) {
|
||||
String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
|
||||
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
||||
if (!StringUtils.hasText(codeChallengeMethod) || !"S256".equals(codeChallengeMethod)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
}
|
||||
else if (registeredClient.getClientSettings().isRequireProofKey()) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient, null);
|
||||
}
|
||||
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_CODE_CHALLENGE_VALIDATOR
|
||||
.accept(authenticationContext);
|
||||
|
||||
// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
|
||||
Set<String> promptValues = Collections.emptySet();
|
||||
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
|
||||
String prompt = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get("prompt");
|
||||
if (StringUtils.hasText(prompt)) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_PROMPT_VALIDATOR
|
||||
.accept(authenticationContext);
|
||||
promptValues = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(prompt, " ")));
|
||||
if (promptValues.contains(OidcPrompts.NONE)) {
|
||||
if (promptValues.contains(OidcPrompts.LOGIN) || promptValues.contains(OidcPrompts.CONSENT)
|
||||
|| promptValues.contains(OidcPrompts.SELECT_ACCOUNT)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authorizationCodeRequestAuthentication,
|
||||
registeredClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +176,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
|
||||
Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
|
||||
if (!isPrincipalAuthenticated(principal)) {
|
||||
if (promptValues.contains(OidcPrompts.NONE)) {
|
||||
if (promptValues.contains(OidcPrompt.NONE)) {
|
||||
// Return an error instead of displaying the login page (via the
|
||||
// configured AuthenticationEntryPoint)
|
||||
throwError("login_required", "prompt", authorizationCodeRequestAuthentication, registeredClient);
|
||||
@@ -219,7 +205,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
}
|
||||
|
||||
if (this.authorizationConsentRequired.test(authenticationContextBuilder.build())) {
|
||||
if (promptValues.contains(OidcPrompts.NONE)) {
|
||||
if (promptValues.contains(OidcPrompt.NONE)) {
|
||||
// Return an error instead of displaying the consent page
|
||||
throwError("consent_required", "prompt", authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
@@ -347,6 +333,37 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
this.authorizationConsentRequired = authorizationConsentRequired;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizationCodeRequestAuthenticationToken fromPushedAuthorizationRequest(
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication) {
|
||||
|
||||
String requestUri = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
|
||||
.get("request_uri");
|
||||
|
||||
OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = null;
|
||||
try {
|
||||
pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri.parse(requestUri);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "request_uri", authorizationCodeRequestAuthentication, null);
|
||||
}
|
||||
|
||||
OAuth2Authorization authorization = this.authorizationService
|
||||
.findByToken(pushedAuthorizationRequestUri.getState(), STATE_TOKEN_TYPE);
|
||||
if (authorization == null) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "request_uri", authorizationCodeRequestAuthentication, null);
|
||||
}
|
||||
|
||||
OAuth2AuthorizationRequest authorizationRequest = authorization
|
||||
.getAttribute(OAuth2AuthorizationRequest.class.getName());
|
||||
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
authorizationCodeRequestAuthentication.getAuthorizationUri(),
|
||||
authorizationCodeRequestAuthentication.getClientId(),
|
||||
(Authentication) authorizationCodeRequestAuthentication.getPrincipal(),
|
||||
authorizationRequest.getRedirectUri(), authorizationRequest.getState(),
|
||||
authorizationRequest.getScopes(), authorizationRequest.getAdditionalParameters());
|
||||
}
|
||||
|
||||
private static boolean isAuthorizationConsentRequired(
|
||||
OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
|
||||
if (!authenticationContext.getRegisteredClient().getClientSettings().isRequireAuthorizationConsent()) {
|
||||
@@ -457,23 +474,4 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* The values defined for the "prompt" parameter for the OpenID Connect 1.0
|
||||
* Authentication Request.
|
||||
*/
|
||||
private static final class OidcPrompts {
|
||||
|
||||
private static final String NONE = "none";
|
||||
|
||||
private static final String LOGIN = "login";
|
||||
|
||||
private static final String CONSENT = "consent";
|
||||
|
||||
private static final String SELECT_ACCOUNT = "select_account";
|
||||
|
||||
private OidcPrompts() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -15,17 +15,12 @@
|
||||
*/
|
||||
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.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -37,23 +32,8 @@ import org.springframework.util.Assert;
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider
|
||||
* @see OAuth2AuthorizationConsentAuthenticationProvider
|
||||
*/
|
||||
public class OAuth2AuthorizationCodeRequestAuthenticationToken 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;
|
||||
public class OAuth2AuthorizationCodeRequestAuthenticationToken
|
||||
extends AbstractOAuth2AuthorizationCodeRequestAuthenticationToken {
|
||||
|
||||
private final OAuth2AuthorizationCode authorizationCode;
|
||||
|
||||
@@ -72,18 +52,7 @@ public class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractA
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationToken(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());
|
||||
super(authorizationUri, clientId, principal, redirectUri, state, scopes, additionalParameters);
|
||||
this.authorizationCode = null;
|
||||
}
|
||||
|
||||
@@ -102,83 +71,12 @@ public class OAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractA
|
||||
public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId,
|
||||
Authentication principal, OAuth2AuthorizationCode authorizationCode, @Nullable String redirectUri,
|
||||
@Nullable String state, @Nullable Set<String> scopes) {
|
||||
super(Collections.emptyList());
|
||||
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
Assert.notNull(principal, "principal cannot be null");
|
||||
super(authorizationUri, clientId, principal, redirectUri, state, scopes, null);
|
||||
Assert.notNull(authorizationCode, "authorizationCode cannot be null");
|
||||
this.authorizationUri = authorizationUri;
|
||||
this.clientId = clientId;
|
||||
this.principal = principal;
|
||||
this.authorizationCode = authorizationCode;
|
||||
this.redirectUri = redirectUri;
|
||||
this.state = state;
|
||||
this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
|
||||
this.additionalParameters = Collections.emptyMap();
|
||||
setAuthenticated(true);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link OAuth2AuthorizationCode}.
|
||||
* @return the {@link OAuth2AuthorizationCode}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -23,9 +25,11 @@ import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -51,19 +55,22 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationContext
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
* @see OAuth2PushedAuthorizationRequestAuthenticationProvider#setAuthenticationValidator(Consumer)
|
||||
*/
|
||||
public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
|
||||
implements Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> {
|
||||
|
||||
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
|
||||
|
||||
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(OAuth2AuthorizationCodeRequestAuthenticationValidator.class);
|
||||
|
||||
/**
|
||||
* The default validator for
|
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
|
||||
*/
|
||||
public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateScope;
|
||||
static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_AUTHORIZATION_GRANT_TYPE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateAuthorizationGrantType;
|
||||
|
||||
static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_CODE_CHALLENGE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateCodeChallenge;
|
||||
|
||||
static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_PROMPT_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validatePrompt;
|
||||
|
||||
/**
|
||||
* The default validator for
|
||||
@@ -71,6 +78,12 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
|
||||
*/
|
||||
public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateRedirectUri;
|
||||
|
||||
/**
|
||||
* The default validator for
|
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
|
||||
*/
|
||||
public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateScope;
|
||||
|
||||
private final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR
|
||||
.andThen(DEFAULT_SCOPE_VALIDATOR);
|
||||
|
||||
@@ -79,20 +92,18 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
|
||||
this.authenticationValidator.accept(authenticationContext);
|
||||
}
|
||||
|
||||
private static void validateScope(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
|
||||
private static void validateAuthorizationGrantType(
|
||||
OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
|
||||
|
||||
Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
|
||||
Set<String> allowedScopes = registeredClient.getScopes();
|
||||
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
|
||||
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(LogMessage.format(
|
||||
"Invalid request: requested scope is not allowed" + " for registered client '%s'",
|
||||
"Invalid request: requested grant_type is not allowed for registered client '%s'",
|
||||
registeredClient.getId()));
|
||||
}
|
||||
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
||||
throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
@@ -151,7 +162,7 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
|
||||
if (!validRedirectUri) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(LogMessage.format(
|
||||
"Invalid request: redirect_uri does not match" + " for registered client '%s'",
|
||||
"Invalid request: redirect_uri does not match for registered client '%s'",
|
||||
registeredClient.getId()));
|
||||
}
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI,
|
||||
@@ -172,6 +183,69 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateScope(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
|
||||
|
||||
Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
|
||||
Set<String> allowedScopes = registeredClient.getScopes();
|
||||
if (!requestedScopes.isEmpty() && !allowedScopes.containsAll(requestedScopes)) {
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(
|
||||
LogMessage.format("Invalid request: requested scope is not allowed for registered client '%s'",
|
||||
registeredClient.getId()));
|
||||
}
|
||||
throwError(OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateCodeChallenge(
|
||||
OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
|
||||
|
||||
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
|
||||
String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
|
||||
.get(PkceParameterNames.CODE_CHALLENGE);
|
||||
if (StringUtils.hasText(codeChallenge)) {
|
||||
String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
|
||||
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
|
||||
if (!StringUtils.hasText(codeChallengeMethod) || !"S256".equals(codeChallengeMethod)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
else if (registeredClient.getClientSettings().isRequireProofKey()) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
|
||||
authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validatePrompt(OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
RegisteredClient registeredClient = authenticationContext.getRegisteredClient();
|
||||
|
||||
// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
|
||||
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
|
||||
String prompt = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get("prompt");
|
||||
if (StringUtils.hasText(prompt)) {
|
||||
Set<String> promptValues = new HashSet<>(
|
||||
Arrays.asList(StringUtils.delimitedListToStringArray(prompt, " ")));
|
||||
if (promptValues.contains(OidcPrompt.NONE)) {
|
||||
if (promptValues.contains(OidcPrompt.LOGIN) || promptValues.contains(OidcPrompt.CONSENT)
|
||||
|| promptValues.contains(OidcPrompt.SELECT_ACCOUNT)) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authorizationCodeRequestAuthentication,
|
||||
registeredClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLoopbackAddress(String host) {
|
||||
if (!StringUtils.hasText(host)) {
|
||||
return false;
|
||||
@@ -201,7 +275,13 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationValidator
|
||||
private static void throwError(String errorCode, String parameterName,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, ERROR_URI);
|
||||
throwError(errorCode, parameterName, ERROR_URI, authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
private static void throwError(String errorCode, String parameterName, String errorUri,
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
|
||||
RegisteredClient registeredClient) {
|
||||
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
|
||||
throwError(error, parameterName, authorizationCodeRequestAuthentication, registeredClient);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -20,6 +20,7 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
|
||||
@@ -42,6 +43,7 @@ import org.springframework.security.oauth2.core.OAuth2Token;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
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.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
@@ -69,6 +71,7 @@ import org.springframework.util.Assert;
|
||||
* @see OAuth2ClientAuthenticationConfigurer
|
||||
* @see OAuth2AuthorizationServerMetadataEndpointConfigurer
|
||||
* @see OAuth2AuthorizationEndpointConfigurer
|
||||
* @see OAuth2PushedAuthorizationRequestEndpointConfigurer
|
||||
* @see OAuth2TokenEndpointConfigurer
|
||||
* @see OAuth2TokenIntrospectionEndpointConfigurer
|
||||
* @see OAuth2TokenRevocationEndpointConfigurer
|
||||
@@ -196,6 +199,27 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OAuth 2.0 Pushed Authorization Request Endpoint.
|
||||
* @param pushedAuthorizationRequestEndpointCustomizer the {@link Customizer}
|
||||
* providing access to the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer}
|
||||
* @return the {@link OAuth2AuthorizationServerConfigurer} for further configuration
|
||||
* @since 1.5
|
||||
*/
|
||||
public OAuth2AuthorizationServerConfigurer pushedAuthorizationRequestEndpoint(
|
||||
Customizer<OAuth2PushedAuthorizationRequestEndpointConfigurer> pushedAuthorizationRequestEndpointCustomizer) {
|
||||
OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
|
||||
OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
|
||||
if (pushedAuthorizationRequestEndpointConfigurer == null) {
|
||||
addConfigurer(OAuth2PushedAuthorizationRequestEndpointConfigurer.class,
|
||||
new OAuth2PushedAuthorizationRequestEndpointConfigurer(this::postProcess));
|
||||
pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
|
||||
OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
|
||||
}
|
||||
pushedAuthorizationRequestEndpointCustomizer.customize(pushedAuthorizationRequestEndpointConfigurer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the OAuth 2.0 Token Endpoint.
|
||||
* @param tokenEndpointCustomizer the {@link Customizer} providing access to the
|
||||
@@ -314,20 +338,28 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
else {
|
||||
// OpenID Connect is disabled.
|
||||
// Add an authentication validator that rejects authentication requests.
|
||||
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> oidcAuthenticationRequestValidator = (
|
||||
authenticationContext) -> {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
|
||||
"OpenID Connect 1.0 authentication requests are restricted.",
|
||||
"https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1");
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,
|
||||
authorizationCodeRequestAuthentication);
|
||||
}
|
||||
};
|
||||
OAuth2AuthorizationEndpointConfigurer authorizationEndpointConfigurer = getConfigurer(
|
||||
OAuth2AuthorizationEndpointConfigurer.class);
|
||||
authorizationEndpointConfigurer
|
||||
.addAuthorizationCodeRequestAuthenticationValidator((authenticationContext) -> {
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationContext
|
||||
.getAuthentication();
|
||||
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
|
||||
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE,
|
||||
"OpenID Connect 1.0 authentication requests are restricted.",
|
||||
"https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1");
|
||||
throw new OAuth2AuthorizationCodeRequestAuthenticationException(error,
|
||||
authorizationCodeRequestAuthentication);
|
||||
}
|
||||
});
|
||||
.addAuthorizationCodeRequestAuthenticationValidator(oidcAuthenticationRequestValidator);
|
||||
OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestEndpointConfigurer = getConfigurer(
|
||||
OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
|
||||
if (pushedAuthorizationRequestEndpointConfigurer != null) {
|
||||
pushedAuthorizationRequestEndpointConfigurer
|
||||
.addAuthorizationCodeRequestAuthenticationValidator(oidcAuthenticationRequestValidator);
|
||||
}
|
||||
}
|
||||
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
@@ -344,11 +376,18 @@ public final class OAuth2AuthorizationServerConfigurer
|
||||
ExceptionHandlingConfigurer<HttpSecurity> exceptionHandling = httpSecurity
|
||||
.getConfigurer(ExceptionHandlingConfigurer.class);
|
||||
if (exceptionHandling != null) {
|
||||
List<RequestMatcher> preferredMatchers = new ArrayList<>();
|
||||
preferredMatchers.add(getRequestMatcher(OAuth2TokenEndpointConfigurer.class));
|
||||
preferredMatchers.add(getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class));
|
||||
preferredMatchers.add(getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class));
|
||||
preferredMatchers.add(getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class));
|
||||
RequestMatcher preferredMatcher = getRequestMatcher(
|
||||
OAuth2PushedAuthorizationRequestEndpointConfigurer.class);
|
||||
if (preferredMatcher != null) {
|
||||
preferredMatchers.add(preferredMatcher);
|
||||
}
|
||||
exceptionHandling.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
|
||||
new OrRequestMatcher(getRequestMatcher(OAuth2TokenEndpointConfigurer.class),
|
||||
getRequestMatcher(OAuth2TokenIntrospectionEndpointConfigurer.class),
|
||||
getRequestMatcher(OAuth2TokenRevocationEndpointConfigurer.class),
|
||||
getRequestMatcher(OAuth2DeviceAuthorizationEndpointConfigurer.class)));
|
||||
new OrRequestMatcher(preferredMatchers));
|
||||
}
|
||||
|
||||
httpSecurity.csrf((csrf) -> csrf.ignoringRequestMatchers(this.endpointsMatcher));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -196,10 +196,15 @@ public final class OAuth2ClientAuthenticationConfigurer extends AbstractOAuth2Co
|
||||
? OAuth2ConfigurerUtils
|
||||
.withMultipleIssuersPattern(authorizationServerSettings.getDeviceAuthorizationEndpoint())
|
||||
: authorizationServerSettings.getDeviceAuthorizationEndpoint();
|
||||
String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
|
||||
? OAuth2ConfigurerUtils
|
||||
.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
|
||||
: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
|
||||
this.requestMatcher = new OrRequestMatcher(new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(tokenIntrospectionEndpointUri, HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(tokenRevocationEndpointUri, HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(deviceAuthorizationEndpointUri, HttpMethod.POST.name()));
|
||||
new AntPathRequestMatcher(deviceAuthorizationEndpointUri, HttpMethod.POST.name()),
|
||||
new AntPathRequestMatcher(pushedAuthorizationRequestEndpointUri, HttpMethod.POST.name()));
|
||||
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
|
||||
if (!this.authenticationProviders.isEmpty()) {
|
||||
authenticationProviders.addAll(0, this.authenticationProviders);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -72,6 +72,16 @@ public final class AuthorizationServerSettings extends AbstractSettings {
|
||||
return getSetting(ConfigurationSettingNames.AuthorizationServer.AUTHORIZATION_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OAuth 2.0 Pushed Authorization Request endpoint. The default is
|
||||
* {@code /oauth2/par}.
|
||||
* @return the Pushed Authorization Request endpoint
|
||||
* @since 1.5
|
||||
*/
|
||||
public String getPushedAuthorizationRequestEndpoint() {
|
||||
return getSetting(ConfigurationSettingNames.AuthorizationServer.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OAuth 2.0 Device Authorization endpoint. The default is
|
||||
* {@code /oauth2/device_authorization}.
|
||||
@@ -160,6 +170,7 @@ public final class AuthorizationServerSettings extends AbstractSettings {
|
||||
public static Builder builder() {
|
||||
return new Builder().multipleIssuersAllowed(false)
|
||||
.authorizationEndpoint("/oauth2/authorize")
|
||||
.pushedAuthorizationRequestEndpoint("/oauth2/par")
|
||||
.deviceAuthorizationEndpoint("/oauth2/device_authorization")
|
||||
.deviceVerificationEndpoint("/oauth2/device_verification")
|
||||
.tokenEndpoint("/oauth2/token")
|
||||
@@ -236,6 +247,18 @@ public final class AuthorizationServerSettings extends AbstractSettings {
|
||||
return setting(ConfigurationSettingNames.AuthorizationServer.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the OAuth 2.0 Pushed Authorization Request endpoint.
|
||||
* @param pushedAuthorizationRequestEndpoint the Pushed Authorization Request
|
||||
* endpoint
|
||||
* @return the {@link Builder} for further configuration
|
||||
* @since 1.5
|
||||
*/
|
||||
public Builder pushedAuthorizationRequestEndpoint(String pushedAuthorizationRequestEndpoint) {
|
||||
return setting(ConfigurationSettingNames.AuthorizationServer.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT,
|
||||
pushedAuthorizationRequestEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the OAuth 2.0 Device Authorization endpoint.
|
||||
* @param deviceAuthorizationEndpoint the Device Authorization endpoint
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -112,6 +112,13 @@ public final class ConfigurationSettingNames {
|
||||
public static final String AUTHORIZATION_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE
|
||||
.concat("authorization-endpoint");
|
||||
|
||||
/**
|
||||
* Set the OAuth 2.0 Pushed Authorization Request endpoint.
|
||||
* @since 1.5
|
||||
*/
|
||||
public static final String PUSHED_AUTHORIZATION_REQUEST_ENDPOINT = AUTHORIZATION_SERVER_SETTINGS_NAMESPACE
|
||||
.concat("pushed-authorization-request-endpoint");
|
||||
|
||||
/**
|
||||
* Set the OAuth 2.0 Device Authorization endpoint.
|
||||
*/
|
||||
|
||||
@@ -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 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.web.authenticat
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -35,7 +36,12 @@ import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContext;
|
||||
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2PushedAuthorizationRequestEndpointFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationConverter;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
@@ -47,14 +53,17 @@ import org.springframework.util.StringUtils;
|
||||
/**
|
||||
* Attempts to extract an Authorization Request from {@link HttpServletRequest} for the
|
||||
* OAuth 2.0 Authorization Code Grant and then converts it to an
|
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken} used for authenticating the
|
||||
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken} OR
|
||||
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} used for authenticating the
|
||||
* request.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 0.1.2
|
||||
* @see AuthenticationConverter
|
||||
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
|
||||
* @see OAuth2PushedAuthorizationRequestAuthenticationToken
|
||||
* @see OAuth2AuthorizationEndpointFilter
|
||||
* @see OAuth2PushedAuthorizationRequestEndpointFilter
|
||||
*/
|
||||
public final class OAuth2AuthorizationCodeRequestAuthenticationConverter implements AuthenticationConverter {
|
||||
|
||||
@@ -76,13 +85,30 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
|
||||
MultiValueMap<String, String> parameters = "GET".equals(request.getMethod())
|
||||
? OAuth2EndpointUtils.getQueryParameters(request) : OAuth2EndpointUtils.getFormParameters(request);
|
||||
|
||||
// response_type (REQUIRED)
|
||||
String responseType = parameters.getFirst(OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
if (!StringUtils.hasText(responseType) || parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
boolean pushedAuthorizationRequest = isPushedAuthorizationRequest(request);
|
||||
|
||||
// request_uri (OPTIONAL) - provided if an authorization request was previously
|
||||
// pushed (RFC 9126 OAuth 2.0 Pushed Authorization Requests)
|
||||
String requestUri = parameters.getFirst("request_uri");
|
||||
if (StringUtils.hasText(requestUri)) {
|
||||
if (pushedAuthorizationRequest) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "request_uri");
|
||||
}
|
||||
else if (parameters.get("request_uri").size() != 1) {
|
||||
// Authorization Request
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "request_uri");
|
||||
}
|
||||
}
|
||||
else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
||||
throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
|
||||
if (!StringUtils.hasText(requestUri)) {
|
||||
// response_type (REQUIRED)
|
||||
String responseType = parameters.getFirst(OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
if (!StringUtils.hasText(responseType) || parameters.get(OAuth2ParameterNames.RESPONSE_TYPE).size() != 1) {
|
||||
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
}
|
||||
else if (!responseType.equals(OAuth2AuthorizationResponseType.CODE.getValue())) {
|
||||
throwError(OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE, OAuth2ParameterNames.RESPONSE_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
String authorizationUri = request.getRequestURL().toString();
|
||||
@@ -150,8 +176,24 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationConverter impleme
|
||||
}
|
||||
});
|
||||
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal, redirectUri,
|
||||
state, scopes, additionalParameters);
|
||||
if (pushedAuthorizationRequest) {
|
||||
return new OAuth2PushedAuthorizationRequestAuthenticationToken(authorizationUri, clientId, principal,
|
||||
redirectUri, state, scopes, additionalParameters);
|
||||
}
|
||||
else {
|
||||
return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,
|
||||
redirectUri, state, scopes, additionalParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPushedAuthorizationRequest(HttpServletRequest request) {
|
||||
AuthorizationServerContext authorizationServerContext = AuthorizationServerContextHolder.getContext();
|
||||
AuthorizationServerSettings authorizationServerSettings = authorizationServerContext
|
||||
.getAuthorizationServerSettings();
|
||||
return request.getRequestURL()
|
||||
.toString()
|
||||
.toLowerCase(Locale.ROOT)
|
||||
.endsWith(authorizationServerSettings.getPushedAuthorizationRequestEndpoint().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private static RequestMatcher createDefaultRequestMatcher() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -42,6 +42,8 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
@@ -50,6 +52,7 @@ import org.springframework.security.oauth2.server.authorization.context.TestAuth
|
||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -71,6 +74,8 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
|
||||
|
||||
private static final String STATE = "state";
|
||||
|
||||
private static final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
|
||||
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
@@ -602,6 +607,59 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
|
||||
authenticationResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationCodeRequestWithRequestUriThenReturnAuthorizationCode() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
given(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.willReturn(registeredClient);
|
||||
|
||||
OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri
|
||||
.create();
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put("request_uri", pushedAuthorizationRequestUri.getRequestUri());
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||
.authorization(registeredClient, additionalParameters)
|
||||
.build();
|
||||
given(this.authorizationService.findByToken(eq(pushedAuthorizationRequestUri.getState()), eq(STATE_TOKEN_TYPE)))
|
||||
.willReturn(authorization);
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
AUTHORIZATION_URI, registeredClient.getClientId(), this.principal, null, null, null,
|
||||
additionalParameters);
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authenticationResult = (OAuth2AuthorizationCodeRequestAuthenticationToken) this.authenticationProvider
|
||||
.authenticate(authentication);
|
||||
|
||||
assertAuthorizationCodeRequestWithAuthorizationCodeResult(registeredClient, authentication,
|
||||
authenticationResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationCodeRequestWithInvalidRequestUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
given(this.registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.willReturn(registeredClient);
|
||||
|
||||
OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri
|
||||
.create();
|
||||
Map<String, Object> additionalParameters = new HashMap<>();
|
||||
additionalParameters.put("request_uri", pushedAuthorizationRequestUri.getRequestUri());
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations
|
||||
.authorization(registeredClient, additionalParameters)
|
||||
.build();
|
||||
given(this.authorizationService.findByToken(eq(pushedAuthorizationRequestUri.getState()), eq(STATE_TOKEN_TYPE)))
|
||||
.willReturn(authorization);
|
||||
|
||||
OAuth2AuthorizationCodeRequestAuthenticationToken authentication = new OAuth2AuthorizationCodeRequestAuthenticationToken(
|
||||
AUTHORIZATION_URI, registeredClient.getClientId(), this.principal, null, null, null,
|
||||
Collections.singletonMap("request_uri", "invalid_request_uri"));
|
||||
|
||||
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
|
||||
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
|
||||
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
|
||||
OAuth2ErrorCodes.INVALID_REQUEST, "request_uri", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateWhenAuthorizationCodeNotGeneratedThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
@@ -665,11 +723,15 @@ public class OAuth2AuthorizationCodeRequestAuthenticationProviderTests {
|
||||
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());
|
||||
|
||||
String requestUri = (String) authentication.getAdditionalParameters().get("request_uri");
|
||||
if (!StringUtils.hasText(requestUri)) {
|
||||
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(this.principal.getName());
|
||||
assertThat(authorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
@@ -1012,6 +1013,67 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
assertThat(cnfClaims).containsKey("jkt");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenPushedAuthorizationRequestThenReturnAccessTokenResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigurationWithPushedAuthorizationRequests.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
|
||||
this.registeredClientRepository.save(registeredClient);
|
||||
|
||||
MvcResult mvcResult = this.mvc
|
||||
.perform(post("/oauth2/par").params(getAuthorizationRequestParameters(registeredClient))
|
||||
.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
|
||||
.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
|
||||
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
|
||||
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
|
||||
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.request_uri").isNotEmpty())
|
||||
.andExpect(jsonPath("$.expires_in").isNotEmpty())
|
||||
.andReturn();
|
||||
|
||||
String requestUri = JsonPath.read(mvcResult.getResponse().getContentAsString(), "$.request_uri");
|
||||
|
||||
mvcResult = this.mvc
|
||||
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.queryParam("request_uri", requestUri)
|
||||
.with(user("user")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
String authorizationCode = extractParameterFromRedirectUri(mvcResult.getResponse().getRedirectedUrl(), "code");
|
||||
OAuth2Authorization authorizationCodeAuthorization = this.authorizationService.findByToken(authorizationCode,
|
||||
AUTHORIZATION_CODE_TOKEN_TYPE);
|
||||
|
||||
this.mvc
|
||||
.perform(post(DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
|
||||
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER)
|
||||
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
|
||||
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
|
||||
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.token_type").isNotEmpty())
|
||||
.andExpect(jsonPath("$.expires_in").isNotEmpty())
|
||||
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.scope").isNotEmpty())
|
||||
.andReturn();
|
||||
|
||||
OAuth2Authorization accessTokenAuthorization = this.authorizationService
|
||||
.findById(authorizationCodeAuthorization.getId());
|
||||
assertThat(accessTokenAuthorization).isNotNull();
|
||||
assertThat(accessTokenAuthorization.getAccessToken()).isNotNull();
|
||||
|
||||
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCodeToken = accessTokenAuthorization
|
||||
.getToken(OAuth2AuthorizationCode.class);
|
||||
assertThat(authorizationCodeToken).isNotNull();
|
||||
assertThat(authorizationCodeToken.getMetadata().get(OAuth2Authorization.Token.INVALIDATED_METADATA_NAME))
|
||||
.isEqualTo(true);
|
||||
}
|
||||
|
||||
private static String generateDPoPProof(String tokenEndpointUri) {
|
||||
// @formatter:off
|
||||
Map<String, Object> publicJwk = TestJwks.DEFAULT_EC_JWK
|
||||
@@ -1417,4 +1479,29 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class AuthorizationServerConfigurationWithPushedAuthorizationRequests
|
||||
extends AuthorizationServerConfiguration {
|
||||
|
||||
// @formatter:off
|
||||
@Bean
|
||||
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
OAuth2AuthorizationServerConfigurer.authorizationServer();
|
||||
http
|
||||
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
|
||||
.with(authorizationServerConfigurer, (authorizationServer) ->
|
||||
authorizationServer
|
||||
.pushedAuthorizationRequestEndpoint(Customizer.withDefaults())
|
||||
)
|
||||
.authorizeHttpRequests((authorize) ->
|
||||
authorize.anyRequest().authenticated()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -35,6 +35,7 @@ public class AuthorizationServerSettingsTests {
|
||||
assertThat(authorizationServerSettings.getIssuer()).isNull();
|
||||
assertThat(authorizationServerSettings.isMultipleIssuersAllowed()).isFalse();
|
||||
assertThat(authorizationServerSettings.getAuthorizationEndpoint()).isEqualTo("/oauth2/authorize");
|
||||
assertThat(authorizationServerSettings.getPushedAuthorizationRequestEndpoint()).isEqualTo("/oauth2/par");
|
||||
assertThat(authorizationServerSettings.getTokenEndpoint()).isEqualTo("/oauth2/token");
|
||||
assertThat(authorizationServerSettings.getJwkSetEndpoint()).isEqualTo("/oauth2/jwks");
|
||||
assertThat(authorizationServerSettings.getTokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
|
||||
@@ -47,6 +48,7 @@ public class AuthorizationServerSettingsTests {
|
||||
@Test
|
||||
public void buildWhenSettingsProvidedThenSet() {
|
||||
String authorizationEndpoint = "/oauth2/v1/authorize";
|
||||
String pushedAuthorizationRequestEndpoint = "/oauth2/v1/par";
|
||||
String tokenEndpoint = "/oauth2/v1/token";
|
||||
String jwkSetEndpoint = "/oauth2/v1/jwks";
|
||||
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
|
||||
@@ -59,6 +61,7 @@ public class AuthorizationServerSettingsTests {
|
||||
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
|
||||
.issuer(issuer)
|
||||
.authorizationEndpoint(authorizationEndpoint)
|
||||
.pushedAuthorizationRequestEndpoint(pushedAuthorizationRequestEndpoint)
|
||||
.tokenEndpoint(tokenEndpoint)
|
||||
.jwkSetEndpoint(jwkSetEndpoint)
|
||||
.tokenRevocationEndpoint(tokenRevocationEndpoint)
|
||||
@@ -72,6 +75,8 @@ public class AuthorizationServerSettingsTests {
|
||||
assertThat(authorizationServerSettings.getIssuer()).isEqualTo(issuer);
|
||||
assertThat(authorizationServerSettings.isMultipleIssuersAllowed()).isFalse();
|
||||
assertThat(authorizationServerSettings.getAuthorizationEndpoint()).isEqualTo(authorizationEndpoint);
|
||||
assertThat(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
|
||||
.isEqualTo(pushedAuthorizationRequestEndpoint);
|
||||
assertThat(authorizationServerSettings.getTokenEndpoint()).isEqualTo(tokenEndpoint);
|
||||
assertThat(authorizationServerSettings.getJwkSetEndpoint()).isEqualTo(jwkSetEndpoint);
|
||||
assertThat(authorizationServerSettings.getTokenRevocationEndpoint()).isEqualTo(tokenRevocationEndpoint);
|
||||
@@ -100,6 +105,7 @@ public class AuthorizationServerSettingsTests {
|
||||
assertThat(authorizationServerSettings.getIssuer()).isNull();
|
||||
assertThat(authorizationServerSettings.isMultipleIssuersAllowed()).isTrue();
|
||||
assertThat(authorizationServerSettings.getAuthorizationEndpoint()).isEqualTo("/oauth2/authorize");
|
||||
assertThat(authorizationServerSettings.getPushedAuthorizationRequestEndpoint()).isEqualTo("/oauth2/par");
|
||||
assertThat(authorizationServerSettings.getTokenEndpoint()).isEqualTo("/oauth2/token");
|
||||
assertThat(authorizationServerSettings.getJwkSetEndpoint()).isEqualTo("/oauth2/jwks");
|
||||
assertThat(authorizationServerSettings.getTokenRevocationEndpoint()).isEqualTo("/oauth2/revoke");
|
||||
@@ -116,7 +122,7 @@ public class AuthorizationServerSettingsTests {
|
||||
.settings((settings) -> settings.put("name2", "value2"))
|
||||
.build();
|
||||
|
||||
assertThat(authorizationServerSettings.getSettings()).hasSize(13);
|
||||
assertThat(authorizationServerSettings.getSettings()).hasSize(14);
|
||||
assertThat(authorizationServerSettings.<String>getSetting("name1")).isEqualTo("value1");
|
||||
assertThat(authorizationServerSettings.<String>getSetting("name2")).isEqualTo("value2");
|
||||
}
|
||||
@@ -134,6 +140,13 @@ public class AuthorizationServerSettingsTests {
|
||||
.withMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pushedAuthorizationRequestEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> AuthorizationServerSettings.builder().pushedAuthorizationRequestEndpoint(null))
|
||||
.withMessage("value cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tokenEndpointWhenNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> AuthorizationServerSettings.builder().tokenEndpoint(null))
|
||||
|
||||
@@ -54,6 +54,9 @@ import org.springframework.security.oauth2.server.authorization.authentication.O
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationToken;
|
||||
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;
|
||||
@@ -112,11 +115,14 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
Instant issuedAt = Instant.now();
|
||||
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES);
|
||||
this.authorizationCode = new OAuth2AuthorizationCode("code", issuedAt, expiresAt);
|
||||
AuthorizationServerContextHolder
|
||||
.setContext(new TestAuthorizationServerContext(AuthorizationServerSettings.builder().build(), null));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
SecurityContextHolder.clearContext();
|
||||
AuthorizationServerContextHolder.resetContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -181,6 +187,16 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestMultipleRequestUriThenInvalidRequestError() throws Exception {
|
||||
doFilterWhenAuthorizationRequestInvalidParameterThenError(TestRegisteredClients.registeredClient().build(),
|
||||
"request_uri", OAuth2ErrorCodes.INVALID_REQUEST, (request) -> {
|
||||
request.addParameter("request_uri", "request_uri");
|
||||
request.addParameter("request_uri", "request_uri_2");
|
||||
updateQueryString(request);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestMissingResponseTypeThenInvalidRequestError() throws Exception {
|
||||
doFilterWhenAuthorizationRequestInvalidParameterThenError(TestRegisteredClients.registeredClient().build(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user