[java-idp-oidc] 08/14: JOIDC-13 - Support for OIDC Logout
Henri Mikkonen
henri.mikkonen at iki.fi
Wed Jan 3 14:07:15 UTC 2024
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch main
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=b6d8a5b2d46579624fb8a475fdeac6dc86e32b3d
commit b6d8a5b2d46579624fb8a475fdeac6dc86e32b3d
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