Skip to content

Commit 2dff088

Browse files
committed
Add authorization server metadata for OAuth 2.0 Pushed Authorization Requests (PAR)
Issue gh-1925 Closes gh-1975
1 parent 4b78a5e commit 2dff088

9 files changed

+81
-0
lines changed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
* @see <a target="_blank" href=
5353
* "https://datatracker.ietf.org/doc/html/rfc9449#section-5.1">5.1 OAuth 2.0 Demonstrating
5454
* Proof of Possession (DPoP) Metadata</a>
55+
* @see <a target="_blank" href=
56+
* "https://datatracker.ietf.org/doc/html/rfc9126#name-authorization-server-metada">5.
57+
* OAuth 2.0 Pushed Authorization Requests Metadata</a>
5558
*/
5659
public abstract class AbstractOAuth2AuthorizationServerMetadata
5760
implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable {
@@ -119,6 +122,19 @@ public B authorizationEndpoint(String authorizationEndpoint) {
119122
return claim(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint);
120123
}
121124

125+
/**
126+
* Use this {@code pushed_authorization_request_endpoint} in the resulting
127+
* {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
128+
* @param pushedAuthorizationRequestEndpoint the {@code URL} of the OAuth 2.0
129+
* Pushed Authorization Request Endpoint
130+
* @return the {@link AbstractBuilder} for further configuration
131+
* @since 1.5
132+
*/
133+
public B pushedAuthorizationRequestEndpoint(String pushedAuthorizationRequestEndpoint) {
134+
return claim(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT,
135+
pushedAuthorizationRequestEndpoint);
136+
}
137+
122138
/**
123139
* Use this {@code device_authorization_endpoint} in the resulting
124140
* {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL.
@@ -454,6 +470,13 @@ protected void validate() {
454470
"authorizationEndpoint cannot be null");
455471
validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT),
456472
"authorizationEndpoint must be a valid URL");
473+
if (getClaims()
474+
.get(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT) != null) {
475+
validateURL(
476+
getClaims()
477+
.get(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT),
478+
"pushedAuthorizationRequestEndpoint must be a valid URL");
479+
}
457480
if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT) != null) {
458481
validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT),
459482
"deviceAuthorizationEndpoint must be a valid URL");

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
* @see <a target="_blank" href=
4545
* "https://datatracker.ietf.org/doc/html/rfc9449#section-5.1">5.1 OAuth 2.0 Demonstrating
4646
* Proof of Possession (DPoP) Metadata</a>
47+
* @see <a target="_blank" href=
48+
* "https://datatracker.ietf.org/doc/html/rfc9126#name-authorization-server-metada">5.
49+
* OAuth 2.0 Pushed Authorization Requests Metadata</a>
4750
*/
4851
public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAccessor {
4952

@@ -65,6 +68,16 @@ default URL getAuthorizationEndpoint() {
6568
return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT);
6669
}
6770

71+
/**
72+
* Returns the {@code URL} of the OAuth 2.0 Pushed Authorization Request Endpoint
73+
* {@code (pushed_authorization_request_endpoint)}.
74+
* @return the {@code URL} of the OAuth 2.0 Pushed Authorization Request Endpoint
75+
* @since 1.5
76+
*/
77+
default URL getPushedAuthorizationRequestEndpoint() {
78+
return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT);
79+
}
80+
6881
/**
6982
* Returns the {@code URL} of the OAuth 2.0 Device Authorization Endpoint
7083
* {@code (device_authorization_endpoint)}.

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
* @see <a target="_blank" href=
3838
* "https://datatracker.ietf.org/doc/html/rfc9449#section-5.1">5.1 OAuth 2.0 Demonstrating
3939
* Proof of Possession (DPoP) Metadata</a>
40+
* @see <a target="_blank" href=
41+
* "https://datatracker.ietf.org/doc/html/rfc9126#name-authorization-server-metada">5.
42+
* OAuth 2.0 Pushed Authorization Requests Metadata</a>
4043
*/
4144
public class OAuth2AuthorizationServerMetadataClaimNames {
4245

@@ -52,6 +55,13 @@ public class OAuth2AuthorizationServerMetadataClaimNames {
5255
*/
5356
public static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint";
5457

58+
/**
59+
* {@code pushed_authorization_request_endpoint} - the {@code URL} of the OAuth 2.0
60+
* Pushed Authorization Request Endpoint
61+
* @since 1.5
62+
*/
63+
public static final String PUSHED_AUTHORIZATION_REQUEST_ENDPOINT = "pushed_authorization_request_endpoint";
64+
5565
/**
5666
* {@code device_authorization_endpoint} - the {@code URL} of the OAuth 2.0 Device
5767
* Authorization Endpoint

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ private OAuth2AuthorizationServerMetadataConverter() {
148148
Map<String, Converter<Object, ?>> claimConverters = new HashMap<>();
149149
claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, urlConverter);
150150
claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter);
151+
claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT,
152+
urlConverter);
151153
claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT,
152154
urlConverter);
153155
claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, urlConverter);

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
101101
OidcProviderConfiguration.Builder providerConfiguration = OidcProviderConfiguration.builder()
102102
.issuer(issuer)
103103
.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
104+
.pushedAuthorizationRequestEndpoint(
105+
asUrl(issuer, authorizationServerSettings.getPushedAuthorizationRequestEndpoint()))
104106
.deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint()))
105107
.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
106108
.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
101101
.builder()
102102
.issuer(issuer)
103103
.authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint()))
104+
.pushedAuthorizationRequestEndpoint(
105+
asUrl(issuer, authorizationServerSettings.getPushedAuthorizationRequestEndpoint()))
104106
.deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint()))
105107
.tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint()))
106108
.tokenEndpointAuthenticationMethods(clientAuthenticationMethods())

oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public void buildWhenAllClaimsProvidedThenCreated() {
5151
OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder()
5252
.issuer("https://example.com")
5353
.authorizationEndpoint("https://example.com/oauth2/authorize")
54+
.pushedAuthorizationRequestEndpoint("https://example.com/oauth2/par")
5455
.tokenEndpoint("https://example.com/oauth2/token")
5556
.tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue())
5657
.jwkSetUrl("https://example.com/oauth2/jwks")
@@ -72,6 +73,8 @@ public void buildWhenAllClaimsProvidedThenCreated() {
7273
assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com"));
7374
assertThat(authorizationServerMetadata.getAuthorizationEndpoint())
7475
.isEqualTo(url("https://example.com/oauth2/authorize"));
76+
assertThat(authorizationServerMetadata.getPushedAuthorizationRequestEndpoint())
77+
.isEqualTo(url("https://example.com/oauth2/par"));
7578
assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/oauth2/token"));
7679
assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods())
7780
.containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue());
@@ -107,6 +110,7 @@ public void buildWhenOnlyRequiredClaimsProvidedThenCreated() {
107110
assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com"));
108111
assertThat(authorizationServerMetadata.getAuthorizationEndpoint())
109112
.isEqualTo(url("https://example.com/oauth2/authorize"));
113+
assertThat(authorizationServerMetadata.getPushedAuthorizationRequestEndpoint()).isNull();
110114
assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/oauth2/token"));
111115
assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull();
112116
assertThat(authorizationServerMetadata.getJwkSetUrl()).isNull();
@@ -127,6 +131,8 @@ public void withClaimsWhenClaimsProvidedThenCreated() {
127131
claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "https://example.com");
128132
claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT,
129133
"https://example.com/oauth2/authorize");
134+
claims.put(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT,
135+
"https://example.com/oauth2/par");
130136
claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "https://example.com/oauth2/token");
131137
claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "https://example.com/oauth2/jwks");
132138
claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid"));
@@ -145,6 +151,8 @@ public void withClaimsWhenClaimsProvidedThenCreated() {
145151
assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com"));
146152
assertThat(authorizationServerMetadata.getAuthorizationEndpoint())
147153
.isEqualTo(url("https://example.com/oauth2/authorize"));
154+
assertThat(authorizationServerMetadata.getPushedAuthorizationRequestEndpoint())
155+
.isEqualTo(url("https://example.com/oauth2/par"));
148156
assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/oauth2/token"));
149157
assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull();
150158
assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(url("https://example.com/oauth2/jwks"));
@@ -168,6 +176,8 @@ public void withClaimsWhenClaimsWithUrlsProvidedThenCreated() {
168176
claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, url("https://example.com"));
169177
claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT,
170178
url("https://example.com/oauth2/authorize"));
179+
claims.put(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT,
180+
url("https://example.com/oauth2/par"));
171181
claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, url("https://example.com/oauth2/token"));
172182
claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, url("https://example.com/oauth2/jwks"));
173183
claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED,
@@ -185,6 +195,8 @@ public void withClaimsWhenClaimsWithUrlsProvidedThenCreated() {
185195
assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com"));
186196
assertThat(authorizationServerMetadata.getAuthorizationEndpoint())
187197
.isEqualTo(url("https://example.com/oauth2/authorize"));
198+
assertThat(authorizationServerMetadata.getPushedAuthorizationRequestEndpoint())
199+
.isEqualTo(url("https://example.com/oauth2/par"));
188200
assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/oauth2/token"));
189201
assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull();
190202
assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(url("https://example.com/oauth2/jwks"));
@@ -264,6 +276,15 @@ public void buildWhenAuthorizationEndpointNotUrlThenThrowIllegalArgumentExceptio
264276
.withMessage("authorizationEndpoint must be a valid URL");
265277
}
266278

279+
@Test
280+
public void buildWhenPushedAuthorizationRequestEndpointNotUrlThenThrowIllegalArgumentException() {
281+
Builder builder = this.minimalBuilder.claims((claims) -> claims
282+
.put(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT, "not an url"));
283+
284+
assertThatIllegalArgumentException().isThrownBy(builder::build)
285+
.withMessage("pushedAuthorizationRequestEndpoint must be a valid URL");
286+
}
287+
267288
@Test
268289
public void buildWhenMissingTokenEndpointThenThrowsIllegalArgumentException() {
269290
Builder builder = this.minimalBuilder

oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public void doFilterWhenConfigurationRequestPostThenNotProcessed() throws Except
9696
public void doFilterWhenConfigurationRequestThenConfigurationResponse() throws Exception {
9797
String issuer = "https://example.com";
9898
String authorizationEndpoint = "/oauth2/v1/authorize";
99+
String pushedAuthorizationRequestEndpoint = "/oauth2/v1/par";
99100
String tokenEndpoint = "/oauth2/v1/token";
100101
String jwkSetEndpoint = "/oauth2/v1/jwks";
101102
String userInfoEndpoint = "/userinfo";
@@ -106,6 +107,7 @@ public void doFilterWhenConfigurationRequestThenConfigurationResponse() throws E
106107
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
107108
.issuer(issuer)
108109
.authorizationEndpoint(authorizationEndpoint)
110+
.pushedAuthorizationRequestEndpoint(pushedAuthorizationRequestEndpoint)
109111
.tokenEndpoint(tokenEndpoint)
110112
.jwkSetEndpoint(jwkSetEndpoint)
111113
.oidcUserInfoEndpoint(userInfoEndpoint)
@@ -131,6 +133,8 @@ public void doFilterWhenConfigurationRequestThenConfigurationResponse() throws E
131133
assertThat(providerConfigurationResponse).contains("\"issuer\":\"https://example.com\"");
132134
assertThat(providerConfigurationResponse)
133135
.contains("\"authorization_endpoint\":\"https://example.com/oauth2/v1/authorize\"");
136+
assertThat(providerConfigurationResponse)
137+
.contains("\"pushed_authorization_request_endpoint\":\"https://example.com/oauth2/v1/par\"");
134138
assertThat(providerConfigurationResponse)
135139
.contains("\"token_endpoint\":\"https://example.com/oauth2/v1/token\"");
136140
assertThat(providerConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/oauth2/v1/jwks\"");

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public void doFilterWhenAuthorizationServerMetadataRequestPostThenNotProcessed()
9696
public void doFilterWhenAuthorizationServerMetadataRequestThenMetadataResponse() throws Exception {
9797
String issuer = "https://example.com";
9898
String authorizationEndpoint = "/oauth2/v1/authorize";
99+
String pushedAuthorizationRequestEndpoint = "/oauth2/v1/par";
99100
String tokenEndpoint = "/oauth2/v1/token";
100101
String jwkSetEndpoint = "/oauth2/v1/jwks";
101102
String tokenRevocationEndpoint = "/oauth2/v1/revoke";
@@ -104,6 +105,7 @@ public void doFilterWhenAuthorizationServerMetadataRequestThenMetadataResponse()
104105
AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder()
105106
.issuer(issuer)
106107
.authorizationEndpoint(authorizationEndpoint)
108+
.pushedAuthorizationRequestEndpoint(pushedAuthorizationRequestEndpoint)
107109
.tokenEndpoint(tokenEndpoint)
108110
.jwkSetEndpoint(jwkSetEndpoint)
109111
.tokenRevocationEndpoint(tokenRevocationEndpoint)
@@ -127,6 +129,8 @@ public void doFilterWhenAuthorizationServerMetadataRequestThenMetadataResponse()
127129
assertThat(authorizationServerMetadataResponse).contains("\"issuer\":\"https://example.com\"");
128130
assertThat(authorizationServerMetadataResponse)
129131
.contains("\"authorization_endpoint\":\"https://example.com/oauth2/v1/authorize\"");
132+
assertThat(authorizationServerMetadataResponse)
133+
.contains("\"pushed_authorization_request_endpoint\":\"https://example.com/oauth2/v1/par\"");
130134
assertThat(authorizationServerMetadataResponse)
131135
.contains("\"token_endpoint\":\"https://example.com/oauth2/v1/token\"");
132136
assertThat(authorizationServerMetadataResponse).contains(

0 commit comments

Comments
 (0)