[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