[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