diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java index ddc2821798c..343014e84b4 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcClientRegistrationTests.java @@ -333,6 +333,38 @@ public void requestWhenClientConfigurationRequestAuthorizedThenClientRegistratio assertThat(clientConfigurationResponse.getRegistrationAccessToken()).isNull(); } + @Test + public void requestWhenClientRegistersWithBackChannelLogoutThenReturnedInClientConfiguration() throws Exception { + this.spring.register(AuthorizationServerConfiguration.class).autowire(); + + // @formatter:off + OidcClientRegistration clientRegistration = OidcClientRegistration.builder() + .clientName("client-name") + .redirectUri("https://client.example.com") + .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) + .backChannelLogoutUri("https://client.example.com/logout/connect/back-channel") + .backChannelLogoutSessionRequired(true) + .build(); + // @formatter:on + + OidcClientRegistration clientRegistrationResponse = registerClient(clientRegistration); + assertThat(clientRegistrationResponse.getBackChannelLogoutUri().toString()) + .isEqualTo("https://client.example.com/logout/connect/back-channel"); + assertThat(clientRegistrationResponse.isBackChannelLogoutSessionRequired()).isTrue(); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setBearerAuth(clientRegistrationResponse.getRegistrationAccessToken()); + MvcResult mvcResult = this.mvc + .perform(get(clientRegistrationResponse.getRegistrationClientUrl().toURI()).headers(httpHeaders)) + .andExpect(status().isOk()) + .andReturn(); + OidcClientRegistration clientConfigurationResponse = readClientRegistrationResponse(mvcResult.getResponse()); + + assertThat(clientConfigurationResponse.getBackChannelLogoutUri().toString()) + .isEqualTo("https://client.example.com/logout/connect/back-channel"); + assertThat(clientConfigurationResponse.isBackChannelLogoutSessionRequired()).isTrue(); + } + @Test public void requestWhenClientRegistrationEndpointCustomizedThenUsed() throws Exception { this.spring.register(CustomClientRegistrationConfiguration.class).autowire(); @@ -622,6 +654,19 @@ public void requestWhenHttpJwkSetUriThenBadRequest() throws Exception { """)).isEqualTo(HttpStatus.BAD_REQUEST.value()); } + @Test + public void requestWhenHttpBackChannelLogoutUriThenBadRequest() throws Exception { + this.spring.register(DefaultValidatorConfiguration.class).autowire(); + assertThat(requestWhenInvalidClientMetadataThenBadRequest(""" + { + "client_name": "client-name", + "redirect_uris": ["https://client.example.com"], + "grant_types": ["authorization_code"], + "backchannel_logout_uri": "http://169.254.169.254/logout" + } + """)).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + @Test public void requestWhenArbitraryScopeThenBadRequest() throws Exception { this.spring.register(DefaultValidatorConfiguration.class).autowire(); diff --git a/docs/modules/ROOT/pages/servlet/oauth2/authorization-server/core-model-components.adoc b/docs/modules/ROOT/pages/servlet/oauth2/authorization-server/core-model-components.adoc index 748674fbabe..9ddf84ff204 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/authorization-server/core-model-components.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/authorization-server/core-model-components.adoc @@ -113,6 +113,10 @@ public final class ClientSettings extends AbstractSettings { public String getX509CertificateSubjectDN() ... <5> + public String getBackChannelLogoutUri() ... <6> + + public boolean isBackChannelLogoutSessionRequired() ... <7> + ... } @@ -122,6 +126,8 @@ public final class ClientSettings extends AbstractSettings { <3> `getJwkSetUrl()`: The `URL` for the client's JSON Web Key Set. Used for `private_key_jwt`, `self_signed_tls_client_auth` and `client_secret_jwt` client authentication methods. <4> `getTokenEndpointAuthenticationSigningAlgorithm()`: The `JwsAlgorithm` that must be used for signing the JWT used to authenticate the client at the Token Endpoint for `private_key_jwt` and `client_secret_jwt` authentication methods. <5> `getX509CertificateSubjectDN()`: The expected subject distinguished name associated to the client `X509Certificate` received during client authentication when using the `tls_client_auth` method. +<6> `getBackChannelLogoutUri()`: The `URL` that the client exposes to receive a Logout Token, as defined by https://openid.net/specs/openid-connect-backchannel-1_0.html[OpenID Connect Back-Channel Logout 1.0]. +<7> `isBackChannelLogoutSessionRequired()`: If `true`, the client requires a `sid` (session ID) claim in the Logout Token. The default is `false`. [NOTE] https://datatracker.ietf.org/doc/html/rfc7636[Proof Key for Code Exchange (PKCE)] is enabled by default for all clients using the Authorization Code grant. To disable PKCE, set `requireProofKey` to `false`. diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index d92dfca119c..4cc7a770495 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -12,4 +12,5 @@ == OAuth 2.0 * https://github.com/spring-projects/spring-security/pull/18895[gh-18895] - Add `authenticationSuccessHandler` to the Reactive Resource Server DSL +* https://github.com/spring-projects/spring-security/issues/18296[gh-18296] - Add OpenID Connect Back-Channel Logout client and provider metadata to the Authorization Server diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2ClientRegistration.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2ClientRegistration.java index f020eef1a76..d6ec20570b6 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2ClientRegistration.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/AbstractOAuth2ClientRegistration.java @@ -349,7 +349,7 @@ private void acceptClaimValues(String name, Consumer> valuesConsume valuesConsumer.accept(values); } - private static void validateURL(Object url, String errorMessage) { + protected static void validateURL(Object url, String errorMessage) { if (URL.class.isAssignableFrom(url.getClass())) { return; } diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimAccessor.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimAccessor.java index a045577c75f..54d76557545 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimAccessor.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimAccessor.java @@ -83,6 +83,29 @@ public interface OidcClientMetadataClaimAccessor extends OAuth2ClientMetadataCla return getClaimAsString(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG); } + /** + * Returns the {@code URL} that will cause the Client to log itself out when sent a + * Logout Token by the OpenID Provider {@code (backchannel_logout_uri)}. + * @return the {@code URL} that will cause the Client to log itself out when sent a + * Logout Token, or {@code null} if not set + * @since 7.2 + */ + default @Nullable URL getBackChannelLogoutUri() { + return getClaimAsURL(OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_URI); + } + + /** + * Returns {@code true} if the Client requires that a {@code sid} (session ID) Claim + * be included in the Logout Token {@code (backchannel_logout_session_required)}. The + * default is {@code false}. + * @return {@code true} if the Client requires a {@code sid} Claim in the Logout + * Token, {@code false} otherwise + * @since 7.2 + */ + default boolean isBackChannelLogoutSessionRequired() { + return Boolean.TRUE.equals(getClaimAsBoolean(OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_SESSION_REQUIRED)); + } + /** * Returns the Registration Access Token that can be used at the Client Configuration * Endpoint. diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimNames.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimNames.java index 8f971a64a2e..c6f9bc5152d 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimNames.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientMetadataClaimNames.java @@ -36,6 +36,9 @@ * @see 3.1. * Client Registration Metadata + * @see 2.2. + * Indicating RP Support for Back-Channel Logout */ public final class OidcClientMetadataClaimNames extends OAuth2ClientMetadataClaimNames { @@ -62,6 +65,22 @@ public final class OidcClientMetadataClaimNames extends OAuth2ClientMetadataClai */ public static final String ID_TOKEN_SIGNED_RESPONSE_ALG = "id_token_signed_response_alg"; + /** + * {@code backchannel_logout_uri} - the {@code URL} that will cause the Client to log + * itself out when sent a Logout Token by the OpenID Provider. + * @since 7.2 + */ + public static final String BACKCHANNEL_LOGOUT_URI = "backchannel_logout_uri"; + + /** + * {@code backchannel_logout_session_required} - {@code true} if the Client requires + * that a {@code sid} (session ID) Claim be included in the Logout Token to identify + * the Client session with the OpenID Provider when the {@code backchannel_logout_uri} + * is used. + * @since 7.2 + */ + public static final String BACKCHANNEL_LOGOUT_SESSION_REQUIRED = "backchannel_logout_session_required"; + /** * {@code registration_access_token} - the Registration Access Token that can be used * at the Client Configuration Endpoint. diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistration.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistration.java index e20397628c9..486b5fe9bd6 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistration.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistration.java @@ -140,6 +140,31 @@ public Builder idTokenSignedResponseAlgorithm(String idTokenSignedResponseAlgori return claim(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, idTokenSignedResponseAlgorithm); } + /** + * Sets the {@code URL} that will cause the Client to log itself out when sent a + * Logout Token by the OpenID Provider, OPTIONAL. + * @param backChannelLogoutUri the {@code URL} that will cause the Client to log + * itself out when sent a Logout Token + * @return the {@link Builder} for further configuration + * @since 7.2 + */ + public Builder backChannelLogoutUri(String backChannelLogoutUri) { + return claim(OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_URI, backChannelLogoutUri); + } + + /** + * Set to {@code true} if the Client requires that a {@code sid} (session ID) + * Claim be included in the Logout Token, OPTIONAL. + * @param backChannelLogoutSessionRequired {@code true} if the Client requires a + * {@code sid} Claim in the Logout Token + * @return the {@link Builder} for further configuration + * @since 7.2 + */ + public Builder backChannelLogoutSessionRequired(boolean backChannelLogoutSessionRequired) { + return claim(OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_SESSION_REQUIRED, + backChannelLogoutSessionRequired); + } + /** * Sets the Registration Access Token that can be used at the Client Configuration * Endpoint, OPTIONAL. @@ -188,6 +213,10 @@ protected void validate() { Assert.notEmpty((List) getClaims().get(OidcClientMetadataClaimNames.POST_LOGOUT_REDIRECT_URIS), "post_logout_redirect_uris cannot be empty"); } + if (getClaims().get(OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_URI) != null) { + validateURL(getClaims().get(OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_URI), + "backchannel_logout_uri must be a valid URL"); + } } @SuppressWarnings("unchecked") diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfiguration.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfiguration.java index e1f8106225f..ea0edf45c78 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfiguration.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfiguration.java @@ -148,6 +148,31 @@ public Builder endSessionEndpoint(String endSessionEndpoint) { return claim(OidcProviderMetadataClaimNames.END_SESSION_ENDPOINT, endSessionEndpoint); } + /** + * Use this {@code backchannel_logout_supported} in the resulting + * {@link OidcProviderConfiguration}, OPTIONAL. + * @param backChannelLogoutSupported {@code true} if the OpenID Provider supports + * back-channel logout + * @return the {@link Builder} for further configuration + * @since 7.2 + */ + public Builder backChannelLogoutSupported(boolean backChannelLogoutSupported) { + return claim(OidcProviderMetadataClaimNames.BACKCHANNEL_LOGOUT_SUPPORTED, backChannelLogoutSupported); + } + + /** + * Use this {@code backchannel_logout_session_supported} in the resulting + * {@link OidcProviderConfiguration}, OPTIONAL. + * @param backChannelLogoutSessionSupported {@code true} if the OpenID Provider + * can pass a {@code sid} Claim in the Logout Token + * @return the {@link Builder} for further configuration + * @since 7.2 + */ + public Builder backChannelLogoutSessionSupported(boolean backChannelLogoutSessionSupported) { + return claim(OidcProviderMetadataClaimNames.BACKCHANNEL_LOGOUT_SESSION_SUPPORTED, + backChannelLogoutSessionSupported); + } + /** * Validate the claims and build the {@link OidcProviderConfiguration}. *

diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimAccessor.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimAccessor.java index cc2d489c188..7422bea897e 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimAccessor.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimAccessor.java @@ -89,4 +89,28 @@ default URL getEndSessionEndpoint() { return endSessionEndpoint; } + /** + * Returns {@code true} if the OpenID Provider supports back-channel logout + * {@code (backchannel_logout_supported)}. The default is {@code false}. + * @return {@code true} if the OpenID Provider supports back-channel logout, + * {@code false} otherwise + * @since 7.2 + */ + default boolean isBackChannelLogoutSupported() { + return Boolean.TRUE.equals(getClaimAsBoolean(OidcProviderMetadataClaimNames.BACKCHANNEL_LOGOUT_SUPPORTED)); + } + + /** + * Returns {@code true} if the OpenID Provider can pass a {@code sid} (session ID) + * Claim in the Logout Token {@code (backchannel_logout_session_supported)}. The + * default is {@code false}. + * @return {@code true} if the OpenID Provider can pass a {@code sid} Claim in the + * Logout Token, {@code false} otherwise + * @since 7.2 + */ + default boolean isBackChannelLogoutSessionSupported() { + return Boolean.TRUE + .equals(getClaimAsBoolean(OidcProviderMetadataClaimNames.BACKCHANNEL_LOGOUT_SESSION_SUPPORTED)); + } + } diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimNames.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimNames.java index e28916d97ab..6a40cbd8983 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimNames.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderMetadataClaimNames.java @@ -31,6 +31,9 @@ * @see 3. OpenID * Provider Metadata + * @see 2.1. + * Indicating OP Support for Back-Channel Logout */ public final class OidcProviderMetadataClaimNames extends OAuth2AuthorizationServerMetadataClaimNames { @@ -57,6 +60,21 @@ public final class OidcProviderMetadataClaimNames extends OAuth2AuthorizationSer */ public static final String END_SESSION_ENDPOINT = "end_session_endpoint"; + /** + * {@code backchannel_logout_supported} - {@code true} if the OpenID Provider supports + * back-channel logout. + * @since 7.2 + */ + public static final String BACKCHANNEL_LOGOUT_SUPPORTED = "backchannel_logout_supported"; + + /** + * {@code backchannel_logout_session_supported} - {@code true} if the OpenID Provider + * can pass a {@code sid} (session ID) Claim in the Logout Token to identify the + * Client session with the OpenID Provider. + * @since 7.2 + */ + public static final String BACKCHANNEL_LOGOUT_SESSION_SUPPORTED = "backchannel_logout_session_supported"; + private OidcProviderMetadataClaimNames() { } diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationValidator.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationValidator.java index d9804fdd9e1..843ca9df2e1 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationValidator.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationValidator.java @@ -45,7 +45,8 @@ *

* The default implementation validates {@link OidcClientRegistration#getRedirectUris() * redirect_uris}, {@link OidcClientRegistration#getPostLogoutRedirectUris() - * post_logout_redirect_uris}, {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}, and + * post_logout_redirect_uris}, {@link OidcClientRegistration#getJwkSetUrl() jwks_uri}, + * {@link OidcClientRegistration#getBackChannelLogoutUri() backchannel_logout_uri}, and * {@link OidcClientRegistration#getScopes() scope}. If validation fails, an * {@link OAuth2AuthenticationException} is thrown. * @@ -117,6 +118,14 @@ public final class OidcClientRegistrationAuthenticationValidator */ public static final Consumer SIMPLE_JWK_SET_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateJwkSetUriSimple; + /** + * The default validator for {@link OidcClientRegistration#getBackChannelLogoutUri() + * backchannel_logout_uri}. Rejects URIs that contain a fragment or do not use the + * {@code https} scheme. + * @since 7.2 + */ + public static final Consumer DEFAULT_BACK_CHANNEL_LOGOUT_URI_VALIDATOR = OidcClientRegistrationAuthenticationValidator::validateBackChannelLogoutUri; + /** * The default validator for {@link OidcClientRegistration#getScopes() scope}. Rejects * any request that includes a non-empty scope value. Deployers that need to accept @@ -135,6 +144,7 @@ public final class OidcClientRegistrationAuthenticationValidator private final Consumer authenticationValidator = DEFAULT_REDIRECT_URI_VALIDATOR .andThen(DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR) .andThen(DEFAULT_JWK_SET_URI_VALIDATOR) + .andThen(DEFAULT_BACK_CHANNEL_LOGOUT_URI_VALIDATOR) .andThen(DEFAULT_SCOPE_VALIDATOR); @Override @@ -272,6 +282,31 @@ private static void validateJwkSetUriSimple(OidcClientRegistrationAuthentication // No validation. Preserves prior behavior. } + private static void validateBackChannelLogoutUri( + OidcClientRegistrationAuthenticationContext authenticationContext) { + OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext + .getAuthentication(); + Assert.notNull(clientRegistrationAuthentication.getClientRegistration(), "clientRegistration cannot be null"); + URL backChannelLogoutUri = clientRegistrationAuthentication.getClientRegistration().getBackChannelLogoutUri(); + if (backChannelLogoutUri == null) { + return; + } + if (!"https".equalsIgnoreCase(backChannelLogoutUri.getProtocol())) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(LogMessage.format("Invalid request: backchannel_logout_uri does not use https ('%s')", + backChannelLogoutUri)); + } + throw createException("invalid_client_metadata", OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_URI); + } + if (backChannelLogoutUri.getRef() != null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(LogMessage.format("Invalid request: backchannel_logout_uri contains a fragment ('%s')", + backChannelLogoutUri)); + } + throw createException("invalid_client_metadata", OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_URI); + } + } + private static void validateScope(OidcClientRegistrationAuthenticationContext authenticationContext) { OidcClientRegistrationAuthenticationToken clientRegistrationAuthentication = authenticationContext .getAuthentication(); diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/OidcClientRegistrationRegisteredClientConverter.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/OidcClientRegistrationRegisteredClientConverter.java index fd763c43ad4..b1a3d04b16b 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/OidcClientRegistrationRegisteredClientConverter.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/OidcClientRegistrationRegisteredClientConverter.java @@ -136,6 +136,13 @@ else if (ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue().equals(clientRegi clientSettingsBuilder.jwkSetUrl(jwkSetUrl.toString()); } + URL backChannelLogoutUri = clientRegistration.getBackChannelLogoutUri(); + if (backChannelLogoutUri != null) { + clientSettingsBuilder + .backChannelLogoutUri(backChannelLogoutUri.toString()) + .backChannelLogoutSessionRequired(clientRegistration.isBackChannelLogoutSessionRequired()); + } + builder .clientSettings(clientSettingsBuilder.build()) .tokenSettings(TokenSettings.builder() diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java index 048d17bd308..68e36100465 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/converter/RegisteredClientOidcClientRegistrationConverter.java @@ -101,6 +101,12 @@ public OidcClientRegistration convert(RegisteredClient registeredClient) { builder.tokenEndpointAuthenticationSigningAlgorithm(clientSettings.getTokenEndpointAuthenticationSigningAlgorithm().getName()); } + if (clientSettings.getBackChannelLogoutUri() != null) { + builder + .backChannelLogoutUri(clientSettings.getBackChannelLogoutUri()) + .backChannelLogoutSessionRequired(clientSettings.isBackChannelLogoutSessionRequired()); + } + return builder.build(); // @formatter:on } diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcClientRegistrationHttpMessageConverter.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcClientRegistrationHttpMessageConverter.java index 292b054e6f5..b95aa29179b 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcClientRegistrationHttpMessageConverter.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcClientRegistrationHttpMessageConverter.java @@ -145,6 +145,8 @@ private static final class MapOidcClientRegistrationConverter private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class); + private static final TypeDescriptor BOOLEAN_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Boolean.class); + private static final TypeDescriptor INSTANT_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Instant.class); private static final TypeDescriptor URL_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(URL.class); @@ -158,6 +160,7 @@ private MapOidcClientRegistrationConverter() { Converter collectionStringConverter = getConverter( TypeDescriptor.collection(Collection.class, STRING_TYPE_DESCRIPTOR)); Converter urlConverter = getConverter(URL_TYPE_DESCRIPTOR); + Converter booleanConverter = getConverter(BOOLEAN_TYPE_DESCRIPTOR); Map> claimConverters = new HashMap<>(); claimConverters.put(OidcClientMetadataClaimNames.CLIENT_ID, stringConverter); @@ -175,6 +178,8 @@ private MapOidcClientRegistrationConverter() { claimConverters.put(OidcClientMetadataClaimNames.SCOPE, MapOidcClientRegistrationConverter::convertScope); claimConverters.put(OidcClientMetadataClaimNames.JWKS_URI, urlConverter); claimConverters.put(OidcClientMetadataClaimNames.ID_TOKEN_SIGNED_RESPONSE_ALG, stringConverter); + claimConverters.put(OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_URI, urlConverter); + claimConverters.put(OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_SESSION_REQUIRED, booleanConverter); this.claimTypeConverter = new ClaimTypeConverter(claimConverters); } diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettings.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettings.java index 665300fc049..fe475b01f2a 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettings.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettings.java @@ -99,6 +99,29 @@ public boolean isRequireAuthorizationConsent() { return getSetting(ConfigurationSettingNames.Client.X509_CERTIFICATE_SUBJECT_DN); } + /** + * Returns the {@code URL} that will cause the client to log itself out when sent a + * Logout Token by the OpenID Provider, as defined by OpenID Connect Back-Channel + * Logout 1.0. + * @return the {@code URL} of the client's back-channel logout endpoint, or + * {@code null} if not set + * @since 7.2 + */ + public @Nullable String getBackChannelLogoutUri() { + return getSetting(ConfigurationSettingNames.Client.BACK_CHANNEL_LOGOUT_URI); + } + + /** + * Returns {@code true} if the client requires that a {@code sid} (session ID) Claim + * be included in the Logout Token. The default is {@code false}. + * @return {@code true} if the client requires a {@code sid} Claim in the Logout + * Token, {@code false} otherwise + * @since 7.2 + */ + public boolean isBackChannelLogoutSessionRequired() { + return Boolean.TRUE.equals(getSetting(ConfigurationSettingNames.Client.BACK_CHANNEL_LOGOUT_SESSION_REQUIRED)); + } + /** * Constructs a new {@link Builder} with the default settings. * @return the {@link Builder} @@ -185,6 +208,32 @@ public Builder x509CertificateSubjectDN(String x509CertificateSubjectDN) { return setting(ConfigurationSettingNames.Client.X509_CERTIFICATE_SUBJECT_DN, x509CertificateSubjectDN); } + /** + * Sets the {@code URL} that will cause the client to log itself out when sent a + * Logout Token by the OpenID Provider, as defined by OpenID Connect Back-Channel + * Logout 1.0. + * @param backChannelLogoutUri the {@code URL} of the client's back-channel logout + * endpoint + * @return the {@link Builder} for further configuration + * @since 7.2 + */ + public Builder backChannelLogoutUri(String backChannelLogoutUri) { + return setting(ConfigurationSettingNames.Client.BACK_CHANNEL_LOGOUT_URI, backChannelLogoutUri); + } + + /** + * Set to {@code true} if the client requires that a {@code sid} (session ID) + * Claim be included in the Logout Token. + * @param backChannelLogoutSessionRequired {@code true} if the client requires a + * {@code sid} Claim in the Logout Token, {@code false} otherwise + * @return the {@link Builder} for further configuration + * @since 7.2 + */ + public Builder backChannelLogoutSessionRequired(boolean backChannelLogoutSessionRequired) { + return setting(ConfigurationSettingNames.Client.BACK_CHANNEL_LOGOUT_SESSION_REQUIRED, + backChannelLogoutSessionRequired); + } + /** * Builds the {@link ClientSettings}. * @return the {@link ClientSettings} diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java index ff598bc7e0a..0d80a9c8923 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/settings/ConfigurationSettingNames.java @@ -78,6 +78,22 @@ public static final class Client { public static final String X509_CERTIFICATE_SUBJECT_DN = CLIENT_SETTINGS_NAMESPACE .concat("x509-certificate-subject-dn"); + /** + * Set the {@code URL} that will cause the client to log itself out when sent a + * Logout Token by the OpenID Provider. + * @since 7.2 + */ + public static final String BACK_CHANNEL_LOGOUT_URI = CLIENT_SETTINGS_NAMESPACE + .concat("back-channel-logout-uri"); + + /** + * Set to {@code true} if the client requires that a {@code sid} (session ID) + * Claim be included in the Logout Token. + * @since 7.2 + */ + public static final String BACK_CHANNEL_LOGOUT_SESSION_REQUIRED = CLIENT_SETTINGS_NAMESPACE + .concat("back-channel-logout-session-required"); + private Client() { } diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistrationTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistrationTests.java index b3634eaf90a..a3b4187bbbc 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistrationTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcClientRegistrationTests.java @@ -401,6 +401,14 @@ public void buildWhenJwksUriNotUrlThenThrowIllegalArgumentException() { assertThatIllegalArgumentException().isThrownBy(builder::build).withMessage("jwksUri must be a valid URL"); } + @Test + public void buildWhenBackChannelLogoutUriNotUrlThenThrowIllegalArgumentException() { + OidcClientRegistration.Builder builder = this.minimalBuilder.backChannelLogoutUri("not an url"); + + assertThatIllegalArgumentException().isThrownBy(builder::build) + .withMessage("backchannel_logout_uri must be a valid URL"); + } + @Test public void claimWhenNameNullThenThrowIllegalArgumentException() { assertThatIllegalArgumentException() diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java index df90850417c..8a62c84c286 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/OidcProviderConfigurationTests.java @@ -64,6 +64,8 @@ public void buildWhenAllRequiredClaimsAndAdditionalClaimsThenCreated() { .tokenEndpointAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()) .clientRegistrationEndpoint("https://example.com/connect/register") .endSessionEndpoint("https://example.com/connect/logout") + .backChannelLogoutSupported(true) + .backChannelLogoutSessionSupported(true) .claim("a-claim", "a-value") .build(); @@ -84,6 +86,8 @@ public void buildWhenAllRequiredClaimsAndAdditionalClaimsThenCreated() { assertThat(providerConfiguration.getClientRegistrationEndpoint()) .isEqualTo(url("https://example.com/connect/register")); assertThat(providerConfiguration.getEndSessionEndpoint()).isEqualTo(url("https://example.com/connect/logout")); + assertThat(providerConfiguration.isBackChannelLogoutSupported()).isTrue(); + assertThat(providerConfiguration.isBackChannelLogoutSessionSupported()).isTrue(); assertThat(providerConfiguration.getClaim("a-claim")).isEqualTo("a-value"); } @@ -111,6 +115,8 @@ public void buildWhenOnlyRequiredClaimsThenCreated() { assertThat(providerConfiguration.getSubjectTypes()).containsExactly("public"); assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()).isNull(); + assertThat(providerConfiguration.isBackChannelLogoutSupported()).isFalse(); + assertThat(providerConfiguration.isBackChannelLogoutSessionSupported()).isFalse(); } @Test diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProviderTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProviderTests.java index 4f4dbd45566..43c01829e9a 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProviderTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientConfigurationAuthenticationProviderTests.java @@ -331,6 +331,7 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { .clientSettings(ClientSettings.builder() .tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS512) .jwkSetUrl("https://client.example.com/jwks") + .backChannelLogoutUri("https://client.example.com/logout/connect/back-channel") .build()) .build(); OAuth2Authorization authorization = TestOAuth2Authorizations @@ -384,6 +385,9 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { .isEqualTo(registeredClient.getClientSettings().getTokenEndpointAuthenticationSigningAlgorithm().getName()); assertThat(clientRegistrationResult.getJwkSetUrl().toString()) .isEqualTo(registeredClient.getClientSettings().getJwkSetUrl()); + assertThat(clientRegistrationResult.getBackChannelLogoutUri().toString()) + .isEqualTo("https://client.example.com/logout/connect/back-channel"); + assertThat(clientRegistrationResult.isBackChannelLogoutSessionRequired()).isFalse(); assertThat(clientRegistrationResult.getIdTokenSignedResponseAlgorithm()) .isEqualTo(registeredClient.getTokenSettings().getIdTokenSignatureAlgorithm().getName()); diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java index ab30c9bb8e4..50901be3f2a 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationProviderTests.java @@ -667,6 +667,7 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { .setAuthenticationValidator(OidcClientRegistrationAuthenticationValidator.DEFAULT_REDIRECT_URI_VALIDATOR .andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_POST_LOGOUT_REDIRECT_URI_VALIDATOR) .andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_JWK_SET_URI_VALIDATOR) + .andThen(OidcClientRegistrationAuthenticationValidator.DEFAULT_BACK_CHANNEL_LOGOUT_URI_VALIDATOR) .andThen(OidcClientRegistrationAuthenticationValidator.SIMPLE_SCOPE_VALIDATOR)); Jwt jwt = createJwtClientRegistration(); OAuth2AccessToken jwtAccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, @@ -687,6 +688,8 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { .clientName("client-name") .redirectUri("https://client.example.com") .postLogoutRedirectUri("https://client.example.com/oidc-post-logout") + .backChannelLogoutUri("https://client.example.com/logout/connect/back-channel") + .backChannelLogoutSessionRequired(true) .grantType(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()) .grantType(AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) .scope("scope1") @@ -739,6 +742,9 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { assertThat(registeredClientResult.getScopes()).containsExactlyInAnyOrder("scope1", "scope2"); assertThat(registeredClientResult.getClientSettings().isRequireProofKey()).isTrue(); assertThat(registeredClientResult.getClientSettings().isRequireAuthorizationConsent()).isTrue(); + assertThat(registeredClientResult.getClientSettings().getBackChannelLogoutUri()) + .isEqualTo("https://client.example.com/logout/connect/back-channel"); + assertThat(registeredClientResult.getClientSettings().isBackChannelLogoutSessionRequired()).isTrue(); assertThat(registeredClientResult.getTokenSettings().getIdTokenSignatureAlgorithm()) .isEqualTo(SignatureAlgorithm.RS256); @@ -754,6 +760,9 @@ public void authenticateWhenValidAccessTokenThenReturnClientRegistration() { .containsExactlyInAnyOrderElementsOf(registeredClientResult.getRedirectUris()); assertThat(clientRegistrationResult.getPostLogoutRedirectUris()) .containsExactlyInAnyOrderElementsOf(registeredClientResult.getPostLogoutRedirectUris()); + assertThat(clientRegistrationResult.getBackChannelLogoutUri().toString()) + .isEqualTo("https://client.example.com/logout/connect/back-channel"); + assertThat(clientRegistrationResult.isBackChannelLogoutSessionRequired()).isTrue(); List grantTypes = new ArrayList<>(); registeredClientResult.getAuthorizationGrantTypes() diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationValidatorTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationValidatorTests.java index a8ebf50b287..5eb98443245 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationValidatorTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/authentication/OidcClientRegistrationAuthenticationValidatorTests.java @@ -88,6 +88,24 @@ public void defaultJwkSetUriValidatorWhenHttpsThenAccepted() { .accept(context("https://client.example.com", null, "https://client.example.com/jwks"))); } + @Test + public void defaultBackChannelLogoutUriValidatorWhenHttpThenRejected() { + assertRejected(backChannelLogoutContext("http://169.254.169.254/logout"), "invalid_client_metadata", + OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_URI); + } + + @Test + public void defaultBackChannelLogoutUriValidatorWhenFragmentThenRejected() { + assertRejected(backChannelLogoutContext("https://client.example.com/logout#fragment"), + "invalid_client_metadata", OidcClientMetadataClaimNames.BACKCHANNEL_LOGOUT_URI); + } + + @Test + public void defaultBackChannelLogoutUriValidatorWhenHttpsThenAccepted() { + assertThatNoException().isThrownBy( + () -> this.validator.accept(backChannelLogoutContext("https://client.example.com/logout?tenant=a"))); + } + @Test public void defaultScopeValidatorWhenNonEmptyThenRejected() { OidcClientRegistrationAuthenticationContext context = OidcClientRegistrationAuthenticationContext @@ -175,6 +193,16 @@ private static OidcClientRegistrationAuthenticationContext context(String redire .build(); } + private static OidcClientRegistrationAuthenticationContext backChannelLogoutContext(String backChannelLogoutUri) { + OidcClientRegistration clientRegistration = OidcClientRegistration.builder() + .redirectUri("https://client.example.com") + .backChannelLogoutUri(backChannelLogoutUri) + .build(); + return OidcClientRegistrationAuthenticationContext + .with(new OidcClientRegistrationAuthenticationToken(principal(), clientRegistration)) + .build(); + } + private void assertRejected(OidcClientRegistrationAuthenticationContext context, String errorCode, String fieldName) { assertThatExceptionOfType(OAuth2AuthenticationException.class).isThrownBy(() -> this.validator.accept(context)) diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java index 2dd1559de8d..446464fccbb 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcClientRegistrationHttpMessageConverterTests.java @@ -115,6 +115,8 @@ public void readInternalWhenValidParametersThenSuccess() throws Exception { + " \"scope\": \"scope1 scope2\",\n" + " \"jwks_uri\": \"https://client.example.com/jwks\",\n" + " \"id_token_signed_response_alg\": \"RS256\",\n" + + " \"backchannel_logout_uri\": \"https://client.example.com/logout/connect/back-channel\",\n" + + " \"backchannel_logout_session_required\": \"true\",\n" + " \"a-claim\": \"a-value\"\n" + "}\n"; // @formatter:on @@ -141,6 +143,9 @@ public void readInternalWhenValidParametersThenSuccess() throws Exception { assertThat(clientRegistration.getScopes()).containsExactlyInAnyOrder("scope1", "scope2"); assertThat(clientRegistration.getJwkSetUrl()).isEqualTo(new URL("https://client.example.com/jwks")); assertThat(clientRegistration.getIdTokenSignedResponseAlgorithm()).isEqualTo("RS256"); + assertThat(clientRegistration.getBackChannelLogoutUri()) + .isEqualTo(new URL("https://client.example.com/logout/connect/back-channel")); + assertThat(clientRegistration.isBackChannelLogoutSessionRequired()).isTrue(); assertThat(clientRegistration.getClaimAsString("a-claim")).isEqualTo("a-value"); } diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java index ce9355bb8ee..c775d39d88a 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/oidc/http/converter/OidcProviderConfigurationHttpMessageConverterTests.java @@ -107,6 +107,8 @@ public void readInternalWhenValidParametersThenSuccess() throws Exception { + " \"subject_types_supported\": [\"public\"],\n" + " \"id_token_signing_alg_values_supported\": [\"RS256\"],\n" + " \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\"],\n" + + " \"backchannel_logout_supported\": true,\n" + + " \"backchannel_logout_session_supported\": true,\n" + " \"custom_claim\": \"value\",\n" + " \"custom_collection_claim\": [\"value1\", \"value2\"]\n" + "}\n"; @@ -130,6 +132,8 @@ public void readInternalWhenValidParametersThenSuccess() throws Exception { assertThat(providerConfiguration.getIdTokenSigningAlgorithms()).containsExactly("RS256"); assertThat(providerConfiguration.getTokenEndpointAuthenticationMethods()) .containsExactly(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()); + assertThat(providerConfiguration.isBackChannelLogoutSupported()).isTrue(); + assertThat(providerConfiguration.isBackChannelLogoutSessionSupported()).isTrue(); assertThat(providerConfiguration.getClaim("custom_claim")).isEqualTo("value"); assertThat(providerConfiguration.getClaimAsStringList("custom_collection_claim")) .containsExactlyInAnyOrder("value1", "value2"); diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettingsTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettingsTests.java index fe0caa87755..992f410b604 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettingsTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/settings/ClientSettingsTests.java @@ -72,6 +72,22 @@ public void x509CertificateSubjectDNWhenProvidedThenSet() { .isEqualTo("CN=demo-client-sample, OU=Spring Samples, O=Spring, C=US"); } + @Test + public void backChannelLogoutWhenProvidedThenSet() { + ClientSettings clientSettings = ClientSettings.builder() + .backChannelLogoutUri("https://client.example.com/logout/connect/back-channel") + .backChannelLogoutSessionRequired(true) + .build(); + assertThat(clientSettings.getBackChannelLogoutUri()) + .isEqualTo("https://client.example.com/logout/connect/back-channel"); + assertThat(clientSettings.isBackChannelLogoutSessionRequired()).isTrue(); + } + + @Test + public void backChannelLogoutSessionRequiredWhenNotSetThenFalse() { + assertThat(ClientSettings.builder().build().isBackChannelLogoutSessionRequired()).isFalse(); + } + @Test public void settingWhenCustomThenSet() { ClientSettings clientSettings = ClientSettings.builder()