[java-idp-oidc] 02/02: JOIDC-13 - Support for OIDC Logout
Henri Mikkonen
henri.mikkonen at iki.fi
Thu Oct 19 12:47:25 UTC 2023
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch dev/JOIDC-13
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=b81939b244e20fa19c1fededde5fbfb4d8b8e6ce
commit b81939b244e20fa19c1fededde5fbfb4d8b8e6ce
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Oct 19 15:46:40 2023 +0300
JOIDC-13 - Support for OIDC Logout
https://shibboleth.atlassian.net/browse/JOIDC-13
Initial (still incomplete) flow test for the end-session flow (RP-initiated logout).
PrependTestEnvironmentApplicationContextInitializer was needed as an additional
context initializer in order to disable the two properties explicitely set by
TestEnvironmentApplicationContextInitializer, which is wired by the AbstractFlowTest:
...
mock.setProperty("idp.session.trackSPSessions", "false");
mock.setProperty("idp.session.secondaryServiceIndex", "false");
...
That explicit setup in TestEnvironmentApplicationContextInitializer caused those
two properties to be locked into false, even though they were enabled in the
idp.properties. This test requires those to be true.
---
.../oidc/op/profile/flow/EndSessionFlowTest.java | 380 +++++++++++++++++++++
...stEnvironmentApplicationContextInitializer.java | 49 +++
.../shibboleth/idp/module/conf/relying-party.xml | 1 +
3 files changed, 430 insertions(+)
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java
new file mode 100644
index 00000000..c10b5116
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java
@@ -0,0 +1,380 @@
+/*
+ * 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
+ *
+ * http://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 net.shibboleth.idp.plugin.oidc.op.profile.flow;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.security.interfaces.RSAPrivateKey;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+import org.opensaml.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.springframework.webflow.test.MockParameterMap;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.SPSession;
+import net.shibboleth.idp.session.SessionException;
+import net.shibboleth.idp.session.criterion.SessionIdCriterion;
+import net.shibboleth.idp.session.impl.StorageBackedSessionManager;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Tests for the end session-flow.
+ */
+ at ContextConfiguration(
+ initializers = {
+ PrependTestEnvironmentApplicationContextInitializer.class})
+public class EndSessionFlowTest extends AbstractOidcFlowTest {
+
+ public static final String FLOW_ID = "oidc/end-session";
+
+ String issuer = "https://op.example.org";
+ String postLogoutRedirectUri = "https://example.org/postLogout";
+ String sessionId = "mockSessionId";
+ String subject = "mockSubject";
+ String clientId = "mockClientId";
+ String clientSecret = "mockClientSecretmockClientSecretmockClientSecretmockClientSecretmockClientSecret";
+
+ @Autowired
+ @Qualifier("shibboleth.StorageService")
+ StorageService storageService;
+
+ @Autowired
+ @Qualifier("shibboleth.SessionManager")
+ StorageBackedSessionManager sessionManager;
+
+ public EndSessionFlowTest() {
+ super(FLOW_ID);
+ }
+
+ @BeforeMethod
+ public void assertSetup() {
+ Assert.assertTrue(sessionManager.isTrackSPSessions());
+ Assert.assertTrue(sessionManager.isSecondaryServiceIndex());
+ }
+
+ @Test
+ public void testWithNoParameters() {
+ setRequestParameters(Collections.emptyList());
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertEquals("ErrorView", result.getOutcome().getId());
+ }
+
+ @Test
+ public void testWithClientIdNoSession() {
+ setRequestParameters(List.of(new Pair<>("client_id", clientId)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, postLogoutRedirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertEquals("ErrorView", result.getOutcome().getId());
+ }
+
+ @Test
+ public void testWithClientIdAndSession_noRedirection() {
+ setRequestParameters(List.of(
+ new Pair<>("client_id", clientId),
+ new Pair<>("logout_hint", sessionId)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, postLogoutRedirectUri);
+
+ initializeThreadLocals();
+
+ final IdPSession session = buildIdPSessionWithDefaultSP();
+ request.setCookies(response.getCookies());
+ Assert.assertTrue(isSessionValid(session));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertFalse(result.isEnded());
+ Assert.assertFalse(isSessionValid(session));
+
+ ((MockParameterMap) externalContext.getRequestParameterMap()).put("_eventId", "proceed");
+ final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+ Assert.assertFalse(isSessionValid(session));
+ Assert.assertEquals(response.getStatus(), 200);
+ Assert.assertTrue(result2.isEnded());
+ }
+
+ @Test
+ public void testWithClientIdAndSession_withPostLogoutRedirectionNotStored() {
+ setRequestParameters(List.of(
+ new Pair<>("client_id", clientId),
+ new Pair<>("logout_hint", sessionId),
+ new Pair<>("post_logout_redirect_uri", postLogoutRedirectUri)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret);
+
+ initializeThreadLocals();
+
+ final IdPSession session = buildIdPSessionWithDefaultSP();
+ request.setCookies(response.getCookies());
+ Assert.assertTrue(isSessionValid(session));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertEquals("ErrorView", result.getOutcome().getId());
+ }
+
+ @Test
+ public void testWithClientIdAndSession_withPostLogoutRedirection() {
+ setRequestParameters(List.of(
+ new Pair<>("client_id", clientId),
+ new Pair<>("logout_hint", sessionId),
+ new Pair<>("post_logout_redirect_uri", postLogoutRedirectUri)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, postLogoutRedirectUri);
+
+ initializeThreadLocals();
+
+ final IdPSession session = buildIdPSessionWithDefaultSP();
+ request.setCookies(response.getCookies());
+ Assert.assertTrue(isSessionValid(session));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertFalse(result.isEnded());
+ Assert.assertFalse(isSessionValid(session));
+
+ ((MockParameterMap) externalContext.getRequestParameterMap()).put("_eventId", "proceed");
+ final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+ Assert.assertFalse(isSessionValid(session));
+ Assert.assertEquals(response.getStatus(), 302);
+ Assert.assertEquals(response.getHeaderValue("Location"), postLogoutRedirectUri);
+ Assert.assertTrue(result2.isEnded());
+ }
+
+ @Test
+ public void testWithInvalidIdTokenHint() {
+ setRequestParameters(List.of(new Pair<>("id_token_hint", "not a JWT")));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, postLogoutRedirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertEquals("ErrorView", result.getOutcome().getId());
+ }
+
+ @Test
+ public void testWithValidIdTokenHint_noRedirection() {
+ final SignedJWT idTokenHint;
+ try {
+ idTokenHint = createPrivateKeyJWT(JWTClaimsSet.parse(getDefaultIdTokenHintPayload()),
+ (RSAPrivateKey) loadRSSigningCredential().getPrivateKey(), JWSAlgorithm.RS256);
+ } catch (JOSEException | ParseException e) {
+ Assert.fail();
+ return;
+ }
+ setRequestParameters(List.of(new Pair<>("id_token_hint", idTokenHint.serialize())));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, postLogoutRedirectUri);
+
+ initializeThreadLocals();
+
+ final IdPSession session = buildIdPSessionWithDefaultSP();
+ request.setCookies(response.getCookies());
+ Assert.assertTrue(isSessionValid(session));
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertFalse(result.isEnded());
+
+ ((MockParameterMap) externalContext.getRequestParameterMap()).put("_eventId", "proceed");
+ final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+ Assert.assertFalse(isSessionValid(session));
+ Assert.assertEquals(response.getStatus(), 200);
+ Assert.assertTrue(result2.isEnded());
+ }
+
+ @Test
+ public void testWithValidIdTokenHint_withPostLogoutRedirectUriNotStored() {
+ final SignedJWT idTokenHint;
+ try {
+ idTokenHint = createPrivateKeyJWT(JWTClaimsSet.parse(getDefaultIdTokenHintPayload()),
+ (RSAPrivateKey) loadRSSigningCredential().getPrivateKey(), JWSAlgorithm.RS256);
+ } catch (JOSEException | ParseException e) {
+ Assert.fail();
+ return;
+ }
+ setRequestParameters(List.of(new Pair<>("id_token_hint", idTokenHint.serialize()),
+ new Pair<>("post_logout_redirect_uri", postLogoutRedirectUri)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret);
+
+ initializeThreadLocals();
+
+ final IdPSession session = buildIdPSessionWithDefaultSP();
+ request.setCookies(response.getCookies());
+ Assert.assertTrue(isSessionValid(session));
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertEquals("ErrorView", result.getOutcome().getId());
+ }
+
+ @Test
+ public void testWithValidIdTokenHint_withPostLogoutRedirection() {
+ final SignedJWT idTokenHint;
+ try {
+ idTokenHint = createPrivateKeyJWT(JWTClaimsSet.parse(getDefaultIdTokenHintPayload()),
+ (RSAPrivateKey) loadRSSigningCredential().getPrivateKey(), JWSAlgorithm.RS256);
+ } catch (JOSEException | ParseException e) {
+ Assert.fail();
+ return;
+ }
+ setRequestParameters(List.of(new Pair<>("id_token_hint", idTokenHint.serialize()),
+ new Pair<>("post_logout_redirect_uri", postLogoutRedirectUri)));
+ request.setMethod("GET");
+ storeMetadata(storageService, clientId, clientSecret, postLogoutRedirectUri);
+
+ initializeThreadLocals();
+
+ final IdPSession session = buildIdPSessionWithDefaultSP();
+ request.setCookies(response.getCookies());
+ Assert.assertTrue(isSessionValid(session));
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ Assert.assertFalse(result.isEnded());
+
+ ((MockParameterMap) externalContext.getRequestParameterMap()).put("_eventId", "proceed");
+ final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+ Assert.assertFalse(isSessionValid(session));
+ Assert.assertEquals(response.getStatus(), 302);
+ Assert.assertEquals(response.getHeaderValue("Location"), postLogoutRedirectUri);
+ Assert.assertTrue(result2.isEnded());
+ }
+
+ protected IdPSession buildIdPSessionWithDefaultSP() {
+ return buildIdPSession(new OIDCRPSession.Builder()
+ .serviceId(clientId)
+ .issuer(issuer)
+ .creationInstant(Instant.now())
+ .expirationInstant(Instant.now().plusSeconds(300))
+ .rootTokenIdentifier("mockRootId")
+ .sessionIdentifier(sessionId)
+ .subject(subject)
+ .build());
+ }
+
+ protected IdPSession buildIdPSession(SPSession... sessions) {
+ try {
+ final IdPSession idpSession = sessionManager.createSession("mockSessionPrincipal");
+ for (final SPSession session : sessions) {
+ idpSession.addSPSession(session);
+ }
+ return idpSession;
+ } catch (SessionException e) {
+ Assert.fail("Could not add a SPSession to the IdP session", e);
+ }
+ return null;
+ }
+
+ protected void setRequestParameters(final List<Pair<String, String>> pairs) {
+ setRequestParameters(request, pairs);
+ }
+
+ protected static void setRequestParameters(final MockHttpServletRequest request,
+ final List<Pair<String, String>> pairs) {
+ final StringBuffer query = new StringBuffer();
+ for (final Pair<String, String> pair : pairs) {
+ request.addParameter(pair.getFirst(), pair.getSecond());
+ try {
+ query.append(pair.getFirst() + "=" + URLEncoder.encode(pair.getSecond(), "UTF-8") + "&");
+ } catch (UnsupportedEncodingException e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+ request.setQueryString(query.toString());
+ }
+
+ @AfterMethod
+ public void removeMetadata() throws IOException {
+ removeMetadata(storageService, clientId);
+ }
+
+ protected boolean isSessionValid(final IdPSession session) {
+ try {
+ return sessionManager.resolveSingle(new CriteriaSet(new SessionIdCriterion(session.getId()))) != null;
+ } catch (ResolverException e) {
+ Assert.fail();
+ return false;
+ }
+ }
+
+ protected String getDefaultIdTokenHintPayload() {
+ return getIdTokenHintPayload(issuer, subject, clientId, Instant.now().plusSeconds(300), Instant.now(),
+ sessionId);
+ }
+
+ protected static String getIdTokenHintPayload(final String issuer, final String subject, final String clientId,
+ final Instant exp, final Instant iat, final String sid) {
+ return "{\n"
+ + " \"iss\": \"" + issuer + "\",\n"
+ + " \"aud\": \"" + clientId + "\",\n"
+ + " \"sub\": \"" + subject + "\",\n"
+ + " \"exp\": " + exp.getEpochSecond() + ",\n"
+ + " \"iat\": " + iat.getEpochSecond() + ",\n"
+ + " \"sid\": \"" + sid + "\"\n"
+ + "}";
+ }
+
+ protected void storeMetadata(final StorageService storageService, final String clientId, final String clientSecret,
+ final String... redirectUri) {
+ final OIDCClientMetadata metadata = buildMetadataSkeleton();
+ final HashSet<URI> uris = new HashSet<>();
+ if (redirectUri != null) {
+ for (final String uri : redirectUri) {
+ try {
+ uris.add(new URI(uri));
+ } catch (final URISyntaxException e) {
+ Assert.fail();
+ }
+ }
+ }
+ metadata.setPostLogoutRedirectionURIs(uris);
+ metadata.setScope(Scope.parse("openid"));
+ try {
+ storeMetadataObject(storageService, clientId, clientSecret, metadata);
+ } catch (final IOException e) {
+ Assert.fail();
+ }
+
+ }
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PrependTestEnvironmentApplicationContextInitializer.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PrependTestEnvironmentApplicationContextInitializer.java
new file mode 100644
index 00000000..31e71d1f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PrependTestEnvironmentApplicationContextInitializer.java
@@ -0,0 +1,49 @@
+/*
+ * 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
+ *
+ * http://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 net.shibboleth.idp.plugin.oidc.op.profile.flow;
+
+import javax.annotation.Nonnull;
+
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.mock.env.MockPropertySource;
+
+/**
+ * An {@link ApplicationContextInitializer} which prepends properties.
+ *
+ * <ul>
+ * <li>Sets idp.home = classpath:</li>
+ * <li>Sets idp.webflows = classpath*:/flows</li>
+ * <li>Sets idp.storage.htmlLocalStorage" = false</li>
+ * <li>Sets idp.service.metadata.resources = testbed.MetadataResolverResources</li>
+ * </ul>
+ */
+ at Order(Ordered.LOWEST_PRECEDENCE)
+public class PrependTestEnvironmentApplicationContextInitializer
+ implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+ /** {@inheritDoc} */
+ @Override public void initialize(@Nonnull final ConfigurableApplicationContext applicationContext) {
+ final MockPropertySource mock = new MockPropertySource();
+ mock.setProperty("idp.home", "classpath:/net/shibboleth/idp/module");
+ mock.setProperty("idp.webflows", "classpath*:/flows");
+ mock.setProperty("idp.storage.htmlLocalStorage", "false");
+ mock.setProperty("idp.service.metadata.resources", "testbed.MetadataResolverResources");
+ applicationContext.getEnvironment().getPropertySources().addFirst(mock);
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index ec842bc6..429c71b2 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -65,6 +65,7 @@
<ref bean="OIDC.SSO.MDDriven" />
<ref bean="OIDC.UserInfo.MDDriven" />
<ref bean="OIDC.Registration.MDDriven" />
+ <ref bean="OIDC.Logout" />
<ref bean="OAUTH2.Token.MDDriven" />
<ref bean="OAUTH2.Introspection.MDDriven" />
<ref bean="OAUTH2.Revocation.MDDriven" />
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list