Add authorization server metadata for OAuth 2.0 Pushed Authorization Requests (PAR)
Issue gh-1925 Closes gh-1975
This commit is contained in:
@@ -52,6 +52,9 @@ import org.springframework.util.Assert;
|
||||
* @see <a target="_blank" href=
|
||||
* "https://datatracker.ietf.org/doc/html/rfc9449#section-5.1">5.1 OAuth 2.0 Demonstrating
|
||||
* Proof of Possession (DPoP) Metadata</a>
|
||||
* @see <a target="_blank" href=
|
||||
* "https://datatracker.ietf.org/doc/html/rfc9126#name-authorization-server-metada">5.
|
||||
* OAuth 2.0 Pushed Authorization Requests Metadata</a>
|
||||
*/
|
||||
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");
|
||||
|
||||
@@ -44,6 +44,9 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
|
||||
* @see <a target="_blank" href=
|
||||
* "https://datatracker.ietf.org/doc/html/rfc9449#section-5.1">5.1 OAuth 2.0 Demonstrating
|
||||
* Proof of Possession (DPoP) Metadata</a>
|
||||
* @see <a target="_blank" href=
|
||||
* "https://datatracker.ietf.org/doc/html/rfc9126#name-authorization-server-metada">5.
|
||||
* OAuth 2.0 Pushed Authorization Requests Metadata</a>
|
||||
*/
|
||||
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)}.
|
||||
|
||||
@@ -37,6 +37,9 @@ import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
|
||||
* @see <a target="_blank" href=
|
||||
* "https://datatracker.ietf.org/doc/html/rfc9449#section-5.1">5.1 OAuth 2.0 Demonstrating
|
||||
* Proof of Possession (DPoP) Metadata</a>
|
||||
* @see <a target="_blank" href=
|
||||
* "https://datatracker.ietf.org/doc/html/rfc9126#name-authorization-server-metada">5.
|
||||
* OAuth 2.0 Pushed Authorization Requests Metadata</a>
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -148,6 +148,8 @@ public class OAuth2AuthorizationServerMetadataHttpMessageConverter
|
||||
Map<String, Converter<Object, ?>> 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);
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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\"");
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user