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

Henri Mikkonen henri.mikkonen at iki.fi
Mon Oct 16 15:29:21 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=44d406bfc6fdf0770c9b1fd550a86292732e4c8d

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

commit 44d406bfc6fdf0770c9b1fd550a86292732e4c8d
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Mon Oct 16 18:29:02 2023 +0300

    JOIDC-13 - Support for OIDC Logout
    
    https://shibboleth.atlassian.net/browse/JOIDC-13
    
    Initial (not complete) version of RP-initiated logout support
    - Wired new security configuration to OIDC.Logout profile
      - The default shibboleth.oidc.logout.DefaultSecurityConfiguration
        can be changed via idp.security.oidc.logout.config -property
      - The default logout security configuration has different configuration
        for signature validation (OP-only) and decryption (ClientInfo-only)
      - For signature validation, all keys found from
        shibboleth.oidc.SigningCredentialsToPublish are exploited
---
 .../context/OIDCRpInitiatedLogoutContext.java      | 146 ++++++++
 ...tValidPostLogoutRedirectUrisLookupFunction.java |  48 +++
 .../LogoutRequestClientIDLookupFunction.java       |  98 ++++++
 .../idp/plugin/oidc/op/session/OIDCRPSession.java  |  13 +-
 .../op/decoding/impl/OIDCLogoutRequestDecoder.java |  69 ++++
 .../impl/AbstractOIDCRpInitiatedLogoutAction.java  | 140 ++++++++
 .../impl/FormRpInitiatedLogoutResponse.java        |  76 +++++
 ...undRpInitiatedLogoutResponseMessageContext.java |  38 +++
 .../impl/PopulateRpInitiatedLogoutContext.java     |  94 ++++++
 .../impl/ProcessRpInitiatedLogoutRequest.java      | 375 +++++++++++++++++++++
 .../impl/ValidatePostLogoutRedirectURI.java        |  93 +++++
 .../messaging/impl/RpInitiatedLogoutResponse.java  | 129 +++++++
 ...ltPostLogoutRedirectURIValidationPredicate.java |  54 +++
 .../flows/oidc/end-session/end-session-beans.xml   | 276 +++++++++++++++
 .../flows/oidc/end-session/end-session-flow.xml    | 274 +++++++++++++++
 .../idp/service/relying-party/postconfig.xml       |  63 +++-
 16 files changed, 1981 insertions(+), 5 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCRpInitiatedLogoutContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCRpInitiatedLogoutContext.java
new file mode 100644
index 00000000..cba01496
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCRpInitiatedLogoutContext.java
@@ -0,0 +1,146 @@
+/*
+ * 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;
+
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+
+/**
+ * Subcontext carrying information about OIDC RP-initiated logout.
+ * 
+ * @since 4.1.0
+ */
+public final class OIDCRpInitiatedLogoutContext extends BaseContext {
+
+    /** The request client ID. */
+    @Nullable private String requestedClientId;
+
+    /** The location for the post logout redirect URI. */
+    @Nullable private URI postLogoutRedirectUri;
+
+    /** The requested ID token hint. May be {@link EncryptedJWT}. */
+    @Nullable private JWT requestedIdTokenHint;
+
+    /** The processed ID token hint. Not encrypted. */
+    @Nullable private JWT processedIdTokenHint;
+
+    /** The logout hint. */
+    @Nullable private String logoutHint;
+
+    /** The state. */
+    @Nullable private String state;
+
+    /**
+     * Get the requested client ID.
+     * @return The requested client ID.
+     */
+    @Nullable public String getRequestedClientId() {
+        return requestedClientId;
+    }
+
+    /**
+     * Set the requested client ID.
+     * @param id What to set
+     */
+    public void setRequestedClientId(@Nullable final String id) {
+        requestedClientId = id;
+    }
+
+    /**
+     * Get the location for the post logout redirect URI.
+     * @return The location for the post logout redirect URI.
+     */
+    @Nullable public URI getPostLogoutRedirectUri() {
+        return postLogoutRedirectUri;
+    }
+
+    /**
+     * Set the location for the post logout redirect URI.
+     * @param uri What to set
+     */
+    public void setPostLogoutRedirectUri(@Nullable final URI uri) {
+        postLogoutRedirectUri = uri;
+    }
+
+    /**
+     * Get the requested ID token hint. May be {@link EncryptedJWT}.
+     * @return The requested ID token hint.
+     */
+    @Nullable  public JWT getRequestedIdTokenHint() {
+        return requestedIdTokenHint;
+    }
+
+    /**
+     * Set the requested ID token hint. May be {@link EncryptedJWT}.
+     * @param jwt What to set
+     */
+    public void setRequestedIdTokenHint(@Nullable final JWT jwt) {
+        requestedIdTokenHint = jwt;
+    }
+
+    /**
+     * Get the processed ID token hint. Not encrypted.
+     * @return The processed ID token hint.
+     */
+    @Nullable  public JWT getProcessedIdTokenHint() {
+        return processedIdTokenHint;
+    }
+
+    /**
+     * Set the processed ID token hint. Not encrypted.
+     * @param jwt What to set
+     */
+    public void setProcessedIdTokenHint(@Nullable final JWT jwt) {
+        processedIdTokenHint = jwt;
+    }
+
+    /**
+     * Get the logout hint.
+     * @return The logout hint.
+     */
+    @Nullable public String getLogoutHint() {
+        return logoutHint;
+    }
+
+    /**
+     * Set the logout hint.
+     * @param hint What to set
+     */
+    public void setLogoutHint(@Nullable final String hint) {
+        logoutHint = hint;
+    }
+
+    /**
+     * Get the state.
+     * @return The state.
+     */
+    @Nullable public String getState() {
+        return state;
+    }
+
+    /**
+     * Set the state.
+     * @param value What to set
+     */
+    public void setState(@Nullable final String value) {
+        state = value;
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultValidPostLogoutRedirectUrisLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultValidPostLogoutRedirectUrisLookupFunction.java
new file mode 100644
index 00000000..4c035d1f
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultValidPostLogoutRedirectUrisLookupFunction.java
@@ -0,0 +1,48 @@
+/*
+ * 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 java.net.URI;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+
+/**
+ * A function that returns registered post logout redirection uris from metadata.
+ *
+ * @since 4.1.0
+ */
+public class DefaultValidPostLogoutRedirectUrisLookupFunction
+        implements ContextDataLookupFunction<ProfileRequestContext, Set<URI>> {
+
+    /** {@inheritDoc} */
+    @Nullable
+    public Set<URI> apply(@Nullable final ProfileRequestContext input) {
+        if (input == null || input.getInboundMessageContext() == null) {
+            return null;
+        }
+        final OIDCMetadataContext ctx = input.getInboundMessageContext().getSubcontext(OIDCMetadataContext.class);
+        if (ctx == null || ctx.getClientInformation() == null || ctx.getClientInformation().getOIDCMetadata() == null) {
+            return null;
+        }
+        return ctx.getClientInformation().getOIDCMetadata().getPostLogoutRedirectionURIs();
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/LogoutRequestClientIDLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/LogoutRequestClientIDLookupFunction.java
new file mode 100644
index 00000000..08c425ff
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/LogoutRequestClientIDLookupFunction.java
@@ -0,0 +1,98 @@
+/*
+ * 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 java.text.ParseException;
+import java.util.List;
+
+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.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.LogoutRequest;
+
+/**
+ * A function that returns client id of the OIDC logout request. This lookup locates client id from then request if
+ * available from the client_id parameter or in the audience of the ID token hint. If multiple audiences exist, the
+ * first one is assumed to be the client id.
+ * 
+ * If information is not available, null is returned.
+ * 
+ * @since 4.1.0
+ */
+public class LogoutRequestClientIDLookupFunction implements ContextDataLookupFunction<MessageContext, ClientID> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(LogoutRequestClientIDLookupFunction.class);
+
+    /** {@inheritDoc} */
+    @Nullable public ClientID apply(@Nullable final MessageContext input) {
+        if (input == null) {
+            return null;
+        }
+        if (input.getMessage() instanceof LogoutRequest logoutRequest) {
+            final ClientID requestClientId = logoutRequest.getClientID();
+            final JWT idTokenHint = logoutRequest.getIDTokenHint();
+            if (idTokenHint != null) {
+                if (idTokenHint instanceof EncryptedJWT && requestClientId == null) {
+                    log.error("id_token_hint is encrypted and no client_id found in request, no client_id resolved");
+                    return null;
+                }
+                final String idTokenClientId = getClientIdFromJwt(idTokenHint);
+                if (idTokenClientId == null) {
+                    return null;
+                }
+                if (requestClientId != null) {
+                    if (requestClientId.toString().equals(idTokenClientId)) {
+                        return requestClientId;
+                    }
+                    log.error("The cliend_id in id_token_hint {} did not match with the requested client_id {}",
+                            idTokenClientId, requestClientId);
+                    return null;
+                } else {
+                    return new ClientID(idTokenClientId);
+                }
+            }
+            return requestClientId;
+        }
+        return null;
+    }
+
+    @Nullable protected String getClientIdFromJwt(final JWT jwt) {
+        try {
+            final List<String> audience = jwt.getJWTClaimsSet().getAudience();
+            if (audience == null || audience.isEmpty()) {
+                log.error("No audience defined in the id_token_hint");
+                return null;
+            }
+            if (audience.size() > 1) {
+                log.warn("The id_token_hint contained multiple audience ({}), returning first {}", audience.size(),
+                        audience.get(0));
+            }
+            return audience.get(0);
+        } catch (final ParseException e) {
+            log.error("Could not parse the claims set from id_token_hint", e);
+        }
+        return null;        
+    }
+
+}
\ No newline at end of file
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
index 63b81f80..33ab9d96 100644
--- 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
@@ -72,6 +72,11 @@ public class OIDCRPSession extends BasicSPSession implements SPSession {
         supportsLogoutPropagation = supportsPropagation;
     }
 
+    /** {@inheritDoc} */
+    @Nonnull @NotEmpty public String getSPSessionKey() {
+        return sessionIdentifier;
+    }
+
     @Nonnull @NotEmpty
     @Override
     public String getProtocol() {
@@ -83,7 +88,7 @@ public class OIDCRPSession extends BasicSPSession implements SPSession {
      * 
      * @return the issuer value to interact with the service
      */
-    public String getIssuer() {
+    @Nonnull @NotEmpty public String getIssuer() {
         return issuer;
     }
 
@@ -92,7 +97,7 @@ public class OIDCRPSession extends BasicSPSession implements SPSession {
      * 
      * @return the root token identifier
      */
-    public String getRootTokenIdentifier() {
+    @Nonnull @NotEmpty public String getRootTokenIdentifier() {
         return rootTokenIdentifier;
     }
 
@@ -101,7 +106,7 @@ public class OIDCRPSession extends BasicSPSession implements SPSession {
      * 
      * @return the session identifier
      */
-    public String getSessionIdentifier() {
+    @Nonnull @NotEmpty public String getSessionIdentifier() {
         return sessionIdentifier;
     }
 
@@ -110,7 +115,7 @@ public class OIDCRPSession extends BasicSPSession implements SPSession {
      * 
      * @return The subject value
      */
-    public String getSubject() {
+    @Nonnull @NotEmpty public String getSubject() {
         return subject;
     }
 
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCLogoutRequestDecoder.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCLogoutRequestDecoder.java
new file mode 100644
index 00000000..db1c5bd6
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/decoding/impl/OIDCLogoutRequestDecoder.java
@@ -0,0 +1,69 @@
+/*
+ * 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.decoding.impl;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.MoreObjects;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.http.JakartaServletUtils;
+import com.nimbusds.openid.connect.sdk.LogoutRequest;
+
+import net.shibboleth.idp.plugin.oidc.op.oauth2.decoding.impl.BaseOAuth2RequestDecoder;
+
+/**
+ * Message decoder decoding OpenID Connect {@link LogoutRequest}s.
+ */
+public class OIDCLogoutRequestDecoder extends BaseOAuth2RequestDecoder<LogoutRequest> {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(OIDCLogoutRequestDecoder.class);
+
+    /** {@inheritDoc} */
+    @Override @Nonnull
+    protected LogoutRequest parseMessage() throws MessageDecodingException {
+        try {
+            final HTTPRequest httpReq = JakartaServletUtils.createHTTPRequest(getHttpServletRequest());
+            getProtocolMessageLogger().trace("Inbound request {}", RequestUtil.toString(httpReq));
+            return LogoutRequest.parse(httpReq);
+        } catch (final com.nimbusds.oauth2.sdk.ParseException | IOException e) {
+            log.error("Unable to decode inbound request: {}", e.getMessage());
+            throw new MessageDecodingException(e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    protected String getMessageToLog(@Nullable final LogoutRequest message) {
+        return message == null ? null : MoreObjects.toStringHelper(this).omitNullValues()
+                .add("logoutHint", message.getLogoutHint())
+                .add("clientId", message.getClientID())
+                .add("endpointURI", getEndpointURI(message))
+                .add("idTokenHint", message.getIDTokenHint() == null ? null : message.getIDTokenHint().serialize())
+                .add("postLogoutRedirectionUri",  message.getPostLogoutRedirectionURI())
+                .add("state", message.getState())
+                .add("uiLocales", message.getUILocales())
+                .toString();
+    }
+
+}
\ 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/AbstractOIDCRpInitiatedLogoutAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCRpInitiatedLogoutAction.java
new file mode 100644
index 00000000..8fa687c1
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCRpInitiatedLogoutAction.java
@@ -0,0 +1,140 @@
+/*
+ * 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.OIDCRpInitiatedLogoutContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCRequestAction;
+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.LogoutRequest;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * An abstract action for OIDC logout actions dealing with OIDC RP-initiated logout.
+ */
+public class AbstractOIDCRpInitiatedLogoutAction extends AbstractOIDCRequestAction<LogoutRequest> {
+
+    /** Class logger. */
+    @Nonnull private static final Logger log = LoggerFactory.getLogger(AbstractOIDCRpInitiatedLogoutAction.class);
+
+    /** Strategy function to lookup the {@link OIDCMetadataContext}. */
+    @Nonnull private Function<ProfileRequestContext, OIDCMetadataContext> oidcMetadataCtxLookupStrategy;
+
+    /** Strategy function to lookup the {@link OIDCRpInitiatedLogoutContext}. */
+    @Nonnull
+    private Function<ProfileRequestContext, OIDCRpInitiatedLogoutContext> oidcRpInitatedLogoutCtxLookupStrategy;
+
+    /** OIDC metadata context. */
+    @Nullable private OIDCMetadataContext oidcMetadataContext;
+
+    /** RP-initiated OIDC logout context. */
+    @Nullable private OIDCRpInitiatedLogoutContext rpInitiatedLogoutContext;
+
+    /**
+     * Constructor.
+     */
+    public AbstractOIDCRpInitiatedLogoutAction() {
+        oidcMetadataCtxLookupStrategy = new ChildContextLookup<>(OIDCMetadataContext.class).compose(
+                new InboundMessageContextLookup());
+        oidcRpInitatedLogoutCtxLookupStrategy = new ChildContextLookup<>(OIDCRpInitiatedLogoutContext.class).compose(
+                new OutboundMessageContextLookup());
+    }
+
+    /**
+     * Set the mechanism to lookup the {@link OIDCRpInitiatedLogoutContext}.
+     * 
+     * @param strategy What to set.
+     */
+    public void setOIDCRpInitatedLogoutCtxLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCRpInitiatedLogoutContext> strategy) {
+        checkSetterPreconditions();
+        oidcRpInitatedLogoutCtxLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+        
+    }
+    /**
+     * Set the mechanism to lookup the {@link OIDCMetadataContext} from the {@link ProfileRequestContext}.
+     * 
+     * @param strategy What to set.
+     */
+    public void setOIDCMetadataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCMetadataContext> strategy) {
+        checkSetterPreconditions();
+        oidcMetadataCtxLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        oidcMetadataContext = oidcMetadataCtxLookupStrategy.apply(profileRequestContext);
+        if (oidcMetadataContext == null) {
+            log.error("{} No OIDC metadata context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        rpInitiatedLogoutContext = oidcRpInitatedLogoutCtxLookupStrategy.apply(profileRequestContext);
+        if (rpInitiatedLogoutContext == null) {
+            log.error("{} No RP-initiated OIDC logout context found", this.getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Returns the OIDC Metadata context.
+     * 
+     * @return The OIDC Metadata context.
+     */
+    public OIDCMetadataContext getMetadataContext() {
+        return oidcMetadataContext;
+    }
+
+    /**
+     * Get the RP-initiated OIDC logout context.
+     * @return The RP-initiated OIDC logout context.
+     */
+    @Nullable public OIDCRpInitiatedLogoutContext getRpInitiatedLogoutContext() {
+        return rpInitiatedLogoutContext;
+    }
+
+    /**
+     * Returns OIDC RP-initiated logout request.
+     * 
+     * @return request
+     */
+    public LogoutRequest getLogoutRequest() {
+        return getRequest();
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormRpInitiatedLogoutResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormRpInitiatedLogoutResponse.java
new file mode 100644
index 00000000..4261703f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormRpInitiatedLogoutResponse.java
@@ -0,0 +1,76 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.impl.RpInitiatedLogoutResponse;
+
+/**
+ * Action that forms outbound message based on request and response context. Formed message is set to
+ * {@link ProfileRequestContext#getOutboundMessageContext()}.
+ */
+public class FormRpInitiatedLogoutResponse extends AbstractOIDCRpInitiatedLogoutAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(FormRpInitiatedLogoutResponse.class);
+
+    /** The post logout redirection endpoint. */
+    @Nullable private URI postLogoutRedirectUri;
+
+    /** The optional state parameter to the logout. */
+    @Nullable private String state;
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+
+        postLogoutRedirectUri = getRpInitiatedLogoutContext().getPostLogoutRedirectUri();
+        if (postLogoutRedirectUri == null) {
+            log.debug("{} No post logout redirection URI set in the context", getLogPrefix());
+        }
+
+        state = getRpInitiatedLogoutContext().getState();
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final RpInitiatedLogoutResponse response;
+        if (postLogoutRedirectUri == null) {
+            response = new RpInitiatedLogoutResponse(null);
+        } else {
+            if (state == null) {
+                response = new RpInitiatedLogoutResponse(postLogoutRedirectUri.toString());
+            } else {
+                response = new RpInitiatedLogoutResponse(postLogoutRedirectUri.toString(), state);
+            }
+        }
+        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/InitializeOutboundRpInitiatedLogoutResponseMessageContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/InitializeOutboundRpInitiatedLogoutResponseMessageContext.java
new file mode 100644
index 00000000..41026565
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/InitializeOutboundRpInitiatedLogoutResponseMessageContext.java
@@ -0,0 +1,38 @@
+/*
+ * 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 org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractInitializeOutboundResponseMessageContext;
+
+/**
+ * Action that adds an outbound {@link MessageContext} and related contexts to the {@link ProfileRequestContext}.
+ *
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ */
+public class InitializeOutboundRpInitiatedLogoutResponseMessageContext
+        extends AbstractInitializeOutboundResponseMessageContext {
+
+    /**
+     * Constructor.
+     */
+    public InitializeOutboundRpInitiatedLogoutResponseMessageContext() {
+        setContextType(OIDCRpInitiatedLogoutContext.class);
+    }
+
+}
\ 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/PopulateRpInitiatedLogoutContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PopulateRpInitiatedLogoutContext.java
new file mode 100644
index 00000000..4622b865
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PopulateRpInitiatedLogoutContext.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.Predicate;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicates;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Populates the {@link OIDCRpInitiatedLogoutContext} with the values found from the incoming logout request.
+ */
+public class PopulateRpInitiatedLogoutContext extends AbstractOIDCRpInitiatedLogoutAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(PopulateRpInitiatedLogoutContext.class);
+
+    /** Predicate for enforcing the use of ID token hints. */
+    @Nonnull private Predicate<ProfileRequestContext> idTokenHintEnforcedPredicate;
+
+    /**
+     * Constructor.
+     */
+    public PopulateRpInitiatedLogoutContext() {
+        idTokenHintEnforcedPredicate = Predicates.alwaysFalse();
+    }
+
+    /**
+     * Set the predicate for enforcing the use of ID token hints.
+     * 
+     * @param predicate the predicate for enforcing the use of ID token hints
+     */
+    public void setIdTokenHintEnforcedPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        checkSetterPreconditions();
+        idTokenHintEnforcedPredicate = Constraint.isNotNull(predicate,
+                "ID token hint enforced predicate annot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        if (getLogoutRequest().getIDTokenHint() == null) {
+            if (idTokenHintEnforcedPredicate.test(profileRequestContext)) {
+                log.warn("{} No id_token_hint found, even though it's enforced", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.MISSING_MANDATORY_ID_TOKEN_HINT);
+                return false;
+            }
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        final JWT requestedIdTokenHint = getLogoutRequest().getIDTokenHint();
+        if (requestedIdTokenHint != null) {
+            getRpInitiatedLogoutContext().setRequestedIdTokenHint(requestedIdTokenHint);
+            if (!(requestedIdTokenHint instanceof EncryptedJWT)) {
+                log.debug("{} ID token hint is not encrypted, setting it to processedIdTokenHint.", getLogPrefix());
+                getRpInitiatedLogoutContext().setProcessedIdTokenHint(requestedIdTokenHint);
+            }
+        }
+        getRpInitiatedLogoutContext().setLogoutHint(StringSupport.trimOrNull(getLogoutRequest().getLogoutHint()));
+        getRpInitiatedLogoutContext().setPostLogoutRedirectUri(getLogoutRequest().getPostLogoutRedirectionURI());
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequest.java
new file mode 100644
index 00000000..cf3a8b7c
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ProcessRpInitiatedLogoutRequest.java
@@ -0,0 +1,375 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.text.ParseException;
+import java.util.Iterator;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+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.saml.saml2.core.LogoutRequest;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+import net.shibboleth.idp.authn.context.SubjectContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.LogoutRequestClientIDLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.idp.saml.session.SAML2SPSession;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.SPSession;
+import net.shibboleth.idp.session.SessionResolver;
+import net.shibboleth.idp.session.context.LogoutContext;
+import net.shibboleth.idp.session.context.SessionContext;
+import net.shibboleth.idp.session.criterion.HttpServletRequestCriterion;
+import net.shibboleth.idp.session.criterion.SPSessionCriterion;
+import net.shibboleth.oidc.profile.config.navigate.LogoutHintMatchingStrategyLookupFunction;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+
+/**
+ * Profile action that processes a {@link LogoutRequest} by resolving matching sessions, and destroys them,
+ * populating the associated {@link SPSession} objects (excepting the one initiating the logout) into a
+ * {@link LogoutContext}.
+ * 
+ * <p>A {@link SubjectContext} is also populated. If and only if a single {@link IdPSession} is resolved,
+ * a {@link SessionContext} is also populated.</p>
+ * 
+ * <p>Each {@link SPSession} is also assigned a unique number and inserted into the map
+ * returned by {@link LogoutContext#getKeyedSessionMap()}.</p> 
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#IO_ERROR}
+ * @event {@link OidcEventIds#SESSION_NOT_FOUND}
+ * @post If at least one {@link IdPSession} was found, then a {@link SubjectContext} and {@link LogoutContext}
+ *  will be populated.
+ * @post If a single {@link IdPSession} was found, then a {@link SessionContext} will be populated.
+ */
+public class ProcessRpInitiatedLogoutRequest extends AbstractOIDCRpInitiatedLogoutAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessRpInitiatedLogoutRequest.class);
+
+    /** Session resolver. */
+    @NonnullAfterInit private SessionResolver sessionResolver;
+
+    /** Creation/lookup function for SubjectContext. */
+    @Nonnull private Function<ProfileRequestContext,SubjectContext> subjectContextCreationStrategy;
+
+    /** Creation/lookup function for SessionContext. */
+    @Nonnull private Function<ProfileRequestContext,SessionContext> sessionContextCreationStrategy;
+
+    /** Creation/lookup function for LogoutContext. */
+    @Nonnull private Function<ProfileRequestContext,LogoutContext> logoutContextCreationStrategy;
+
+    /** Lookup function for issuer. */
+    @Nonnull private Function<ProfileRequestContext,ClientID> issuerLookupStrategy;
+
+    /** Function to return {@link CriteriaSet} to give to session resolver. */
+    @Nonnull private Function<ProfileRequestContext,CriteriaSet> sessionResolverCriteriaStrategy;
+
+    /** Lookup function for the RP-initiated logout's logout_hint parameter matching against SPSession. */
+    @Nonnull
+    private Function<ProfileRequestContext,BiPredicate<String,SPSession>> logoutHintMatchingStrategyLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public ProcessRpInitiatedLogoutRequest() {
+        subjectContextCreationStrategy = new ChildContextLookup<>(SubjectContext.class, true);
+        sessionContextCreationStrategy = new ChildContextLookup<>(SessionContext.class, true);
+        logoutContextCreationStrategy = new ChildContextLookup<>(LogoutContext.class, true);
+        issuerLookupStrategy = new LogoutRequestClientIDLookupFunction().compose(new InboundMessageContextLookup());
+        sessionResolverCriteriaStrategy = new Function<>() {
+            public CriteriaSet apply(final ProfileRequestContext input) {
+                final ClientID clientID = issuerLookupStrategy.apply(input);
+                final JWT processedIdTokenHint = getRpInitiatedLogoutContext().getProcessedIdTokenHint();
+                if (clientID != null && processedIdTokenHint != null) {
+                    log.debug("{} Building criteria set with clientID {} and idTokenHint {}", getLogPrefix(),
+                            clientID.getValue(), processedIdTokenHint);
+                    try {
+                        final String sessionId =
+                                processedIdTokenHint.getJWTClaimsSet().getStringClaim(TokenClaimsSet.KEY_SESSION_ID);
+                        if (sessionId != null) {
+                            return new CriteriaSet(new SPSessionCriterion(clientID.getValue(), sessionId));
+                        }
+                    } catch (final ParseException e) {
+                        log.error("{} Could not parse session ID from id_token_hint", getLogPrefix(), e);
+                    }
+                }
+                log.debug("{} Building criteria set with HttpServletRequestCriterion", getLogPrefix());
+                return new CriteriaSet(new HttpServletRequestCriterion());
+            }
+        };
+        
+        logoutHintMatchingStrategyLookupStrategy = new LogoutHintMatchingStrategyLookupFunction();
+    }
+
+    /**
+     * Set the {@link SessionResolver} to use.
+     * 
+     * @param resolver  session resolver to use
+     */
+    public void setSessionResolver(@Nonnull final SessionResolver resolver) {
+        checkSetterPreconditions();
+        sessionResolver = Constraint.isNotNull(resolver, "SessionResolver cannot be null");
+    }
+    
+    /**
+     * Set the creation/lookup strategy for the {@link SubjectContext} to populate.
+     * 
+     * @param strategy  creation/lookup strategy
+     */
+    public void setSubjectContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,SubjectContext> strategy) {
+        checkSetterPreconditions();
+        subjectContextCreationStrategy = Constraint.isNotNull(strategy,
+                "SubjectContext creation strategy cannot be null");
+    }
+
+    /**
+     * Set the creation/lookup strategy for the {@link SessionContext} to populate.
+     * 
+     * @param strategy  creation/lookup strategy
+     */
+    public void setSessionContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,SessionContext> strategy) {
+        checkSetterPreconditions();
+        sessionContextCreationStrategy = Constraint.isNotNull(strategy,
+                "SessionContext creation strategy cannot be null");
+    }
+    
+    /**
+     * Set the creation/lookup strategy for the {@link LogoutContext} to populate.
+     * 
+     * @param strategy  creation/lookup strategy
+     */
+    public void setLogoutContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,LogoutContext> strategy) {
+        checkSetterPreconditions();
+        logoutContextCreationStrategy = Constraint.isNotNull(strategy,
+                "LogoutContext creation strategy cannot be null");
+    }
+
+    /**
+     * Set the issuer lookup strategy.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,ClientID> strategy) {
+        checkSetterPreconditions();
+        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy for building the {@link CriteriaSet} to feed into the {@link SessionResolver}.
+     * 
+     * @param strategy  building strategy
+     */
+    public void setSessionResolverCriteriaStrategy(
+            @Nonnull final Function<ProfileRequestContext,CriteriaSet> strategy) {
+        checkSetterPreconditions();
+        sessionResolverCriteriaStrategy = Constraint.isNotNull(strategy,
+                "SessionResolver CriteriaSet strategy cannot be null");
+    }
+
+    /**
+     * Set the lookup function to the RP-initiated logout's logout_hint parameter matching against SPSession.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setLogoutHintMatchingStrategyLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,BiPredicate<String,SPSession>> strategy) {
+        checkSetterPreconditions();
+        logoutHintMatchingStrategyLookupStrategy = Constraint.isNotNull(strategy,
+                "LogoutHintMatchingStrategy lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (!PredicateSupport.isAlwaysFalse(getActivationCondition())) {
+            if (sessionResolver == null) {
+                throw new ComponentInitializationException("SessionResolver cannot be null");
+            }
+        }
+    }
+
+ // Checkstyle: CyclomaticComplexity|ReturnCount OFF
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        try {
+            final Iterable<IdPSession> sessions =
+                    sessionResolver.resolve(sessionResolverCriteriaStrategy.apply(profileRequestContext));
+            final Iterator<IdPSession> sessionIterator = sessions.iterator();
+
+            LogoutContext logoutCtx = null;
+            
+            int count = 1;
+            
+            log.debug("{} Has next? {}", getLogPrefix(), sessionIterator.hasNext());
+            
+            while (sessionIterator.hasNext()) {
+                final IdPSession session = sessionIterator.next();
+                assert session!=null;
+                
+                if (!sessionMatches(profileRequestContext, session)) {
+                    log.debug("{} IdP session {} does not contain a matching SP session", getLogPrefix(),
+                            session.getId());
+                    continue;
+                }
+
+                log.debug("{} LogoutRequest matches IdP session {}", getLogPrefix(), session.getId());
+                
+                if (logoutCtx == null) {
+                    logoutCtx = logoutContextCreationStrategy.apply(profileRequestContext);
+                    if (logoutCtx == null) {
+                        log.error("{} Unable to create or locate LogoutContext", getLogPrefix());
+                        ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+                        return;
+                    }
+
+                    final SubjectContext subjectCtx = subjectContextCreationStrategy.apply(profileRequestContext);
+                    if (subjectCtx != null) {
+                        subjectCtx.setPrincipalName(session.getPrincipalName());
+                    }
+                }
+
+                logoutCtx.getIdPSessions().add(session);
+                
+                for (final SPSession spSession : session.getSPSessions()) {
+                    assert spSession!=null;
+                    if (!sessionMatches(profileRequestContext, spSession)) {
+                        logoutCtx.getSessionMap().put(spSession.getId(), spSession);
+                        logoutCtx.getKeyedSessionMap().put(Integer.toString(count++), spSession);
+                    }
+                }
+            }
+            
+            if (logoutCtx == null) {
+                log.info("{} No active session(s) found matching LogoutRequest", getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.SESSION_NOT_FOUND);
+            } else if (logoutCtx.getIdPSessions().size() == 1) {
+                final SessionContext sessionCtx = sessionContextCreationStrategy.apply(profileRequestContext);
+                if (sessionCtx != null) {
+                    sessionCtx.setIdPSession(logoutCtx.getIdPSessions().iterator().next());
+                }
+            }
+
+        } catch (final ResolverException e) {
+            log.error("{} Error resolving matching session(s)", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.SESSION_NOT_FOUND);
+        }
+    }
+// Checkstyle: CyclomaticComplexity|ReturnCount ON
+
+    /**
+     * Check if the session contains a {@link SAML2SPSession} with the appropriate service ID and SessionIndex.
+     * 
+     * @param profileRequestContext current profile request context
+     * @param session {@link IdPSession} to check
+     * 
+     * @return  true iff the set of {@link SPSession}s includes one applicable to the logout request
+     */
+    private boolean sessionMatches(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final IdPSession session) {
+        
+        for (final SPSession spSession : session.getSPSessions()) {
+            assert spSession!=null;
+            log.trace("{} Matching session with SP session key {}", getLogPrefix(), spSession.getSPSessionKey());
+            if (sessionMatches(profileRequestContext, spSession)) {
+                return true;
+            }
+        }
+        
+        return false;
+    }
+
+    /**
+     * Check if the {@link SPSession} has the appropriate service ID and SessionIndex.
+     * 
+     * @param profileRequestContext current profile request context
+     * @param session {@link SPSession} to check
+     * 
+     * @return  true iff the {@link SPSession} directly matches the logout request
+     */
+    private boolean sessionMatches(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final SPSession session) {
+        
+        assert isPreExecuteCalled();
+        
+        if (session instanceof OIDCRPSession oidcRpSession) {
+            final ClientID issuer = issuerLookupStrategy.apply(profileRequestContext);
+            
+            // Make sure the RP matches.
+            if (issuer == null || !oidcRpSession.getId().equals(issuer.getValue())) {
+                log.trace("{} The session ID {} did not match with the issuer {}", getLogPrefix(),
+                        oidcRpSession.getId(), issuer.getValue());
+                return false;
+            } 
+
+            // Match subject
+            final JWT idTokenHint = getRpInitiatedLogoutContext().getProcessedIdTokenHint();
+            if (idTokenHint != null) {
+                try {
+                    final String subject = idTokenHint.getJWTClaimsSet().getSubject();
+                    log.trace("{} Matching id_token_hint subject {} with session subject {}", getLogPrefix(), subject,
+                            oidcRpSession.getSubject());
+                    if (subject != null && subject.equals(oidcRpSession.getSubject())) {
+                        log.debug("{} Found a matching session via id_token_hint subject", getLogPrefix());
+                        return true;
+                    }
+                } catch (final ParseException e) {
+                    log.error("{} Could not parse subject from id_token_hint", getLogPrefix(), e);
+                }
+                return false;
+            }
+
+            final String logoutHint = getRpInitiatedLogoutContext().getLogoutHint();
+            final BiPredicate<String,SPSession> matchingPredicate =
+                    logoutHintMatchingStrategyLookupStrategy.apply(profileRequestContext);
+            if (matchingPredicate != null && matchingPredicate.test(logoutHint, oidcRpSession)) {
+                log.debug("{} Found a matching session via logout_hint {}", getLogPrefix(), logoutHint);
+                return true;
+            }
+
+        }
+        
+        return false;
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURI.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURI.java
new file mode 100644
index 00000000..414fcfbf
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidatePostLogoutRedirectURI.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.logout.profile.impl;
+
+import java.net.URI;
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultPostLogoutRedirectURIValidationPredicate;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Action that validates post redirect URI is expected if it's being requested.
+ */
+public class ValidatePostLogoutRedirectURI extends AbstractOIDCRpInitiatedLogoutAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidatePostLogoutRedirectURI.class);
+
+    /** Strategy used to validate the post logout redirect URIs. */
+    @Nonnull private BiPredicate<ProfileRequestContext, URI> redirectURIValidationStrategy;
+
+    /** The redirect URI to be validated. */
+    @Nullable private URI requestedRedirectURI;
+
+    /**
+     * Constructor.
+     */
+    public ValidatePostLogoutRedirectURI() {
+        redirectURIValidationStrategy = new DefaultPostLogoutRedirectURIValidationPredicate();
+    }
+
+    /**
+     * Set the strategy used to validate the post logout redirect URIs.
+     * 
+     * @param strategy What to set
+     */
+    public void setRedirectURIValidationStrategy(@Nonnull final BiPredicate<ProfileRequestContext, URI> strategy) {
+        checkSetterPreconditions();
+        redirectURIValidationStrategy =
+                Constraint.isNotNull(strategy, "RedirectURILookupStrategy lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        requestedRedirectURI = getRpInitiatedLogoutContext().getPostLogoutRedirectUri();
+        if (requestedRedirectURI == null) {
+            log.debug("{} No post logout redirect URI found, nothing to do", getLogPrefix());
+            return false;
+        }
+        
+        return true;
+    }
+
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!redirectURIValidationStrategy.test(profileRequestContext, requestedRedirectURI)) {
+            log.warn("{} Post logout redirection URI {} did not pass the validation", getLogPrefix(),
+                    requestedRedirectURI);
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_REDIRECT_URI);
+            return;
+        }
+        
+        log.debug("{} Post logout redirection URI {} successfully validated", getLogPrefix(), requestedRedirectURI);
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/impl/RpInitiatedLogoutResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/impl/RpInitiatedLogoutResponse.java
new file mode 100644
index 00000000..c78a4148
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/impl/RpInitiatedLogoutResponse.java
@@ -0,0 +1,129 @@
+/*
+ * 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.primitive.StringSupport;
+
+/**
+ * A Nimbus {@link Response} implementation representing a post front-channel logout redirection message that is sent
+ * to the RP's post front-channel logout URI endpoint. If the URI is null, an empty response with OK status is returned.
+ */
+public class RpInitiatedLogoutResponse implements Response {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(RpInitiatedLogoutResponse.class);
+
+    /** The post front-channel logout redirection endpoint. */
+    @Nullable final String postLogoutRedirectionUri;
+
+    /** The optional state value. */
+    @Nullable final String state;
+
+    /**
+     * Constructor.
+     *
+     * @param uri The post front-channel logout redirection endpoint.
+     */
+    public RpInitiatedLogoutResponse(@Nullable final String uri) {
+        this(uri, null);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param uri The post front-channel logout redirection endpoint.
+     * @param relayState The optional state -parameter value.
+     */
+    public RpInitiatedLogoutResponse(@Nullable final String uri, @Nullable final String relayState) {
+        postLogoutRedirectionUri = uri;
+        state = StringSupport.trimOrNull(relayState);
+    }
+
+    @Override
+    public boolean indicatesSuccess() {
+        return true;
+    }
+
+    /**
+     * Returns an HTTP response for this logout response. If a post logout redirect URI was set, then the response
+     * contains 302 redirection to it optionally with the state parameter. If the URI was not set, then the response
+     * simpy contains HTTP 200 OK without any content.
+     *
+     * <p>Example HTTP response:</p>
+     *
+     * <pre>
+     * HTTP/1.1 302 Found
+     * Location: http://example.org/logout?state=1234567890ABCDEFG
+     * </pre>
+     *
+     * @see #toHTTPRequest()
+     *
+     * @return An HTTP response for this message.
+     */
+    @Override
+    public HTTPResponse toHTTPResponse() {
+
+        if (postLogoutRedirectionUri == null) {
+            return new HTTPResponse(HTTPResponse.SC_OK);
+        }
+        
+        final HTTPResponse response = new HTTPResponse(HTTPResponse.SC_FOUND);
+        final URI uri;
+        try {
+            if (state != null) {
+                if (postLogoutRedirectionUri.contains("?")) {
+                    uri = new URI(serializeParameters(postLogoutRedirectionUri + "&"));
+                } else {
+                    uri = new URI(serializeParameters(postLogoutRedirectionUri + "?"));
+                }
+            } else {
+                uri = new URI(postLogoutRedirectionUri);
+            }
+            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 (state != null) {
+            return prefix + "state" + URLEncoder.encode(state, "UTF-8");
+        }
+        return prefix;
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPostLogoutRedirectURIValidationPredicate.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPostLogoutRedirectURIValidationPredicate.java
new file mode 100644
index 00000000..4ed44201
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultPostLogoutRedirectURIValidationPredicate.java
@@ -0,0 +1,54 @@
+/*
+ * 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.logic;
+
+import java.net.URI;
+import java.util.Set;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultValidPostLogoutRedirectUrisLookupFunction;
+
+/**
+ * Default validation strategy for post logout redirection URIs. This simply checks if the URI given in the input
+ * parameters is found from the set of valid post logout redirections URIs.
+ */
+public class DefaultPostLogoutRedirectURIValidationPredicate implements BiPredicate<ProfileRequestContext, URI> {
+
+    /** Strategy used to obtain the redirect uris to compare request value to. */
+    @Nonnull private Function<ProfileRequestContext, Set<URI>> validRedirectURIsLookupStrategy;
+
+    /**
+     * Constructor.
+     */
+    public DefaultPostLogoutRedirectURIValidationPredicate() {
+        validRedirectURIsLookupStrategy = new DefaultValidPostLogoutRedirectUrisLookupFunction();
+    }
+    
+    @Override
+    public boolean test(@Nullable final ProfileRequestContext profileRequestContext, @Nullable final URI uri) {
+        final Set<URI> validURIs = validRedirectURIsLookupStrategy.apply(profileRequestContext);
+        if (validURIs != null && uri != null && validURIs.contains(uri)) {
+            return true;
+        }
+        return false;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/end-session/end-session-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/end-session/end-session-beans.xml
new file mode 100644
index 00000000..d401e520
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/end-session/end-session-beans.xml
@@ -0,0 +1,276 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <bean id="shibboleth.oidc.browserProfile" class="java.lang.Boolean" c:_0="true" />
+    
+    <bean id="shibboleth.oidc.profileId" class="java.lang.String"
+        c:_0="#{T(net.shibboleth.oidc.profile.config.OIDCLogoutProfileConfiguration).PROFILE_ID}" />
+    
+    <bean id="shibboleth.oidc.loggingId" class="java.lang.String" c:_0="%{idp.service.logging.oidclogout:OIDC.Logout}" />
+
+    <util:constant id="shibboleth.metrics.ProfileCounter"
+        static-field="net.shibboleth.oidc.profile.config.impl.DefaultOIDCLogoutConfiguration.PROFILE_COUNTER" />
+
+    <bean id="DecodeMessage" class="org.opensaml.profile.action.impl.DecodeMessage" scope="prototype">
+        <constructor-arg>
+            <bean class="net.shibboleth.idp.plugin.oidc.op.decoding.impl.OIDCLogoutRequestDecoder"
+                scope="prototype" p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+                p:removeIpAddressFromEndpointUri="%{idp.oidc.logging.removeIpAddressFromProtocolMessage:false}"/>
+        </constructor-arg>
+    </bean>
+
+    <bean id="PopulateUserAgentContext" class="net.shibboleth.idp.profile.impl.PopulateUserAgentContext"
+        scope="prototype" p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
+
+    <bean id="shibboleth.ClientIDLookupStrategy"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.LogoutRequestClientIDLookupFunction"
+        scope="prototype" />
+
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.InitializeOutboundRpInitiatedLogoutResponseMessageContext"
+        scope="prototype" />
+
+    <bean id="PopulateRpInitiatedLogoutContext"
+        class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.PopulateRpInitiatedLogoutContext" scope="prototype" />
+        <!-- wire idTokenHintEnforcedPredicate -->
+
+    <bean id="PopulateClientStorageLoadContext"
+        class="org.opensaml.storage.impl.client.PopulateClientStorageLoadContext" scope="prototype"
+        p:storageServices="#{ getObject('shibboleth.ClientStorageServices') ?: getObject('shibboleth.DefaultClientStorageServices') }" />
+
+    <bean id="PopulateIdTokenHintSignatureValidationParameters"
+            class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureValidationParameters"
+            scope="prototype"
+            c:strategy-ref="shibboleth.MessageContextLookup.Inbound">
+        <property name="configurationLookupStrategy">
+            <bean class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureValidationConfigurationLookupFunction" />
+        </property>
+        <property name="signatureValidationParametersResolver">
+            <bean class="net.shibboleth.oidc.security.jose.impl.BasicSignatureValidationParametersResolver" />
+        </property>
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.Expression"
+                   c:expression="#input.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext)).getRequestedIdTokenHint() != null" />
+        </property>
+    </bean>
+
+    <bean id="ValidateIdTokenHintSignature" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+        scope="prototype" c:executionDirection="INBOUND">
+        <constructor-arg>
+            <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+                <property name="handlers">
+                    <list>
+                        <bean class="net.shibboleth.oidc.security.impl.CheckClientJWTSignatureAlgorithmHandler"
+                            scope="prototype" p:defaultAlgorithmValue="">
+                            <property name="jwtTokenLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                                    c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+                                    c:outputType="#{T(com.nimbusds.jwt.SignedJWT)}"
+                                    c:expression="#input.getParent().getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext)).getProcessedIdTokenHint()" />
+                            </property>
+                            <property name="clientInformationLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                                    c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+                                    c:expression="#input.getSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext)).getClientInformation()" />
+                            </property>
+                            <property name="signatureAlgorithmLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                                    c:keyName="id_token_signed_response_alg" />
+                            </property>
+                        </bean>
+                        <bean class="net.shibboleth.oidc.security.impl.JWTMessageSignatureSecurityHandler"
+                            scope="prototype">
+                            <property name="jwtTokenLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                                    c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+                                    c:outputType="#{T(com.nimbusds.jwt.SignedJWT)}"
+                                    c:expression="#input.getParent().getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext)).getProcessedIdTokenHint()" />
+                            </property>
+                            <property name="clientInformationLookupStrategy">
+                                <bean
+                                    class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                                    c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+                                    c:expression="#input.getSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext)).getClientInformation()" />
+                            </property>
+                        </bean>
+                    </list>
+                </property>
+            </bean>
+        </constructor-arg>
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.Expression"
+                   c:expression="#input.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext)).getProcessedIdTokenHint() != null" />
+        </property>
+    </bean>
+
+    <bean id="ValidateRequestObject" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateRequestObject"
+        scope="prototype"
+        p:plainClaimsValidator="#{getObject('shibboleth.oidc.PlainRequestObjectClaimsValidation') ?: getObject('shibboleth.oidc.DefaultPlainRequestObjectClaimsValidation')}"
+        p:signedClaimsValidator="#{getObject('shibboleth.oidc.SignedRequestObjectClaimsValidation') ?: getObject('shibboleth.oidc.DefaultSignedRequestObjectClaimsValidation')}">
+    </bean>
+
+    <bean id="shibboleth.oidc.DefaultPlainRequestObjectClaimsValidation"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="PlainClaimsValidators" />
+
+    <bean id="shibboleth.oidc.DefaultSignedRequestObjectClaimsValidation"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="SignedClaimsValidators" />
+
+    <bean id="ExpiryClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+    <bean id="NotBeforeClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.NotBeforeClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
+    <bean id="IssuerClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+        p:claimName="iss">
+        <property name="valueToMatchLookupStrategy">
+            <bean parent="shibboleth.BiFunctions.Expression"
+                c:expression="#custom.apply(#input1.getInboundMessageContext()) == null ? null : #custom.apply(#input1.getInboundMessageContext()).toString()"
+                p:customObject-ref="shibboleth.ClientIDLookupStrategy" />
+        </property>
+    </bean>
+
+    <bean id="AudienceClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator">
+         <property name="audienceLookupStrategy">
+            <bean parent="shibboleth.BiFunctions.Expression"
+                c:expression="#custom.apply(#input1)"
+                p:customObject-ref="shibboleth.ResponderIdLookup.Simple" />
+        </property>
+    </bean>
+
+    <util:list id="PlainClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="ExpiryClaimsValidator" />
+        <ref bean="NotBeforeClaimsValidator" />
+    </util:list>
+
+    <util:list id="SignedClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="ExpiryClaimsValidator" />
+        <ref bean="NotBeforeClaimsValidator" />
+        <ref bean="IssuerClaimsValidator" />
+        <ref bean="AudienceClaimsValidator" />
+    </util:list>
+
+    <bean id="ValidatePostLogoutRedirectURI" class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.ValidatePostLogoutRedirectURI"
+        scope="prototype" />
+
+    <bean id="ProcessRpInitiatedLogoutRequest" class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.ProcessRpInitiatedLogoutRequest"
+        scope="prototype" p:sessionResolver-ref="shibboleth.SessionManager"/>
+
+    <!--TODO extractors --><bean id="LogoutPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.LogoutRequestAuditExtractors') ?: getObject('shibboleth.DefaultLogoutRequestAuditExtractors')}" />
+
+    <alias alias="UserPromptCondition" name="%{idp.logout.promptUser:shibboleth.Conditions.FALSE}" />
+
+    <bean id="DestroySessions"
+        class="net.shibboleth.idp.session.impl.DestroySessions" scope="prototype"
+        p:sessionManager-ref="shibboleth.SessionManager" />
+
+    <bean id="SetIssuerRPUIInformation"
+            class="net.shibboleth.idp.ui.impl.SetRPUIInformation" scope="prototype"
+            p:activationCondition="%{idp.logout.elaboration:false}"
+            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+        <property name="fallbackLanguages">
+            <bean parent="shibboleth.CommaDelimStringArray" c:_0="#{'%{idp.ui.fallbackLanguages:}'.trim()}" />
+        </property>
+        <property name="RPUIContextCreateStrategy">
+            <bean parent="shibboleth.Functions.Compose">
+                <constructor-arg name="g">
+                    <ref bean="shibboleth.ChildLookupOrCreate.RelyingPartyUIContext" />
+                </constructor-arg>
+                <constructor-arg name="f">
+                    <ref bean="shibboleth.ChildLookup.RelyingParty" />
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="PopulateMultiRPContextFromLogoutContext"
+        class="net.shibboleth.idp.session.impl.PopulateMultiRPContextFromLogoutContext" scope="prototype"
+        p:activationCondition="%{idp.logout.elaboration:false}"
+        p:roleDescriptorResolver-ref="shibboleth.RoleDescriptorResolver" />
+
+    <bean id="SetRPUIInformation"
+            class="net.shibboleth.idp.ui.impl.SetRPUIInformation" scope="prototype"
+            p:activationCondition="%{idp.logout.elaboration:false}"
+            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier">
+        <property name="fallbackLanguages">
+            <bean parent="shibboleth.CommaDelimStringArray" c:_0="#{'%{idp.ui.fallbackLanguages:}'.trim()}" />
+        </property>
+        <property name="metadataContextLookupStrategy">
+            <bean parent="shibboleth.Functions.Compose">
+                <constructor-arg name="g">
+                    <bean parent="shibboleth.Functions.Compose">
+                        <constructor-arg name="g">
+                            <ref bean="shibboleth.ChildLookup.SAMLMetadataContext" />
+                        </constructor-arg>
+                        <constructor-arg name="f">
+                            <bean class="net.shibboleth.idp.profile.context.navigate.RelyingPartyContextLookupByCurrent" />
+                        </constructor-arg>
+                    </bean>
+                </constructor-arg>
+                <constructor-arg name="f">
+                    <ref bean="shibboleth.ChildLookup.MultiRelyingParty" />
+                </constructor-arg>
+            </bean>
+        </property>
+        <property name="RPUIContextCreateStrategy">
+            <bean parent="shibboleth.Functions.Compose">
+                <constructor-arg name="g">
+                    <bean parent="shibboleth.Functions.Compose">
+                        <constructor-arg name="g">
+                            <ref bean="shibboleth.ChildLookupOrCreate.RelyingPartyUIContext" />
+                        </constructor-arg>
+                        <constructor-arg name="f">
+                            <bean class="net.shibboleth.idp.profile.context.navigate.RelyingPartyContextLookupByCurrent" />
+                        </constructor-arg>
+                    </bean>
+                </constructor-arg>
+                <constructor-arg name="f">
+                    <ref bean="shibboleth.ChildLookup.MultiRelyingParty" />
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+
+    <bean id="PopulateOutboundInterceptContext"
+            class="net.shibboleth.idp.profile.interceptor.impl.PopulateProfileInterceptorContext" scope="prototype"
+            p:availableFlows="#{@'shibboleth.ProfileInterceptorFlowDescriptorManager'.getComponents()}"
+            p:loggingLabel="outbound">
+        <property name="activeFlowsLookupStrategy">
+            <bean class="net.shibboleth.idp.profile.config.navigate.OutboundFlowsLookupFunction" />
+        </property>
+    </bean>
+
+    <bean id="SaveLogoutContext"
+          class="net.shibboleth.idp.session.impl.SaveLogoutContext" />
+
+    <bean id="FormOutboundMessage"
+        class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.FormRpInitiatedLogoutResponse" scope="prototype" />
+
+    <bean id="PopulateClientStorageSaveContext"
+        class="org.opensaml.storage.impl.client.PopulateClientStorageSaveContext" scope="prototype"
+        p:storageServices="#{ getObject('shibboleth.ClientStorageServices') ?: getObject('shibboleth.DefaultClientStorageServices') }" />
+
+    <bean id="ErrorViewPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.ErrorViewAuditExtractors') ?: getObject('shibboleth.DefaultErrorViewAuditExtractors')}" />
+
+    <bean id="MapEventToView" class="net.shibboleth.idp.profile.context.navigate.SpringEventToViewLookupFunction"
+        p:defaultView-ref="shibboleth.DefaultErrorView" p:eventMap="#{getObject('shibboleth.EventViewMap')}" />
+
+</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/end-session/end-session-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/end-session/end-session-flow.xml
new file mode 100644
index 00000000..9a43bc60
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/end-session/end-session-flow.xml
@@ -0,0 +1,274 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<flow xmlns="http://www.springframework.org/schema/webflow"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+      parent="oidc/abstract, oidc/metadata-lookup">
+
+    <action-state id="InitializeProfileRequestContext">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="PopulateMetricContext" />
+        <evaluate expression="FlowStartPopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="DecodeMessage" />
+    </action-state>
+
+    <action-state id="DecodeMessage">
+        <evaluate expression="DecodeMessage" />
+        <evaluate expression="PostDecodePopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        <!-- DoMetadataLookup is expected to proceed to SelectConfiguration -->
+        <transition on="proceed" to="DoMetadataLookup" />
+    </action-state>
+
+    <action-state id="SelectConfiguration">
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+        <evaluate expression="SelectProfileConfiguration" />
+        <evaluate expression="PostLookupPopulateAuditContext" />
+        <evaluate expression="PopulateInboundInterceptContext" />
+        <evaluate expression="PopulateUserAgentContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="CheckInboundInterceptContext" />
+    </action-state>
+
+    <decision-state id="CheckInboundInterceptContext">
+        <if test="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+            then="OutboundContextsAndSecurityParameters" else="DoInboundInterceptSubflow" />
+    </decision-state>
+
+    <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="OutboundContextsAndSecurityParameters" />
+    </subflow-state>
+
+    <action-state id="OutboundContextsAndSecurityParameters">
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="PopulateRpInitiatedLogoutContext" />
+        <!-- TODO <evaluate expression="PopulateIdTokenHintDecryptionParameters" /> -->
+        <evaluate expression="PopulateIdTokenHintSignatureValidationParameters" />
+        <!-- TODO add decrypt <evaluate expression="CheckClientJWTDecryptionConfiguration" />
+        <evaluate expression="DecryptIdTokenHint" />-->
+        <evaluate expression="ValidateIdTokenHintSignature" />
+        <!-- TODO add validators <evaluate expression="ValidateIdTokenHint" /> -->
+        <evaluate expression="ValidatePostLogoutRedirectURI" />
+        <evaluate expression="'proceed'"/>
+        <transition on="proceed" to="PopulateClientStorageLoadContext" />
+    </action-state>
+
+    <action-state id="PopulateClientStorageLoadContext">
+        <evaluate expression="PopulateClientStorageLoadContext" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="ClientStorageLoad" />
+        <transition on="NoLoadNeeded" to="DoLogoutRequest" />
+    </action-state>
+
+    <subflow-state id="ClientStorageLoad" subflow="client-storage/read">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="DoLogoutRequest" />
+    </subflow-state>
+
+    <action-state id="DoLogoutRequest">
+        <evaluate expression="ProcessRpInitiatedLogoutRequest" />
+        <evaluate expression="LogoutPopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CheckPromptCondition1">
+            <set name="flowScope.transitionAfterDestroy" value="'ContinueLogout'" />
+            <set name="flowScope.promptForIdP" value="UserPromptCondition.test(opensamlProfileRequestContext)" />
+        </transition>
+    </action-state>
+
+    <decision-state id="CheckPromptCondition1">
+        <if test="promptForIdP"
+            then="ContinueLogout" else="DestroySessions" />
+    </decision-state>
+
+    <action-state id="DestroySessions">
+        <evaluate expression="DestroySessions" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="PopulateClientStorageSaveContext" />
+    </action-state>
+
+    <!-- We may need to save client storage. -->
+
+    <action-state id="PopulateClientStorageSaveContext">
+        <evaluate expression="PopulateClientStorageSaveContext" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="ClientStorageSave" />
+        <transition on="NoSaveNeeded" to="#{transitionAfterDestroy}" />
+    </action-state>
+    
+    <subflow-state id="ClientStorageSave" subflow="client-storage/write">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="#{transitionAfterDestroy}"/>
+    </subflow-state>
+
+    <!-- Continue the logout process. -->
+
+    <action-state id="ContinueLogout">
+        <evaluate expression="SetIssuerRPUIInformation" />
+        <evaluate expression="PopulateMultiRPContextFromLogoutContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="NextRelyingPartyContext" />
+    </action-state>
+
+    <decision-state id="NextRelyingPartyContext">
+        <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.MultiRelyingPartyContext)).getRelyingPartyContextIterator().hasNext()"
+            then="SetRPUIInformation" else="LogoutView" />
+    </decision-state>
+    
+    <action-state id="SetRPUIInformation">
+        <on-entry>
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.profile.context.MultiRelyingPartyContext)).getRelyingPartyContextIterator().next()" />
+        </on-entry>
+        <evaluate expression="SetRPUIInformation" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="NextRelyingPartyContext" />
+    </action-state>
+
+    <view-state id="LogoutView" view="logout">
+        <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="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext))" result="viewScope.oidcRpInitiatedLogoutContext" />
+            <evaluate expression="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext)).getPostLogoutRedirectUri()" result="viewScope.postLogoutRedirectUri" />
+            <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="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null" result="viewScope.custom" />
+        </on-render>
+        
+        <transition on="proceed" to="PopulateOutboundInterceptContext" />
+        <transition on="local" to="DestroySessions">
+            <set name="flowScope.transitionAfterDestroy" value="'LogoutCompleteView'" />
+        </transition>
+        <transition on="propagate" to="CheckPromptCondition2">
+            <set name="flowScope.transitionAfterDestroy" value="'LogoutPropagateView'" />
+        </transition>
+        <transition on="end" to="LogoutCompleteView" />
+    </view-state>
+    
+    <decision-state id="CheckPromptCondition2">
+        <if test="promptForIdP"
+            then="DestroySessions" else="#{transitionAfterDestroy}" />
+    </decision-state>
+
+    <view-state id="LogoutPropagateView" view="logout-propagate">
+        <attribute name="csrf_excluded" value="true" type="boolean"/>
+        <on-render>
+            <evaluate expression="SaveLogoutContext" />
+            <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="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext))" result="viewScope.oidcRpInitiatedLogoutContext" />
+            <evaluate expression="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext)).getPostLogoutRedirectUri()" result="viewScope.postLogoutRedirectUri" />
+            <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.htmlEncoder" />
+            <evaluate expression="T(java.net.URLEncoder)" result="viewScope.urlEncoder" />
+            <evaluate expression="T(org.cryptacular.util.CodecUtil)" result="viewScope.codecUtil" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.LogoutPropagationFlowSelector')" result="viewScope.flowSelector" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null" result="viewScope.custom" />
+        </on-render>
+        
+        <transition on="proceed" to="PopulateOutboundInterceptContext" />
+    </view-state>
+
+    <end-state id="LogoutCompleteView" view="logout-complete">
+        <on-entry>
+            <evaluate expression="environment" result="viewScope.environment" />
+            <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.session.context.LogoutContext))" result="requestScope.logoutContext" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.profile.context.MultiRelyingPartyContext))" result="requestScope.multiRPContext" />
+            <evaluate expression="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext))" result="viewScope.oidcRpInitiatedLogoutContext" />
+            <evaluate expression="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext)).getPostLogoutRedirectUri()" result="viewScope.postLogoutRedirectUri" />
+            <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="requestScope.encoder" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="requestScope.request" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="requestScope.response" />
+            <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null" result="requestScope.custom" />
+        </on-entry>
+    </end-state>
+
+    <action-state id="PopulateOutboundInterceptContext">
+        <evaluate expression="PopulateOutboundInterceptContext" />
+        <evaluate expression="'proceed'" />
+    
+        <transition on="proceed" to="CheckOutboundInterceptContext" />
+        <transition to="HandleError" />
+    </action-state>
+
+    <decision-state id="CheckOutboundInterceptContext">
+        <if test="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
+            then="HandleOutboundMessage" else="DoOutboundInterceptSubflow" />
+    </decision-state>
+
+    <subflow-state id="DoOutboundInterceptSubflow" subflow="intercept">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="HandleOutboundMessage" />
+        <transition to="HandleError" />
+    </subflow-state>
+
+    <action-state id="HandleOutboundMessage">
+        <evaluate expression="FormOutboundMessage" />
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="CommitResponse" />
+    </action-state>
+
+    <!-- Error Response Generation -->
+
+    <!-- First we check if error is mapped as local audited error -->
+    <decision-state id="HandleError">
+        <if
+            test="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.LocalEventMap').containsKey(currentEvent.id) and flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.LocalEventMap').get(currentEvent.id)"
+            then="AuditedErrorView" else="ErrorView" />
+    </decision-state>
+
+    <end-state id="AuditedErrorView" view="#{MapEventToView.apply(currentEvent)}">
+        <on-entry>
+            <evaluate expression="ErrorViewPopulateAuditContext" />
+            <evaluate expression="WriteAuditLog" />
+            <evaluate expression="environment" result="requestScope.environment" />
+            <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
+            <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)"
+                result="requestScope.encoder" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()"
+                result="requestScope.request" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()"
+                result="requestScope.response" />
+            <evaluate
+                expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null"
+                result="requestScope.custom" />
+        </on-entry>
+        <exception-handler bean="RethrowingFlowExecutionExceptionHandler" />
+    </end-state>
+
+    <end-state id="ErrorView" view="#{MapEventToView.apply(currentEvent)}">
+        <on-entry>
+            <evaluate expression="environment" result="requestScope.environment" />
+            <evaluate expression="opensamlProfileRequestContext" result="requestScope.profileRequestContext" />
+            <evaluate expression="T(net.shibboleth.utilities.java.support.codec.HTMLEncoder)"
+                result="requestScope.encoder" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()"
+                result="requestScope.request" />
+            <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()"
+                result="requestScope.response" />
+            <evaluate
+                expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null"
+                result="requestScope.custom" />
+        </on-entry>
+        <exception-handler bean="RethrowingFlowExecutionExceptionHandler" />
+    </end-state>
+
+    <end-state id="end" />
+   
+    <bean-import resource="end-session-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 b0ec4c55..7b305bc0 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
@@ -74,7 +74,8 @@
 
     <bean id="OIDC.Logout" parent="AbstractOIDCProfile" lazy-init="true"
           class="net.shibboleth.oidc.profile.config.impl.DefaultOIDCLogoutConfiguration"
-          p:issuer-ref="shibboleth.oidc.issuer"/>
+          p:issuer-ref="shibboleth.oidc.issuer"
+          p:securityConfiguration-ref="%{idp.security.oidc.logout.config:shibboleth.oidc.logout.DefaultSecurityConfiguration}" />
         <!-- TODO: default values via configuration properties once the set is final -->
 
     <!-- Metadata-driven variants. -->
@@ -804,4 +805,64 @@
     -->
     <import resource="classpath*:/META-INF/net/shibboleth/idp/service/relying-party/oidc/**/postconfig.xml" />
 
+    <!-- Default security configuration for the logout profile configuration (OIDC.Logout) -->
+    <bean id="shibboleth.oidc.logout.DefaultSecurityConfiguration"
+        class="net.shibboleth.oidc.profile.config.JSONSecurityConfiguration">
+        <property name="jwtSignatureSigningConfiguration">
+            <ref bean="#{'%{idp.oidc.logout.signing.config:shibboleth.oidc.SigningConfiguration}'.trim()}" />
+        </property>
+        <property name="jwtEncryptionConfiguration">
+            <ref bean="#{'%{idp.oidc.logout.encryption.config:shibboleth.oidc.EncryptionConfiguration}'.trim()}" />
+        </property>
+        <property name="jwtDecryptionConfiguration">
+            <ref bean="#{'%{idp.oidc.logout.decryption.config:shibboleth.oidc.logout.DecryptionConfiguration}'.trim()}" />
+        </property>
+        <property name="jwtSignatureValidationConfiguration">
+            <ref bean="#{'%{idp.oidc.logout.validation.config:shibboleth.oidc.logout.SignatureValidationConfiguration}'.trim()}" />
+        </property>
+    </bean>
+
+    <bean id="shibboleth.oidc.logout.DecryptionConfiguration"
+        parent="shibboleth.oidc.DecryptionConfiguration"
+        p:KEKCredentialResolver-ref="defaultLogoutOIDCKeyDecryptionCredentialResolver"
+        p:contentEncryptionKeyCredentialResolver-ref="defaultLogoutOIDCContentDecryptionKeyCredentialResolver">
+    </bean>
+
+    <bean id="defaultLogoutOIDCKeyDecryptionCredentialResolver"
+        class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
+        <constructor-arg>
+            <list>
+                <bean id="ClientInformationCredentialResolver"
+                    class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
+                    c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
+                    c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
+            </list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="defaultLogoutOIDCContentDecryptionKeyCredentialResolver"
+        class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
+        <constructor-arg>
+            <list>
+                <bean id="ClientInformationCredentialResolver"
+                    class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
+                    c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
+                    c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
+            </list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="shibboleth.oidc.logout.SignatureValidationConfiguration"
+        parent="shibboleth.oidc.SignatureValidationConfiguration"
+        p:signatureTrustEngine-ref="ExplicitKeySignedJWTTrustEngineForLogout"/>
+
+    <bean id="ExplicitKeySignedJWTTrustEngineForLogout"
+        class="net.shibboleth.oidc.security.impl.ExplicitKeySignedJWTTrustEngine"
+        c:resolver-ref="defaultLogoutSignedJWTTrustedCredentialResolver"
+        c:JOSEObjectResolver-ref="defaultSignedJWTJOSEHeaderCredentialResolver" />
+
+    <bean id="defaultLogoutSignedJWTTrustedCredentialResolver"
+          class="net.shibboleth.oidc.security.credential.impl.ReturnAllCollectionJOSEObjectCredentialResolver"
+          c:credentials="#{getObject('shibboleth.oidc.SigningCredentialsToPublish') ?: getObject('shibboleth.oidc.SigningCredentialsFactory')}" />
+
 </beans>

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


More information about the commits mailing list