diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java index e1254f0c..04fe1e5a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2AuthorizationServerMetadata.java @@ -52,6 +52,9 @@ import org.springframework.util.Assert; * @see 5.1 OAuth 2.0 Demonstrating * Proof of Possession (DPoP) Metadata + * @see 5. + * OAuth 2.0 Pushed Authorization Requests Metadata */ public abstract class AbstractOAuth2AuthorizationServerMetadata implements OAuth2AuthorizationServerMetadataClaimAccessor, Serializable { @@ -119,6 +122,19 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata return claim(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, authorizationEndpoint); } + /** + * Use this {@code pushed_authorization_request_endpoint} in the resulting + * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. + * @param pushedAuthorizationRequestEndpoint the {@code URL} of the OAuth 2.0 + * Pushed Authorization Request Endpoint + * @return the {@link AbstractBuilder} for further configuration + * @since 1.5 + */ + public B pushedAuthorizationRequestEndpoint(String pushedAuthorizationRequestEndpoint) { + return claim(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT, + pushedAuthorizationRequestEndpoint); + } + /** * Use this {@code device_authorization_endpoint} in the resulting * {@link AbstractOAuth2AuthorizationServerMetadata}, OPTIONAL. @@ -454,6 +470,13 @@ public abstract class AbstractOAuth2AuthorizationServerMetadata "authorizationEndpoint cannot be null"); validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT), "authorizationEndpoint must be a valid URL"); + if (getClaims() + .get(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT) != null) { + validateURL( + getClaims() + .get(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT), + "pushedAuthorizationRequestEndpoint must be a valid URL"); + } if (getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT) != null) { validateURL(getClaims().get(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT), "deviceAuthorizationEndpoint must be a valid URL"); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java index a7ceb35e..ba88f8b5 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimAccessor.java @@ -44,6 +44,9 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; * @see 5.1 OAuth 2.0 Demonstrating * Proof of Possession (DPoP) Metadata + * @see 5. + * OAuth 2.0 Pushed Authorization Requests Metadata */ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAccessor { @@ -65,6 +68,16 @@ public interface OAuth2AuthorizationServerMetadataClaimAccessor extends ClaimAcc return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT); } + /** + * Returns the {@code URL} of the OAuth 2.0 Pushed Authorization Request Endpoint + * {@code (pushed_authorization_request_endpoint)}. + * @return the {@code URL} of the OAuth 2.0 Pushed Authorization Request Endpoint + * @since 1.5 + */ + default URL getPushedAuthorizationRequestEndpoint() { + return getClaimAsURL(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT); + } + /** * Returns the {@code URL} of the OAuth 2.0 Device Authorization Endpoint * {@code (device_authorization_endpoint)}. diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java index 7a47498b..c8ee980e 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataClaimNames.java @@ -37,6 +37,9 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; * @see 5.1 OAuth 2.0 Demonstrating * Proof of Possession (DPoP) Metadata + * @see 5. + * OAuth 2.0 Pushed Authorization Requests Metadata */ public class OAuth2AuthorizationServerMetadataClaimNames { @@ -52,6 +55,13 @@ public class OAuth2AuthorizationServerMetadataClaimNames { */ public static final String AUTHORIZATION_ENDPOINT = "authorization_endpoint"; + /** + * {@code pushed_authorization_request_endpoint} - the {@code URL} of the OAuth 2.0 + * Pushed Authorization Request Endpoint + * @since 1.5 + */ + public static final String PUSHED_AUTHORIZATION_REQUEST_ENDPOINT = "pushed_authorization_request_endpoint"; + /** * {@code device_authorization_endpoint} - the {@code URL} of the OAuth 2.0 Device * Authorization Endpoint diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java index 3339ebea..f904987a 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/http/converter/OAuth2AuthorizationServerMetadataHttpMessageConverter.java @@ -148,6 +148,8 @@ public class OAuth2AuthorizationServerMetadataHttpMessageConverter Map> claimConverters = new HashMap<>(); claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, urlConverter); claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, urlConverter); + claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT, + urlConverter); claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.DEVICE_AUTHORIZATION_ENDPOINT, urlConverter); claimConverters.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, urlConverter); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java index b527396f..43dffe51 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilter.java @@ -101,6 +101,8 @@ public final class OidcProviderConfigurationEndpointFilter extends OncePerReques OidcProviderConfiguration.Builder providerConfiguration = OidcProviderConfiguration.builder() .issuer(issuer) .authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint())) + .pushedAuthorizationRequestEndpoint( + asUrl(issuer, authorizationServerSettings.getPushedAuthorizationRequestEndpoint())) .deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint())) .tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint())) .tokenEndpointAuthenticationMethods(clientAuthenticationMethods()) diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java index 608766ae..c2f9e067 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilter.java @@ -101,6 +101,8 @@ public final class OAuth2AuthorizationServerMetadataEndpointFilter extends OnceP .builder() .issuer(issuer) .authorizationEndpoint(asUrl(issuer, authorizationServerSettings.getAuthorizationEndpoint())) + .pushedAuthorizationRequestEndpoint( + asUrl(issuer, authorizationServerSettings.getPushedAuthorizationRequestEndpoint())) .deviceAuthorizationEndpoint(asUrl(issuer, authorizationServerSettings.getDeviceAuthorizationEndpoint())) .tokenEndpoint(asUrl(issuer, authorizationServerSettings.getTokenEndpoint())) .tokenEndpointAuthenticationMethods(clientAuthenticationMethods()) diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java index 86fa599a..b9afdeed 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/OAuth2AuthorizationServerMetadataTests.java @@ -51,6 +51,7 @@ public class OAuth2AuthorizationServerMetadataTests { OAuth2AuthorizationServerMetadata authorizationServerMetadata = OAuth2AuthorizationServerMetadata.builder() .issuer("https://example.com") .authorizationEndpoint("https://example.com/oauth2/authorize") + .pushedAuthorizationRequestEndpoint("https://example.com/oauth2/par") .tokenEndpoint("https://example.com/oauth2/token") .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()) .jwkSetUrl("https://example.com/oauth2/jwks") @@ -72,6 +73,8 @@ public class OAuth2AuthorizationServerMetadataTests { assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com")); assertThat(authorizationServerMetadata.getAuthorizationEndpoint()) .isEqualTo(url("https://example.com/oauth2/authorize")); + assertThat(authorizationServerMetadata.getPushedAuthorizationRequestEndpoint()) + .isEqualTo(url("https://example.com/oauth2/par")); assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/oauth2/token")); assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()) .containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); @@ -107,6 +110,7 @@ public class OAuth2AuthorizationServerMetadataTests { assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com")); assertThat(authorizationServerMetadata.getAuthorizationEndpoint()) .isEqualTo(url("https://example.com/oauth2/authorize")); + assertThat(authorizationServerMetadata.getPushedAuthorizationRequestEndpoint()).isNull(); assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/oauth2/token")); assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull(); assertThat(authorizationServerMetadata.getJwkSetUrl()).isNull(); @@ -127,6 +131,8 @@ public class OAuth2AuthorizationServerMetadataTests { claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, "https://example.com"); claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, "https://example.com/oauth2/authorize"); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT, + "https://example.com/oauth2/par"); claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, "https://example.com/oauth2/token"); claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, "https://example.com/oauth2/jwks"); claims.put(OAuth2AuthorizationServerMetadataClaimNames.SCOPES_SUPPORTED, Collections.singletonList("openid")); @@ -145,6 +151,8 @@ public class OAuth2AuthorizationServerMetadataTests { assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com")); assertThat(authorizationServerMetadata.getAuthorizationEndpoint()) .isEqualTo(url("https://example.com/oauth2/authorize")); + assertThat(authorizationServerMetadata.getPushedAuthorizationRequestEndpoint()) + .isEqualTo(url("https://example.com/oauth2/par")); assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/oauth2/token")); assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull(); assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(url("https://example.com/oauth2/jwks")); @@ -168,6 +176,8 @@ public class OAuth2AuthorizationServerMetadataTests { claims.put(OAuth2AuthorizationServerMetadataClaimNames.ISSUER, url("https://example.com")); claims.put(OAuth2AuthorizationServerMetadataClaimNames.AUTHORIZATION_ENDPOINT, url("https://example.com/oauth2/authorize")); + claims.put(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT, + url("https://example.com/oauth2/par")); claims.put(OAuth2AuthorizationServerMetadataClaimNames.TOKEN_ENDPOINT, url("https://example.com/oauth2/token")); claims.put(OAuth2AuthorizationServerMetadataClaimNames.JWKS_URI, url("https://example.com/oauth2/jwks")); claims.put(OAuth2AuthorizationServerMetadataClaimNames.RESPONSE_TYPES_SUPPORTED, @@ -185,6 +195,8 @@ public class OAuth2AuthorizationServerMetadataTests { assertThat(authorizationServerMetadata.getIssuer()).isEqualTo(url("https://example.com")); assertThat(authorizationServerMetadata.getAuthorizationEndpoint()) .isEqualTo(url("https://example.com/oauth2/authorize")); + assertThat(authorizationServerMetadata.getPushedAuthorizationRequestEndpoint()) + .isEqualTo(url("https://example.com/oauth2/par")); assertThat(authorizationServerMetadata.getTokenEndpoint()).isEqualTo(url("https://example.com/oauth2/token")); assertThat(authorizationServerMetadata.getTokenEndpointAuthenticationMethods()).isNull(); assertThat(authorizationServerMetadata.getJwkSetUrl()).isEqualTo(url("https://example.com/oauth2/jwks")); @@ -264,6 +276,15 @@ public class OAuth2AuthorizationServerMetadataTests { .withMessage("authorizationEndpoint must be a valid URL"); } + @Test + public void buildWhenPushedAuthorizationRequestEndpointNotUrlThenThrowIllegalArgumentException() { + Builder builder = this.minimalBuilder.claims((claims) -> claims + .put(OAuth2AuthorizationServerMetadataClaimNames.PUSHED_AUTHORIZATION_REQUEST_ENDPOINT, "not an url")); + + assertThatIllegalArgumentException().isThrownBy(builder::build) + .withMessage("pushedAuthorizationRequestEndpoint must be a valid URL"); + } + @Test public void buildWhenMissingTokenEndpointThenThrowsIllegalArgumentException() { Builder builder = this.minimalBuilder diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java index 80b70e30..467682d4 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcProviderConfigurationEndpointFilterTests.java @@ -96,6 +96,7 @@ public class OidcProviderConfigurationEndpointFilterTests { public void doFilterWhenConfigurationRequestThenConfigurationResponse() throws Exception { String issuer = "https://example.com"; String authorizationEndpoint = "/oauth2/v1/authorize"; + String pushedAuthorizationRequestEndpoint = "/oauth2/v1/par"; String tokenEndpoint = "/oauth2/v1/token"; String jwkSetEndpoint = "/oauth2/v1/jwks"; String userInfoEndpoint = "/userinfo"; @@ -106,6 +107,7 @@ public class OidcProviderConfigurationEndpointFilterTests { AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder() .issuer(issuer) .authorizationEndpoint(authorizationEndpoint) + .pushedAuthorizationRequestEndpoint(pushedAuthorizationRequestEndpoint) .tokenEndpoint(tokenEndpoint) .jwkSetEndpoint(jwkSetEndpoint) .oidcUserInfoEndpoint(userInfoEndpoint) @@ -131,6 +133,8 @@ public class OidcProviderConfigurationEndpointFilterTests { assertThat(providerConfigurationResponse).contains("\"issuer\":\"https://example.com\""); assertThat(providerConfigurationResponse) .contains("\"authorization_endpoint\":\"https://example.com/oauth2/v1/authorize\""); + assertThat(providerConfigurationResponse) + .contains("\"pushed_authorization_request_endpoint\":\"https://example.com/oauth2/v1/par\""); assertThat(providerConfigurationResponse) .contains("\"token_endpoint\":\"https://example.com/oauth2/v1/token\""); assertThat(providerConfigurationResponse).contains("\"jwks_uri\":\"https://example.com/oauth2/v1/jwks\""); diff --git a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java index 4f9de8e1..34ee4307 100644 --- a/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java +++ b/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/web/OAuth2AuthorizationServerMetadataEndpointFilterTests.java @@ -96,6 +96,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { public void doFilterWhenAuthorizationServerMetadataRequestThenMetadataResponse() throws Exception { String issuer = "https://example.com"; 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"; @@ -104,6 +105,7 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { AuthorizationServerSettings authorizationServerSettings = AuthorizationServerSettings.builder() .issuer(issuer) .authorizationEndpoint(authorizationEndpoint) + .pushedAuthorizationRequestEndpoint(pushedAuthorizationRequestEndpoint) .tokenEndpoint(tokenEndpoint) .jwkSetEndpoint(jwkSetEndpoint) .tokenRevocationEndpoint(tokenRevocationEndpoint) @@ -127,6 +129,8 @@ public class OAuth2AuthorizationServerMetadataEndpointFilterTests { assertThat(authorizationServerMetadataResponse).contains("\"issuer\":\"https://example.com\""); assertThat(authorizationServerMetadataResponse) .contains("\"authorization_endpoint\":\"https://example.com/oauth2/v1/authorize\""); + assertThat(authorizationServerMetadataResponse) + .contains("\"pushed_authorization_request_endpoint\":\"https://example.com/oauth2/v1/par\""); assertThat(authorizationServerMetadataResponse) .contains("\"token_endpoint\":\"https://example.com/oauth2/v1/token\""); assertThat(authorizationServerMetadataResponse).contains(