[java-idp-oidc] 02/02: JOIDC-13 - Support for OIDC Logout

Henri Mikkonen henri.mikkonen at iki.fi
Fri Nov 17 10:31:35 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=a123b815d24f3561fc822a299f4f288f74b4cbac

commit a123b815d24f3561fc822a299f4f288f74b4cbac
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Nov 17 12:30:51 2023 +0200

    JOIDC-13 - Support for OIDC Logout
    
    https://shibboleth.atlassian.net/browse/JOIDC-13
    
    Initial flow test for the OIDC logout propagation flow
---
 .../oidc/oidc-logout-propagation-beans.xml         |   3 +
 .../oidc/oidc-logout-propagation-flow.xml          |   8 +-
 .../op/profile/flow/LogoutPropagationFlowTest.java | 257 +++++++++++++++++++++
 .../shibboleth/idp/module/conf/relying-party.xml   |  29 +++
 4 files changed, 296 insertions(+), 1 deletion(-)

diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/logoutprop/oidc/oidc-logout-propagation-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/logoutprop/oidc/oidc-logout-propagation-beans.xml
index cc6163a0..c8f835e9 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/logoutprop/oidc/oidc-logout-propagation-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/logoutprop/oidc/oidc-logout-propagation-beans.xml
@@ -129,6 +129,9 @@
             <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultChainRevocationLifetimeLookupStrategy"
                 p:clockSkew="%{idp.policy.clockSkew:PT5M}" p:useActiveProfileOnly="false" />
         </property>
+        <property name="activationCondition">
+            <bean class="net.shibboleth.oidc.profile.config.logic.RevokeTokensInLogoutPredicate"/>
+        </property>
     </bean>
 
     <bean id="ExecuteBackChannelLogoutRequest"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/logoutprop/oidc/oidc-logout-propagation-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/logoutprop/oidc/oidc-logout-propagation-flow.xml
index 8d097360..d88bd3ee 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/logoutprop/oidc/oidc-logout-propagation-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/logoutprop/oidc/oidc-logout-propagation-flow.xml
@@ -96,7 +96,7 @@
 
     <action-state id="HandleError">
         <evaluate expression="'proceed'"/>
-        <transition on="proceed" to="proceed"/>
+        <transition on="proceed" to="LogoutFailed"/>
     </action-state>
 
     <!-- Successful terminal state (success meaning outbound LogoutRequest encoded). -->
@@ -114,6 +114,12 @@
         </on-entry>
     </end-state>
 
+    <end-state id="LogoutFailed">
+        <on-entry>
+            <set name="requestScope.logoutPropCtx" value="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.session.context.LogoutPropagationContext))"/>
+        </on-entry>
+    </end-state>
+
     <bean-import resource="oidc-logout-propagation-beans.xml"/>
 
 </flow>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/LogoutPropagationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/LogoutPropagationFlowTest.java
new file mode 100644
index 00000000..dfd89d3f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/LogoutPropagationFlowTest.java
@@ -0,0 +1,257 @@
+/*
+ * 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.time.Instant;
+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.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 OIDC logout propagation flow.
+ */
+ at ContextConfiguration(
+        initializers = {
+                PrependTestEnvironmentApplicationContextInitializer.class})
+public class LogoutPropagationFlowTest extends AbstractOidcFlowTest {
+    
+    public static final String FLOW_ID = "Logout";
+    
+    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 LogoutPropagationFlowTest() {
+        super(FLOW_ID, "LogoutPropagateView");
+    }
+    
+    @BeforeMethod
+    public void assertSetup() {
+        Assert.assertTrue(sessionManager.isTrackSPSessions());
+        Assert.assertTrue(sessionManager.isSecondaryServiceIndex());
+    }
+
+    protected void runLogoutPrePropagation(final String clientId, final URI frontChannelUri, final URI backChannelUri) {
+        storeMetadata(storageService, clientId, clientSecret, frontChannelUri, backChannelUri);
+        request.setMethod("GET");
+        
+        initializeThreadLocals();
+
+        final IdPSession session = buildIdPSessionWithDefaultSP(clientId);
+        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", "propagate");
+
+        final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+        Assert.assertFalse(isSessionValid(session));
+        Assert.assertEquals(response.getStatus(), 200);
+        Assert.assertTrue(result2.isEnded());
+        Assert.assertEquals("LogoutPropagateView", result2.getOutcome().getId());
+
+        ((MockParameterMap) externalContext.getRequestParameterMap()).put("SessionKey", "1");
+    }
+
+    @Test
+    public void testWithNoFrontNorBackUri() throws URISyntaxException {
+        runLogoutPrePropagation(clientId, null, null);
+        final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
+        Assert.assertTrue(result.isEnded());
+        Assert.assertEquals(result.getOutcome().getId(), "LogoutFailed");
+    }
+
+    @Test
+    public void testWithFrontPendingNoBackUri() throws URISyntaxException {
+        runLogoutPrePropagation(clientId, new URI("https://example.org/front"), null);
+        final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
+        Assert.assertTrue(result.isEnded());
+        Assert.assertEquals(result.getOutcome().getId(), "LogoutPending");
+    }
+
+    @Test
+    public void testWithFrontSuccessNoBackUriNoRevoke() throws URISyntaxException {
+        runLogoutPrePropagation("mockClientIdLogoutFrontSuccessNoRevoke", new URI("https://example.org/front"), null);
+        final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
+        Assert.assertTrue(result.isEnded());
+        Assert.assertEquals(result.getOutcome().getId(), "LogoutFailed");
+    }
+
+    @Test
+    public void testWithFrontSuccessNoBackUriNoRevokeRevocationDisabled() throws URISyntaxException {
+        runLogoutPrePropagation("mockClientIdLogoutFrontSuccessNoRevokeRevocationDisabled",
+                new URI("https://example.org/front"), null);
+        final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
+        Assert.assertFalse(result.isEnded());
+        ((MockParameterMap) externalContext.getRequestParameterMap()).put("_eventId", "proceed");
+        final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+        Assert.assertEquals(result2.getOutcome().getId(), "RenderResult");
+    }
+
+    @Test
+    public void testWithFrontSuccessNoBackUriWithRevoke() throws URISyntaxException {
+        runLogoutPrePropagation("mockClientIdLogoutFrontSuccessWithRevoke", new URI("https://example.org/front"), null);
+        final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
+        Assert.assertFalse(result.isEnded());
+        ((MockParameterMap) externalContext.getRequestParameterMap()).put("_eventId", "proceed");
+        final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+        Assert.assertEquals(result2.getOutcome().getId(), "RenderResult");
+    }
+
+    @Test
+    public void testWithoBackUriNoFront() throws URISyntaxException {
+        runLogoutPrePropagation(clientId, null, new URI("https://example.org/back"));
+        final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
+        Assert.assertTrue(result.isEnded());
+        Assert.assertEquals(result.getOutcome().getId(), "RenderResult");
+    }
+
+    protected IdPSession buildIdPSessionWithDefaultSP(final String clientId) {
+       return buildIdPSession(sessionManager, new OIDCRPSession.Builder()
+               .serviceId(clientId)
+               .issuer(issuer)
+               .creationInstant(Instant.now())
+               .expirationInstant(Instant.now().plusSeconds(300))
+               .rootTokenIdentifier("mockRootId")
+               .sessionIdentifier(sessionId)
+               .subject(subject)
+               .supportLogoutPropagation(true)
+               .build());
+    }
+
+    protected static IdPSession buildIdPSession(final StorageBackedSessionManager sessionManager,
+            final 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) {
+        return isSessionValid(sessionManager, session);
+    }
+
+    protected static boolean isSessionValid(final StorageBackedSessionManager sessionManager,
+            final IdPSession session) {
+        try {
+            return sessionManager.resolveSingle(new CriteriaSet(new SessionIdCriterion(session.getId()))) != null;
+        } catch (ResolverException e) {
+            Assert.fail();
+            return false;
+        }
+    }
+
+    protected void storeMetadata(final StorageService storageService, final String clientId, final String clientSecret,
+            final String... redirectUri) {
+        storeMetadata(storageService, clientId, clientSecret, (URI) null, (URI) null, redirectUri);
+    }
+        
+    protected void storeMetadata(final StorageService storageService, final String clientId, final String clientSecret,
+            final URI frontChannelUri, final URI backChannelUri, 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.setFrontChannelLogoutURI(frontChannelUri);
+        metadata.setBackChannelLogoutURI(backChannelUri);
+        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/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 6347a3cc..4fa141b8 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
@@ -128,6 +128,35 @@
                  </list>
             </property>
         </bean>
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdLogoutFrontSuccessNoRevoke">
+            <property name="profileConfigurations">
+                 <list>
+                     <bean parent="OIDC.Logout" p:frontChannelSuccess="true"/>
+                 </list>
+            </property>
+        </bean>
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdLogoutFrontSuccessNoRevokeRevocationDisabled">
+            <property name="profileConfigurations">
+                 <list>
+                     <bean parent="OIDC.Logout" p:frontChannelSuccess="true" p:revokeTokens="false"/>
+                 </list>
+            </property>
+        </bean>
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdLogoutFrontSuccessWithRevoke">
+            <property name="profileConfigurations">
+                 <list>
+                     <bean parent="OIDC.Logout" p:frontChannelSuccess="true"/>
+                     <ref bean="OAUTH2.Revocation" />
+                 </list>
+            </property>
+        </bean>
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdLogoutPreferBackChannel">
+            <property name="profileConfigurations">
+                 <list>
+                     <bean parent="OIDC.Logout" p:preferFrontChannel="false"/>
+                 </list>
+            </property>
+        </bean>
         <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdRefreshTokenRotation">
             <property name="profileConfigurations">
                  <list>

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list