Skip to content

Commit e2b735b

Browse files
committed
Add support for OAuth 2.0 Pushed Authorization Requests (PAR)
Closes spring-projectsgh-210
1 parent 629239f commit e2b735b

20 files changed

+1332
-181
lines changed

etc/checkstyle/checkstyle-suppressions.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
<suppress files="SpringAuthorizationServerVersion\.java" checks="HideUtilityClassConstructor"/>
88
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toLowerCaseWithoutLocale"/>
99
<suppress files="[\\/]src[\\/]test[\\/]" checks="RegexpSinglelineJava" id="toUpperCaseWithoutLocale"/>
10+
<suppress files="AbstractOAuth2AuthorizationCodeRequestAuthenticationToken\.java" checks="SpringMethodVisibility"/>
1011
</suppressions>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.server.authorization.authentication;
17+
18+
import java.util.Collections;
19+
import java.util.HashMap;
20+
import java.util.HashSet;
21+
import java.util.Map;
22+
import java.util.Set;
23+
24+
import org.springframework.lang.Nullable;
25+
import org.springframework.security.authentication.AbstractAuthenticationToken;
26+
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion;
28+
import org.springframework.util.Assert;
29+
30+
/**
31+
* An {@link Authentication} implementation for the OAuth 2.0 Authorization Request used
32+
* in the Authorization Code Grant.
33+
*
34+
* @author Joe Grandja
35+
* @since 1.5
36+
* @see OAuth2AuthorizationCodeRequestAuthenticationToken
37+
*/
38+
class AbstractOAuth2AuthorizationCodeRequestAuthenticationToken extends AbstractAuthenticationToken {
39+
40+
private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;
41+
42+
private final String authorizationUri;
43+
44+
private final String clientId;
45+
46+
private final Authentication principal;
47+
48+
private final String redirectUri;
49+
50+
private final String state;
51+
52+
private final Set<String> scopes;
53+
54+
private final Map<String, Object> additionalParameters;
55+
56+
/**
57+
* Constructs an {@code OAuth2AuthorizationCodeRequestAuthenticationToken} using the
58+
* provided parameters.
59+
* @param authorizationUri the authorization URI
60+
* @param clientId the client identifier
61+
* @param principal the {@code Principal} (Resource Owner)
62+
* @param redirectUri the redirect uri
63+
* @param state the state
64+
* @param scopes the requested scope(s)
65+
* @param additionalParameters the additional parameters
66+
*/
67+
protected AbstractOAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId,
68+
Authentication principal, @Nullable String redirectUri, @Nullable String state,
69+
@Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
70+
super(Collections.emptyList());
71+
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
72+
Assert.hasText(clientId, "clientId cannot be empty");
73+
Assert.notNull(principal, "principal cannot be null");
74+
this.authorizationUri = authorizationUri;
75+
this.clientId = clientId;
76+
this.principal = principal;
77+
this.redirectUri = redirectUri;
78+
this.state = state;
79+
this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
80+
this.additionalParameters = Collections.unmodifiableMap(
81+
(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
82+
}
83+
84+
@Override
85+
public Object getPrincipal() {
86+
return this.principal;
87+
}
88+
89+
@Override
90+
public Object getCredentials() {
91+
return "";
92+
}
93+
94+
/**
95+
* Returns the authorization URI.
96+
* @return the authorization URI
97+
*/
98+
public String getAuthorizationUri() {
99+
return this.authorizationUri;
100+
}
101+
102+
/**
103+
* Returns the client identifier.
104+
* @return the client identifier
105+
*/
106+
public String getClientId() {
107+
return this.clientId;
108+
}
109+
110+
/**
111+
* Returns the redirect uri.
112+
* @return the redirect uri
113+
*/
114+
@Nullable
115+
public String getRedirectUri() {
116+
return this.redirectUri;
117+
}
118+
119+
/**
120+
* Returns the state.
121+
* @return the state
122+
*/
123+
@Nullable
124+
public String getState() {
125+
return this.state;
126+
}
127+
128+
/**
129+
* Returns the requested (or authorized) scope(s).
130+
* @return the requested (or authorized) scope(s), or an empty {@code Set} if not
131+
* available
132+
*/
133+
public Set<String> getScopes() {
134+
return this.scopes;
135+
}
136+
137+
/**
138+
* Returns the additional parameters.
139+
* @return the additional parameters, or an empty {@code Map} if not available
140+
*/
141+
public Map<String, Object> getAdditionalParameters() {
142+
return this.additionalParameters;
143+
}
144+
145+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2023 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -206,6 +206,8 @@ private static List<String> getAudience() {
206206
authorizationServerSettings.getTokenIntrospectionEndpoint()));
207207
audience.add(asUrl(authorizationServerContext.getIssuer(),
208208
authorizationServerSettings.getTokenRevocationEndpoint()));
209+
audience.add(asUrl(authorizationServerContext.getIssuer(),
210+
authorizationServerSettings.getPushedAuthorizationRequestEndpoint()));
209211
return audience;
210212
}
211213

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

Lines changed: 46 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,7 +27,6 @@
2727
import org.apache.commons.logging.Log;
2828
import org.apache.commons.logging.LogFactory;
2929

30-
import org.springframework.core.log.LogMessage;
3130
import org.springframework.security.authentication.AnonymousAuthenticationToken;
3231
import org.springframework.security.authentication.AuthenticationProvider;
3332
import org.springframework.security.core.Authentication;
@@ -39,7 +38,6 @@
3938
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
4039
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
4140
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
42-
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
4341
import org.springframework.security.oauth2.core.oidc.OidcScopes;
4442
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
4543
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
@@ -81,8 +79,6 @@ public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implemen
8179

8280
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";
8381

84-
private static final String PKCE_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1";
85-
8682
private static final StringKeyGenerator DEFAULT_STATE_GENERATOR = new Base64StringKeyGenerator(
8783
Base64.getUrlEncoder());
8884

@@ -122,6 +118,13 @@ public OAuth2AuthorizationCodeRequestAuthenticationProvider(RegisteredClientRepo
122118
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
123119
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
124120

121+
String requestUri = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
122+
.get("request_uri");
123+
if (StringUtils.hasText(requestUri)) {
124+
authorizationCodeRequestAuthentication = loadPushedAuthorizationRequest(
125+
authorizationCodeRequestAuthentication, requestUri);
126+
}
127+
125128
RegisteredClient registeredClient = this.registeredClientRepository
126129
.findByClientId(authorizationCodeRequestAuthentication.getClientId());
127130
if (registeredClient == null) {
@@ -136,47 +139,28 @@ public Authentication authenticate(Authentication authentication) throws Authent
136139
OAuth2AuthorizationCodeRequestAuthenticationContext.Builder authenticationContextBuilder = OAuth2AuthorizationCodeRequestAuthenticationContext
137140
.with(authorizationCodeRequestAuthentication)
138141
.registeredClient(registeredClient);
139-
this.authenticationValidator.accept(authenticationContextBuilder.build());
142+
OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext = authenticationContextBuilder
143+
.build();
140144

141-
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE)) {
142-
if (this.logger.isDebugEnabled()) {
143-
this.logger.debug(LogMessage.format(
144-
"Invalid request: requested grant_type is not allowed" + " for registered client '%s'",
145-
registeredClient.getId()));
146-
}
147-
throwError(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT, OAuth2ParameterNames.CLIENT_ID,
148-
authorizationCodeRequestAuthentication, registeredClient);
149-
}
145+
// grant_type
146+
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_AUTHORIZATION_GRANT_TYPE_VALIDATOR
147+
.accept(authenticationContext);
148+
149+
// scope and redirect_uri
150+
this.authenticationValidator.accept(authenticationContext);
150151

151152
// code_challenge (REQUIRED for public clients) - RFC 7636 (PKCE)
152-
String codeChallenge = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
153-
.get(PkceParameterNames.CODE_CHALLENGE);
154-
if (StringUtils.hasText(codeChallenge)) {
155-
String codeChallengeMethod = (String) authorizationCodeRequestAuthentication.getAdditionalParameters()
156-
.get(PkceParameterNames.CODE_CHALLENGE_METHOD);
157-
if (!StringUtils.hasText(codeChallengeMethod) || !"S256".equals(codeChallengeMethod)) {
158-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE_METHOD, PKCE_ERROR_URI,
159-
authorizationCodeRequestAuthentication, registeredClient, null);
160-
}
161-
}
162-
else if (registeredClient.getClientSettings().isRequireProofKey()) {
163-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, PkceParameterNames.CODE_CHALLENGE, PKCE_ERROR_URI,
164-
authorizationCodeRequestAuthentication, registeredClient, null);
165-
}
153+
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_CODE_CHALLENGE_VALIDATOR
154+
.accept(authenticationContext);
166155

167156
// prompt (OPTIONAL for OpenID Connect 1.0 Authentication Request)
168157
Set<String> promptValues = Collections.emptySet();
169158
if (authorizationCodeRequestAuthentication.getScopes().contains(OidcScopes.OPENID)) {
170159
String prompt = (String) authorizationCodeRequestAuthentication.getAdditionalParameters().get("prompt");
171160
if (StringUtils.hasText(prompt)) {
161+
OAuth2AuthorizationCodeRequestAuthenticationValidator.DEFAULT_PROMPT_VALIDATOR
162+
.accept(authenticationContext);
172163
promptValues = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(prompt, " ")));
173-
if (promptValues.contains(OidcPrompts.NONE)) {
174-
if (promptValues.contains(OidcPrompts.LOGIN) || promptValues.contains(OidcPrompts.CONSENT)
175-
|| promptValues.contains(OidcPrompts.SELECT_ACCOUNT)) {
176-
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "prompt", authorizationCodeRequestAuthentication,
177-
registeredClient);
178-
}
179-
}
180164
}
181165
}
182166

@@ -283,6 +267,32 @@ else if (registeredClient.getClientSettings().isRequireProofKey()) {
283267
authorizationRequest.getState(), authorizationRequest.getScopes());
284268
}
285269

270+
private OAuth2AuthorizationCodeRequestAuthenticationToken loadPushedAuthorizationRequest(
271+
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
272+
String requestUri) {
273+
274+
final OAuth2TokenType STATE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.STATE);
275+
276+
String state = requestUri.replace("urn:ietf:params:oauth:request_uri:", "");
277+
278+
OAuth2Authorization authorization = this.authorizationService.findByToken(state, STATE_TOKEN_TYPE);
279+
if (authorization == null) {
280+
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "request_uri", authorizationCodeRequestAuthentication, null);
281+
}
282+
283+
OAuth2AuthorizationRequest authorizationRequest = authorization
284+
.getAttribute(OAuth2AuthorizationRequest.class.getName());
285+
286+
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthenticationResult = new OAuth2AuthorizationCodeRequestAuthenticationToken(
287+
authorizationCodeRequestAuthentication.getAuthorizationUri(),
288+
authorizationCodeRequestAuthentication.getClientId(),
289+
(Authentication) authorizationCodeRequestAuthentication.getPrincipal(),
290+
authorizationRequest.getRedirectUri(), authorizationRequest.getState(),
291+
authorizationRequest.getScopes(), authorizationRequest.getAdditionalParameters());
292+
293+
return authorizationCodeRequestAuthenticationResult;
294+
}
295+
286296
@Override
287297
public boolean supports(Class<?> authentication) {
288298
return OAuth2AuthorizationCodeRequestAuthenticationToken.class.isAssignableFrom(authentication);
@@ -457,23 +467,4 @@ private static String resolveRedirectUri(
457467
return null;
458468
}
459469

460-
/*
461-
* The values defined for the "prompt" parameter for the OpenID Connect 1.0
462-
* Authentication Request.
463-
*/
464-
private static final class OidcPrompts {
465-
466-
private static final String NONE = "none";
467-
468-
private static final String LOGIN = "login";
469-
470-
private static final String CONSENT = "consent";
471-
472-
private static final String SELECT_ACCOUNT = "select_account";
473-
474-
private OidcPrompts() {
475-
}
476-
477-
}
478-
479470
}

0 commit comments

Comments
 (0)