[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