credential : session.getAuthorities().entrySet()) {
+ headers.add(credential.getKey(), credential.getValue());
+ }
+ String url = request.getRequestURL().toString();
+ String logout = UriComponentsBuilder.fromHttpUrl(url).replacePath(this.logoutEndpointName).build()
+ .toUriString();
+ HttpEntity> entity = new HttpEntity<>(null, headers);
+ this.restOperations.postForEntity(logout, entity, Object.class);
+ }
+
+ /**
+ * Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
+ * this class uses
+ * {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
+ * sessions.
+ * @param sessionRegistry the {@link OidcSessionRegistry} to use
+ */
+ public void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
+ Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
+ this.sessionRegistry = sessionRegistry;
+ }
+
+ /**
+ * Use this {@link RestOperations} to perform the per-session back-channel logout
+ * @param restOperations the {@link RestOperations} to use
+ */
+ public void setRestOperations(RestOperations restOperations) {
+ Assert.notNull(restOperations, "restOperations cannot be null");
+ this.restOperations = restOperations;
+ }
+
+ /**
+ * Use this logout URI for performing per-session logout. Defaults to {@code /logout}
+ * since that is the default URI for
+ * {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
+ * @param logoutUri the URI to use
+ */
+ public void setLogoutUri(String logoutUri) {
+ Assert.hasText(logoutUri, "logoutUri cannot be empty");
+ this.logoutEndpointName = logoutUri;
+ }
+
+ /**
+ * Use this cookie name for the session identifier. Defaults to {@code JSESSIONID}.
+ *
+ *
+ * Note that if you are using Spring Session, this likely needs to change to SESSION.
+ * @param sessionCookieName the cookie name to use
+ */
+ public void setSessionCookieName(String sessionCookieName) {
+ Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
+ this.sessionCookieName = sessionCookieName;
+ }
+
+}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcBackChannelLogoutTokenValidatorTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcBackChannelLogoutTokenValidatorTests.java
new file mode 100644
index 00000000000..c4747837ad4
--- /dev/null
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcBackChannelLogoutTokenValidatorTests.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.client.oidc.authentication.logout;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link OidcBackChannelLogoutTokenValidator}
+ */
+public class OidcBackChannelLogoutTokenValidatorTests {
+
+ // @formatter:off
+ private final ClientRegistration clientRegistration = TestClientRegistrations
+ .clientRegistration()
+ .issuerUri("https://issuer")
+ .scope("openid").build();
+ // @formatter:on
+
+ private final OidcBackChannelLogoutTokenValidator logoutTokenValidator = new OidcBackChannelLogoutTokenValidator(
+ this.clientRegistration);
+
+ @Test
+ public void createDecoderWhenTokenValidThenNoErrors() {
+ Jwt valid = valid(this.clientRegistration).build();
+ assertThat(this.logoutTokenValidator.validate(valid).hasErrors()).isFalse();
+ }
+
+ @Test
+ public void createDecoderWhenInvalidAudienceThenErrors() {
+ Jwt valid = valid(this.clientRegistration).audience(List.of("wrong")).build();
+ assertThat(this.logoutTokenValidator.validate(valid).hasErrors()).isTrue();
+ }
+
+ @Test
+ public void createDecoderWhenMissingEventsThenErrors() {
+ Jwt valid = valid(this.clientRegistration).claims((claims) -> claims.remove(LogoutTokenClaimNames.EVENTS))
+ .build();
+ assertThat(this.logoutTokenValidator.validate(valid).hasErrors()).isTrue();
+ }
+
+ @Test
+ public void createDecoderWhenInvalidIssuerThenErrors() {
+ Jwt valid = valid(this.clientRegistration).issuer("https://wrong").build();
+ assertThat(this.logoutTokenValidator.validate(valid).hasErrors()).isTrue();
+ }
+
+ @Test
+ public void createDecoderWhenMissingSubjectThenErrors() {
+ Jwt valid = valid(this.clientRegistration).claims((claims) -> claims.remove(LogoutTokenClaimNames.SUB)).build();
+ assertThat(this.logoutTokenValidator.validate(valid).hasErrors()).isTrue();
+ }
+
+ @Test
+ public void createDecoderWhenMissingAudienceThenErrors() {
+ Jwt valid = valid(this.clientRegistration).claims((claims) -> claims.remove(LogoutTokenClaimNames.AUD)).build();
+ assertThat(this.logoutTokenValidator.validate(valid).hasErrors()).isTrue();
+ }
+
+ private Jwt.Builder valid(ClientRegistration clientRegistration) {
+ String issuerUri = clientRegistration.getProviderDetails().getIssuerUri();
+ OidcLogoutToken logoutToken = TestOidcLogoutTokens.withSubject(issuerUri, "subject").build();
+ return Jwt.withTokenValue(logoutToken.getTokenValue()).header("header", "value")
+ .claims((claims) -> claims.putAll(logoutToken.getClaims()));
+ }
+
+}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutTokenValidatorTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutTokenValidatorTests.java
deleted file mode 100644
index dcff8a917f2..00000000000
--- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/logout/OidcLogoutTokenValidatorTests.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2002-2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.oauth2.client.oidc.authentication.logout;
-
-import org.junit.jupiter.api.Test;
-
-import org.springframework.security.oauth2.client.registration.ClientRegistration;
-import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class OidcLogoutTokenValidatorTests {
-
- // @formatter:off
- private ClientRegistration.Builder registration = TestClientRegistrations
- .clientRegistration()
- .scope("openid");
- // @formatter:on
-
- @Test
- public void createDecoderWhenClientRegistrationValidThenReturnDecoder() {
- OidcLogoutTokenValidator validator = new OidcLogoutTokenValidator(this.registration.build());
- assertThat(validator).isNotNull();
- }
-
-}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/session/InMemoryOidcSessionRegistryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/session/InMemoryOidcSessionRegistryTests.java
deleted file mode 100644
index ca495d4181d..00000000000
--- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/session/InMemoryOidcSessionRegistryTests.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2002-2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.oauth2.client.oidc.authentication.session;
-
-import org.junit.jupiter.api.Test;
-
-import org.springframework.security.core.authority.AuthorityUtils;
-import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
-import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens;
-import org.springframework.security.oauth2.core.oidc.OidcIdToken;
-import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens;
-import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
-import org.springframework.security.oauth2.core.oidc.user.OidcUser;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class InMemoryOidcSessionRegistryTests {
-
- @Test
- public void registerWhenDefaultsThenStoresSessionInformation() {
- InMemoryOidcSessionRegistry registry = new InMemoryOidcSessionRegistry();
- String sessionId = "client";
- OidcSessionRegistration info = TestOidcSessionRegistrations.create(sessionId);
- registry.register(info);
- OidcLogoutToken token = TestOidcLogoutTokens.withUser(info.getPrincipal()).build();
- Iterable infos = registry.deregister(token);
- assertThat(infos).containsExactly(info);
- }
-
- @Test
- public void registerWhenIdTokenHasSessionIdThenStoresSessionInformation() {
- InMemoryOidcSessionRegistry registry = new InMemoryOidcSessionRegistry();
- OidcIdToken token = TestOidcIdTokens.idToken().claim("sid", "provider").build();
- OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, token);
- OidcSessionRegistration info = TestOidcSessionRegistrations.create("client", user);
- registry.register(info);
- OidcLogoutToken logoutToken = TestOidcLogoutTokens.withSessionId(token.getIssuer().toString(), "provider")
- .build();
- Iterable infos = registry.deregister(logoutToken);
- assertThat(infos).containsExactly(info);
- }
-
- @Test
- public void unregisterWhenMultipleSessionsThenRemovesAllMatching() {
- InMemoryOidcSessionRegistry registry = new InMemoryOidcSessionRegistry();
- OidcIdToken token = TestOidcIdTokens.idToken().claim("sid", "providerOne").subject("otheruser").build();
- OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, token);
- OidcSessionRegistration one = TestOidcSessionRegistrations.create("clientOne", user);
- registry.register(one);
- token = TestOidcIdTokens.idToken().claim("sid", "providerTwo").build();
- user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, token);
- OidcSessionRegistration two = TestOidcSessionRegistrations.create("clientTwo", user);
- registry.register(two);
- token = TestOidcIdTokens.idToken().claim("sid", "providerThree").build();
- user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, token);
- OidcSessionRegistration three = TestOidcSessionRegistrations.create("clientThree", user);
- registry.register(three);
- OidcLogoutToken logoutToken = TestOidcLogoutTokens.withSubject(token.getIssuer().toString(), token.getSubject())
- .build();
- Iterable infos = registry.deregister(logoutToken);
- assertThat(infos).containsExactlyInAnyOrder(two, three);
- logoutToken = TestOidcLogoutTokens.withSubject(token.getIssuer().toString(), "otheruser").build();
- infos = registry.deregister(logoutToken);
- assertThat(infos).containsExactly(one);
- }
-
- @Test
- public void unregisterWhenNoSessionsThenEmptyList() {
- InMemoryOidcSessionRegistry registry = new InMemoryOidcSessionRegistry();
- OidcIdToken token = TestOidcIdTokens.idToken().claim("sid", "provider").build();
- OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, token);
- OidcSessionRegistration registration = TestOidcSessionRegistrations.create("client", user);
- registry.register(registration);
- OidcLogoutToken logoutToken = TestOidcLogoutTokens.withSessionId(token.getIssuer().toString(), "wrong").build();
- Iterable> infos = registry.deregister(logoutToken);
- assertThat(infos).isNotNull();
- assertThat(infos).isEmpty();
- logoutToken = TestOidcLogoutTokens.withSessionId("https://wrong", "provider").build();
- infos = registry.deregister(logoutToken);
- assertThat(infos).isNotNull();
- assertThat(infos).isEmpty();
- }
-
-}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistryTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistryTests.java
new file mode 100644
index 00000000000..861eccce7ea
--- /dev/null
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/InMemoryOidcSessionRegistryTests.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2002-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.oauth2.client.oidc.session;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
+import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.TestOidcIdTokens;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link InMemoryOidcSessionRegistry}
+ */
+public class InMemoryOidcSessionRegistryTests {
+
+ @Test
+ public void registerWhenDefaultsThenStoresSessionInformation() {
+ InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
+ String sessionId = "client";
+ OidcSessionInformation info = TestOidcSessionInformations.create(sessionId);
+ sessionRegistry.saveSessionInformation(info);
+ OidcLogoutToken logoutToken = TestOidcLogoutTokens.withUser(info.getPrincipal()).build();
+ Iterable infos = sessionRegistry.removeSessionInformation(logoutToken);
+ assertThat(infos).containsExactly(info);
+ }
+
+ @Test
+ public void registerWhenIdTokenHasSessionIdThenStoresSessionInformation() {
+ InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
+ OidcIdToken idToken = TestOidcIdTokens.idToken().claim("sid", "provider").build();
+ OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken);
+ OidcSessionInformation info = TestOidcSessionInformations.create("client", user);
+ sessionRegistry.saveSessionInformation(info);
+ OidcLogoutToken logoutToken = TestOidcLogoutTokens.withSessionId(idToken.getIssuer().toString(), "provider")
+ .build();
+ Iterable infos = sessionRegistry.removeSessionInformation(logoutToken);
+ assertThat(infos).containsExactly(info);
+ }
+
+ @Test
+ public void unregisterWhenMultipleSessionsThenRemovesAllMatching() {
+ InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
+ OidcIdToken idToken = TestOidcIdTokens.idToken().claim("sid", "providerOne").subject("otheruser").build();
+ OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken);
+ OidcSessionInformation oneSession = TestOidcSessionInformations.create("clientOne", user);
+ sessionRegistry.saveSessionInformation(oneSession);
+ idToken = TestOidcIdTokens.idToken().claim("sid", "providerTwo").build();
+ user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken);
+ OidcSessionInformation twoSession = TestOidcSessionInformations.create("clientTwo", user);
+ sessionRegistry.saveSessionInformation(twoSession);
+ idToken = TestOidcIdTokens.idToken().claim("sid", "providerThree").build();
+ user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken);
+ OidcSessionInformation threeSession = TestOidcSessionInformations.create("clientThree", user);
+ sessionRegistry.saveSessionInformation(threeSession);
+ OidcLogoutToken logoutToken = TestOidcLogoutTokens
+ .withSubject(idToken.getIssuer().toString(), idToken.getSubject()).build();
+ Iterable infos = sessionRegistry.removeSessionInformation(logoutToken);
+ assertThat(infos).containsExactlyInAnyOrder(twoSession, threeSession);
+ logoutToken = TestOidcLogoutTokens.withSubject(idToken.getIssuer().toString(), "otheruser").build();
+ infos = sessionRegistry.removeSessionInformation(logoutToken);
+ assertThat(infos).containsExactly(oneSession);
+ }
+
+ @Test
+ public void unregisterWhenNoSessionsThenEmptyList() {
+ InMemoryOidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
+ OidcIdToken idToken = TestOidcIdTokens.idToken().claim("sid", "provider").build();
+ OidcUser user = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, idToken);
+ OidcSessionInformation info = TestOidcSessionInformations.create("client", user);
+ sessionRegistry.saveSessionInformation(info);
+ OidcLogoutToken logoutToken = TestOidcLogoutTokens.withSessionId(idToken.getIssuer().toString(), "wrong")
+ .build();
+ Iterable> infos = sessionRegistry.removeSessionInformation(logoutToken);
+ assertThat(infos).isNotNull();
+ assertThat(infos).isEmpty();
+ logoutToken = TestOidcLogoutTokens.withSessionId("https://wrong", "provider").build();
+ infos = sessionRegistry.removeSessionInformation(logoutToken);
+ assertThat(infos).isNotNull();
+ assertThat(infos).isEmpty();
+ }
+
+}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/session/TestOidcSessionRegistrations.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/TestOidcSessionInformations.java
similarity index 64%
rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/session/TestOidcSessionRegistrations.java
rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/TestOidcSessionInformations.java
index 3beb9b9bc5d..47f64868de1 100644
--- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/session/TestOidcSessionRegistrations.java
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/session/TestOidcSessionInformations.java
@@ -14,28 +14,31 @@
* limitations under the License.
*/
-package org.springframework.security.oauth2.client.oidc.authentication.session;
+package org.springframework.security.oauth2.client.oidc.session;
import java.util.Map;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers;
-public final class TestOidcSessionRegistrations {
+/**
+ * Sample {@link OidcSessionInformation} instances
+ */
+public final class TestOidcSessionInformations {
- public static OidcSessionRegistration create() {
+ public static OidcSessionInformation create() {
return create("sessionId");
}
- public static OidcSessionRegistration create(String sessionId) {
+ public static OidcSessionInformation create(String sessionId) {
return create(sessionId, TestOidcUsers.create());
}
- public static OidcSessionRegistration create(String sessionId, OidcUser user) {
- return new OidcSessionRegistration("client-id", sessionId, Map.of("_csrf", "token"), user);
+ public static OidcSessionInformation create(String sessionId, OidcUser user) {
+ return new OidcSessionInformation(sessionId, Map.of("_csrf", "token"), user);
}
- private TestOidcSessionRegistrations() {
+ private TestOidcSessionInformations() {
}
diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/authentication/logout/OidcBackChannelLogoutFilterTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/OidcBackChannelLogoutFilterTests.java
similarity index 53%
rename from oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/authentication/logout/OidcBackChannelLogoutFilterTests.java
rename to oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/OidcBackChannelLogoutFilterTests.java
index 0da06be9f2f..5d87f8baeb4 100644
--- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/authentication/logout/OidcBackChannelLogoutFilterTests.java
+++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/OidcBackChannelLogoutFilterTests.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.security.oauth2.client.oidc.web.authentication.logout;
+package org.springframework.security.oauth2.client.oidc.web;
import java.util.Set;
@@ -25,14 +25,16 @@
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcBackChannelLogoutAuthentication;
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
import org.springframework.security.oauth2.client.oidc.authentication.logout.TestOidcLogoutTokens;
-import org.springframework.security.oauth2.client.oidc.authentication.session.OidcSessionRegistration;
-import org.springframework.security.oauth2.client.oidc.authentication.session.TestOidcSessionRegistrations;
+import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
+import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
+import org.springframework.security.oauth2.client.oidc.session.TestOidcSessionInformations;
+import org.springframework.security.oauth2.client.oidc.web.logout.OidcBackChannelLogoutHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
-import org.springframework.security.web.authentication.logout.BackchannelLogoutAuthentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import static org.assertj.core.api.Assertions.assertThat;
@@ -46,91 +48,99 @@ public class OidcBackChannelLogoutFilterTests {
@Test
public void doFilterRequestDoesNotMatchThenDoesNotRun() throws Exception {
- ClientRegistrationRepository clients = mock(ClientRegistrationRepository.class);
- AuthenticationManager factory = mock(AuthenticationManager.class);
- OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(clients, factory);
+ ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
+ AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
+ OidcBackChannelLogoutFilter backChannelLogoutFilter = new OidcBackChannelLogoutFilter(
+ clientRegistrationRepository, authenticationManager);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
- filter.doFilter(request, response, chain);
- verifyNoInteractions(clients, factory);
+ backChannelLogoutFilter.doFilter(request, response, chain);
+ verifyNoInteractions(clientRegistrationRepository, authenticationManager);
verify(chain).doFilter(request, response);
}
@Test
public void doFilterRequestDoesNotMatchContainLogoutTokenThenBadRequest() throws Exception {
- ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
- ClientRegistrationRepository clients = mock(ClientRegistrationRepository.class);
- given(clients.findByRegistrationId(any())).willReturn(registration);
- AuthenticationManager factory = mock(AuthenticationManager.class);
- OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(clients, factory);
+ ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
+ ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
+ given(clientRegistrationRepository.findByRegistrationId(any())).willReturn(clientRegistration);
+ AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
+ OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(clientRegistrationRepository,
+ authenticationManager);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/connect/back-channel/id");
request.setServletPath("/logout/connect/back-channel/id");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
filter.doFilter(request, response, chain);
- verifyNoInteractions(factory, chain);
+ verifyNoInteractions(authenticationManager, chain);
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
public void doFilterWithNoMatchingClientThenBadRequest() throws Exception {
- ClientRegistrationRepository clients = mock(ClientRegistrationRepository.class);
- AuthenticationManager factory = mock(AuthenticationManager.class);
- OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(clients, factory);
+ ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
+ AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
+ OidcBackChannelLogoutFilter backChannelLogoutFilter = new OidcBackChannelLogoutFilter(
+ clientRegistrationRepository, authenticationManager);
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/logout/connect/back-channel/id");
request.setServletPath("/logout/connect/back-channel/id");
request.setParameter("logout_token", "logout_token");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
- filter.doFilter(request, response, chain);
- verify(clients).findByRegistrationId("id");
- verifyNoInteractions(factory, chain);
+ backChannelLogoutFilter.doFilter(request, response, chain);
+ verify(clientRegistrationRepository).findByRegistrationId("id");
+ verifyNoInteractions(authenticationManager, chain);
assertThat(response.getStatus()).isEqualTo(400);
}
@Test
public void doFilterWithSessionMatchingLogoutTokenThenInvalidates() throws Exception {
- ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
- ClientRegistrationRepository clients = mock(ClientRegistrationRepository.class);
- given(clients.findByRegistrationId(any())).willReturn(registration);
- AuthenticationManager factory = mock(AuthenticationManager.class);
+ ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
+ ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
+ given(clientRegistrationRepository.findByRegistrationId(any())).willReturn(clientRegistration);
+ AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
OidcLogoutToken token = TestOidcLogoutTokens.withSessionId("issuer", "provider").build();
- Iterable infos = Set.of(TestOidcSessionRegistrations.create("clientOne"),
- TestOidcSessionRegistrations.create("clientTwo"));
- given(factory.authenticate(any())).willReturn(new BackchannelLogoutAuthentication(token, token, infos));
- LogoutHandler logoutHandler = mock(LogoutHandler.class);
- OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(clients, factory);
- filter.setLogoutHandler(logoutHandler);
+ Iterable infos = Set.of(TestOidcSessionInformations.create("clientOne"),
+ TestOidcSessionInformations.create("clientTwo"));
+ given(authenticationManager.authenticate(any())).willReturn(new OidcBackChannelLogoutAuthentication(token));
+ OidcBackChannelLogoutHandler backChannelLogoutHandler = new OidcBackChannelLogoutHandler();
+ OidcSessionRegistry sessionRegistry = mock(OidcSessionRegistry.class);
+ given(sessionRegistry.removeSessionInformation(any(OidcLogoutToken.class))).willReturn(infos);
+ backChannelLogoutHandler.setSessionRegistry(sessionRegistry);
+ OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(clientRegistrationRepository,
+ authenticationManager);
+ filter.setLogoutHandler(backChannelLogoutHandler);
MockHttpServletRequest request = new MockHttpServletRequest("POST",
- "/oauth2/" + registration.getRegistrationId() + "/logout");
+ "/oauth2/" + clientRegistration.getRegistrationId() + "/logout");
request.setServletPath("/logout/connect/back-channel/id");
request.setParameter("logout_token", "logout_token");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
filter.doFilter(request, response, chain);
- verify(logoutHandler).logout(any(), any(), any());
+ verify(sessionRegistry).removeSessionInformation(token);
verifyNoInteractions(chain);
assertThat(response.getStatus()).isEqualTo(200);
}
@Test
public void doFilterWhenInvalidJwtThenBadRequest() throws Exception {
- ClientRegistration registration = TestClientRegistrations.clientRegistration().build();
- ClientRegistrationRepository clients = mock(ClientRegistrationRepository.class);
- given(clients.findByRegistrationId(any())).willReturn(registration);
- AuthenticationManager factory = mock(AuthenticationManager.class);
- given(factory.authenticate(any())).willThrow(new BadCredentialsException("bad"));
+ ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
+ ClientRegistrationRepository clientRegistrationRepository = mock(ClientRegistrationRepository.class);
+ given(clientRegistrationRepository.findByRegistrationId(any())).willReturn(clientRegistration);
+ AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
+ given(authenticationManager.authenticate(any())).willThrow(new BadCredentialsException("bad"));
LogoutHandler logoutHandler = mock(LogoutHandler.class);
- OidcBackChannelLogoutFilter filter = new OidcBackChannelLogoutFilter(clients, factory);
- filter.setLogoutHandler(logoutHandler);
+ OidcBackChannelLogoutFilter backChannelLogoutFilter = new OidcBackChannelLogoutFilter(
+ clientRegistrationRepository, authenticationManager);
+ backChannelLogoutFilter.setLogoutHandler(logoutHandler);
MockHttpServletRequest request = new MockHttpServletRequest("POST",
- "/oauth2/" + registration.getRegistrationId() + "/logout");
+ "/oauth2/" + clientRegistration.getRegistrationId() + "/logout");
request.setServletPath("/logout/connect/back-channel/id");
request.setParameter("logout_token", "logout_token");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
- filter.doFilter(request, response, chain);
+ backChannelLogoutFilter.doFilter(request, response, chain);
verifyNoInteractions(logoutHandler, chain);
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getContentAsString()).contains("bad");
diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/TestOidcIdTokens.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/TestOidcIdTokens.java
index ca859473d1e..2271a52e00f 100644
--- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/TestOidcIdTokens.java
+++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/TestOidcIdTokens.java
@@ -17,6 +17,7 @@
package org.springframework.security.oauth2.core.oidc;
import java.time.Instant;
+import java.util.List;
/**
* Test {@link OidcIdToken}s
@@ -32,6 +33,7 @@ public static OidcIdToken.Builder idToken() {
// @formatter:off
return OidcIdToken.withTokenValue("id-token")
.issuer("https://example.com")
+ .audience(List.of("client-id"))
.subject("subject")
.issuedAt(Instant.now())
.expiresAt(Instant.now()
diff --git a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java
index 3bda7ec32d7..ca2c37abf78 100644
--- a/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java
+++ b/oauth2/oauth2-core/src/test/java/org/springframework/security/oauth2/core/oidc/user/TestOidcUsers.java
@@ -50,7 +50,7 @@ private static OidcIdToken idToken() {
.expiresAt(expiresAt)
.subject("subject")
.issuer("http://localhost/issuer")
- .audience(Collections.unmodifiableSet(new LinkedHashSet<>(Collections.singletonList("client"))))
+ .audience(Collections.unmodifiableSet(new LinkedHashSet<>(Collections.singletonList("client-id"))))
.authorizedParty("client")
.build();
// @formatter:on
diff --git a/web/src/main/java/org/springframework/security/web/authentication/logout/BackchannelLogoutAuthentication.java b/web/src/main/java/org/springframework/security/web/authentication/logout/BackchannelLogoutAuthentication.java
deleted file mode 100644
index c61488bddb7..00000000000
--- a/web/src/main/java/org/springframework/security/web/authentication/logout/BackchannelLogoutAuthentication.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2002-2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.web.authentication.logout;
-
-import java.util.Collections;
-
-import org.springframework.security.authentication.AbstractAuthenticationToken;
-import org.springframework.security.core.session.SessionInformation;
-import org.springframework.util.Assert;
-
-public class BackchannelLogoutAuthentication extends AbstractAuthenticationToken {
-
- private final Object principal;
-
- private final Object credentials;
-
- private final Iterable extends SessionInformation> sessions;
-
- public BackchannelLogoutAuthentication(Object principal, Object credentials,
- Iterable extends SessionInformation> sessions) {
- super(Collections.emptyList());
- Assert.notNull(sessions, "sessions cannot be null");
- this.sessions = sessions;
- this.principal = principal;
- this.credentials = credentials;
- setAuthenticated(true);
- }
-
- @Override
- public Object getPrincipal() {
- return this.principal;
- }
-
- @Override
- public Object getCredentials() {
- return this.credentials;
- }
-
- public Iterable extends SessionInformation> getSessions() {
- return this.sessions;
- }
-
-}
diff --git a/web/src/main/java/org/springframework/security/web/authentication/logout/BackchannelLogoutHandler.java b/web/src/main/java/org/springframework/security/web/authentication/logout/BackchannelLogoutHandler.java
deleted file mode 100644
index 2e16a339e67..00000000000
--- a/web/src/main/java/org/springframework/security/web/authentication/logout/BackchannelLogoutHandler.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2002-2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.web.authentication.logout;
-
-import java.util.Map;
-
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.session.SessionInformation;
-import org.springframework.util.Assert;
-import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestOperations;
-import org.springframework.web.client.RestTemplate;
-import org.springframework.web.util.UriComponentsBuilder;
-
-public final class BackchannelLogoutHandler implements LogoutHandler {
-
- private final Log logger = LogFactory.getLog(getClass());
-
- private RestOperations rest = new RestTemplate();
-
- private String logoutEndpointName = "/logout";
-
- private String clientSessionCookieName = "JSESSIONID";
-
- @Override
- public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
- if (!(authentication instanceof BackchannelLogoutAuthentication token)) {
- if (this.logger.isDebugEnabled()) {
- String message = "Did not perform Backchannel Logout since authentication [%s] was of the wrong type";
- this.logger.debug(String.format(message, authentication.getClass().getSimpleName()));
- }
- return;
- }
- Iterable extends SessionInformation> sessions = token.getSessions();
- for (SessionInformation session : sessions) {
- eachLogout(request, session);
- }
- }
-
- private void eachLogout(HttpServletRequest request, SessionInformation session) {
- HttpHeaders headers = new HttpHeaders();
- headers.add(HttpHeaders.COOKIE, this.clientSessionCookieName + "=" + session.getSessionId());
- for (Map.Entry credential : session.getHeaders().entrySet()) {
- headers.add(credential.getKey(), credential.getValue());
- }
- String url = request.getRequestURL().toString();
- String logout = UriComponentsBuilder.fromHttpUrl(url).replacePath(this.logoutEndpointName).build()
- .toUriString();
- HttpEntity> entity = new HttpEntity<>(null, headers);
- try {
- this.rest.postForEntity(logout, entity, Object.class);
- if (this.logger.isTraceEnabled()) {
- this.logger.trace("Invalidated session");
- }
- }
- catch (RestClientException ex) {
- this.logger.debug("Failed to invalidate session", ex);
- }
- }
-
- public void setRestOperations(RestOperations rest) {
- Assert.notNull(rest, "rest cannot be null");
- this.rest = rest;
- }
-
- public void setLogoutEndpointName(String logoutEndpointName) {
- Assert.hasText(logoutEndpointName, "logoutEndpointName cannot be empty");
- this.logoutEndpointName = logoutEndpointName;
- }
-
- public void setClientSessionCookieName(String clientSessionCookieName) {
- Assert.hasText(clientSessionCookieName, "clientSessionCookieName cannot be empty");
- this.clientSessionCookieName = clientSessionCookieName;
- }
-
-}