Browse Source

Add support for OAuth 2.0 Pushed Authorization Requests (PAR)

Closes gh-210

Signed-off-by: Joe Grandja <10884212+jgrandja@users.noreply.github.com>
pull/1927/head
Joe Grandja 10 months ago
parent
commit
4337884e87
  1. 1
      etc/checkstyle/checkstyle-suppressions.xml
  2. 135
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/AbstractOAuth2AuthorizationCodeRequestAuthenticationToken.java
  3. 4
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtClientAssertionDecoderFactory.java
  4. 110
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java
  5. 112
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java
  6. 108
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java
  7. 184
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProvider.java
  8. 94
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationToken.java
  9. 80
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUri.java
  10. 38
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OidcPrompt.java
  11. 71
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java
  12. 9
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java
  13. 265
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2PushedAuthorizationRequestEndpointConfigurer.java
  14. 25
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java
  15. 9
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java
  16. 63
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/HttpMessageConverters.java
  17. 223
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2PushedAuthorizationRequestEndpointFilter.java
  18. 60
      oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java
  19. 72
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java
  20. 422
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProviderTests.java
  21. 87
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java
  22. 17
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettingsTests.java
  23. 16
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java
  24. 490
      oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2PushedAuthorizationRequestEndpointFilterTests.java

1
etc/checkstyle/checkstyle-suppressions.xml

@ -7,4 +7,5 @@ @@ -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>

135
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/AbstractOAuth2AuthorizationCodeRequestAuthenticationToken.java

@ -0,0 +1,135 @@ @@ -0,0 +1,135 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
import org.springframework.util.Assert;
/**
* An {@link Authentication} base implementation for the OAuth 2.0 Authorization Request
* used in the Authorization Code Grant.
*
* @author Joe Grandja
* @since 1.5
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
* @see OAuth2PushedAuthorizationRequestAuthenticationToken
*/
abstract class AbstractOAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
private final String authorizationUri;
private final String clientId;
private final Authentication principal;
private final String redirectUri;
private final String state;
private final Set<String> scopes;
private final Map<String, Object> additionalParameters;
protected AbstractOAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId,
Authentication principal, @Nullable String redirectUri, @Nullable String state,
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
super(Collections.emptyList());
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
Assert.hasText(clientId, "clientId cannot be empty");
Assert.notNull(principal, "principal cannot be null");
this.authorizationUri = authorizationUri;
this.clientId = clientId;
this.principal = principal;
this.redirectUri = redirectUri;
this.state = state;
this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
this.additionalParameters = Collections.unmodifiableMap(
(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the authorization URI.
* @return the authorization URI
*/
public String getAuthorizationUri() {
return this.authorizationUri;
}
/**
* Returns the client identifier.
* @return the client identifier
*/
public String getClientId() {
return this.clientId;
}
/**
* Returns the redirect uri.
* @return the redirect uri
*/
@Nullable
public String getRedirectUri() {
return this.redirectUri;
}
/**
* Returns the state.
* @return the state
*/
@Nullable
public String getState() {
return this.state;
}
/**
* Returns the requested (or authorized) scope(s).
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not
* available
*/
public Set<String> getScopes() {
return this.scopes;
}
/**
* Returns the additional parameters.
* @return the additional parameters, or an empty {@code Map} if not available
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}

4
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtClientAssertionDecoderFactory.java

@ -1,5 +1,5 @@ @@ -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 @@ -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;
}

110
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java

@ -1,5 +1,5 @@ @@ -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; @@ -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; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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() {
}
}
}

112
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationToken.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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; @@ -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 @@ -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 @@ -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}

108
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationValidator.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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; @@ -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,25 +55,34 @@ import org.springframework.web.util.UriComponentsBuilder; @@ -51,25 +55,34 @@ 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);
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
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}.
*/
public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_SCOPE_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateScope;
public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateRedirectUri;
/**
* The default validator for
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getRedirectUri()}.
* {@link OAuth2AuthorizationCodeRequestAuthenticationToken#getScopes()}.
*/
public static final Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> DEFAULT_REDIRECT_URI_VALIDATOR = OAuth2AuthorizationCodeRequestAuthenticationValidator::validateRedirectUri;
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 @@ -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 @@ -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 @@ -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 @@ -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);
}

184
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProvider.java

@ -0,0 +1,184 @@ @@ -0,0 +1,184 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Pushed Authorization
* Request used in the Authorization Code Grant.
*
* @author Joe Grandja
* @since 1.5
* @see OAuth2PushedAuthorizationRequestAuthenticationToken
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
* @see OAuth2AuthorizationCodeRequestAuthenticationValidator
* @see OAuth2AuthorizationService
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc9126#section-2.1">Section 2.1 Pushed
* Authorization Request</a>
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc9126#section-2.2">Section 2.2 Pushed
* Authorization Response</a>
*/
public final class OAuth2PushedAuthorizationRequestAuthenticationProvider implements AuthenticationProvider {
private final Log logger = LogFactory.getLog(getClass());
private final OAuth2AuthorizationService authorizationService;
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = new OAuth2AuthorizationCodeRequestAuthenticationValidator();
/**
* Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationProvider} using
* the provided parameters.
* @param authorizationService the authorization service
*/
public OAuth2PushedAuthorizationRequestAuthenticationProvider(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.authorizationService = authorizationService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthentication = (OAuth2PushedAuthorizationRequestAuthenticationToken) authentication;
OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils
.getAuthenticatedClientElseThrowInvalidClient(pushedAuthorizationRequestAuthentication);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
if (this.logger.isTraceEnabled()) {
this.logger.trace("Retrieved registered client");
}
OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = OAuth2AuthorizationCodeRequestAuthenticationContext
.with(toAuthorizationCodeRequestAuthentication(pushedAuthorizationRequestAuthentication))
.registeredClient(registeredClient)
.build();
// grant_type
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_AUTHORIZATION_GRANT_TYPE_VALIDATOR
.accept(authenticationContext);
// redirect_uri and scope
this.authenticationValidator.accept(authenticationContext);
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_CODE_CHALLENGE_VALIDATOR
.accept(authenticationContext);
// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_PROMPT_VALIDATOR.accept(authenticationContext);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Validated pushed authorization request parameters");
}
// @formatter:off
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri(pushedAuthorizationRequestAuthentication.getAuthorizationUri())
.clientId(registeredClient.getClientId())
.redirectUri(pushedAuthorizationRequestAuthentication.getRedirectUri())
.scopes(pushedAuthorizationRequestAuthentication.getScopes())
.state(pushedAuthorizationRequestAuthentication.getState())
.additionalParameters(pushedAuthorizationRequestAuthentication.getAdditionalParameters())
.build();
// @formatter:on
OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = OAuth2PushedAuthorizationRequestUri
.create();
if (this.logger.isTraceEnabled()) {
this.logger.trace("Generated pushed authorization request uri");
}
// @formatter:off
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(clientPrincipal.getName())
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
.attribute(OAuth2ParameterNames.STATE, pushedAuthorizationRequestUri.getState())
.build();
// @formatter:on
this.authorizationService.save(authorization);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Saved authorization");
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authenticated pushed authorization request");
}
return new OAuth2PushedAuthorizationRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),
authorizationRequest.getClientId(), clientPrincipal, pushedAuthorizationRequestUri.getRequestUri(),
pushedAuthorizationRequestUri.getExpiresAt(), authorizationRequest.getRedirectUri(),
authorizationRequest.getState(), authorizationRequest.getScopes());
}
@Override
public boolean supports(Class<?> authentication) {
return OAuth2PushedAuthorizationRequestAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Sets the {@code Consumer} providing access to the
* {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for
* validating specific OAuth 2.0 Pushed Authorization Request parameters associated in
* the {@link OAuth2AuthorizationCodeRequestAuthenticationToken}. The default
* authentication validator is
* {@link OAuth2AuthorizationCodeRequestAuthenticationValidator}.
*
* <p>
* <b>NOTE:</b> The authentication validator MUST throw
* {@link OAuth2AuthorizationCodeRequestAuthenticationException} if validation fails.
* @param authenticationValidator the {@code Consumer} providing access to the
* {@link OAuth2AuthorizationCodeRequestAuthenticationContext} and is responsible for
* validating specific OAuth 2.0 Pushed Authorization Request parameters
*/
public void setAuthenticationValidator(
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
Assert.notNull(authenticationValidator, "authenticationValidator cannot be null");
this.authenticationValidator = authenticationValidator;
}
private static OAuth2AuthorizationCodeRequestAuthenticationToken toAuthorizationCodeRequestAuthentication(
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationCodeRequestAuthentication) {
return new OAuth2AuthorizationCodeRequestAuthenticationToken(
pushedAuthorizationCodeRequestAuthentication.getAuthorizationUri(),
pushedAuthorizationCodeRequestAuthentication.getClientId(),
(Authentication) pushedAuthorizationCodeRequestAuthentication.getPrincipal(),
pushedAuthorizationCodeRequestAuthentication.getRedirectUri(),
pushedAuthorizationCodeRequestAuthentication.getState(),
pushedAuthorizationCodeRequestAuthentication.getScopes(),
pushedAuthorizationCodeRequestAuthentication.getAdditionalParameters());
}
}

94
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationToken.java

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.time.Instant;
import java.util.Map;
import java.util.Set;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
/**
* An {@link Authentication} implementation for the OAuth 2.0 Pushed Authorization Request
* used in the Authorization Code Grant.
*
* @author Joe Grandja
* @since 1.5
* @see OAuth2PushedAuthorizationRequestAuthenticationProvider
*/
public class OAuth2PushedAuthorizationRequestAuthenticationToken
extends AbstractOAuth2AuthorizationCodeRequestAuthenticationToken {
private final String requestUri;
private final Instant requestUriExpiresAt;
/**
* Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationToken} using the
* provided parameters.
* @param authorizationUri the authorization URI
* @param clientId the client identifier
* @param principal the authenticated client principal
* @param redirectUri the redirect uri
* @param state the state
* @param scopes the requested scope(s)
* @param additionalParameters the additional parameters
*/
public OAuth2PushedAuthorizationRequestAuthenticationToken(String authorizationUri, String clientId,
Authentication principal, @Nullable String redirectUri, @Nullable String state,
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
super(authorizationUri, clientId, principal, redirectUri, state, scopes, additionalParameters);
this.requestUri = null;
this.requestUriExpiresAt = null;
}
/**
* Constructs an {@code OAuth2PushedAuthorizationRequestAuthenticationToken} using the
* provided parameters.
* @param authorizationUri the authorization URI
* @param clientId the client identifier
* @param principal the authenticated client principal
* @param requestUri the request URI corresponding to the authorization request posted
* @param requestUriExpiresAt the expiration time on or after which the
* {@code requestUri} MUST NOT be accepted
* @param redirectUri the redirect uri
* @param state the state
* @param scopes the authorized scope(s)
*/
public OAuth2PushedAuthorizationRequestAuthenticationToken(String authorizationUri, String clientId,
Authentication principal, String requestUri, Instant requestUriExpiresAt, @Nullable String redirectUri,
@Nullable String state, @Nullable Set<String> scopes) {
super(authorizationUri, clientId, principal, redirectUri, state, scopes, null);
Assert.hasText(requestUri, "requestUri cannot be empty");
Assert.notNull(requestUriExpiresAt, "requestUriExpiresAt cannot be null");
this.requestUri = requestUri;
this.requestUriExpiresAt = requestUriExpiresAt;
setAuthenticated(true);
}
@Nullable
public String getRequestUri() {
return this.requestUri;
}
@Nullable
public Instant getRequestUriExpiresAt() {
return this.requestUriExpiresAt;
}
}

80
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestUri.java

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.time.Instant;
import java.util.Base64;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator;
/**
* @author Joe Grandja
* @since 1.5
*/
final class OAuth2PushedAuthorizationRequestUri {
private static final String REQUEST_URI_PREFIX = "urn:ietf:params:oauth:request_uri:";
private static final String REQUEST_URI_DELIMITER = "___";
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(
Base64.getUrlEncoder());
private String requestUri;
private String state;
private Instant expiresAt;
static OAuth2PushedAuthorizationRequestUri create() {
String state = DEFAULT_STATE_GENERATOR.generateKey();
Instant expiresAt = Instant.now().plusSeconds(30);
OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = new OAuth2PushedAuthorizationRequestUri();
pushedAuthorizationRequestUri.requestUri = REQUEST_URI_PREFIX + state + REQUEST_URI_DELIMITER
+ expiresAt.toEpochMilli();
pushedAuthorizationRequestUri.state = state + REQUEST_URI_DELIMITER + expiresAt.toEpochMilli();
pushedAuthorizationRequestUri.expiresAt = expiresAt;
return pushedAuthorizationRequestUri;
}
static OAuth2PushedAuthorizationRequestUri parse(String requestUri) {
int stateStartIndex = REQUEST_URI_PREFIX.length();
int expiresAtStartIndex = requestUri.indexOf(REQUEST_URI_DELIMITER) + REQUEST_URI_DELIMITER.length();
OAuth2PushedAuthorizationRequestUri pushedAuthorizationRequestUri = new OAuth2PushedAuthorizationRequestUri();
pushedAuthorizationRequestUri.requestUri = requestUri;
pushedAuthorizationRequestUri.state = requestUri.substring(stateStartIndex);
pushedAuthorizationRequestUri.expiresAt = Instant
.ofEpochMilli(Long.parseLong(requestUri.substring(expiresAtStartIndex)));
return pushedAuthorizationRequestUri;
}
String getRequestUri() {
return this.requestUri;
}
String getState() {
return this.state;
}
Instant getExpiresAt() {
return this.expiresAt;
}
private OAuth2PushedAuthorizationRequestUri() {
}
}

38
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OidcPrompt.java

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
/**
* The values defined for the "prompt" parameter for the OpenID Connect 1.0 Authentication
* Request.
*
* @author Joe Grandja
* @since 1.5
*/
final class OidcPrompt {
static final String NONE = "none";
static final String LOGIN = "login";
static final String CONSENT = "consent";
static final String SELECT_ACCOUNT = "select_account";
private OidcPrompt() {
}
}

71
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationServerConfigurer.java

@ -1,5 +1,5 @@ @@ -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; @@ -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; @@ -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; @@ -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 @@ -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 @@ -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 @@ -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));

9
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2ClientAuthenticationConfigurer.java

@ -1,5 +1,5 @@ @@ -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 @@ -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);

265
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2PushedAuthorizationRequestEndpointConfigurer.java

@ -0,0 +1,265 @@ @@ -0,0 +1,265 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationValidator;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.web.OAuth2PushedAuthorizationRequestEndpointFilter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
/**
* Configurer for the OAuth 2.0 Pushed Authorization Request Endpoint.
*
* @author Joe Grandja
* @since 1.5
* @see OAuth2AuthorizationServerConfigurer#pushedAuthorizationRequestEndpoint
* @see OAuth2PushedAuthorizationRequestEndpointFilter
*/
public final class OAuth2PushedAuthorizationRequestEndpointConfigurer extends AbstractOAuth2Configurer {
private RequestMatcher requestMatcher;
private final List<AuthenticationConverter> pushedAuthorizationRequestConverters = new ArrayList<>();
private Consumer<List<AuthenticationConverter>> pushedAuthorizationRequestConvertersConsumer = (
authorizationRequestConverters) -> {
};
private final List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
private Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer = (authenticationProviders) -> {
};
private AuthenticationSuccessHandler pushedAuthorizationResponseHandler;
private AuthenticationFailureHandler errorResponseHandler;
private Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authorizationCodeRequestAuthenticationValidator;
/**
* Restrict for internal use only.
* @param objectPostProcessor an {@code ObjectPostProcessor}
*/
OAuth2PushedAuthorizationRequestEndpointConfigurer(ObjectPostProcessor<Object> objectPostProcessor) {
super(objectPostProcessor);
}
/**
* Adds an {@link AuthenticationConverter} used when attempting to extract a Pushed
* Authorization Request from {@link HttpServletRequest} to an instance of
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} used for authenticating
* the request.
* @param pushedAuthorizationRequestConverter an {@link AuthenticationConverter} used
* when attempting to extract a Pushed Authorization Request from
* {@link HttpServletRequest}
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestConverter(
AuthenticationConverter pushedAuthorizationRequestConverter) {
Assert.notNull(pushedAuthorizationRequestConverter, "pushedAuthorizationRequestConverter cannot be null");
this.pushedAuthorizationRequestConverters.add(pushedAuthorizationRequestConverter);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added
* {@link #pushedAuthorizationRequestConverter(AuthenticationConverter)
* AuthenticationConverter}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationConverter}.
* @param pushedAuthorizationRequestConvertersConsumer the {@code Consumer} providing
* access to the {@code List} of default and (optionally) added
* {@link AuthenticationConverter}'s
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationRequestConverters(
Consumer<List<AuthenticationConverter>> pushedAuthorizationRequestConvertersConsumer) {
Assert.notNull(pushedAuthorizationRequestConvertersConsumer,
"pushedAuthorizationRequestConvertersConsumer cannot be null");
this.pushedAuthorizationRequestConvertersConsumer = pushedAuthorizationRequestConvertersConsumer;
return this;
}
/**
* Adds an {@link AuthenticationProvider} used for authenticating an
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken}.
* @param authenticationProvider an {@link AuthenticationProvider} used for
* authenticating an {@link OAuth2PushedAuthorizationRequestAuthenticationToken}
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer authenticationProvider(
AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProviders.add(authenticationProvider);
return this;
}
/**
* Sets the {@code Consumer} providing access to the {@code List} of default and
* (optionally) added {@link #authenticationProvider(AuthenticationProvider)
* AuthenticationProvider}'s allowing the ability to add, remove, or customize a
* specific {@link AuthenticationProvider}.
* @param authenticationProvidersConsumer the {@code Consumer} providing access to the
* {@code List} of default and (optionally) added {@link AuthenticationProvider}'s
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer authenticationProviders(
Consumer<List<AuthenticationProvider>> authenticationProvidersConsumer) {
Assert.notNull(authenticationProvidersConsumer, "authenticationProvidersConsumer cannot be null");
this.authenticationProvidersConsumer = authenticationProvidersConsumer;
return this;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} and returning the
* Pushed Authorization Response.
* @param pushedAuthorizationResponseHandler the {@link AuthenticationSuccessHandler}
* used for handling an {@link OAuth2PushedAuthorizationRequestAuthenticationToken}
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer pushedAuthorizationResponseHandler(
AuthenticationSuccessHandler pushedAuthorizationResponseHandler) {
this.pushedAuthorizationResponseHandler = pushedAuthorizationResponseHandler;
return this;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthorizationCodeRequestAuthenticationException} and returning the
* {@link OAuth2Error Error Response}.
* @param errorResponseHandler the {@link AuthenticationFailureHandler} used for
* handling an {@link OAuth2AuthorizationCodeRequestAuthenticationException}
* @return the {@link OAuth2PushedAuthorizationRequestEndpointConfigurer} for further
* configuration
*/
public OAuth2PushedAuthorizationRequestEndpointConfigurer errorResponseHandler(
AuthenticationFailureHandler errorResponseHandler) {
this.errorResponseHandler = errorResponseHandler;
return this;
}
void addAuthorizationCodeRequestAuthenticationValidator(
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator) {
this.authorizationCodeRequestAuthenticationValidator = (this.authorizationCodeRequestAuthenticationValidator == null)
? authenticationValidator
: this.authorizationCodeRequestAuthenticationValidator.andThen(authenticationValidator);
}
@Override
void init(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
this.requestMatcher = new AntPathRequestMatcher(pushedAuthorizationRequestEndpointUri, HttpMethod.POST.name());
List<AuthenticationProvider> authenticationProviders = createDefaultAuthenticationProviders(httpSecurity);
if (!this.authenticationProviders.isEmpty()) {
authenticationProviders.addAll(0, this.authenticationProviders);
}
this.authenticationProvidersConsumer.accept(authenticationProviders);
authenticationProviders.forEach(
(authenticationProvider) -> httpSecurity.authenticationProvider(postProcess(authenticationProvider)));
}
@Override
void configure(HttpSecurity httpSecurity) {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
AuthorizationServerSettings authorizationServerSettings = OAuth2ConfigurerUtils
.getAuthorizationServerSettings(httpSecurity);
String pushedAuthorizationRequestEndpointUri = authorizationServerSettings.isMultipleIssuersAllowed()
? OAuth2ConfigurerUtils
.withMultipleIssuersPattern(authorizationServerSettings.getPushedAuthorizationRequestEndpoint())
: authorizationServerSettings.getPushedAuthorizationRequestEndpoint();
OAuth2PushedAuthorizationRequestEndpointFilter pushedAuthorizationRequestEndpointFilter = new OAuth2PushedAuthorizationRequestEndpointFilter(
authenticationManager, pushedAuthorizationRequestEndpointUri);
List<AuthenticationConverter> authenticationConverters = createDefaultAuthenticationConverters();
if (!this.pushedAuthorizationRequestConverters.isEmpty()) {
authenticationConverters.addAll(0, this.pushedAuthorizationRequestConverters);
}
this.pushedAuthorizationRequestConvertersConsumer.accept(authenticationConverters);
pushedAuthorizationRequestEndpointFilter
.setAuthenticationConverter(new DelegatingAuthenticationConverter(authenticationConverters));
if (this.pushedAuthorizationResponseHandler != null) {
pushedAuthorizationRequestEndpointFilter
.setAuthenticationSuccessHandler(this.pushedAuthorizationResponseHandler);
}
if (this.errorResponseHandler != null) {
pushedAuthorizationRequestEndpointFilter.setAuthenticationFailureHandler(this.errorResponseHandler);
}
httpSecurity.addFilterAfter(postProcess(pushedAuthorizationRequestEndpointFilter), AuthorizationFilter.class);
}
@Override
RequestMatcher getRequestMatcher() {
return this.requestMatcher;
}
private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
authenticationConverters.add(new OAuth2AuthorizationCodeRequestAuthenticationConverter());
return authenticationConverters;
}
private List<AuthenticationProvider> createDefaultAuthenticationProviders(HttpSecurity httpSecurity) {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
OAuth2PushedAuthorizationRequestAuthenticationProvider pushedAuthorizationRequestAuthenticationProvider = new OAuth2PushedAuthorizationRequestAuthenticationProvider(
OAuth2ConfigurerUtils.getAuthorizationService(httpSecurity));
if (this.authorizationCodeRequestAuthenticationValidator != null) {
pushedAuthorizationRequestAuthenticationProvider
.setAuthenticationValidator(new OAuth2AuthorizationCodeRequestAuthenticationValidator()
.andThen(this.authorizationCodeRequestAuthenticationValidator));
}
authenticationProviders.add(pushedAuthorizationRequestAuthenticationProvider);
return authenticationProviders;
}
}

25
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettings.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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

9
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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.
*/

63
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/HttpMessageConverters.java

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.ClassUtils;
/**
* Utility methods for {@link HttpMessageConverter}'s.
*
* @author Joe Grandja
* @since 1.5
*/
final class HttpMessageConverters {
private static final boolean jackson2Present;
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
static {
ClassLoader classLoader = HttpMessageConverters.class.getClassLoader();
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
}
private HttpMessageConverters() {
}
static GenericHttpMessageConverter<Object> getJsonMessageConverter() {
if (jackson2Present) {
return new MappingJackson2HttpMessageConverter();
}
if (gsonPresent) {
return new GsonHttpMessageConverter();
}
if (jsonbPresent) {
return new JsonbHttpMessageConverter();
}
return null;
}
}

223
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2PushedAuthorizationRequestEndpointFilter.java

@ -0,0 +1,223 @@ @@ -0,0 +1,223 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;
import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.LinkedHashMap;
import java.util.Map;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeRequestAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ErrorAuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* A {@code Filter} for the OAuth 2.0 Pushed Authorization Request endpoint, which handles
* the processing of the OAuth 2.0 Pushed Authorization Request.
*
* @author Joe Grandja
* @since 1.5
* @see AuthenticationManager
* @see OAuth2PushedAuthorizationRequestAuthenticationProvider
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc9126#name-pushed-authorization-reques">Section
* 2. Pushed Authorization Request Endpoint</a>
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc9126#section-2.1">Section 2.1 Pushed
* Authorization Request</a>
* @see <a target="_blank" href=
* "https://datatracker.ietf.org/doc/html/rfc9126#section-2.2">Section 2.2 Pushed
* Authorization Response</a>
*/
public final class OAuth2PushedAuthorizationRequestEndpointFilter extends OncePerRequestFilter {
/**
* The default endpoint {@code URI} for pushed authorization requests.
*/
private static final String DEFAULT_PUSHED_AUTHORIZATION_REQUEST_ENDPOINT_URI = "/oauth2/par";
private static final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
};
private static final GenericHttpMessageConverter<Object> JSON_MESSAGE_CONVERTER = HttpMessageConverters
.getJsonMessageConverter();
private final AuthenticationManager authenticationManager;
private final RequestMatcher pushedAuthorizationRequestEndpointMatcher;
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationConverter authenticationConverter;
private AuthenticationSuccessHandler authenticationSuccessHandler = this::sendPushedAuthorizationResponse;
private AuthenticationFailureHandler authenticationFailureHandler = new OAuth2ErrorAuthenticationFailureHandler();
/**
* Constructs an {@code OAuth2PushedAuthorizationRequestEndpointFilter} using the
* provided parameters.
* @param authenticationManager the authentication manager
*/
public OAuth2PushedAuthorizationRequestEndpointFilter(AuthenticationManager authenticationManager) {
this(authenticationManager, DEFAULT_PUSHED_AUTHORIZATION_REQUEST_ENDPOINT_URI);
}
/**
* Constructs an {@code OAuth2PushedAuthorizationRequestEndpointFilter} using the
* provided parameters.
* @param authenticationManager the authentication manager
* @param pushedAuthorizationRequestEndpointUri the endpoint {@code URI} for pushed
* authorization requests
*/
public OAuth2PushedAuthorizationRequestEndpointFilter(AuthenticationManager authenticationManager,
String pushedAuthorizationRequestEndpointUri) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.hasText(pushedAuthorizationRequestEndpointUri, "pushedAuthorizationRequestEndpointUri cannot be empty");
this.authenticationManager = authenticationManager;
this.pushedAuthorizationRequestEndpointMatcher = new AntPathRequestMatcher(
pushedAuthorizationRequestEndpointUri, HttpMethod.POST.name());
this.authenticationConverter = new OAuth2AuthorizationCodeRequestAuthenticationConverter();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.pushedAuthorizationRequestEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
Authentication pushedAuthorizationRequestAuthentication = this.authenticationConverter.convert(request);
if (pushedAuthorizationRequestAuthentication instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken) pushedAuthorizationRequestAuthentication)
.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
Authentication pushedAuthorizationRequestAuthenticationResult = this.authenticationManager
.authenticate(pushedAuthorizationRequestAuthentication);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response,
pushedAuthorizationRequestAuthenticationResult);
}
catch (OAuth2AuthenticationException ex) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Pushed authorization request failed: %s", ex.getError()), ex);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
}
}
/**
* Sets the {@link AuthenticationDetailsSource} used for building an authentication
* details instance from {@link HttpServletRequest}.
* @param authenticationDetailsSource the {@link AuthenticationDetailsSource} used for
* building an authentication details instance from {@link HttpServletRequest}
*/
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "authenticationDetailsSource cannot be null");
this.authenticationDetailsSource = authenticationDetailsSource;
}
/**
* Sets the {@link AuthenticationConverter} used when attempting to extract a Pushed
* Authorization Request from {@link HttpServletRequest} to an instance of
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} used for authenticating
* the request.
* @param authenticationConverter the {@link AuthenticationConverter} used when
* attempting to extract a Pushed Authorization Request from
* {@link HttpServletRequest}
*/
public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) {
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
this.authenticationConverter = authenticationConverter;
}
/**
* Sets the {@link AuthenticationSuccessHandler} used for handling an
* {@link OAuth2PushedAuthorizationRequestAuthenticationToken} and returning the
* Pushed Authorization Response.
* @param authenticationSuccessHandler the {@link AuthenticationSuccessHandler} used
* for handling an {@link OAuth2PushedAuthorizationRequestAuthenticationToken}
*/
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler) {
Assert.notNull(authenticationSuccessHandler, "authenticationSuccessHandler cannot be null");
this.authenticationSuccessHandler = authenticationSuccessHandler;
}
/**
* Sets the {@link AuthenticationFailureHandler} used for handling an
* {@link OAuth2AuthenticationException} and returning the {@link OAuth2Error Error
* Response}.
* @param authenticationFailureHandler the {@link AuthenticationFailureHandler} used
* for handling an {@link OAuth2AuthenticationException}
*/
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
Assert.notNull(authenticationFailureHandler, "authenticationFailureHandler cannot be null");
this.authenticationFailureHandler = authenticationFailureHandler;
}
private void sendPushedAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthentication = (OAuth2PushedAuthorizationRequestAuthenticationToken) authentication;
Map<String, Object> pushedAuthorizationResponse = new LinkedHashMap<>();
pushedAuthorizationResponse.put("request_uri", pushedAuthorizationRequestAuthentication.getRequestUri());
long expiresIn = ChronoUnit.SECONDS.between(Instant.now(),
pushedAuthorizationRequestAuthentication.getRequestUriExpiresAt());
pushedAuthorizationResponse.put(OAuth2ParameterNames.EXPIRES_IN, expiresIn);
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
httpResponse.setStatusCode(HttpStatus.CREATED);
JSON_MESSAGE_CONVERTER.write(pushedAuthorizationResponse, STRING_OBJECT_MAP.getType(),
MediaType.APPLICATION_JSON, httpResponse);
}
}

60
oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/authentication/OAuth2AuthorizationCodeRequestAuthenticationConverter.java

@ -18,6 +18,7 @@ package org.springframework.security.oauth2.server.authorization.web.authenticat @@ -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; @@ -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; @@ -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 @@ -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 @@ -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() {

72
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProviderTests.java

@ -1,5 +1,5 @@ @@ -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 @@ -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 @@ -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 { @@ -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 { @@ -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 { @@ -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);

422
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2PushedAuthorizationRequestAuthenticationProviderTests.java

@ -0,0 +1,422 @@ @@ -0,0 +1,422 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link OAuth2PushedAuthorizationRequestAuthenticationProvider}.
*
* @author Joe Grandja
*/
public class OAuth2PushedAuthorizationRequestAuthenticationProviderTests {
private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/par";
private static final String STATE = "state";
private OAuth2AuthorizationService authorizationService;
private OAuth2PushedAuthorizationRequestAuthenticationProvider authenticationProvider;
@BeforeEach
public void setUp() {
this.authorizationService = mock(OAuth2AuthorizationService.class);
this.authenticationProvider = new OAuth2PushedAuthorizationRequestAuthenticationProvider(
this.authorizationService);
}
@Test
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2PushedAuthorizationRequestAuthenticationProvider(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authorizationService cannot be null");
}
@Test
public void supportsWhenTypeOAuth2PushedAuthorizationRequestAuthenticationTokenThenReturnTrue() {
assertThat(this.authenticationProvider.supports(OAuth2PushedAuthorizationRequestAuthenticationToken.class))
.isTrue();
}
@Test
public void setAuthenticationValidatorWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.authenticationProvider.setAuthenticationValidator(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationValidator cannot be null");
}
@Test
public void authenticateWhenClientNotAuthenticatedThenThrowOAuth2AuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[1];
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(
registeredClient.getClientId(), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null, null);
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
registeredClient.getScopes(), null);
// @formatter:off
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.extracting(OAuth2AuthenticationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo(OAuth2ErrorCodes.INVALID_CLIENT);
// @formatter:on
}
@Test
public void authenticateWhenClientNotAuthorizedToRequestCodeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.authorizationGrantTypes(Set::clear)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[1];
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, null,
registeredClient.getScopes(), null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
authentication.getRedirectUri()));
}
@Test
public void authenticateWhenInvalidRedirectUriHostThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https:///invalid", STATE,
registeredClient.getScopes(), null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null));
}
@Test
public void authenticateWhenInvalidRedirectUriFragmentThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://example.com#fragment",
STATE, registeredClient.getScopes(), null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null));
}
@Test
public void authenticateWhenUnregisteredRedirectUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://invalid-example.com",
STATE, registeredClient.getScopes(), null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null));
}
@Test
public void authenticateWhenRedirectUriIPv4LoopbackAndDifferentPortThenReturnPushedAuthorizationResponse() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.redirectUri("https://127.0.0.1:8080")
.build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://127.0.0.1:5000", STATE,
registeredClient.getScopes(), null);
OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult);
}
@Test
public void authenticateWhenRedirectUriIPv6LoopbackAndDifferentPortThenReturnPushedAuthorizationResponse() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.redirectUri("https://[::1]:8080")
.build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, "https://[::1]:5000", STATE,
registeredClient.getScopes(), null);
OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult);
}
@Test
public void authenticateWhenMissingRedirectUriAndMultipleRegisteredThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.redirectUri("https://example2.com")
.build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, null, STATE,
registeredClient.getScopes(), null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null));
}
@Test
public void authenticateWhenAuthenticationRequestMissingRedirectUriThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
// redirect_uri is REQUIRED for OpenID Connect requests
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, null, STATE,
registeredClient.getScopes(), null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REDIRECT_URI, null));
}
@Test
public void authenticateWhenInvalidScopeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
Collections.singleton("invalid-scope"), null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_SCOPE, OAuth2ParameterNames.SCOPE, authentication.getRedirectUri()));
}
@Test
public void authenticateWhenPkceRequiredAndMissingCodeChallengeThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
.clientSettings(ClientSettings.builder().requireProofKey(true).build())
.build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
registeredClient.getScopes(), null);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE,
authentication.getRedirectUri()));
}
@Test
public void authenticateWhenPkceUnsupportedCodeChallengeMethodThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "unsupported");
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
registeredClient.getScopes(), additionalParameters);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD,
authentication.getRedirectUri()));
}
@Test
public void authenticateWhenPkceMissingCodeChallengeMethodThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
registeredClient.getScopes(), additionalParameters);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD,
authentication.getRedirectUri()));
}
@Test
public void authenticateWhenAuthenticationRequestWithPromptNoneLoginThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
"none login");
}
@Test
public void authenticateWhenAuthenticationRequestWithPromptNoneConsentThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
"none consent");
}
@Test
public void authenticateWhenAuthenticationRequestWithPromptNoneSelectAccountThenThrowOAuth2AuthorizationCodeRequestAuthenticationException() {
assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
"none select_account");
}
private void assertWhenAuthenticationRequestWithInvalidPromptThenThrowOAuth2AuthorizationCodeRequestAuthenticationException(
String prompt) {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scope(OidcScopes.OPENID).build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put("prompt", prompt);
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
registeredClient.getScopes(), additionalParameters);
assertThatThrownBy(() -> this.authenticationProvider.authenticate(authentication))
.isInstanceOf(OAuth2AuthorizationCodeRequestAuthenticationException.class)
.satisfies((ex) -> assertAuthenticationException((OAuth2AuthorizationCodeRequestAuthenticationException) ex,
OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authentication.getRedirectUri()));
}
@Test
public void authenticateWhenPushedAuthorizationRequestValidThenReturnPushedAuthorizationResponse() {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[0];
Map<String, Object> additionalParameters = new HashMap<>();
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
additionalParameters.put(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
registeredClient.getScopes(), additionalParameters);
OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult);
}
@Test
public void authenticateWhenCustomAuthenticationValidatorThenUsed() {
@SuppressWarnings("unchecked")
Consumer<OAuth2AuthorizationCodeRequestAuthenticationContext> authenticationValidator = mock(Consumer.class);
this.authenticationProvider.setAuthenticationValidator(authenticationValidator);
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2ClientAuthenticationToken clientPrincipal = new OAuth2ClientAuthenticationToken(registeredClient,
ClientAuthenticationMethod.CLIENT_SECRET_BASIC, registeredClient.getClientSecret());
String redirectUri = registeredClient.getRedirectUris().toArray(new String[0])[2];
OAuth2PushedAuthorizationRequestAuthenticationToken authentication = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), clientPrincipal, redirectUri, STATE,
registeredClient.getScopes(), null);
OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult = (OAuth2PushedAuthorizationRequestAuthenticationToken) this.authenticationProvider
.authenticate(authentication);
assertPushedAuthorizationResponse(registeredClient, authentication, authenticationResult);
verify(authenticationValidator).accept(any());
}
private void assertPushedAuthorizationResponse(RegisteredClient registeredClient,
OAuth2PushedAuthorizationRequestAuthenticationToken authentication,
OAuth2PushedAuthorizationRequestAuthenticationToken authenticationResult) {
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
verify(this.authorizationService).save(authorizationCaptor.capture());
OAuth2Authorization authorization = authorizationCaptor.getValue();
OAuth2AuthorizationRequest authorizationRequest = authorization
.getAttribute(OAuth2AuthorizationRequest.class.getName());
assertThat(authorizationRequest.getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(authorizationRequest.getResponseType()).isEqualTo(OAuth2AuthorizationResponseType.CODE);
assertThat(authorizationRequest.getAuthorizationUri()).isEqualTo(authentication.getAuthorizationUri());
assertThat(authorizationRequest.getClientId()).isEqualTo(registeredClient.getClientId());
assertThat(authorizationRequest.getRedirectUri()).isEqualTo(authentication.getRedirectUri());
assertThat(authorizationRequest.getScopes()).isEqualTo(authentication.getScopes());
assertThat(authorizationRequest.getState()).isEqualTo(authentication.getState());
assertThat(authorizationRequest.getAdditionalParameters()).isEqualTo(authentication.getAdditionalParameters());
assertThat(authorization.getRegisteredClientId()).isEqualTo(registeredClient.getId());
assertThat(authorization.getPrincipalName()).isEqualTo(authentication.getName());
assertThat(authorization.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(authorization.<String>getAttribute(OAuth2ParameterNames.STATE)).isNotNull();
assertThat(authenticationResult.getClientId()).isEqualTo(authorizationRequest.getClientId());
assertThat(authenticationResult.getPrincipal()).isEqualTo(authentication.getPrincipal());
assertThat(authenticationResult.getAuthorizationUri()).isEqualTo(authorizationRequest.getAuthorizationUri());
assertThat(authenticationResult.getRedirectUri()).isEqualTo(authorizationRequest.getRedirectUri());
assertThat(authenticationResult.getScopes()).isEqualTo(authorizationRequest.getScopes());
assertThat(authenticationResult.getState()).isEqualTo(authorizationRequest.getState());
assertThat(authenticationResult.getRequestUri()).isNotNull();
assertThat(authenticationResult.getRequestUriExpiresAt()).isNotNull();
assertThat(authenticationResult.isAuthenticated()).isTrue();
}
private static void assertAuthenticationException(
OAuth2AuthorizationCodeRequestAuthenticationException authenticationException, String errorCode,
String parameterName, String redirectUri) {
OAuth2Error error = authenticationException.getError();
assertThat(error.getErrorCode()).isEqualTo(errorCode);
assertThat(error.getDescription()).contains(parameterName);
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = authenticationException
.getAuthorizationCodeRequestAuthentication();
assertThat(authorizationCodeRequestAuthentication.getRedirectUri()).isEqualTo(redirectUri);
}
}

87
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/config/annotation/web/configurers/OAuth2AuthorizationCodeGrantTests.java

@ -32,6 +32,7 @@ import java.util.Set; @@ -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 { @@ -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 { @@ -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
}
}

17
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/AuthorizationServerSettingsTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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))

16
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationEndpointFilterTests.java

@ -54,6 +54,9 @@ import org.springframework.security.oauth2.server.authorization.authentication.O @@ -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 { @@ -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 { @@ -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(),

490
oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2PushedAuthorizationRequestEndpointFilterTests.java

@ -0,0 +1,490 @@ @@ -0,0 +1,490 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.server.authorization.web;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.function.Consumer;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.mock.http.client.MockClientHttpResponse;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2PushedAuthorizationRequestAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.context.TestAuthorizationServerContext;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link OAuth2PushedAuthorizationRequestEndpointFilter}.
*
* @author Joe Grandja
*/
public class OAuth2PushedAuthorizationRequestEndpointFilterTests {
private static final String AUTHORIZATION_URI = "https://provider.com/oauth2/par";
private static final String STATE = "state";
private static final String REMOTE_ADDRESS = "remote-address";
private final HttpMessageConverter<OAuth2Error> errorHttpResponseConverter = new OAuth2ErrorHttpMessageConverter();
private final GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters
.getJsonMessageConverter();
private AuthenticationManager authenticationManager;
private OAuth2PushedAuthorizationRequestEndpointFilter filter;
private TestingAuthenticationToken clientPrincipal;
@BeforeEach
public void setUp() {
this.authenticationManager = mock(AuthenticationManager.class);
this.filter = new OAuth2PushedAuthorizationRequestEndpointFilter(this.authenticationManager);
this.clientPrincipal = new TestingAuthenticationToken("client-id", "client-secret");
this.clientPrincipal.setAuthenticated(true);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(this.clientPrincipal);
SecurityContextHolder.setContext(securityContext);
AuthorizationServerContextHolder
.setContext(new TestAuthorizationServerContext(AuthorizationServerSettings.builder().build(), null));
}
@AfterEach
public void cleanup() {
SecurityContextHolder.clearContext();
AuthorizationServerContextHolder.resetContext();
}
@Test
public void constructorWhenAuthenticationManagerNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2PushedAuthorizationRequestEndpointFilter(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationManager cannot be null");
}
@Test
public void constructorWhenPushedAuthorizationRequestEndpointUriNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> new OAuth2PushedAuthorizationRequestEndpointFilter(this.authenticationManager, null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("pushedAuthorizationRequestEndpointUri cannot be empty");
}
@Test
public void setAuthenticationDetailsSourceWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationDetailsSource(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationDetailsSource cannot be null");
}
@Test
public void setAuthenticationConverterWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationConverter(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationConverter cannot be null");
}
@Test
public void setAuthenticationSuccessHandlerWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationSuccessHandler(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationSuccessHandler cannot be null");
}
@Test
public void setAuthenticationFailureHandlerWhenNullThenThrowIllegalArgumentException() {
assertThatThrownBy(() -> this.filter.setAuthenticationFailureHandler(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("authenticationFailureHandler cannot be null");
}
@Test
public void doFilterWhenNotPushedAuthorizationRequestThenNotProcessed() throws Exception {
String requestUri = "/path";
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
@Test
public void doFilterWhenPushedAuthorizationRequestIncludesRequestUriThenInvalidRequestError() throws Exception {
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
TestRegisteredClients.registeredClient().build(), "request_uri", OAuth2ErrorCodes.INVALID_REQUEST,
(request) -> request.addParameter("request_uri", "request_uri"));
}
@Test
public void doFilterWhenPushedAuthorizationRequestMultipleResponseTypeThenInvalidRequestError() throws Exception {
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.RESPONSE_TYPE,
OAuth2ErrorCodes.INVALID_REQUEST,
(request) -> request.addParameter(OAuth2ParameterNames.RESPONSE_TYPE, "id_token"));
}
@Test
public void doFilterWhenPushedAuthorizationRequestInvalidResponseTypeThenUnsupportedResponseTypeError()
throws Exception {
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.RESPONSE_TYPE,
OAuth2ErrorCodes.UNSUPPORTED_RESPONSE_TYPE,
(request) -> request.setParameter(OAuth2ParameterNames.RESPONSE_TYPE, "id_token"));
}
@Test
public void doFilterWhenPushedAuthorizationRequestMissingClientIdThenInvalidRequestError() throws Exception {
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.CLIENT_ID,
OAuth2ErrorCodes.INVALID_REQUEST, (request) -> request.removeParameter(OAuth2ParameterNames.CLIENT_ID));
}
@Test
public void doFilterWhenPushedAuthorizationRequestMultipleClientIdThenInvalidRequestError() throws Exception {
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.CLIENT_ID,
OAuth2ErrorCodes.INVALID_REQUEST,
(request) -> request.addParameter(OAuth2ParameterNames.CLIENT_ID, "client-2"));
}
@Test
public void doFilterWhenPushedAuthorizationRequestMultipleRedirectUriThenInvalidRequestError() throws Exception {
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.REDIRECT_URI,
OAuth2ErrorCodes.INVALID_REQUEST,
(request) -> request.addParameter(OAuth2ParameterNames.REDIRECT_URI, "https://example2.com"));
}
@Test
public void doFilterWhenPushedAuthorizationRequestMultipleScopeThenInvalidRequestError() throws Exception {
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.SCOPE,
OAuth2ErrorCodes.INVALID_REQUEST,
(request) -> request.addParameter(OAuth2ParameterNames.SCOPE, "scope2"));
}
@Test
public void doFilterWhenPushedAuthorizationRequestMultipleStateThenInvalidRequestError() throws Exception {
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
TestRegisteredClients.registeredClient().build(), OAuth2ParameterNames.STATE,
OAuth2ErrorCodes.INVALID_REQUEST,
(request) -> request.addParameter(OAuth2ParameterNames.STATE, "state2"));
}
@Test
public void doFilterWhenPushedAuthorizationRequestMultipleCodeChallengeThenInvalidRequestError() throws Exception {
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
TestRegisteredClients.registeredClient().build(), PkceParameterNames.CODE_CHALLENGE,
OAuth2ErrorCodes.INVALID_REQUEST, (request) -> {
request.addParameter(PkceParameterNames.CODE_CHALLENGE, "code-challenge");
request.addParameter(PkceParameterNames.CODE_CHALLENGE, "another-code-challenge");
});
}
@Test
public void doFilterWhenPushedAuthorizationRequestMultipleCodeChallengeMethodThenInvalidRequestError()
throws Exception {
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(
TestRegisteredClients.registeredClient().build(), PkceParameterNames.CODE_CHALLENGE_METHOD,
OAuth2ErrorCodes.INVALID_REQUEST, (request) -> {
request.addParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
request.addParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
});
}
@Test
public void doFilterWhenPushedAuthenticationRequestMultiplePromptThenInvalidRequestError() throws Exception {
// Setup OpenID Connect request
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scopes((scopes) -> {
scopes.clear();
scopes.add(OidcScopes.OPENID);
}).build();
doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(registeredClient, "prompt",
OAuth2ErrorCodes.INVALID_REQUEST, (request) -> {
request.addParameter("prompt", "none");
request.addParameter("prompt", "login");
});
}
@Test
public void doFilterWhenPushedAuthorizationRequestAuthenticationExceptionThenErrorResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "error description", "error uri");
given(this.authenticationManager.authenticate(any())).willThrow(new OAuth2AuthenticationException(error));
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
OAuth2Error errorResponse = readError(response);
assertThat(errorResponse.getErrorCode()).isEqualTo(error.getErrorCode());
assertThat(errorResponse.getDescription()).isEqualTo(error.getDescription());
assertThat(errorResponse.getUri()).isEqualTo(error.getUri());
assertThat(SecurityContextHolder.getContext().getAuthentication()).isSameAs(this.clientPrincipal);
}
@Test
public void doFilterWhenCustomAuthenticationConverterThenUsed() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, "request_uri",
Instant.now().plusSeconds(30), registeredClient.getRedirectUris().iterator().next(), STATE,
registeredClient.getScopes());
AuthenticationConverter authenticationConverter = mock(AuthenticationConverter.class);
given(authenticationConverter.convert(any())).willReturn(pushedAuthorizationRequestAuthenticationResult);
this.filter.setAuthenticationConverter(authenticationConverter);
given(this.authenticationManager.authenticate(any()))
.willReturn(pushedAuthorizationRequestAuthenticationResult);
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(authenticationConverter).convert(any());
verify(this.authenticationManager).authenticate(any());
}
@Test
public void doFilterWhenCustomAuthenticationSuccessHandlerThenUsed() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, "request_uri",
Instant.now().plusSeconds(30), registeredClient.getRedirectUris().iterator().next(), STATE,
registeredClient.getScopes());
given(this.authenticationManager.authenticate(any()))
.willReturn(pushedAuthorizationRequestAuthenticationResult);
AuthenticationSuccessHandler authenticationSuccessHandler = mock(AuthenticationSuccessHandler.class);
this.filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(),
same(pushedAuthorizationRequestAuthenticationResult));
}
@Test
public void doFilterWhenCustomAuthenticationFailureHandlerThenUsed() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
OAuth2Error error = new OAuth2Error("errorCode", "errorDescription", "errorUri");
OAuth2AuthenticationException authenticationException = new OAuth2AuthenticationException(error);
given(this.authenticationManager.authenticate(any())).willThrow(authenticationException);
AuthenticationFailureHandler authenticationFailureHandler = mock(AuthenticationFailureHandler.class);
this.filter.setAuthenticationFailureHandler(authenticationFailureHandler);
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(this.authenticationManager).authenticate(any());
verifyNoInteractions(filterChain);
verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), same(authenticationException));
}
@Test
public void doFilterWhenCustomAuthenticationDetailsSourceThenUsed() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource = mock(
AuthenticationDetailsSource.class);
WebAuthenticationDetails webAuthenticationDetails = new WebAuthenticationDetails(request);
given(authenticationDetailsSource.buildDetails(request)).willReturn(webAuthenticationDetails);
this.filter.setAuthenticationDetailsSource(authenticationDetailsSource);
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, "request_uri",
Instant.now().plusSeconds(30), registeredClient.getRedirectUris().iterator().next(), STATE,
registeredClient.getScopes());
given(this.authenticationManager.authenticate(any()))
.willReturn(pushedAuthorizationRequestAuthenticationResult);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verify(authenticationDetailsSource).buildDetails(any());
verify(this.authenticationManager).authenticate(any());
}
@Test
public void doFilterWhenPushedAuthorizationRequestAuthenticatedThenPushedAuthorizationResponse() throws Exception {
RegisteredClient registeredClient = TestRegisteredClients.registeredClient().build();
String requestUri = "request_uri";
Instant requestUriExpiresAt = Instant.now().plusSeconds(30);
OAuth2PushedAuthorizationRequestAuthenticationToken pushedAuthorizationRequestAuthenticationResult = new OAuth2PushedAuthorizationRequestAuthenticationToken(
AUTHORIZATION_URI, registeredClient.getClientId(), this.clientPrincipal, requestUri,
requestUriExpiresAt, registeredClient.getRedirectUris().iterator().next(), STATE,
registeredClient.getScopes());
given(this.authenticationManager.authenticate(any()))
.willReturn(pushedAuthorizationRequestAuthenticationResult);
MockHttpServletRequest request = createPushedAuthorizationRequest(registeredClient);
request.addParameter("custom-param", "custom-value-1", "custom-value-2");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
ArgumentCaptor<OAuth2PushedAuthorizationRequestAuthenticationToken> pushedAuthorizationRequestAuthenticationCaptor = ArgumentCaptor
.forClass(OAuth2PushedAuthorizationRequestAuthenticationToken.class);
verify(this.authenticationManager).authenticate(pushedAuthorizationRequestAuthenticationCaptor.capture());
verifyNoInteractions(filterChain);
assertThat(pushedAuthorizationRequestAuthenticationCaptor.getValue().getDetails())
.asInstanceOf(InstanceOfAssertFactories.type(WebAuthenticationDetails.class))
.extracting(WebAuthenticationDetails::getRemoteAddress)
.isEqualTo(REMOTE_ADDRESS);
// Assert that multi-valued request parameters are preserved
assertThat(pushedAuthorizationRequestAuthenticationCaptor.getValue().getAdditionalParameters())
.extracting((params) -> params.get("custom-param"))
.asInstanceOf(InstanceOfAssertFactories.type(String[].class))
.isEqualTo(new String[] { "custom-value-1", "custom-value-2" });
assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED.value());
Map<String, Object> responseParameters = readPushedAuthorizationResponse(response);
assertThat(responseParameters.get("request_uri")).isEqualTo(requestUri);
assertThat(responseParameters.get("expires_in"))
.isEqualTo((int) ChronoUnit.SECONDS.between(Instant.now(), requestUriExpiresAt));
}
private void doFilterWhenPushedAuthorizationRequestInvalidParameterThenError(RegisteredClient registeredClient,
String parameterName, String errorCode, Consumer<MockHttpServletRequest> requestConsumer) throws Exception {
doFilterWhenRequestInvalidParameterThenError(createPushedAuthorizationRequest(registeredClient), parameterName,
errorCode, requestConsumer);
}
private void doFilterWhenRequestInvalidParameterThenError(MockHttpServletRequest request, String parameterName,
String errorCode, Consumer<MockHttpServletRequest> requestConsumer) throws Exception {
requestConsumer.accept(request);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain filterChain = mock(FilterChain.class);
this.filter.doFilter(request, response, filterChain);
verifyNoInteractions(filterChain);
assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
OAuth2Error error = readError(response);
assertThat(error.getErrorCode()).isEqualTo(errorCode);
assertThat(error.getDescription()).isEqualTo("OAuth 2.0 Parameter: " + parameterName);
}
private static MockHttpServletRequest createPushedAuthorizationRequest(RegisteredClient registeredClient) {
String requestUri = AuthorizationServerContextHolder.getContext()
.getAuthorizationServerSettings()
.getPushedAuthorizationRequestEndpoint();
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
request.setServletPath(requestUri);
request.setRemoteAddr(REMOTE_ADDRESS);
request.addParameter(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
request.addParameter(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
request.addParameter(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
request.addParameter(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
request.addParameter(OAuth2ParameterNames.STATE, "state");
return request;
}
private OAuth2Error readError(MockHttpServletResponse response) throws Exception {
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
HttpStatus.valueOf(response.getStatus()));
return this.errorHttpResponseConverter.read(OAuth2Error.class, httpResponse);
}
@SuppressWarnings("unchecked")
private Map<String, Object> readPushedAuthorizationResponse(MockHttpServletResponse response) throws Exception {
final ParameterizedTypeReference<Map<String, Object>> STRING_OBJECT_MAP = new ParameterizedTypeReference<>() {
};
MockClientHttpResponse httpResponse = new MockClientHttpResponse(response.getContentAsByteArray(),
HttpStatus.valueOf(response.getStatus()));
return (Map<String, Object>) this.jsonMessageConverter.read(STRING_OBJECT_MAP.getType(), null, httpResponse);
}
}
Loading…
Cancel
Save