[java-idp-oidc] branch dev/JOIDC-13 updated: JOIDC-13 - Support for OIDC Logout

Henri Mikkonen henri.mikkonen at iki.fi
Fri Dec 1 13:53:49 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=38970eab9e768c417ec9c9efc70f33a9602509e1

The following commit(s) were added to refs/heads/dev/JOIDC-13 by this push:
     new 38970eab JOIDC-13 - Support for OIDC Logout
38970eab is described below

commit 38970eab9e768c417ec9c9efc70f33a9602509e1
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Dec 1 15:52:45 2023 +0200

    JOIDC-13 - Support for OIDC Logout
    
    https://shibboleth.atlassian.net/browse/JOIDC-13
    
    Improved flow tests for OIDC logout propagation flow. Integrated the
    JWS/JWE security tests to the back-channel logout token (JWT).
---
 .../flow/AbstractIssuedJWTSecurityTest.java        | 89 ++++++++++++++++++++--
 .../oidc/op/profile/flow/AbstractOidcFlowTest.java |  2 +-
 .../op/profile/flow/IssuedEncryptedJWTTest.java    |  7 +-
 .../oidc/op/profile/flow/IssuedSignedJWTTest.java  |  5 +-
 .../op/profile/flow/LogoutPropagationFlowTest.java | 83 ++++++++++++++++----
 .../shibboleth/idp/module/conf/relying-party.xml   |  2 +
 6 files changed, 165 insertions(+), 23 deletions(-)

diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractIssuedJWTSecurityTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractIssuedJWTSecurityTest.java
index 4c18314b..cba65a84 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractIssuedJWTSecurityTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractIssuedJWTSecurityTest.java
@@ -29,8 +29,12 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.webflow.executor.FlowExecutionResult;
+import org.springframework.webflow.test.MockParameterMap;
 import org.testng.Assert;
 
 import com.nimbusds.jose.EncryptionMethod;
@@ -63,6 +67,9 @@ import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
 import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.impl.StorageBackedSessionManager;
 import net.shibboleth.oidc.security.credential.JWKCredential;
 import net.shibboleth.oidc.security.impl.support.TestCredentialHelper;
 import net.shibboleth.shared.collection.Pair;
@@ -79,6 +86,9 @@ public abstract class AbstractIssuedJWTSecurityTest extends AbstractOidcFlowTest
     String defaultClientSecret = "/A?D(G+KbPdSgVkYp3s6v9y$B&E)H at Mc";
     String defaultClientSecret64B = "/A?D(G+KbPdSgVkYp3s6v9y$B&E)H at Mc/A?D(G+KbPdSgVkYp3s6v9y$B&E)H at Mc";
 
+    @Autowired
+    @Qualifier("shibboleth.SessionManager")
+    StorageBackedSessionManager sessionManager;
 
     public enum JWT_FETCHING_TYPE {
         
@@ -94,7 +104,9 @@ public abstract class AbstractIssuedJWTSecurityTest extends AbstractOidcFlowTest
         
         REQUEST_OBJECT,
         
-        ID_TOKEN_HINT
+        ID_TOKEN_HINT,
+        
+        LOGOUT_TOKEN
     }
 
     protected final JWT_FETCHING_TYPE fetchingType;
@@ -102,12 +114,16 @@ public abstract class AbstractIssuedJWTSecurityTest extends AbstractOidcFlowTest
     protected final boolean encryptionOptional;
 
     protected AbstractIssuedJWTSecurityTest(final JWT_FETCHING_TYPE type, final String flowId) {
-        this(type, flowId, true);
+        this(type, flowId, null, true);
+    }
+
+    protected AbstractIssuedJWTSecurityTest(final JWT_FETCHING_TYPE type, final String flowId, final String endId) {
+        this(type, flowId, endId, true);
     }
 
-    protected AbstractIssuedJWTSecurityTest(final JWT_FETCHING_TYPE type, final String flowId,
+    protected AbstractIssuedJWTSecurityTest(final JWT_FETCHING_TYPE type, final String flowId, final String endId,
             final boolean optionalEncryption) {
-        super(flowId);
+        super(flowId, endId);
         fetchingType = type;
         encryptionOptional = optionalEncryption;
     }
@@ -144,6 +160,9 @@ public abstract class AbstractIssuedJWTSecurityTest extends AbstractOidcFlowTest
             case AUTHORIZE_ACCESS_TOKEN:
                 return obtainAccessTokenFromAuthorizeEndpoint(clientId, clientSecret, publicKey, jwsAlgorithm,
                         jweAlgorithm, encryptionMethod);
+            case LOGOUT_TOKEN:
+                return obtainLogoutToken(clientId, clientSecret, publicKey, jwsAlgorithm,
+                        jweAlgorithm, encryptionMethod);
             default:
                 Assert.fail();
         }
@@ -542,7 +561,67 @@ public abstract class AbstractIssuedJWTSecurityTest extends AbstractOidcFlowTest
         return null;
     }
 
-    
+    protected JWT obtainLogoutToken(final String clientId, final String clientSecret,
+            final PublicKey publicKey,final JWSAlgorithm storedJwsAlgorithm, final JWEAlgorithm storedJweAlgorithm,
+            final EncryptionMethod storedJweMethod) {
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setScope(Scope.parse("openid"));
+        try {
+            metadata.setBackChannelLogoutURI(new URI("https://mock.localhost:12345/back"));
+            metadata.setIDTokenJWSAlg(storedJwsAlgorithm);
+            metadata.setIDTokenJWEAlg(storedJweAlgorithm);
+            metadata.setIDTokenJWEEnc(storedJweMethod);
+            if (publicKey != null) {
+                metadata.setJWKSet(super.buildJWKSet(publicKey));
+            }
+            storeMetadataObject(storageService, clientId, clientSecret, metadata);
+        } catch (final IOException | URISyntaxException e) {
+            Assert.fail();
+        }
+        runLogoutPrePropagation(clientId, clientSecret);
+        final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
+
+        try {
+            removeMetadata(storageService, clientId);
+        } catch (IOException e) {
+            Assert.fail();
+        }
+
+        Assert.assertTrue(result.isEnded());
+        if ("RenderResult".equals(result.getOutcome().getId())) {
+            final ProfileRequestContext prc =
+                    (ProfileRequestContext)request.getAttribute(ProfileRequestContext.BINDING_KEY);
+            return prc.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class)
+                    .getProcessedToken();
+        }
+        return null;
+    }
+
+    protected void runLogoutPrePropagation(final String clientId, final String clientSecret) {
+        request.setMethod("GET");
+        
+        initializeThreadLocals();
+
+        final IdPSession session = LogoutPropagationFlowTest.buildIdPSessionWithDefaultSP(sessionManager, clientId,
+                "mockIssuer", "mockSessionId", "mockSubject");
+        request.setCookies(response.getCookies());
+        Assert.assertTrue(LogoutPropagationFlowTest.isSessionValid(sessionManager, session));
+
+        final FlowExecutionResult result = flowExecutor.launchExecution("Logout", null, externalContext);
+        Assert.assertFalse(result.isEnded());
+        Assert.assertFalse(LogoutPropagationFlowTest.isSessionValid(sessionManager, session));
+
+        ((MockParameterMap) externalContext.getRequestParameterMap()).put("_eventId", "propagate");
+
+        final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+        Assert.assertFalse(LogoutPropagationFlowTest.isSessionValid(sessionManager, session));
+        Assert.assertEquals(response.getStatus(), 200);
+        Assert.assertTrue(result2.isEnded());
+        Assert.assertEquals("LogoutPropagateView", result2.getOutcome().getId());
+
+        ((MockParameterMap) externalContext.getRequestParameterMap()).put("SessionKey", "1");
+    }
+
     protected JWT parseEncryptedOrSignedJWT(final String input) {
         try {
             return SignedJWT.parse(input);
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
index ae36ca74..123e61c4 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
@@ -128,7 +128,7 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
 
     protected AbstractOidcFlowTest(final String id, final String endId) {
         flowId = id;
-        endStateId = endId;
+        endStateId = endId == null ? END_STATE_ID : endId;
     }
 
     /**
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssuedEncryptedJWTTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssuedEncryptedJWTTest.java
index 40b3c780..a436b793 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssuedEncryptedJWTTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssuedEncryptedJWTTest.java
@@ -57,7 +57,12 @@ public class IssuedEncryptedJWTTest extends AbstractIssuedJWTSecurityTest {
 
     public IssuedEncryptedJWTTest(final JWT_FETCHING_TYPE type, final String flowId, final boolean testSigned,
             final boolean encryptionOptional) {
-        super(type, flowId, encryptionOptional);
+        this(type, flowId, null, testSigned, encryptionOptional);
+    }
+
+    public IssuedEncryptedJWTTest(final JWT_FETCHING_TYPE type, final String flowId, final String endId,
+            final boolean testSigned, final boolean encryptionOptional) {
+        super(type, flowId, endId, encryptionOptional);
         testSignedJwt = testSigned;
     }
 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssuedSignedJWTTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssuedSignedJWTTest.java
index a76e2756..c7f96792 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssuedSignedJWTTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IssuedSignedJWTTest.java
@@ -37,6 +37,10 @@ public class IssuedSignedJWTTest extends AbstractIssuedJWTSecurityTest {
         super(type, flowId);
     }
 
+    public IssuedSignedJWTTest(final JWT_FETCHING_TYPE type, final String flowId, final String endId) {
+        super(type, flowId, endId);
+    }
+
     @Test
     public void testJwtSecurity_jwtSigAlgAndEncNotSpecified() throws Exception {
         if (fetchingType.equals(JWT_FETCHING_TYPE.USERINFO)) {
@@ -158,5 +162,4 @@ public class IssuedSignedJWTTest extends AbstractIssuedJWTSecurityTest {
     public void testJwtSecurity_jwtHS512SigAlgAndEncNotSpecified_NoKey() throws Exception {
         assertNoJwtResponse(defaultClientId, null, null, JWSAlgorithm.HS512, fetchingType);
     }
-
 }
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
index dfd89d3f..8f10d4f2 100644
--- 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
@@ -23,6 +23,7 @@ import java.time.Instant;
 import java.util.HashSet;
 import java.util.List;
 
+import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.storage.StorageService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -33,15 +34,22 @@ import org.springframework.webflow.test.MockParameterMap;
 import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Factory;
 import org.testng.annotations.Test;
 
+import com.nimbusds.jwt.JWT;
+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.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCBackChannelLogoutPropagationContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext;
 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.context.LogoutPropagationContext;
 import net.shibboleth.idp.session.criterion.SessionIdCriterion;
 import net.shibboleth.idp.session.impl.StorageBackedSessionManager;
 import net.shibboleth.shared.collection.Pair;
@@ -114,66 +122,111 @@ public class LogoutPropagationFlowTest extends AbstractOidcFlowTest {
         final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
         Assert.assertTrue(result.isEnded());
         Assert.assertEquals(result.getOutcome().getId(), "LogoutFailed");
+        assertPropagationResult(LogoutPropagationContext.Result.Failure);
     }
 
     @Test
     public void testWithFrontPendingNoBackUri() throws URISyntaxException {
-        runLogoutPrePropagation(clientId, new URI("https://example.org/front"), null);
+        runLogoutPrePropagation(clientId, new URI("https://mock.localhost:12345/back/front"), null);
         final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
         Assert.assertTrue(result.isEnded());
         Assert.assertEquals(result.getOutcome().getId(), "LogoutPending");
+        assertPropagationResult(LogoutPropagationContext.Result.Failure);
     }
 
     @Test
     public void testWithFrontSuccessNoBackUriNoRevoke() throws URISyntaxException {
-        runLogoutPrePropagation("mockClientIdLogoutFrontSuccessNoRevoke", new URI("https://example.org/front"), null);
+        runLogoutPrePropagation("mockClientIdLogoutFrontSuccessNoRevoke", new URI("https://mock.localhost:12345/back/front"), null);
         final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
         Assert.assertTrue(result.isEnded());
         Assert.assertEquals(result.getOutcome().getId(), "LogoutFailed");
+        assertPropagationResult(LogoutPropagationContext.Result.Failure);
     }
 
     @Test
     public void testWithFrontSuccessNoBackUriNoRevokeRevocationDisabled() throws URISyntaxException {
         runLogoutPrePropagation("mockClientIdLogoutFrontSuccessNoRevokeRevocationDisabled",
-                new URI("https://example.org/front"), null);
+                new URI("https://mock.localhost:12345/back/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");
+        assertPropagationResult(LogoutPropagationContext.Result.Success);
     }
 
     @Test
     public void testWithFrontSuccessNoBackUriWithRevoke() throws URISyntaxException {
-        runLogoutPrePropagation("mockClientIdLogoutFrontSuccessWithRevoke", new URI("https://example.org/front"), null);
+        runLogoutPrePropagation("mockClientIdLogoutFrontSuccessWithRevoke", new URI("https://mock.localhost:12345/back/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");
+        assertPropagationResult(LogoutPropagationContext.Result.Success);
     }
 
     @Test
     public void testWithoBackUriNoFront() throws URISyntaxException {
-        runLogoutPrePropagation(clientId, null, new URI("https://example.org/back"));
+        runLogoutPrePropagation(clientId, null, new URI("https://mock.localhost:12345/back/back"));
         final FlowExecutionResult result = flowExecutor.launchExecution("PropagateLogout", null, externalContext);
         Assert.assertTrue(result.isEnded());
         Assert.assertEquals(result.getOutcome().getId(), "RenderResult");
+        final ProfileRequestContext prc = retrievePrcViaRequest();
+        final OIDCBackChannelLogoutPropagationContext backChannelCtx =
+                prc.getOutboundMessageContext().getSubcontext(OIDCLogoutPropagationContext.class)
+                    .getSubcontext(OIDCBackChannelLogoutPropagationContext.class);
+        Assert.assertNotNull(backChannelCtx.getLogoutTokenClaimsSet());
+        final JWT logoutToken =
+                prc.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class)
+                    .getProcessedToken();
+        Assert.assertNotNull(logoutToken);
+        Assert.assertTrue(logoutToken instanceof SignedJWT);
+    }
+
+    @Factory
+    public Object[] createLogoutTokenSecurityTests() {
+        return new Object[] {
+                new IssuedSignedJWTTest(AbstractIssuedJWTSecurityTest.JWT_FETCHING_TYPE.LOGOUT_TOKEN, FLOW_ID,
+                        "LogoutPropagateView"),
+                new IssuedEncryptedJWTTest(AbstractIssuedJWTSecurityTest.JWT_FETCHING_TYPE.LOGOUT_TOKEN, FLOW_ID,
+                        "LogoutPropagateView", true, false),
+                new IssuedEncryptedJWTTest(AbstractIssuedJWTSecurityTest.JWT_FETCHING_TYPE.LOGOUT_TOKEN, FLOW_ID,
+                        "LogoutPropagateView", true, true)
+        };
+    }
+
+    protected ProfileRequestContext retrievePrcViaRequest() {
+        final ProfileRequestContext prc = (ProfileRequestContext)request.getAttribute(ProfileRequestContext.BINDING_KEY);
+        Assert.assertNotNull(prc);
+        return prc;
+    }
+
+    protected void assertPropagationResult(final LogoutPropagationContext.Result expected) {
+        final ProfileRequestContext prc = retrievePrcViaRequest();
+        final LogoutPropagationContext propCtx = prc.getSubcontext(LogoutPropagationContext.class);
+        Assert.assertNotNull(propCtx);
+        Assert.assertEquals(propCtx.getResult(), expected);
     }
 
     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());
+       return buildIdPSessionWithDefaultSP(sessionManager, clientId, issuer, sessionId, subject);
     }
 
+    protected static IdPSession buildIdPSessionWithDefaultSP(final StorageBackedSessionManager sessionManager,
+            final String clientId, final String issuer, final String sessionId, final String subject) {
+        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 {
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 4fa141b8..2992cdc2 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
@@ -91,6 +91,8 @@
                      <bean parent="OIDC.SSO.MDDriven" p:encryptionOptional="false" />
                      <bean parent="OAUTH2.Token.MDDriven" p:encryptionOptional="false" />
                      <bean parent="OIDC.UserInfo.MDDriven" p:encryptionOptional="false" />
+                     <bean parent="OIDC.Logout" p:encryptionOptional="false"/>
+                     <ref bean="OAUTH2.Revocation" />
                  </list>
             </property>
         </bean>

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


More information about the commits mailing list