[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