[java-idp-oidc] 02/02: JOIDC-13 - Support for OIDC Logout
Henri Mikkonen
henri.mikkonen at iki.fi
Tue Oct 10 15:44: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=b1141776c481b2d473c41cb9390a71fb9f5b24f8
commit b1141776c481b2d473c41cb9390a71fb9f5b24f8
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue Oct 10 18:43:40 2023 +0300
JOIDC-13 - Support for OIDC Logout
https://shibboleth.atlassian.net/browse/JOIDC-13
Adds a new 'logoutprop/oidc' logout propagation flow
- exploits the OIDC.Logout profile configuration (class from oidc-commons)
- front- and back-channel propagation supported
- gets the chain revocation lifetime from OAUTH2.Revocation (revocationLifetime)
Missing unit / flow tests.
Big thanks to Steffen Hoffman / DAASI for implementing the OIDC back-channel logout
plugin for OIDC OP. It was used as a basis for this implementation.
---
.../OIDCBackChannelLogoutPropagationContext.java | 55 +++++
.../context/OIDCLogoutPropagationContext.java | 93 ++++++++
...WTClaimsSetFromLogoutContextLookupFunction.java | 84 +++++++
.../OIDCRPSessionClientIDLookupFunction.java | 67 ++++++
.../idp/plugin/oidc/op/session/OIDCRPSession.java | 258 +++++++++++++++++++++
.../idp/plugin/oidc/op/session/package-info.java | 18 ++
idp-oidc-extension-impl/pom.xml | 6 +
...ractOIDCBackChannelLogoutPropagationAction.java | 65 ++++++
.../impl/AbstractOIDCLogoutPropagationAction.java | 65 ++++++
.../impl/ExecuteBackChannelLogoutRequest.java | 253 ++++++++++++++++++++
.../impl/FormOutboundLogoutRequestMessage.java | 153 ++++++++++++
.../logout/profile/impl/PopulateLogoutContext.java | 125 ++++++++++
.../impl/PrepareBackChannelLogoutRequest.java | 116 +++++++++
.../op/logout/profile/impl/RevokeTokenChain.java | 156 +++++++++++++
.../profile/impl/SetFrontChannelLogoutSuccess.java | 94 ++++++++
.../FrontChannelLogoutPropagationResponse.java | 136 +++++++++++
.../impl/OIDCRPSessionCreationStrategy.java | 145 ++++++++++++
.../op/session/impl/OIDCRPSessionSerializer.java | 78 +++++++
.../plugin/oidc/op/session/impl/package-info.java | 18 ++
.../META-INF/net.shibboleth.idp/postconfig.xml | 15 ++
.../oidc/oidc-logout-propagation-beans.xml | 148 ++++++++++++
.../oidc/oidc-logout-propagation-flow.xml | 119 ++++++++++
.../idp/service/relying-party/postconfig.xml | 14 ++
.../idp/views/logout/oidc-front-iframe.vm | 8 +
24 files changed, 2289 insertions(+)
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCBackChannelLogoutPropagationContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCBackChannelLogoutPropagationContext.java
new file mode 100644
index 00000000..c68db0e5
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCBackChannelLogoutPropagationContext.java
@@ -0,0 +1,55 @@
+/*
+ * 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.messaging.context;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.openid.connect.sdk.claims.LogoutTokenClaimsSet;
+
+/**
+ * Subcontext carrying information about OIDC back-channel logout.
+ *
+ * @since 4.1.0
+ */
+public final class OIDCBackChannelLogoutPropagationContext extends BaseContext {
+
+ /** The claims set related to the back-channel logout. */
+ @Nullable LogoutTokenClaimsSet logoutTokenClaimsSet;
+
+ /**
+ * Constructor.
+ */
+ public OIDCBackChannelLogoutPropagationContext() {
+ super();
+ }
+
+ /**
+ * Get the claims set related to the back-channel logout.
+ * @return The claims set related to the back-channel logout.
+ */
+ @Nullable public LogoutTokenClaimsSet getLogoutTokenClaimsSet() {
+ return logoutTokenClaimsSet;
+ }
+
+ /**
+ * Set the claims set related to the back-channel logout.
+ * @param claimsSet What to set.
+ */
+ public void setLogoutTokenClaimsSet(@Nullable final LogoutTokenClaimsSet claimsSet) {
+ logoutTokenClaimsSet = claimsSet;
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCLogoutPropagationContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCLogoutPropagationContext.java
new file mode 100644
index 00000000..72a03e74
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCLogoutPropagationContext.java
@@ -0,0 +1,93 @@
+/*
+ * 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.messaging.context;
+
+import java.net.URI;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+/**
+ * Subcontext carrying information about OIDC logout propagation (both front- and back-channel).
+ *
+ * @since 4.1.0
+ */
+public final class OIDCLogoutPropagationContext extends BaseContext {
+
+ /** A flag indicating if an error has occured during revocation of tokens related to the session. */
+ private boolean revocationError;
+
+ /** The location for the OIDC front-channel logout endpoint. */
+ @Nullable private URI frontChannelLogoutUri;
+
+ /** The location for the OIDC back-channel logout endpoint. */
+ @Nullable private URI backChannelLogoutUri;
+
+ /**
+ * Constructor.
+ */
+ public OIDCLogoutPropagationContext() {
+ revocationError = false;
+ }
+
+ /**
+ * Get the flag indicating if an error has occured during revocation of tokens related to the session.
+ * @return True if error has occured, false otherwise.
+ */
+ public boolean hasRevocationError() {
+ return revocationError;
+ }
+
+ /**
+ * Set the flag indicating if an error has occured during revocation of tokens related to the session.
+ * @param flag What to set
+ */
+ public void setRevocationError(final boolean flag) {
+ revocationError = flag;
+ }
+
+ /**
+ * Get the location for the OIDC front-channel logout endpoint.
+ * @return The location for the OIDC front-channel logout endpoint.
+ */
+ public URI getFrontChannelLogoutUri() {
+ return frontChannelLogoutUri;
+ }
+
+ /**
+ * Set the location for the OIDC front-channel logout endpoint.
+ * @param uri What to set
+ */
+ public void setFrontChannelLogoutUri(final URI uri) {
+ frontChannelLogoutUri = uri;
+ }
+
+ /**
+ * Get the location for the OIDC back-channel logout endpoint.
+ * @return The location for the OIDC back-channel logout endpoint.
+ */
+ public URI getBackChannelLogoutUri() {
+ return backChannelLogoutUri;
+ }
+
+ /**
+ * Set the location for the OIDC back-channel logout endpoint.
+ * @param uri What to set
+ */
+ public void setBackChannelLogoutUri(final URI uri) {
+ backChannelLogoutUri = uri;
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/logic/JWTClaimsSetFromLogoutContextLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/logic/JWTClaimsSetFromLogoutContextLookupFunction.java
new file mode 100644
index 00000000..461f28d2
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/logic/JWTClaimsSetFromLogoutContextLookupFunction.java
@@ -0,0 +1,84 @@
+/*
+ * 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.messaging.context.logic;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.oauth2.sdk.ParseException;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCBackChannelLogoutPropagationContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Extract the {@link JWTClaimsSet} from the JWT in {@link OIDCBackChannelLogoutPropagationContext}.
+ *
+ * @since 4.1.0
+ */
+public class JWTClaimsSetFromLogoutContextLookupFunction implements Function<MessageContext, JWTClaimsSet> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(JWTClaimsSetFromLogoutContextLookupFunction.class);
+
+ /** Strategy used to locate the subcontext with the token. */
+ @Nonnull private Function<MessageContext,OIDCBackChannelLogoutPropagationContext> logoutContextLookupStrategy;
+
+ /**
+ * Constructor.
+ */
+ public JWTClaimsSetFromLogoutContextLookupFunction() {
+ // message context -> OIDC response context -> ATC
+ logoutContextLookupStrategy = new ChildContextLookup<>(OIDCBackChannelLogoutPropagationContext.class).compose(
+ new ChildContextLookup<>(OIDCLogoutPropagationContext.class));
+ }
+
+ /**
+ * Set the strategy used to lookup the {@link OIDCBackChannelLogoutPropagationContext} to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setLogoutContextLookupStrategy(
+ @Nonnull final Function<MessageContext,OIDCBackChannelLogoutPropagationContext> strategy) {
+ logoutContextLookupStrategy =
+ Constraint.isNotNull(strategy, "OIDCBackChannelLogoutContext lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable public JWTClaimsSet apply(@Nullable final MessageContext messageContext) {
+ if (messageContext == null) {
+ return null;
+ }
+ final OIDCBackChannelLogoutPropagationContext tokenCtx = logoutContextLookupStrategy.apply(messageContext);
+ if (tokenCtx == null || tokenCtx.getLogoutTokenClaimsSet() == null) {
+ return null;
+ }
+ try {
+ return tokenCtx.getLogoutTokenClaimsSet().toJWTClaimsSet();
+ } catch (final ParseException e) {
+ log.error("Could not fetch the claims set from the logout context token claims set", e);
+ }
+ return null;
+ }
+}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCRPSessionClientIDLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCRPSessionClientIDLookupFunction.java
new file mode 100644
index 00000000..33299b2a
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCRPSessionClientIDLookupFunction.java
@@ -0,0 +1,67 @@
+/*
+ * 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.context.navigate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+
+/**
+ * A lookup function that fetches {@link ClientID} value from the {@link OIDCRPSession} found from the session stored in
+ * the {@link LogoutPropagationContext}.
+ *
+ * @since 4.1.0
+ */
+public class OIDCRPSessionClientIDLookupFunction implements ContextDataLookupFunction<MessageContext, ClientID> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(OIDCRPSessionClientIDLookupFunction.class);
+
+ @Override @Nullable
+ public ClientID apply(@Nullable final MessageContext messageContext) {
+ if (messageContext == null) {
+ log.warn("MessageContext cannot be null, return null for ClientID.");
+ return null;
+ }
+ if (messageContext.getParent() == null) {
+ log.warn("ProfileRequestContext cannot be null, return null for ClientID.");
+ return null;
+ }
+ final LogoutPropagationContext logoutPropagationContext =
+ messageContext.getParent().getSubcontext(LogoutPropagationContext.class);
+ if (logoutPropagationContext == null) {
+ log.warn("LogoutPropagationContext cannot be null, return null for ClientID.");
+ return null;
+ }
+ if (logoutPropagationContext.getSession() instanceof OIDCRPSession rpSession) {
+ final ClientID clientID = new ClientID(((OIDCRPSession) rpSession).getId());
+ log.debug("Return ClientID '{}'.", clientID);
+ return clientID;
+ }
+ log.warn("SPSession is null or not of type OIDCRPSession {}, return null for ClientID.",
+ logoutPropagationContext.getSession());
+ return null;
+ }
+
+}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/OIDCRPSession.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/OIDCRPSession.java
new file mode 100644
index 00000000..63b81f80
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/OIDCRPSession.java
@@ -0,0 +1,258 @@
+/*
+ * 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.session;
+
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.idp.session.BasicSPSession;
+import net.shibboleth.idp.session.SPSession;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A concrete {@link SPSession} implementation for OIDC relying parties.
+ *
+ * @since 4.1.0
+ */
+ at ThreadSafe
+public class OIDCRPSession extends BasicSPSession implements SPSession {
+
+ /** The issuer value to interact with the service. */
+ @Nonnull @NotEmpty private final String issuer;
+
+ /** The root token identifier. */
+ @Nonnull @NotEmpty private final String rootTokenIdentifier;
+
+ /** The session identifier. */
+ @Nonnull @NotEmpty private final String sessionIdentifier;
+
+ /** The subject value. */
+ @Nonnull @NotEmpty private final String subject;
+
+ /** The flag whether the session allows for logout propagation. */
+ private final boolean supportsLogoutPropagation;
+
+ /**
+ * Constructor.
+ *
+ * @param id the identifier of the service associated with this session
+ * @param creation creation time of session
+ * @param expiration expiration time of session
+ * @param iss issuer value to interact with the service
+ * @param rootJti root token identifier
+ * @param sid session identifier
+ * @param sub subject value
+ * @param supportsPropagation flag whether the session allows for logout propagation
+ */
+ private OIDCRPSession(@Nonnull @NotEmpty final String id, @Nonnull final Instant creation,
+ @Nonnull final Instant expiration, @Nonnull @NotEmpty final String iss,
+ @Nonnull @NotEmpty final String rootJti, @Nonnull @NotEmpty final String sid,
+ @Nonnull @NotEmpty final String sub, final boolean supportsPropagation) {
+ super(id, creation, expiration);
+ issuer = Constraint.isNotEmpty(iss, "The issuer value cannot be empty");
+ rootTokenIdentifier = Constraint.isNotEmpty(rootJti, "The root token identifier cannot be empty");
+ sessionIdentifier = Constraint.isNotEmpty(sid, "The session identifier cannot be empty");
+ subject = Constraint.isNotEmpty(sub, "The subject value cannot be empty");
+ supportsLogoutPropagation = supportsPropagation;
+ }
+
+ @Nonnull @NotEmpty
+ @Override
+ public String getProtocol() {
+ return "http://openid.net/specs/openid-connect-core-1_0.html";
+ }
+
+ /**
+ * Get the issuer value to interact with the service.
+ *
+ * @return the issuer value to interact with the service
+ */
+ public String getIssuer() {
+ return issuer;
+ }
+
+ /**
+ * Get the root token identifier.
+ *
+ * @return the root token identifier
+ */
+ public String getRootTokenIdentifier() {
+ return rootTokenIdentifier;
+ }
+
+ /**
+ * Get the session identifier.
+ *
+ * @return the session identifier
+ */
+ public String getSessionIdentifier() {
+ return sessionIdentifier;
+ }
+
+ /**
+ * Get the subject value.
+ *
+ * @return The subject value
+ */
+ public String getSubject() {
+ return subject;
+ }
+
+ @Override
+ public boolean supportsLogoutPropagation() {
+ return supportsLogoutPropagation;
+ }
+
+ /**
+ * Builder class for building {@link OIDCRPSession}.
+ */
+ public static class Builder {
+
+ /** The unique identifier of the service. */
+ @Nullable private String serviceId;
+
+ /** The time when this session was created. */
+ @Nullable private Instant creationInstant;
+
+ /** The time when this session expires. */
+ @Nullable private Instant expirationInstant;
+
+ /** The issuer value to interact with the service. */
+ @Nullable private String issuer;
+
+ /** The root token identifier. */
+ @Nullable private String rootTokenIdentifier;
+
+ /** The session identifier. */
+ @Nullable private String sessionIdentifier;
+
+ /** The subject value. */
+ @Nullable private String subject;
+
+ /** The flag whether the session allows for logout propagation. */
+ private boolean supportsLogoutPropagation;
+
+ /**
+ * Constructor.
+ */
+ public Builder() {
+ // no op
+ }
+
+ /**
+ * Set the unique identifier of the service.
+ *
+ * @param id unique identifier of the service
+ * @return this builder
+ */
+ public Builder serviceId(@Nonnull final String id) {
+ serviceId = id;
+ return this;
+ }
+
+ /**
+ * Set the time when this session was created.
+ *
+ * @param creation time when this session was created
+ * @return this builder
+ */
+ public Builder creationInstant(@Nonnull final Instant creation) {
+ creationInstant = creation;
+ return this;
+ }
+
+ /**
+ * Set the time when this session expires.
+ *
+ * @param expiration time when this session expires
+ * @return this builder
+ */
+ public Builder expirationInstant(@Nonnull final Instant expiration) {
+ expirationInstant = expiration;
+ return this;
+ }
+
+ /**
+ * Set the issuer value to interact with the service.
+ *
+ * @param iss issuer value to interact with the service
+ * @return this builder
+ */
+ public Builder issuer(@Nonnull final String iss) {
+ issuer = iss;
+ return this;
+ }
+
+ /**
+ * Set the root token identifier.
+ *
+ * @param rootJti root token identifier
+ * @return this builder
+ */
+ public Builder rootTokenIdentifier(@Nonnull final String rootJti) {
+ rootTokenIdentifier = rootJti;
+ return this;
+ }
+
+ /**
+ * Set the session identifier.
+ *
+ * @param sid session identifier
+ * @return this builder
+ */
+ public Builder sessionIdentifier(@Nonnull final String sid) {
+ sessionIdentifier = sid;
+ return this;
+ }
+
+ /**
+ * Set the subject value.
+ *
+ * @param sub subject value
+ * @return this builder
+ */
+ public Builder subject(@Nonnull final String sub) {
+ subject = sub;
+ return this;
+ }
+
+ /**
+ * Set the flag whether the session allows for logout propagation.
+ *
+ * @param supportsPropagation flag whether the session allows for logout propagation
+ * @return this builder
+ */
+ public Builder supportLogoutPropagation(final boolean supportsPropagation) {
+ supportsLogoutPropagation = supportsPropagation;
+ return this;
+ }
+
+ /**
+ * Build the {@link OIDCRPSession}.
+ *
+ * @return the newly built object
+ */
+ public OIDCRPSession build() {
+ return new OIDCRPSession(serviceId, creationInstant, expirationInstant, issuer, rootTokenIdentifier,
+ sessionIdentifier, subject, supportsLogoutPropagation);
+ }
+
+ }
+
+}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/package-info.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/package-info.java
new file mode 100644
index 00000000..ba254d24
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * Extensions related to the construction and management of sessions.
+ */
+package net.shibboleth.idp.plugin.oidc.op.session;
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/pom.xml b/idp-oidc-extension-impl/pom.xml
index f5acfa46..e3703488 100644
--- a/idp-oidc-extension-impl/pom.xml
+++ b/idp-oidc-extension-impl/pom.xml
@@ -316,6 +316,12 @@
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>jakarta.json</groupId>
+ <artifactId>jakarta.json-api</artifactId>
+ <scope>provided</scope>
</dependency>
<!-- Test Dependencies -->
<dependency>
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCBackChannelLogoutPropagationAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCBackChannelLogoutPropagationAction.java
new file mode 100644
index 00000000..e698c6f2
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCBackChannelLogoutPropagationAction.java
@@ -0,0 +1,65 @@
+/*
+ * 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.logout.profile.impl;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCBackChannelLogoutPropagationContext;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * An abstract action for OIDC back-channel logout actions dealing with {@link OIDCBackChannelLogoutPropagationContext}.
+ */
+public class AbstractOIDCBackChannelLogoutPropagationAction extends AbstractOIDCLogoutPropagationAction {
+
+ /** Class logger. */
+ @Nonnull private static final Logger log =
+ LoggerFactory.getLogger(AbstractOIDCBackChannelLogoutPropagationAction.class);
+
+ /** OIDC back-channel logout context. */
+ @Nullable private OIDCBackChannelLogoutPropagationContext oidcBackChannelLogoutContext;
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ oidcBackChannelLogoutContext =
+ getOidcLogoutContext().getSubcontext(OIDCBackChannelLogoutPropagationContext.class);
+ if (oidcBackChannelLogoutContext == null) {
+ log.error("{} No OIDC back-channel logout context found", this.getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Get the OIDC back-channel logout context.
+ * @return The OIDC back-channel logout context.
+ */
+ @Nullable public OIDCBackChannelLogoutPropagationContext getOidcBackChannelLogoutContext() {
+ return oidcBackChannelLogoutContext;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCLogoutPropagationAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCLogoutPropagationAction.java
new file mode 100644
index 00000000..e36d8e26
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCLogoutPropagationAction.java
@@ -0,0 +1,65 @@
+/*
+ * 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.logout.profile.impl;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * An abstract action for OIDC logout actions dealing with {@link OIDCLogoutPropagationContext}.
+ */
+public class AbstractOIDCLogoutPropagationAction extends AbstractOIDCResponseAction {
+
+ /** Class logger. */
+ @Nonnull private static final Logger log = LoggerFactory.getLogger(AbstractOIDCLogoutPropagationAction.class);
+
+ /** OIDC logout propagation context. */
+ @Nullable private OIDCLogoutPropagationContext oidcLogoutContext;
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ oidcLogoutContext =
+ profileRequestContext.getOutboundMessageContext().getSubcontext(OIDCLogoutPropagationContext.class);
+ if (oidcLogoutContext == null) {
+ log.error("{} No OIDC logout context found", this.getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Get the OIDC logout propagation context.
+ * @return The OIDC logout propagation context.
+ */
+ @Nullable public OIDCLogoutPropagationContext getOidcLogoutContext() {
+ return oidcLogoutContext;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ExecuteBackChannelLogoutRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ExecuteBackChannelLogoutRequest.java
new file mode 100644
index 00000000..20d39129
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ExecuteBackChannelLogoutRequest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.logout.profile.impl;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * An action that executes the OIDC back-channel logout request and sets the result via
+ * {@link LogoutPropagationContext#setResult(net.shibboleth.idp.session.context.LogoutPropagationContext.Result).
+ */
+public class ExecuteBackChannelLogoutRequest extends AbstractOIDCBackChannelLogoutPropagationAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExecuteBackChannelLogoutRequest.class);
+
+ /** HTTP Client used to post the data. */
+ @NonnullAfterInit private HttpClient httpClient;
+
+ /** HTTP client security parameters. */
+ @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+
+ /** The lookup strategy for the logout propagation context. */
+ @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
+
+ /** The lookup strategy for back-channel logout endpoint. */
+ @Nonnull private final Function<OIDCClientInformation, String> oidcBackChannelLogoutURILookupFunction;
+
+ /** The logout propagation context to operate on. */
+ @Nullable private LogoutPropagationContext logoutPropagationContext;
+
+ /** The OIDC RP session resolved from the logout propagation context. */
+ @Nullable private OIDCRPSession oidcRPSession;
+
+ /** The back-channel logout URI for which to send the back-channel logout request. */
+ @Nullable private URI backChannelLogoutURI;
+
+ /** The back-channel logout token. */
+ @Nullable private JWT logoutToken;
+
+ /**
+ * Constructor.
+ */
+ public ExecuteBackChannelLogoutRequest() {
+ oidcBackChannelLogoutURILookupFunction =
+ new ClientInformationStringValueLookupFunction("backchannel_logout_uri");
+ logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
+ }
+
+ /**
+ * Set the {@link HttpClient} to use.
+ * @param client What to set
+ */
+ public void setHttpClient(@Nonnull final HttpClient client) {
+ checkSetterPreconditions();
+ httpClient = Constraint.isNotNull(client, "HttpClient cannot be null");
+ }
+
+ /**
+ * Set the optional client security parameters.
+ * @param params What to set
+ */
+ public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+ checkSetterPreconditions();
+ httpClientSecurityParameters = params;
+ }
+
+ /**
+ * Set the lookup strategy for logout propagation context.
+ * @param strategy What to set
+ */
+ public void setLogoutPropagationContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
+ checkSetterPreconditions();
+ logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
+ "LogoutPropagationContext lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ Constraint.isNotNull(httpClient, "Httpclient cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ logoutPropagationContext = logoutPropagationContextLookupStrategy.apply(profileRequestContext);
+ if (logoutPropagationContext == null) {
+ log.error("{} No bclogout propagation context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ } else if (!(logoutPropagationContext.getSession() instanceof OIDCRPSession)) {
+ log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ oidcRPSession = (OIDCRPSession) logoutPropagationContext.getSession();
+ if (oidcRPSession == null) {
+ log.error("{} No OIDCRPSession available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ if (getMetadataContext() == null) {
+ log.error("{} No OIDC metadata context available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ final String backChannelLogoutURIValue =
+ oidcBackChannelLogoutURILookupFunction.apply(getMetadataContext().getClientInformation());
+ if (backChannelLogoutURIValue == null) {
+ log.error("{} Back-channel logout URI cannot be determined.", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ try {
+ backChannelLogoutURI = new URI(backChannelLogoutURIValue);
+ } catch (URISyntaxException e) {
+ log.error("{} Could not build an URI of the back-channel logout URI value {}", getLogPrefix(),
+ backChannelLogoutURIValue);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ logoutToken = getOidcResponseContext().getProcessedToken();
+ if (logoutToken == null) {
+ log.error("{} OIDCAuthenticationResponseContext did not contain any back-channel logout token.",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final String relyingPartyId = oidcRPSession.getId();
+ final String subject = oidcRPSession.getSubject();
+ final String sessionId = oidcRPSession.getSessionIdentifier();
+
+ final HttpPost httpRequest = new HttpPost(backChannelLogoutURI);
+
+ final List<NameValuePair> nvps = new ArrayList<>();
+ nvps.add(new BasicNameValuePair("logout_token", logoutToken.serialize()));
+ final UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(nvps, StandardCharsets.UTF_8);
+ httpRequest.setEntity(urlEncodedFormEntity);
+
+ httpRequest.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_URLENCODED.getType());
+
+ final HttpClientContext httpContext = buildHttpContext(httpRequest);
+ log.debug("{} Sending the back-channel logout request to {}", getLogPrefix(), backChannelLogoutURI);
+
+ try (final ClassicHttpResponse response = httpClient.executeOpen(null, httpRequest, httpContext)) {
+
+ HttpClientSecuritySupport.checkTLSCredentialEvaluated(httpContext, httpRequest.getUri().getScheme());
+
+ if (response.getCode() == HttpStatus.SC_OK || response.getCode() == HttpStatus.SC_NO_CONTENT) {
+ if (!getOidcLogoutContext().hasRevocationError()) {
+ logoutPropagationContext.setResult(LogoutPropagationContext.Result.Success);
+ log.debug("{} back-channel logout for rp '{}', sub '{}' and sid '{}' succeeded.", getLogPrefix(),
+ relyingPartyId, subject, sessionId);
+ } else {
+ logoutPropagationContext.setResult(LogoutPropagationContext.Result.Failure);
+ log.debug("{} back-channel logout for rp '{}', sub '{}' and sid '{}' succeeded, but overall " +
+ "result remains 'failure' due to previous error",
+ getLogPrefix(), relyingPartyId, subject, sessionId);
+ }
+ } else {
+ logoutPropagationContext.setResult(LogoutPropagationContext.Result.Failure);
+ log.error("{} back-channel logout for rp '{}', sub '{}' and sid '{}' failed. HTTP code: {}",
+ getLogPrefix(), relyingPartyId, subject, sessionId, response.getCode());
+ }
+ } catch (final IOException | URISyntaxException e) {
+ log.error("{} back-channel logout for rp '{}' with token '{}' failed.", getLogPrefix(), relyingPartyId,
+ logoutToken.serialize(), e);
+ logoutPropagationContext.setResult(LogoutPropagationContext.Result.Failure);
+ }
+ }
+
+ /**
+ * Build the {@link HttpClientContext} instance to be used by the HttpClient.
+ *
+ * @param request the HTTP client request
+ * @return the client context instance
+ */
+ @Nonnull
+ private HttpClientContext buildHttpContext(@Nonnull final HttpUriRequest request) {
+ final HttpClientContext clientContext = HttpClientContext.create();
+ HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, false);
+ HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, request);
+ return clientContext;
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormOutboundLogoutRequestMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormOutboundLogoutRequestMessage.java
new file mode 100644
index 00000000..3f1be412
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormOutboundLogoutRequestMessage.java
@@ -0,0 +1,153 @@
+/*
+ * 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.logout.profile.impl;
+
+import java.net.URI;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.impl.FrontChannelLogoutPropagationResponse;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Action that forms outbound message based on request and response context. Formed message is set to
+ * {@link ProfileRequestContext#getOutboundMessageContext()}.
+ */
+public class FormOutboundLogoutRequestMessage extends AbstractOIDCResponseAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(FormOutboundLogoutRequestMessage.class);
+
+ /** Strategy used to obtain the response issuer value. */
+ @Nonnull private Function<ProfileRequestContext, String> issuerLookupStrategy;
+
+ /** The lookup strategy for the logout propagation context. */
+ @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
+
+ /** Issuer value to included in the response message, if configured to be included. */
+ @Nullable private String issuer;
+
+ /** The logout propagation context to operate on. */
+ @Nullable private LogoutPropagationContext logoutPropagationContext;
+
+ /** The RP metadata where the front-channel logout related data is fetched. */
+ @Nullable private OIDCClientInformation clientInformation;
+
+ /** The front-channel logout endpoint. */
+ @Nullable private URI frontChannelLogoutUri;
+
+ /**
+ * Constructor.
+ */
+ public FormOutboundLogoutRequestMessage() {
+ issuerLookupStrategy = new IssuerLookupFunction();
+ logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
+ }
+
+ /**
+ * Set the strategy used to locate the issuer value to use.
+ *
+ * @param strategy What to set
+ */
+ public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ checkSetterPreconditions();
+ issuerLookupStrategy = Constraint.isNotNull(strategy, "IssuerLookupStrategy lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate logout propagation context to use.
+ *
+ * @param strategy What to set
+ */
+ public void setLogoutPropagationContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
+ checkSetterPreconditions();
+ logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
+ "LogoutPropagationContext lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ issuer = issuerLookupStrategy.apply(profileRequestContext);
+ if (StringSupport.trimOrNull(issuer) == null) {
+ log.error("{} Could not resolve value for issuer even though it's required", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+ return false;
+ }
+
+ logoutPropagationContext = logoutPropagationContextLookupStrategy.apply(profileRequestContext);
+ if (logoutPropagationContext == null) {
+ log.error("{} No bclogout propagation context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ } else if (!(logoutPropagationContext.getSession() instanceof OIDCRPSession)) {
+ log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ if (getMetadataContext() == null || getMetadataContext().getClientInformation() == null) {
+ log.error("{} Could not find OIDC metadata", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+ return false;
+ }
+
+ clientInformation = getMetadataContext().getClientInformation();
+ frontChannelLogoutUri = clientInformation.getOIDCMetadata().getFrontChannelLogoutURI();
+ if (frontChannelLogoutUri == null) {
+ log.error("{} No front-channel logout URI registered for the client", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final FrontChannelLogoutPropagationResponse response;
+ if (clientInformation.getOIDCMetadata().requiresFrontChannelLogoutSession()) {
+ response = new FrontChannelLogoutPropagationResponse(frontChannelLogoutUri.toString(), issuer,
+ ((OIDCRPSession) logoutPropagationContext.getSession()).getSessionIdentifier());
+ } else {
+ response = new FrontChannelLogoutPropagationResponse(frontChannelLogoutUri.toString());
+ }
+ profileRequestContext.getOutboundMessageContext().setMessage(response);
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PopulateLogoutContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PopulateLogoutContext.java
new file mode 100644
index 00000000..c2f72be7
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PopulateLogoutContext.java
@@ -0,0 +1,125 @@
+/*
+ * 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.logout.profile.impl;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractInitializeOutboundResponseMessageContext;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.shared.logic.Constraint;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * This action creates {@link OIDCLogoutPropagationContext} and populates it with the front- and back-channel URIs if
+ * found from the RP metadata.
+ */
+public class PopulateLogoutContext extends AbstractInitializeOutboundResponseMessageContext {
+
+ /** Class logger. */
+ @Nonnull private static final Logger log = LoggerFactory.getLogger(PopulateLogoutContext.class);
+
+ /** The strategy used for creating {@link OIDCLogoutPropagationContext}. */
+ @Nonnull private Function<ProfileRequestContext, OIDCLogoutPropagationContext> oidcLogoutContextCreationStrategy;
+
+ /** The strategy used for looking up {@link OIDCMetadataContext}. */
+ @Nonnull private Function<ProfileRequestContext, OIDCMetadataContext> oidcMetadataContextLookupStrategy;
+
+ /** The OIDC client information found from {@link OIDCMetadataContext}. */
+ @Nullable private OIDCClientInformation clientInformation;
+
+ /**
+ * Constructor.
+ */
+ public PopulateLogoutContext() {
+ oidcLogoutContextCreationStrategy = new ChildContextLookup<>(OIDCLogoutPropagationContext.class, true)
+ .compose(new OutboundMessageContextLookup());
+ oidcMetadataContextLookupStrategy = new ChildContextLookup<>(OIDCMetadataContext.class)
+ .compose(new InboundMessageContextLookup());
+ }
+
+ /**
+ * Set the strategy used for creating {@link OIDCLogoutPropagationContext}.
+ * @param strategy What to set.
+ */
+ public void setOidcLogoutContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext, OIDCLogoutPropagationContext> strategy) {
+ checkSetterPreconditions();
+ oidcLogoutContextCreationStrategy = Constraint.isNotNull(strategy,
+ "The OIDC logout context creation strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used for looking up {@link OIDCMetadataContext}.
+ * @param strategy What to set.
+ */
+ public void setOidcMetadataContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, OIDCMetadataContext> strategy) {
+ checkSetterPreconditions();
+ oidcMetadataContextLookupStrategy = Constraint.isNotNull(strategy,
+ "The OIDC metadata context lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+ final OIDCMetadataContext oidcMetadataContext = oidcMetadataContextLookupStrategy.apply(profileRequestContext);
+ if (oidcMetadataContext == null || oidcMetadataContext.getClientInformation() == null) {
+ log.error("{} Could not find OIDC client metadata", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+
+ clientInformation = oidcMetadataContext.getClientInformation();
+ return true;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ super.doExecute(profileRequestContext);
+ final OIDCLogoutPropagationContext logoutContext = oidcLogoutContextCreationStrategy.apply(profileRequestContext);
+ if (logoutContext == null) {
+ log.error("{} Unable to locate/create OIDCLogoutContext to populate", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return;
+ }
+ final OIDCClientMetadata metadata = clientInformation.getOIDCMetadata();
+ logoutContext.setBackChannelLogoutUri(metadata.getBackChannelLogoutURI());
+ logoutContext.setFrontChannelLogoutUri(metadata.getFrontChannelLogoutURI());
+ log.debug("{} Front-channel URI set to {} and back-channel URI set to {}", getLogPrefix(),
+ logoutContext.getFrontChannelLogoutUri(), logoutContext.getBackChannelLogoutUri());
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PrepareBackChannelLogoutRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PrepareBackChannelLogoutRequest.java
new file mode 100644
index 00000000..93f98e00
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PrepareBackChannelLogoutRequest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.logout.profile.impl;
+
+import com.nimbusds.oauth2.sdk.id.Audience;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.oauth2.sdk.id.JWTID;
+import com.nimbusds.oauth2.sdk.id.Subject;
+import com.nimbusds.openid.connect.sdk.claims.LogoutTokenClaimsSet;
+import com.nimbusds.openid.connect.sdk.claims.SessionID;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCBackChannelLogoutPropagationContext;
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.shared.logic.Constraint;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * An action that constructs {@link LogoutTokenClaimsSet} and attaches it to the
+ * {@link OIDCBackChannelLogoutPropagationContext}.
+ */
+public class PrepareBackChannelLogoutRequest extends AbstractOIDCBackChannelLogoutPropagationAction {
+
+ /** Class logger. */
+ @Nonnull private static final Logger log = LoggerFactory.getLogger(PrepareBackChannelLogoutRequest.class);
+
+ /** The lookup strategy for fetching logout propagation context containing the OIDC RP session. */
+ @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
+
+ /** The OIDC RP session containing the data for back-channel logout. */
+ @Nullable private OIDCRPSession oidcRPSession;
+
+ /**
+ * Constructor.
+ */
+ public PrepareBackChannelLogoutRequest() {
+ logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
+ }
+
+ /**
+ * Set the lookup strategy for fetching logout propagation context containing the OIDC RP session.
+ *
+ * @param strategy What to set
+ */
+ public void setLogoutPropagationContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
+ checkSetterPreconditions();
+ logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
+ "LogoutPropagationContext lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ final LogoutPropagationContext logoutPropagationContext =
+ logoutPropagationContextLookupStrategy.apply(profileRequestContext);
+ if (logoutPropagationContext == null) {
+ log.error("{} No bclogout propagation context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ } else if (!(logoutPropagationContext.getSession() instanceof OIDCRPSession)) {
+ log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ oidcRPSession = (OIDCRPSession) logoutPropagationContext.getSession();
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@NotNull ProfileRequestContext profileRequestContext) {
+ final Issuer iss = new Issuer(oidcRPSession.getIssuer());
+ final Subject sub = new Subject(oidcRPSession.getSubject());
+ final List<Audience> aud = Collections.singletonList(new Audience(oidcRPSession.getId()));
+ final Date iat = Calendar.getInstance().getTime();
+ final JWTID jti = new JWTID(oidcRPSession.getRootTokenIdentifier());
+ final SessionID sid = new SessionID(oidcRPSession.getSessionIdentifier());
+
+ final LogoutTokenClaimsSet logoutTokenClaimsSet = new LogoutTokenClaimsSet(iss, sub, aud, iat, jti, sid);
+ getOidcBackChannelLogoutContext().setLogoutTokenClaimsSet(logoutTokenClaimsSet);
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/RevokeTokenChain.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/RevokeTokenChain.java
new file mode 100644
index 00000000..0933bea3
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/RevokeTokenChain.java
@@ -0,0 +1,156 @@
+/*
+ * 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.logout.profile.impl;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultChainRevocationLifetimeLookupStrategy;
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.logic.Constraint;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.RevocationCache;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nonnull;
+import java.time.Duration;
+import java.util.function.Function;
+
+/**
+ * An action that revokes the token chain related to the logout propagation.
+ */
+public class RevokeTokenChain extends AbstractOIDCLogoutPropagationAction {
+
+ /** Ckass logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(RevokeTokenChain.class);
+
+ /** Token revocation cache instance to use. */
+ @NonnullAfterInit private RevocationCache revocationCache;
+
+ /** Lookup function to logout propagation context. */
+ @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
+
+ /** Lookup function to supply chain revocation lifetime. */
+ @Nonnull private Function<ProfileRequestContext,Duration> chainRevocationLifetimeLookupStrategy;
+
+ /** Revocation lifetime to use. */
+ @Nullable private Duration revocationLifetime;
+
+ /** Root token identifier to be revoked. */
+ @Nullable private String rootTokenIdentifier;
+
+ /**
+ * Constructor.
+ */
+ public RevokeTokenChain() {
+ chainRevocationLifetimeLookupStrategy = new DefaultChainRevocationLifetimeLookupStrategy();
+ logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
+ }
+
+ /**
+ * Set the revocation cache instance to use.
+ *
+ * @param cache What to set.
+ */
+ public void setRevocationCache(@Nonnull final RevocationCache cache) {
+ ifInitializedThrowUnmodifiabledComponentException();
+ revocationCache = Constraint.isNotNull(cache, "RevocationCache cannot be null");
+ }
+
+ /**
+ * Set a lookup strategy for the chain revocation lifetime.
+ *
+ * @param strategy What to set.
+ */
+ public void setChainRevocationLifetimeLookupStrategy(
+ @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+ chainRevocationLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the lookup strategy for logout propagation context.
+ *
+ * @param strategy What to set
+ */
+ public void setLogoutPropagationContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
+ checkSetterPreconditions();
+ logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
+ "LogoutPropagationContext lookup strategy cannot be null");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ revocationLifetime = chainRevocationLifetimeLookupStrategy.apply(profileRequestContext);
+ if (revocationLifetime == null || Duration.ZERO.equals(revocationLifetime)) {
+ log.error("{} Unable to obtain revocation lifetime to use", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+
+ final LogoutPropagationContext logoutPropagationContext =
+ logoutPropagationContextLookupStrategy.apply(profileRequestContext);
+ if (logoutPropagationContext == null) {
+ log.error("{} No bclogout propagation context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ if (!(logoutPropagationContext.getSession() instanceof OIDCRPSession)) {
+ log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ rootTokenIdentifier = ((OIDCRPSession) logoutPropagationContext.getSession()).getRootTokenIdentifier();
+ if (rootTokenIdentifier == null) {
+ log.error("{} OIDCRPSession context did not contain a root json web token identifier.", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void doExecute(@NotNull ProfileRequestContext profileRequestContext) {
+ if (revocationCache.revoke(
+ RevocationCacheContexts.AUTHORIZATION_CODE, rootTokenIdentifier, revocationLifetime)) {
+ getOidcLogoutContext().setRevocationError(false);
+ log.debug("{} Revoked all tokens based on ID '{}'", getLogPrefix(), rootTokenIdentifier);
+ } else {
+ getOidcLogoutContext().setRevocationError(true);
+ log.warn("{} Failed to revoke tokens based on ID '{}'", getLogPrefix(), rootTokenIdentifier);
+ }
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/SetFrontChannelLogoutSuccess.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/SetFrontChannelLogoutSuccess.java
new file mode 100644
index 00000000..f80a2d30
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/SetFrontChannelLogoutSuccess.java
@@ -0,0 +1,94 @@
+/*
+ * 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.logout.profile.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.idp.session.context.LogoutPropagationContext.Result;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Action that sets the {@link LogoutPropagationContext#setResult(Result)} as 'success'.
+ */
+public class SetFrontChannelLogoutSuccess extends AbstractOIDCResponseAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(SetFrontChannelLogoutSuccess.class);
+
+ /** The lookup strategy for the logout propagation context. */
+ @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
+
+ /** The logout propagation context to operate on. */
+ @Nullable private LogoutPropagationContext logoutPropagationContext;
+
+ /**
+ * Constructor.
+ */
+ public SetFrontChannelLogoutSuccess() {
+ logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
+ }
+
+ /**
+ * Set the lookup strategy for the logout propagation context.
+ * @param strategy What to set
+ */
+ public void setLogoutPropagationContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
+ checkSetterPreconditions();
+ logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
+ "LogoutPropagationContext lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ logoutPropagationContext = logoutPropagationContextLookupStrategy.apply(profileRequestContext);
+ if (logoutPropagationContext == null) {
+ log.error("{} No bclogout propagation context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ } else if (!(logoutPropagationContext.getSession() instanceof OIDCRPSession)) {
+ log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ log.debug("{} Set front-channel logout propagation result 'success'", getLogPrefix());
+ logoutPropagationContext.setResult(Result.Success);
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/impl/FrontChannelLogoutPropagationResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/impl/FrontChannelLogoutPropagationResponse.java
new file mode 100644
index 00000000..1e928135
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/impl/FrontChannelLogoutPropagationResponse.java
@@ -0,0 +1,136 @@
+/*
+ * 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.messaging.impl;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.Response;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * A Nimbus {@link Response} implementation representing a front-channel logout propagation message that is sent to the
+ * RP's front-channel logout URI endpoint.
+ */
+public class FrontChannelLogoutPropagationResponse implements Response {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(FrontChannelLogoutPropagationResponse.class);
+
+ /** The front-channel logout endpoint. */
+ @Nonnull final String frontChannelLogoutUri;
+
+ /** The optional issuer (iss-parameter) value. */
+ @Nullable final String issuer;
+
+ /** The optional sessionId (sid-parameter) value. */
+ @Nullable final String sessionId;
+
+ /**
+ * Constructor.
+ *
+ * @param uri The front-channel logout URI.
+ */
+ public FrontChannelLogoutPropagationResponse(@Nonnull final String uri) {
+ this(uri, null, null);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param uri The front-channel logout URI.
+ * @param iss The optional issuer -parameter value.
+ * @param sid The optional sessionId -parameter value.
+ */
+ public FrontChannelLogoutPropagationResponse(@Nonnull final String uri, @Nullable final String iss,
+ @Nullable final String sid) {
+ frontChannelLogoutUri = Constraint.isNotEmpty(uri, "Front-channel logout URI cannot be empty");
+ issuer = StringSupport.trimOrNull(iss);
+ sessionId = StringSupport.trimOrNull(sid);
+ if ((issuer == null && sessionId != null) || (issuer != null && sessionId == null)) {
+ throw new ConstraintViolationException("Issuer and session ID must be both present or absent");
+ }
+ }
+
+ @Override
+ public boolean indicatesSuccess() {
+ return true;
+ }
+
+ /**
+ * Returns an HTTP response for this logout propagation response by using
+ * HTTP 302 redirection.
+ *
+ * <p>Example HTTP response:</p>
+ *
+ * <pre>
+ * HTTP/1.1 302 Found
+ * Location: http://example.org/logout?iss=https://op.example.org&sid=1234567890ABCDEFG
+ * </pre>
+ *
+ * @see #toHTTPRequest()
+ *
+ * @return An HTTP response for this message.
+ */
+ @Override
+ public HTTPResponse toHTTPResponse() {
+
+ final HTTPResponse response = new HTTPResponse(HTTPResponse.SC_FOUND);
+ final URI uri;
+ try {
+ if (issuer != null && sessionId != null) {
+ if (frontChannelLogoutUri.contains("?")) {
+ uri = new URI(serializeParameters(frontChannelLogoutUri + "&"));
+ } else {
+ uri = new URI(serializeParameters(frontChannelLogoutUri + "?"));
+ }
+ } else {
+ uri = new URI(frontChannelLogoutUri);
+ }
+ response.setLocation(uri);
+ } catch (@Nonnull final URISyntaxException | UnsupportedEncodingException e) {
+ log.error("Could not construct an URI object", e);
+ }
+ return response;
+ }
+
+ /**
+ * URL-encode the iss and sid -parameters to the given prefix if parameter values have been set.
+
+ * @param prefix The prefix/URL for which to add the parameters.
+ * @return The given prefix possibly appended with parameter names and values
+ * @throws UnsupportedEncodingException If the parameter values could not be encoded with UTF-8
+ */
+ @Nonnull
+ protected String serializeParameters(@Nonnull final String prefix) throws UnsupportedEncodingException {
+ if (issuer != null && sessionId != null) {
+ return prefix + "iss=" + URLEncoder.encode(issuer, "UTF-8")
+ + "&sid=" + URLEncoder.encode(sessionId, "UTF-8");
+ }
+ return prefix;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/impl/OIDCRPSessionCreationStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/impl/OIDCRPSessionCreationStrategy.java
new file mode 100644
index 00000000..9dc90880
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/impl/OIDCRPSessionCreationStrategy.java
@@ -0,0 +1,145 @@
+/*
+ * 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.session.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+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.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.OIDCAuthenticationResponseContextLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.idp.session.SPSession;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * A function to create a {@link OIDCRPSession} based on profile execution state.
+ */
+public class OIDCRPSessionCreationStrategy implements Function<ProfileRequestContext, SPSession> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(OIDCRPSessionCreationStrategy.class);
+
+ /** Lifetime of sessions to create. */
+ @Nonnull private final Duration sessionLifetime;
+
+ /** Lookup strategy for OIDC metadata context. */
+ @Nonnull private final Function<ProfileRequestContext, OIDCMetadataContext> oidcMetadataContextLookupFunction;
+
+ /** Lookup strategy for OIDC authenticatin response context. */
+ @Nonnull
+ private final Function<ProfileRequestContext, OIDCAuthenticationResponseContext> oidcResponseContextLookupStrategy;
+
+ /**
+ * Constructor.
+ *
+ * @param lifetime determines upper bound for expiration of {@link OIDCRPSession} to be created
+ */
+ public OIDCRPSessionCreationStrategy(@Nonnull final Duration lifetime) {
+ sessionLifetime = Constraint.isNotNull(lifetime, "Lifetime cannot be null");
+ oidcMetadataContextLookupFunction = new DefaultOIDCMetadataContextLookupFunction();
+ oidcResponseContextLookupStrategy = new OIDCAuthenticationResponseContextLookupFunction();
+ }
+
+ /** {@inheritDoc} */
+ @Nullable
+ public SPSession apply(@Nullable final ProfileRequestContext profileRequestContext) {
+ final OIDCAuthenticationResponseContext oidcAuthenticationResponseContext =
+ oidcResponseContextLookupStrategy.apply(profileRequestContext);
+ if (oidcAuthenticationResponseContext == null) {
+ log.debug("OIDCAuthenticationResponseContext cannot be null, no OIDCRPSession created");
+ return null;
+ }
+
+ final TokenClaimsSet tokenClaimsSet = oidcAuthenticationResponseContext.getAuthorizationGrantClaimsSet();
+ if (tokenClaimsSet == null) {
+ log.debug("AuthorizationGrantClaimsSet cannot be null, no OIDCRPSession created.");
+ return null;
+ }
+
+ final ClientID clientIdentifier = tokenClaimsSet.getClientID();
+ if (clientIdentifier == null || StringSupport.trimOrNull(clientIdentifier.getValue()) == null) {
+ log.debug("ClientID/RelyingPartyId cannot be null, no OIDCRPSession created.");
+ return null;
+ }
+
+ final Instant now = Instant.now();
+ final String issuer = tokenClaimsSet.getIssuer();
+
+ final String rootTokenIdentifier = tokenClaimsSet.getRootTokenIdentifier() != null
+ ? tokenClaimsSet.getRootTokenIdentifier() : tokenClaimsSet.getID();
+ if (rootTokenIdentifier == null) {
+ log.debug("RootTokenIdentifier (root jti) cannot be null, no OIDCRPSession created.");
+ return null;
+ }
+
+ final String sessionIdentifier = tokenClaimsSet.getSessionIdentifier();
+ if (sessionIdentifier == null) {
+ log.debug("SessionIdentifier cannot be null, no OIDCRPSession created.");
+ return null;
+ }
+
+ final String subject = tokenClaimsSet.getSubject();
+ if (subject == null) {
+ log.debug("Subject cannot be null, no OIDCRPSession created.");
+ return null;
+ }
+
+ final boolean supportsLogoutPropagation = isSupportsLogoutPropagation(profileRequestContext);
+
+ return new OIDCRPSession.Builder()
+ .serviceId(clientIdentifier.getValue())
+ .creationInstant(now)
+ .expirationInstant(now.plus(sessionLifetime))
+ .issuer(issuer)
+ .rootTokenIdentifier(rootTokenIdentifier)
+ .sessionIdentifier(sessionIdentifier)
+ .subject(subject)
+ .supportLogoutPropagation(supportsLogoutPropagation)
+ .build();
+ }
+
+ /**
+ * Check if the {@link OIDCClientMetadata} attached to the given PRC contains a front- or back-channel logout URI.
+ *
+ * @param profileRequestContext The profile request context
+ * @return true iff the attached metadata contains a front- or back-channel logout URI
+ */
+ protected boolean isSupportsLogoutPropagation(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final OIDCMetadataContext oidcMetadataContext = oidcMetadataContextLookupFunction.apply(profileRequestContext);
+ if (oidcMetadataContext == null || oidcMetadataContext.getClientInformation() == null) {
+ log.debug("No metadata found, does not support propagation");
+ return false;
+ }
+ final OIDCClientMetadata metadata = oidcMetadataContext.getClientInformation().getOIDCMetadata();
+ final boolean result =
+ metadata.getBackChannelLogoutURI() != null || metadata.getFrontChannelLogoutURI() != null;
+ return result;
+ }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/impl/OIDCRPSessionSerializer.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/impl/OIDCRPSessionSerializer.java
new file mode 100644
index 00000000..ca62b077
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/impl/OIDCRPSessionSerializer.java
@@ -0,0 +1,78 @@
+/*
+ * 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.session.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import jakarta.json.JsonObject;
+import jakarta.json.stream.JsonGenerator;
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.idp.session.AbstractSPSessionSerializer;
+import net.shibboleth.idp.session.SPSession;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+
+/**
+ * A serializer for {@link OIDCRPSession} objects.
+ */
+ at ThreadSafeAfterInit
+public class OIDCRPSessionSerializer extends AbstractSPSessionSerializer {
+
+ /** Field name of logout propagation indicator. */
+ @Nonnull @NotEmpty private static final String LOGOUT_PROP_FIELD = "slop";
+
+ /**
+ * Constructor.
+ *
+ * @param offset time to subtract from record expiration to establish session expiration value
+ */
+ public OIDCRPSessionSerializer(@Nonnull @ParameterName(name = "offset") final Duration offset) {
+ super(offset);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doSerializeAdditional(@Nonnull final SPSession instance, @Nonnull final JsonGenerator generator) {
+ final OIDCRPSession oidcRpSession = (OIDCRPSession) instance;
+ generator.write(TokenClaimsSet.KEY_SESSION_ID, oidcRpSession.getSessionIdentifier());
+ generator.write(TokenClaimsSet.KEY_ISSUER, oidcRpSession.getIssuer());
+ generator.write(TokenClaimsSet.KEY_SUBJECT, oidcRpSession.getSubject());
+ generator.write(TokenClaimsSet.KEY_ROOT_JTI, oidcRpSession.getRootTokenIdentifier());
+ generator.write(LOGOUT_PROP_FIELD, oidcRpSession.supportsLogoutPropagation());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull
+ protected SPSession doDeserialize(@Nonnull final JsonObject obj, @Nonnull @NotEmpty final String id,
+ @Nonnull final Instant creation, @Nonnull final Instant expiration) {
+ return new OIDCRPSession.Builder()
+ .serviceId(id)
+ .creationInstant(creation)
+ .expirationInstant(expiration)
+ .issuer(obj.getString(TokenClaimsSet.KEY_ISSUER, null))
+ .rootTokenIdentifier(obj.getString(TokenClaimsSet.KEY_ROOT_JTI, null))
+ .sessionIdentifier(obj.getString(TokenClaimsSet.KEY_SESSION_ID, null))
+ .subject(obj.getString(TokenClaimsSet.KEY_SUBJECT, null))
+ .supportLogoutPropagation(obj.getBoolean(LOGOUT_PROP_FIELD, true))
+ .build();
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/impl/package-info.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/impl/package-info.java
new file mode 100644
index 00000000..50922a1f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/session/impl/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * Extensions related to the construction and management of sessions.
+ */
+package net.shibboleth.idp.plugin.oidc.op.session.impl;
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index c7eae656..b74e6067 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -741,4 +741,19 @@
<bean id="shibboleth.oidc.Conditions.MetadataValueEquals"
class="net.shibboleth.idp.plugin.oidc.op.profile.logic.MetadataValueEqualsCondition" abstract="true" />
+ <bean id="shibboleth.oidc.OIDCRPSessionCreationStrategy"
+ class="net.shibboleth.idp.plugin.oidc.op.session.impl.OIDCRPSessionCreationStrategy"
+ c:lifetime="%{idp.session.defaultSPlifetime:PT2H}"/>
+
+ <bean id="logoutprop/oidc" class="net.shibboleth.idp.session.LogoutPropagationFlowDescriptor"
+ c:_0="net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession"/>
+
+ <bean parent="shibboleth.SPSessionSerializer"
+ c:claz="net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession">
+ <constructor-arg name="object">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.session.impl.OIDCRPSessionSerializer"
+ c:offset="%{idp.session.slop:PT0S}"/>
+ </constructor-arg>
+ </bean>
+
</beans>
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
new file mode 100644
index 00000000..cc6163a0
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/logoutprop/oidc/oidc-logout-propagation-beans.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns:c="http://www.springframework.org/schema/c" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <bean id="shibboleth.ClientIDLookupStrategy"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.OIDCRPSessionClientIDLookupFunction"/>
+
+ <bean id="SelectProfileLogoutConfiguration"
+ class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration"
+ scope="prototype"
+ p:profileId="#{T(net.shibboleth.oidc.profile.config.OIDCLogoutProfileConfiguration).PROFILE_ID}"/>
+
+ <bean id="FrontChannelLogoutSuccess"
+ class="net.shibboleth.oidc.profile.config.logic.FrontChannelLogoutSuccessPredicate" />
+
+ <bean id="FrontChannelPreferred"
+ class="net.shibboleth.oidc.profile.config.logic.PreferFrontChannelLogoutPredicate" />
+
+ <bean id="PopulateLogoutContext"
+ class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.PopulateLogoutContext"
+ scope="prototype"/>
+
+ <bean id="PopulateBackChannelLogoutTokenSignatureSigningParameters"
+ class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters"
+ scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Inbound">
+ <property name="configurationLookupStrategy">
+ <bean lazy-init="true"
+ class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+ </property>
+ <property name="signatureSigningParametersResolver">
+ <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationSignatureSigningParametersResolver">
+ <constructor-arg name="signatureAlgorithmLookupStrategy">
+ <bean class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+ c:keyName="id_token_signed_response_alg" />
+ </constructor-arg>
+ <constructor-arg name="defaultAlgorithmValue" value="RS256" />
+ </bean>
+ </property>
+ </bean>
+
+ <bean id="PopulateBackChannelLogoutTokenEncryptionParameters"
+ class="net.shibboleth.oidc.profile.impl.PopulateJWTEncryptionParameters"
+ scope="prototype"
+ p:forFriendlyName="BC Logout Token">
+ <property name="encryptionOptionalPredicate">
+ <bean class="net.shibboleth.oidc.profile.config.logic.EncryptionOptionalPredicate" />
+ </property>
+ <property name="clientMetadataContextLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction"
+ p:inbound="true" />
+ </property>
+ <property name="configurationLookupStrategy">
+ <bean lazy-init="true"
+ class="net.shibboleth.oidc.profile.config.navigate.JWTEncryptionConfigurationLookupFunction" />
+ </property>
+ <property name="encryptionParametersResolver">
+ <bean class="net.shibboleth.oidc.security.jose.impl.DefaultEncryptionParametersResolver">
+ <property name="keyTransportEncryptionAlgorithmsLookupStrategy">
+ <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationKeyTransportEncryptionAlgorithmsLookupStrategy">
+ <constructor-arg>
+ <bean class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+ c:keyName="id_token_encrypted_response_alg"/>
+ </constructor-arg>
+ </bean>
+ </property>
+ <property name="dataEncryptionAlgorithmsLookupStrategy">
+ <bean class="net.shibboleth.oidc.security.jose.impl.ClientInformationDataEncryptionAlgorithmsLookupStrategy">
+ <constructor-arg>
+ <bean class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+ c:keyName="id_token_encrypted_response_enc"/>
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
+ </property>
+ </bean>
+
+ <bean id="PrepareBackChannelLogoutRequest"
+ class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.PrepareBackChannelLogoutRequest"
+ scope="prototype"/>
+
+ <bean id="SignBackChannelLogoutToken"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+ scope="prototype"
+ c:executionDirection="OUTBOUND ">
+ <constructor-arg name="messageHandler">
+ <bean id="SignBackChannelLogoutTokenHandler"
+ class="net.shibboleth.oidc.security.impl.SignJWTHandler"
+ scope="prototype"
+ p:logName="BC Logout Token"
+ p:typeHeader="logout+jwt">
+ <property name="claimsToSignLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.JWTClaimsSetFromLogoutContextLookupFunction" />
+ </property>
+ <property name="jwtUpdateConsumer">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ProcessedTokenUpdateStrategy" />
+ </property>
+ </bean>
+ </constructor-arg>
+ </bean>
+
+ <bean id="EncryptBackChannelLogoutToken"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+ scope="prototype"
+ c:executionDirection="OUTBOUND">
+ <constructor-arg name="messageHandler">
+ <bean id="EncryptBackChannelLogoutTokenHandler"
+ class="net.shibboleth.oidc.security.impl.EncryptJWTHandler"
+ scope="prototype"
+ p:logName="BC Logout Token">
+ <property name="payloadToEncryptLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.messaging.context.logic.PayloadFromProcessedTokenLookupFunction" />
+ </property>
+ <property name="jwtUpdateConsumer">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ProcessedTokenUpdateStrategy" />
+ </property>
+ </bean>
+ </constructor-arg>
+ </bean>
+
+ <bean id="RevokeTokenChain"
+ class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.RevokeTokenChain"
+ scope="prototype"
+ p:revocationCache-ref="shibboleth.oidc.RevocationCache">
+ <property name="chainRevocationLifetimeLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultChainRevocationLifetimeLookupStrategy"
+ p:clockSkew="%{idp.policy.clockSkew:PT5M}" p:useActiveProfileOnly="false" />
+ </property>
+ </bean>
+
+ <bean id="ExecuteBackChannelLogoutRequest"
+ class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.ExecuteBackChannelLogoutRequest"
+ scope="prototype"
+ p:httpClient="#{getObject('shibboleth.oidc.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
+ p:httpClientSecurityParameters="#{getObject('shibboleth.oidc.NonBrowser.HttpClientSecurityParameters')}"/>
+
+ <bean id="FormOutboundMessage"
+ class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.FormOutboundLogoutRequestMessage"
+ scope="prototype"/>
+
+ <bean id="SetFrontChannelLogoutSuccess"
+ class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.SetFrontChannelLogoutSuccess"
+ scope="prototype"/>
+
+</beans>
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
new file mode 100644
index 00000000..8d097360
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/logoutprop/oidc/oidc-logout-propagation-flow.xml
@@ -0,0 +1,119 @@
+<flow xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://www.springframework.org/schema/webflow"
+ xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+ parent="oidc/abstract, oidc/metadata-lookup">
+
+ <!-- Rudimentary impediment to direct execution of subflow. -->
+ <input name="calledAsSubflow" type="boolean" required="true" />
+
+ <action-state id="AddInboundMessageContext">
+ <on-entry>
+ <evaluate expression="opensamlProfileRequestContext.setInboundMessageContext(new org.opensaml.messaging.context.MessageContext())"/>
+ </on-entry>
+ <evaluate expression="'proceed'"/>
+ <transition on="proceed" to="DoMetadataLookup"/>
+ </action-state>
+
+ <action-state id="SelectConfiguration">
+ <evaluate expression="SelectRelyingPartyConfiguration"/>
+ <evaluate expression="SelectProfileLogoutConfiguration"/>
+ <evaluate expression="PopulateLogoutContext"/>
+ <evaluate expression="'proceed'"/>
+ <transition on="proceed" to="ChooseFrontChannelIfPreferredAndEnabled">
+ <set name="flowScope.frontChannelPreferred" value="FrontChannelPreferred.test(opensamlProfileRequestContext)" />
+ </transition>
+ </action-state>
+
+ <decision-state id="ChooseFrontChannelIfPreferredAndEnabled">
+ <if test="frontChannelPreferred and opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext('net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext').getFrontChannelLogoutUri() != null"
+ then="DoFrontChannelLogout"
+ else="ChooseBackChannelIfEnabled" />
+ </decision-state>
+
+ <decision-state id="ChooseBackChannelIfEnabled">
+ <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext('net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext').getBackChannelLogoutUri() != null"
+ then="DoBackChannelLogout" else="ChooseFrontChannelIfEnabled" />
+ </decision-state>
+
+ <decision-state id="ChooseFrontChannelIfEnabled">
+ <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext('net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext').getFrontChannelLogoutUri() != null"
+ then="DoFrontChannelLogout" else="HandleError" />
+ </decision-state>
+
+ <action-state id="DoFrontChannelLogout">
+ <evaluate expression="FormOutboundMessage" />
+ <evaluate expression="EncodeMessage" />
+ <evaluate expression="RevokeTokenChain"/>
+ <evaluate expression="PostResponsePopulateAuditContext" />
+ <evaluate expression="'proceed'"/>
+ <transition on="proceed" to="ChooseFrontChannelPropagationResult">
+ <set name="flowScope.frontChannelLogoutSuccess" value="FrontChannelLogoutSuccess.test(opensamlProfileRequestContext)" />
+ </transition>
+ </action-state>
+
+ <decision-state id="ChooseFrontChannelPropagationResult">
+ <if test="frontChannelLogoutSuccess"
+ then="CallFrontChannelLogoutUriView"
+ else="LogoutPending" />
+ </decision-state>
+
+ <view-state id="CallFrontChannelLogoutUriView" view="logout/oidc-front-iframe">
+ <attribute name="csrf_excluded" value="true" type="boolean"/>
+ <on-render>
+ <evaluate expression="environment" result="viewScope.environment" />
+ <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.session.context.LogoutContext))" result="viewScope.logoutContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.profile.context.MultiRelyingPartyContext))" result="viewScope.multiRPContext" />
+ <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+ <evaluate expression="opensamlProfileRequestContext.getOutboundMessageContext().getMessage().toHTTPResponse().getHeaderValue('Location')" result="viewScope.frontChannelLogoutLocation" />
+ <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null" result="viewScope.custom" />
+ </on-render>
+ <transition on="proceed" to="SetFrontChannelLogoutSuccess" />
+ </view-state>
+
+ <action-state id="SetFrontChannelLogoutSuccess">
+ <evaluate expression="SetFrontChannelLogoutSuccess" />
+ <evaluate expression="'proceed'"/>
+ <transition on="proceed" to="proceed"/>
+ </action-state>
+
+ <action-state id="DoBackChannelLogout">
+ <on-entry>
+ <evaluate expression="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext('net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext').ensureSubcontext('net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCBackChannelLogoutPropagationContext')"/>
+ </on-entry>
+ <evaluate expression="PopulateBackChannelLogoutTokenSignatureSigningParameters"/>
+ <evaluate expression="PopulateBackChannelLogoutTokenEncryptionParameters"/>
+ <evaluate expression="PrepareBackChannelLogoutRequest"/>
+ <evaluate expression="SignBackChannelLogoutToken"/>
+ <evaluate expression="EncryptBackChannelLogoutToken"/>
+ <evaluate expression="RevokeTokenChain"/>
+ <evaluate expression="ExecuteBackChannelLogoutRequest"/>
+ <evaluate expression="'proceed'"/>
+ <transition on="proceed" to="proceed"/>
+ </action-state>
+
+ <action-state id="HandleError">
+ <evaluate expression="'proceed'"/>
+ <transition on="proceed" to="proceed"/>
+ </action-state>
+
+ <!-- Successful terminal state (success meaning outbound LogoutRequest encoded). -->
+ <end-state id="LogoutPending">
+ <on-entry>
+ <evaluate expression="PostResponsePopulateAuditContext" />
+ <evaluate expression="WriteAuditLog" />
+ <evaluate expression="RecordResponseComplete" />
+ </on-entry>
+ </end-state>
+
+ <end-state id="proceed">
+ <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/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 6c954926..b0ec4c55 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -72,6 +72,11 @@
p:revocationLifetime="%{idp.oidc.revocationCache.authorizeCode.lifetime:PT6H}"
p:unregisteredClientPolicy="#{getObject('shibboleth.oidc.DefaultUnregisteredClientPolicy')}" />
+ <bean id="OIDC.Logout" parent="AbstractOIDCProfile" lazy-init="true"
+ class="net.shibboleth.oidc.profile.config.impl.DefaultOIDCLogoutConfiguration"
+ p:issuer-ref="shibboleth.oidc.issuer"/>
+ <!-- TODO: default values via configuration properties once the set is final -->
+
<!-- Metadata-driven variants. -->
<bean id="AbstractMDDrivenOIDCProfile" parent="AbstractMDDrivenProfile" abstract="true">
@@ -546,6 +551,15 @@
</property>
</bean>
+ <bean id="OIDC.Logout.MDDriven" parent="AbstractMDDrivenOIDCProfile" lazy-init="true"
+ class="net.shibboleth.oidc.profile.config.impl.DefaultOIDCLogoutConfiguration">
+ <property name="issuerLookupStrategy">
+ <bean parent="shibboleth.MDDrivenStringProperty" p:propertyName="issuer"
+ p:defaultValue-ref="shibboleth.oidc.issuer"/>
+ </property>
+ <!-- TODO: wire other properties with their default values via conf properties once the set is final -->
+ </bean>
+
<!-- Default client-auth JWT validation wiring. -->
<bean id="AdaptedRelyingPartyIdLookup" class="net.shibboleth.shared.logic.BiFunctionSupport"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/views/logout/oidc-front-iframe.vm b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/views/logout/oidc-front-iframe.vm
new file mode 100644
index 00000000..31af34b4
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/views/logout/oidc-front-iframe.vm
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <meta http-equiv = "refresh" content = "0; url = $flowExecutionUrl&_eventId_proceed=1" />
+ </head>
+ <body>
+ <iframe src="$frontChannelLogoutLocation" style="display:none"></iframe>
+ </body>
+</html>
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list