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

Henri Mikkonen henri.mikkonen at iki.fi
Tue Oct 24 13:28:09 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=c88952db00a98b5128b98b0137c3abfc3b052703

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

commit c88952db00a98b5128b98b0137c3abfc3b052703
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue Oct 24 16:27:28 2023 +0300

    JOIDC-13 - Support for OIDC Logout
    
    https://shibboleth.atlassian.net/browse/JOIDC-13
    
    Added support for encrypted id_token_hint
    - Decryption can only be done via client secrets
    
    Added claims validator for id_token_hint claims
    - Issuer and audience are currently validated
    
    Improved flow tests for end-session flow: also JWS and JWE now covered.
---
 .../LogoutRequestClientIDLookupFunction.java       |  15 +-
 .../ProcessedIdTokenHintUpdateStrategy.java        |  47 +++
 .../logout/profile/impl/ValidateIdTokenHint.java   | 106 ++++++
 .../flows/oidc/end-session/end-session-beans.xml   |  97 ++++--
 .../flows/oidc/end-session/end-session-flow.xml    |   8 +-
 .../idp/service/relying-party/postconfig.xml       |  33 +-
 .../flow/AbstractIssuedJWTSecurityTest.java        |   8 +-
 .../oidc/op/profile/flow/EndSessionFlowTest.java   |  80 ++++-
 .../oidc/op/profile/flow/IdTokenHintJWETest.java   | 369 +++++++++++++++++++++
 .../oidc/op/profile/flow/IdTokenHintJWSTest.java   | 212 ++++++++++++
 .../shibboleth/idp/module/conf/relying-party.xml   |   7 +
 11 files changed, 918 insertions(+), 64 deletions(-)

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
index 08c425ff..3f97017b 100644
--- 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
@@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
 
 import com.nimbusds.jwt.EncryptedJWT;
 import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.openid.connect.sdk.LogoutRequest;
 
@@ -52,12 +53,12 @@ public class LogoutRequestClientIDLookupFunction implements ContextDataLookupFun
         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 (requestClientId == null && idTokenHint instanceof EncryptedJWT) {
+                log.error("id_token_hint is encrypted and no client_id found in request, no client_id resolved");
+                return null;
+            }
+            if (idTokenHint instanceof SignedJWT signedJwt) {
+                final String idTokenClientId = getClientIdFromJwt(signedJwt);
                 if (idTokenClientId == null) {
                     return null;
                 }
@@ -77,7 +78,7 @@ public class LogoutRequestClientIDLookupFunction implements ContextDataLookupFun
         return null;
     }
 
-    @Nullable protected String getClientIdFromJwt(final JWT jwt) {
+    @Nullable protected String getClientIdFromJwt(final SignedJWT jwt) {
         try {
             final List<String> audience = jwt.getJWTClaimsSet().getAudience();
             if (audience == null || audience.isEmpty()) {
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ProcessedIdTokenHintUpdateStrategy.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ProcessedIdTokenHintUpdateStrategy.java
new file mode 100644
index 00000000..ec485c08
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ProcessedIdTokenHintUpdateStrategy.java
@@ -0,0 +1,47 @@
+/*
+ * 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.util.function.BiConsumer;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWT;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext;
+
+/**
+ * Add the {@link JWT} back to the processed ID token hint in the {@link OIDCRpInitiatedLogoutContext}.
+ * 
+ * @since 4.1.0
+ */
+public class ProcessedIdTokenHintUpdateStrategy implements BiConsumer<ProfileRequestContext, JWT> {
+    
+    /** {@inheritDoc} */
+    @Override
+    public void accept(@Nullable final ProfileRequestContext profileRequestContext, @Nullable final JWT jwt) {
+        if (profileRequestContext == null || profileRequestContext.getOutboundMessageContext() == null
+                || profileRequestContext.getOutboundMessageContext().getSubcontext(
+                        OIDCRpInitiatedLogoutContext.class) == null) {
+            return;
+        }
+        final OIDCRpInitiatedLogoutContext oidcResponseCtx = profileRequestContext
+                .getOutboundMessageContext()
+                .getSubcontext(OIDCRpInitiatedLogoutContext.class);
+        oidcResponseCtx.setProcessedIdTokenHint(jwt);
+    }
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidateIdTokenHint.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidateIdTokenHint.java
new file mode 100644
index 00000000..7045a860
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ValidateIdTokenHint.java
@@ -0,0 +1,106 @@
+/*
+ * 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.text.ParseException;
+
+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 com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+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;
+
+/**
+ * Action validates ID token hint in response context.
+ */
+public class ValidateIdTokenHint extends AbstractOIDCRpInitiatedLogoutAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ValidateIdTokenHint.class);
+
+    /** ID token hint. */
+    @Nullable private JWT idTokenHint;
+
+    /** The claims validator to be applied for validating the ID tokem hint. */
+    @NonnullAfterInit private ClaimsValidator claimsValidator;
+
+    /**
+     * Set the claims validator used for validating the ID token hint.
+     * 
+     * @param validator What to set
+     */
+    public void setClaimsValidator(@Nonnull final ClaimsValidator validator) {
+        checkSetterPreconditions();
+
+        claimsValidator = Constraint.isNotNull(validator, "Claims validator cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (claimsValidator == null) {
+            throw new ComponentInitializationException("ClaimsValidator for ID token hints cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        idTokenHint = getRpInitiatedLogoutContext().getProcessedIdTokenHint();
+        if (idTokenHint == null) {
+            log.debug("{} No ID token hint, nothing to do", getLogPrefix());
+            return false;
+        }
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!(idTokenHint instanceof SignedJWT)) {
+            log.error("{} ID token hint is not signed", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ID_TOKEN_HINT);
+            return;
+        }
+        try {
+            claimsValidator.validate(idTokenHint.getJWTClaimsSet(), profileRequestContext);
+        } catch (final JWTValidationException | ParseException e) {
+            log.warn("{} JWT validation failed: {}", getLogPrefix(), e.getMessage());
+            ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ID_TOKEN_HINT);
+            return;
+        }
+
+    }    
+
+}
\ No newline at end of file
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
index d401e520..08c7d336 100644
--- 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
@@ -44,6 +44,17 @@
         class="org.opensaml.storage.impl.client.PopulateClientStorageLoadContext" scope="prototype"
         p:storageServices="#{ getObject('shibboleth.ClientStorageServices') ?: getObject('shibboleth.DefaultClientStorageServices') }" />
 
+    <bean id="PopulateIdTokenHintDecryptionParameters"
+        class="net.shibboleth.oidc.profile.impl.PopulateJWTDecryptionParameters" scope="prototype"
+        p:configurationLookupStrategy-ref="DecryptionConfigurationLookup"
+        p:decryptionParametersResolver-ref="JWTDecryptionParametersResolver" />
+
+    <bean id="DecryptionConfigurationLookup" lazy-init="true"
+        class="net.shibboleth.oidc.profile.config.navigate.JWTDecryptionConfigurationLookupFunction" />
+
+    <bean id="JWTDecryptionParametersResolver"
+        class="net.shibboleth.oidc.security.jose.impl.DefaultDecryptionParametersResolver" />
+
     <bean id="PopulateIdTokenHintSignatureValidationParameters"
             class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureValidationParameters"
             scope="prototype"
@@ -60,6 +71,58 @@
         </property>
     </bean>
 
+    <bean id="CheckClientJWTDecryptionConfiguration"
+        class="net.shibboleth.oidc.security.impl.CheckClientJWTDecryptionConfiguration" scope="prototype">
+        <property name="jwtTokenLookupStrategy">
+            <bean
+                class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                c:_0="#{ T(org.opensaml.profile.context.ProfileRequestContext) }"
+                c:outputType="#{T(com.nimbusds.jwt.JWT)}"
+                c:expression="#input.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext)).getRequestedIdTokenHint()" />
+        </property>
+        <property name="clientInformationLookupStrategy">
+            <bean
+                class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                c:_0="#{ T(org.opensaml.profile.context.ProfileRequestContext) }"
+                c:expression="#input.getInboundMessageContext().getSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext)).getClientInformation()" />
+        </property>
+        <property name="encryptionOptionalPredicate">
+            <bean class="net.shibboleth.oidc.profile.config.logic.EncryptionOptionalPredicate"/>
+        </property>
+        <property name="keyTransportEncryptionAlgorithmLookupStrategy">
+            <bean
+                class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                c:keyName="id_token_encrypted_response_alg"/>
+        </property>
+        <property name="dataEncryptionAlgorithmLookupStrategy">
+            <bean
+                class="net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction"
+                c:keyName="id_token_encrypted_response_enc"/>
+        </property>
+        <property name="errorEventId"
+            value="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_ID_TOKEN_HINT}"/>
+    </bean>
+
+    <bean id="IdTokenHintEncryptedCondition" parent="shibboleth.Conditions.Expression"
+        c:expression="#input.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext)).getRequestedIdTokenHint() instanceof T(com.nimbusds.jwt.EncryptedJWT)" />
+
+    <bean id="DecryptIdTokenHint" class="net.shibboleth.oidc.security.impl.DecryptJWE" scope="prototype">
+        <property name="jwtTokenLookupStrategy">
+            <bean class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+                c:_0="#{ T(org.opensaml.profile.context.ProfileRequestContext) }"
+                c:outputType="#{T(com.nimbusds.jwt.EncryptedJWT)}"
+                c:expression="#input.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCRpInitiatedLogoutContext)).getRequestedIdTokenHint()" />
+        </property>
+        <property name="jwtUpdateStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ProcessedIdTokenHintUpdateStrategy" />
+        </property>
+        <property name="errorEventId"
+            value="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_ID_TOKEN_HINT}"/>
+        <property name="activationCondition">
+            <ref bean="IdTokenHintEncryptedCondition" />
+        </property>
+    </bean>
+
     <bean id="ValidateIdTokenHintSignature" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
         scope="prototype" c:executionDirection="INBOUND">
         <constructor-arg>
@@ -113,35 +176,22 @@
         </property>
     </bean>
 
-    <bean id="ValidateRequestObject" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateRequestObject"
+    <bean id="ValidateIdTokenHint" class="net.shibboleth.idp.plugin.oidc.op.logout.profile.impl.ValidateIdTokenHint"
         scope="prototype"
-        p:plainClaimsValidator="#{getObject('shibboleth.oidc.PlainRequestObjectClaimsValidation') ?: getObject('shibboleth.oidc.DefaultPlainRequestObjectClaimsValidation')}"
-        p:signedClaimsValidator="#{getObject('shibboleth.oidc.SignedRequestObjectClaimsValidation') ?: getObject('shibboleth.oidc.DefaultSignedRequestObjectClaimsValidation')}">
+        p:claimsValidator="#{getObject('shibboleth.oidc.logout.IdTokenHintClaimsValidation') ?: getObject('shibboleth.oidc.logout.DefaultIdTokenHintClaimsValidation')}">
     </bean>
 
-    <bean id="shibboleth.oidc.DefaultPlainRequestObjectClaimsValidation"
-        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
-        p:claimValidators-ref="PlainClaimsValidators" />
-
-    <bean id="shibboleth.oidc.DefaultSignedRequestObjectClaimsValidation"
+    <bean id="shibboleth.oidc.logout.DefaultIdTokenHintClaimsValidation"
         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" />
+                c:expression="#custom.apply(#input1)"
+                p:customObject-ref="shibboleth.ResponderIdLookup.Simple" />
         </property>
     </bean>
 
@@ -149,19 +199,12 @@
         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" />
+                c:expression="#custom.apply(#input1.getInboundMessageContext()) == null ? null : #custom.apply(#input1.getInboundMessageContext()).toString()"
+                p:customObject-ref="shibboleth.ClientIDLookupStrategy" />
         </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>
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
index 9a43bc60..5fb4815c 100644
--- 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
@@ -44,12 +44,12 @@
     <action-state id="OutboundContextsAndSecurityParameters">
         <evaluate expression="InitializeOutboundMessageContext" />
         <evaluate expression="PopulateRpInitiatedLogoutContext" />
-        <!-- TODO <evaluate expression="PopulateIdTokenHintDecryptionParameters" /> -->
+        <evaluate expression="PopulateIdTokenHintDecryptionParameters" />
         <evaluate expression="PopulateIdTokenHintSignatureValidationParameters" />
-        <!-- TODO add decrypt <evaluate expression="CheckClientJWTDecryptionConfiguration" />
-        <evaluate expression="DecryptIdTokenHint" />-->
+        <evaluate expression="CheckClientJWTDecryptionConfiguration" />
+        <evaluate expression="DecryptIdTokenHint" />
         <evaluate expression="ValidateIdTokenHintSignature" />
-        <!-- TODO add validators <evaluate expression="ValidateIdTokenHint" /> -->
+        <evaluate expression="ValidateIdTokenHint" />
         <evaluate expression="ValidatePostLogoutRedirectURI" />
         <evaluate expression="'proceed'"/>
         <transition on="proceed" to="PopulateClientStorageLoadContext" />
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 98fa2e60..b64bec47 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
@@ -833,28 +833,10 @@
     </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>
+        class="net.shibboleth.oidc.security.credential.impl.ClientInformationClientSecretCredentialResolver"/>
 
     <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>
+        class="net.shibboleth.oidc.security.credential.impl.ClientInformationClientSecretCredentialResolver"/>
 
     <bean id="shibboleth.oidc.logout.SignatureValidationConfiguration"
         parent="shibboleth.oidc.SignatureValidationConfiguration"
@@ -866,7 +848,14 @@
         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')}" />
+        class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
+        <constructor-arg name="resolverChain">
+            <list>
+                <bean class="net.shibboleth.oidc.security.credential.impl.ClientInformationClientSecretCredentialResolver"/>
+                <bean class="net.shibboleth.oidc.security.credential.impl.ReturnAllCollectionJOSEObjectCredentialResolver"
+                    c:credentials="#{getObject('shibboleth.oidc.SigningCredentialsToPublish') ?: getObject('shibboleth.oidc.SigningCredentialsFactory')}" />
+            </list>
+        </constructor-arg>
+    </bean>
 
 </beans>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractIssuedJWTSecurityTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractIssuedJWTSecurityTest.java
index daac399c..4c18314b 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractIssuedJWTSecurityTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractIssuedJWTSecurityTest.java
@@ -29,6 +29,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import org.springframework.test.context.ContextConfiguration;
 import org.springframework.webflow.executor.FlowExecutionResult;
 import org.testng.Assert;
 
@@ -68,6 +69,9 @@ import net.shibboleth.shared.collection.Pair;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.security.DataSealerException;
 
+ at ContextConfiguration(
+        initializers = {
+                PrependTestEnvironmentApplicationContextInitializer.class})
 public abstract class AbstractIssuedJWTSecurityTest extends AbstractOidcFlowTest {
 
     String defaultClientId = "mockClientId";
@@ -88,7 +92,9 @@ public abstract class AbstractIssuedJWTSecurityTest extends AbstractOidcFlowTest
         
         AUTHORIZE_ACCESS_TOKEN,
         
-        REQUEST_OBJECT
+        REQUEST_OBJECT,
+        
+        ID_TOKEN_HINT
     }
 
     protected final JWT_FETCHING_TYPE fetchingType;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java
index c10b5116..5df919a8 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/EndSessionFlowTest.java
@@ -36,6 +36,7 @@ import org.springframework.webflow.test.MockParameterMap;
 import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Factory;
 import org.testng.annotations.Test;
 
 import com.nimbusds.jose.JOSEException;
@@ -194,6 +195,54 @@ public class EndSessionFlowTest extends AbstractOidcFlowTest {
         Assert.assertEquals("ErrorView", result.getOutcome().getId());
     }
 
+    @Test
+    public void testWithInvalidIdTokenHint_wrongAudience() {
+        final SignedJWT idTokenHint;
+        try {
+            idTokenHint = createPrivateKeyJWT(JWTClaimsSet.parse(getWrongIssuerIdTokenHintPayload()),
+                    (RSAPrivateKey) loadRSSigningCredential().getPrivateKey(), JWSAlgorithm.RS256);
+        } catch (JOSEException | ParseException e) {
+            Assert.fail();
+            return;
+        }
+        setRequestParameters(List.of(new Pair<>("id_token_hint", idTokenHint.serialize())));
+        request.setMethod("GET");
+        storeMetadata(storageService, clientId, clientSecret, postLogoutRedirectUri);
+
+        initializeThreadLocals();
+
+        final IdPSession session = buildIdPSessionWithDefaultSP();
+        request.setCookies(response.getCookies());
+        Assert.assertTrue(isSessionValid(session));
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        Assert.assertEquals("ErrorView", result.getOutcome().getId());
+    }
+
+    @Test
+    public void testWithInvalidIdTokenHint_wrongIssuer() {
+        final SignedJWT idTokenHint;
+        try {
+            idTokenHint = createPrivateKeyJWT(JWTClaimsSet.parse(getWrongIssuerIdTokenHintPayload()),
+                    (RSAPrivateKey) loadRSSigningCredential().getPrivateKey(), JWSAlgorithm.RS256);
+        } catch (JOSEException | ParseException e) {
+            Assert.fail();
+            return;
+        }
+        setRequestParameters(List.of(new Pair<>("id_token_hint", idTokenHint.serialize())));
+        request.setMethod("GET");
+        storeMetadata(storageService, clientId, clientSecret, postLogoutRedirectUri);
+
+        initializeThreadLocals();
+
+        final IdPSession session = buildIdPSessionWithDefaultSP();
+        request.setCookies(response.getCookies());
+        Assert.assertTrue(isSessionValid(session));
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        Assert.assertEquals("ErrorView", result.getOutcome().getId());
+    }
+
     @Test
     public void testWithValidIdTokenHint_noRedirection() {
         final SignedJWT idTokenHint;
@@ -281,8 +330,17 @@ public class EndSessionFlowTest extends AbstractOidcFlowTest {
         Assert.assertTrue(result2.isEnded());
     }
 
+    @Factory
+    public Object[] createRequestObjectSecurityTests() {
+        return new Object[] {
+                new IdTokenHintJWSTest(),
+                new IdTokenHintJWETest(true),
+                new IdTokenHintJWETest(false)
+        };
+    }
+
     protected IdPSession buildIdPSessionWithDefaultSP() {
-       return buildIdPSession(new OIDCRPSession.Builder()
+       return buildIdPSession(sessionManager, new OIDCRPSession.Builder()
                .serviceId(clientId)
                .issuer(issuer)
                .creationInstant(Instant.now())
@@ -293,7 +351,8 @@ public class EndSessionFlowTest extends AbstractOidcFlowTest {
                .build());
     }
 
-    protected IdPSession buildIdPSession(SPSession... sessions) {
+    protected static IdPSession buildIdPSession(final StorageBackedSessionManager sessionManager,
+            final SPSession... sessions) {
         try {
             final IdPSession idpSession = sessionManager.createSession("mockSessionPrincipal");
             for (final SPSession session : sessions) {
@@ -330,6 +389,11 @@ public class EndSessionFlowTest extends AbstractOidcFlowTest {
     }
 
     protected boolean isSessionValid(final IdPSession session) {
+        return isSessionValid(sessionManager, session);
+    }
+
+    protected static boolean isSessionValid(final StorageBackedSessionManager sessionManager,
+            final IdPSession session) {
         try {
             return sessionManager.resolveSingle(new CriteriaSet(new SessionIdCriterion(session.getId()))) != null;
         } catch (ResolverException e) {
@@ -342,7 +406,17 @@ public class EndSessionFlowTest extends AbstractOidcFlowTest {
         return getIdTokenHintPayload(issuer, subject, clientId, Instant.now().plusSeconds(300), Instant.now(),
                 sessionId);
     }
-    
+
+    protected String getWrongAudienceIdTokenHintPayload() {
+        return getIdTokenHintPayload(issuer, subject, clientId + "2", Instant.now().plusSeconds(300), Instant.now(),
+                sessionId);
+    }
+
+    protected String getWrongIssuerIdTokenHintPayload() {
+        return getIdTokenHintPayload(issuer + "2", subject, clientId, Instant.now().plusSeconds(300), Instant.now(),
+                sessionId);
+    }
+
     protected static String getIdTokenHintPayload(final String issuer, final String subject, final String clientId,
             final Instant exp, final Instant iat, final String sid) {
         return "{\n"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IdTokenHintJWETest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IdTokenHintJWETest.java
new file mode 100644
index 00000000..b19b93db
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IdTokenHintJWETest.java
@@ -0,0 +1,369 @@
+/*
+ * 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.flow;
+
+import java.io.IOException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.springframework.webflow.test.MockParameterMap;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.impl.StorageBackedSessionManager;
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.shared.collection.Pair;
+
+public class IdTokenHintJWETest extends IssuedEncryptedJWTTest {
+
+    String issuer = "https://op.example.org";
+    String sessionId = "mockSessionId";
+    String subject = "mockSubject";
+    String defaultClientIdEncryptionEnforced = "mockClientIdLogoutEncryptionEnforced";
+
+    @Autowired
+    @Qualifier("shibboleth.SessionManager")
+    StorageBackedSessionManager sessionManager;
+
+    public IdTokenHintJWETest(final boolean encryptionOptional) {
+        super(JWT_FETCHING_TYPE.ID_TOKEN_HINT, EndSessionFlowTest.FLOW_ID, true, encryptionOptional);
+    }
+
+    @Override @Test
+    public void testJwtEncryption_noSigAlgNorEncSpecified() throws Exception {
+        final JWT jwt = obtainIdTokenHint(null, null, null, null, null, null);
+        if (encryptionOptional) {
+            assertSuccessIdTokenHintResponse(jwt.serialize(), null, null, null, defaultClientSecret64B, null);
+        } else {
+            assertErrorIdTokenHintResponse(jwt.serialize(), null, null, null, defaultClientSecret64B, null);            
+        }
+    }
+
+    @Test
+    public void testJwtEncryption_noSigAlgNorEncSpecified_noIdTokenHint() throws Exception {
+        assertErrorIdTokenHintResponse("", null, null, null, defaultClientSecret64B, null);
+    }
+
+    @Test
+    public void testJwtEncryption_noSigAlgNorEncSpecified_signedIdTokenHint() throws Exception {
+        final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, null, null, JWSAlgorithm.HS256, null, null);
+        if (encryptionOptional) {
+            assertSuccessIdTokenHintResponse(jwt.serialize(), null, null, null, defaultClientSecret64B, null);
+        } else {
+            assertErrorIdTokenHintResponse(jwt.serialize(), null, null, null, defaultClientSecret64B, null);
+        }
+    }
+
+    @Test
+    public void testJwtEncryption_noSigAlgNorEncSpecified_encryptedIdTokenHint() throws Exception {
+        for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+            for (final JWEAlgorithm jwe : JWE_ALGORITHMS) {
+                for (final EncryptionMethod method : ENCRYPTION_METHODS) {
+                    final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, getProviderEncryptionKey(jwe),
+                            getSigningKey(jwsAlgorithm), jwsAlgorithm, jwe, method);
+                    if (JWEAlgorithm.Family.ASYMMETRIC.contains(jwe)) {
+                        assertErrorIdTokenHintResponse(jwt.serialize(), jwsAlgorithm, jwe, method,
+                                defaultClientSecret64B, getSignatureVerificationKey(jwsAlgorithm));
+                    } else {
+                        assertSuccessIdTokenHintResponse(jwt.serialize(), jwsAlgorithm, jwe, method,
+                                defaultClientSecret64B, getSignatureVerificationKey(jwsAlgorithm));                        
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testIdTokenHintEncryption_onlySigAlgNoEncSpecified() throws Exception {
+        final JWT jwt = obtainIdTokenHint(null, null, rsaPrivateKey, JWSAlgorithm.RS256, null, null);
+        if (encryptionOptional) {
+            assertSuccessIdTokenHintResponse(jwt.serialize(), JWSAlgorithm.RS256, null, null, null, rsaPublicKey);
+        } else {
+            assertErrorIdTokenHintResponse(jwt.serialize(), JWSAlgorithm.RS256, null, null, null, rsaPublicKey);
+        }
+    }
+
+    protected void assertSecretBasedEncryption(final JWEAlgorithm jweAlgorithm, final EncryptionMethod method) {
+        for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+            final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, null, getSigningKey(jwsAlgorithm),
+                    jwsAlgorithm, jweAlgorithm, method);
+            assertSuccessIdTokenHintResponse(jwt.serialize(), jwsAlgorithm, jweAlgorithm, method,
+                    defaultClientSecret64B, getSignatureVerificationKey(jwsAlgorithm));
+        }
+    }
+    
+    protected PublicKey getProviderEncryptionKeyViaKeyType(final PublicKey publicKey) {
+        if (publicKey instanceof ECPublicKey) {
+            return loadCredential("/credentials/idp-encryption-ec.jwk").getPublicKey();
+        } else {
+            return loadEncryptionCredential().getPublicKey();
+        }
+    }
+
+    protected PublicKey getRandomEncryptionKey(final JWEAlgorithm jweAlgorithm) {
+        if (JWEAlgorithm.Family.ECDH_ES.contains(jweAlgorithm)) {
+            try {
+                return ecKey.toPublicKey();
+            } catch (JOSEException e) {
+                Assert.fail("Could not obtain a public key from the ECKey object", e);
+            }
+        }
+        return rsaPublicKey;
+    }
+
+    protected PublicKey getProviderEncryptionKey(final JWEAlgorithm jweAlgorithm) {
+        if (JWEAlgorithm.Family.ECDH_ES.contains(jweAlgorithm)) {
+            return loadCredential("/credentials/idp-encryption-ec.jwk").getPublicKey();
+        }
+        return loadEncryptionCredential().getPublicKey();
+    }
+
+    @Override
+    protected void assertPublicKeyBasedEncryption(final PublicKey publicKey, final PrivateKey privateKey,
+            final JWEAlgorithm jweAlgorithm, final EncryptionMethod method) {
+        final PublicKey encryptionKey = getProviderEncryptionKeyViaKeyType(publicKey);
+        for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+            final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, encryptionKey, getSigningKey(jwsAlgorithm),
+                    jwsAlgorithm, jweAlgorithm, method);
+            assertErrorIdTokenHintResponse(jwt.serialize(), jwsAlgorithm, jweAlgorithm, method,
+                    defaultClientSecret64B, getSignatureVerificationKey(jwsAlgorithm));
+        }
+    }
+    
+    @Override
+    protected void assertNoSymmetricKeyResponse(final JWEAlgorithm jweAlgorithm, final EncryptionMethod method)
+            throws Exception {
+        final PublicKey encryptionKey = getProviderEncryptionKey(jweAlgorithm);
+        if (testSignedJwt) {
+            for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+                final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, rsaPublicKey, getSigningKey(jwsAlgorithm),
+                        jwsAlgorithm, jweAlgorithm, method);
+                Assert.assertNotNull(jwt, "The JWT could not be obtained with JWS alg " + jwsAlgorithm);
+                assertErrorIdTokenHintResponse(jwt.serialize(), jwsAlgorithm, jweAlgorithm, method, null,
+                        getSignatureVerificationKey(jwsAlgorithm));
+            }
+        } else {
+            final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, encryptionKey, rsaPrivateKey, null,
+                    jweAlgorithm, method);
+            assertErrorIdTokenHintResponse(jwt.serialize(),
+                    null, jweAlgorithm, method, null, encryptionKey);
+        }
+    }
+
+    @Override
+    protected void assertExcludedAlgorithm(final JWEAlgorithm jweAlgorithm, final EncryptionMethod method) {
+        if (testSignedJwt) {
+            for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+                final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, getProviderEncryptionKey(jweAlgorithm),
+                        getSigningKey(jwsAlgorithm), jwsAlgorithm, jweAlgorithm, method);
+                assertErrorIdTokenHintResponse(jwt.serialize(), jwsAlgorithm, jweAlgorithm, method,
+                        defaultClientSecret64B, getSignatureVerificationKey(jwsAlgorithm));
+            }
+        } else {
+            final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, getProviderEncryptionKey(jweAlgorithm), null,
+                    null, jweAlgorithm, method);
+            Assert.assertTrue(jwt instanceof EncryptedJWT, "Was not encrypted " + jweAlgorithm);
+            assertErrorIdTokenHintResponse(jwt.serialize(), null, jweAlgorithm, method, defaultClientSecret64B,
+                    rsaPublicKey);
+        }
+    }
+    
+    @Override
+    protected void assertNoPublicKeyResponse(final JWEAlgorithm jweAlgorithm, final EncryptionMethod method)
+ {
+        final PublicKey encryptionKey = getRandomEncryptionKey(jweAlgorithm);
+        if (testSignedJwt) {
+            for (final JWSAlgorithm jwsAlgorithm : JWS_ALGORITHMS) {
+                final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, encryptionKey, getSigningKey(jwsAlgorithm),
+                        jwsAlgorithm, jweAlgorithm, method);
+                assertErrorIdTokenHintResponse(jwt.serialize(),
+                            jwsAlgorithm, jweAlgorithm, method, defaultClientSecret64B, null);
+            }
+        } else {
+            final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, encryptionKey, rsaPrivateKey, null,
+                    jweAlgorithm, method);
+            assertErrorIdTokenHintResponse(jwt.serialize(), null, jweAlgorithm, method, defaultClientSecret64B, null);
+        }
+    }
+
+    protected void assertErrorIdTokenHintResponse(final String idTokenHint,
+            final JWSAlgorithm idTokenHintSigAlg, final JWEAlgorithm idTokenHintEncAlg,
+            final EncryptionMethod idTokenHintEncMethod, final String clientSecret, final PublicKey publicKey) {
+        request.setMethod("GET");
+        final String clientId = encryptionOptional ? defaultClientId : defaultClientIdEncryptionEnforced;
+
+        EndSessionFlowTest.setRequestParameters(request, List.of(
+                new Pair<>("id_token_hint", idTokenHint),
+                new Pair<>("client_id", clientId)));
+        initializeThreadLocals();
+
+        final IdPSession session = EndSessionFlowTest.buildIdPSession(sessionManager, new OIDCRPSession.Builder()
+                .serviceId(clientId)
+                .issuer(issuer)
+                .creationInstant(Instant.now())
+                .expirationInstant(Instant.now().plusSeconds(300))
+                .rootTokenIdentifier("mockRootId")
+                .sessionIdentifier(sessionId)
+                .subject(subject)
+                .build());
+        request.setCookies(response.getCookies());
+        Assert.assertTrue(EndSessionFlowTest.isSessionValid(sessionManager, session));
+
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setScope(new Scope("openid"));
+        metadata.setIDTokenJWSAlg(idTokenHintSigAlg);
+        metadata.setIDTokenJWEAlg(idTokenHintEncAlg);
+        metadata.setIDTokenJWEEnc(idTokenHintEncMethod);
+        if (publicKey != null) {
+            metadata.setJWKSet(super.buildJWKSet(publicKey));
+        }
+        try {
+            storeMetadataObject(storageService, clientId, clientSecret, metadata);
+            final FlowExecutionResult result = flowExecutor.launchExecution(flowId, null, externalContext);
+            removeMetadata(storageService, clientId);
+            Assert.assertEquals(result.getOutcome().getId(), "ErrorView");
+        } catch (final IOException e) {
+            Assert.fail();
+        }
+    }
+    
+    protected void assertSuccessIdTokenHintResponse(final String idTokenHint,
+            final JWSAlgorithm idTokenHintSigAlg, final JWEAlgorithm idTokenHintEncAlg,
+            final EncryptionMethod idTokenHintEncMethod, final String clientSecret, final PublicKey publicKey) {
+        request.setMethod("GET");
+        final String clientId = encryptionOptional ? defaultClientId : defaultClientIdEncryptionEnforced;
+        EndSessionFlowTest.setRequestParameters(request, List.of(
+                new Pair<>("id_token_hint", idTokenHint),
+                new Pair<>("client_id", clientId)));
+        initializeThreadLocals();
+
+        final IdPSession session = EndSessionFlowTest.buildIdPSession(sessionManager, new OIDCRPSession.Builder()
+                .serviceId(clientId)
+                .issuer(issuer)
+                .creationInstant(Instant.now())
+                .expirationInstant(Instant.now().plusSeconds(300))
+                .rootTokenIdentifier("mockRootId")
+                .sessionIdentifier(sessionId)
+                .subject(subject)
+                .build());
+        request.setCookies(response.getCookies());
+        Assert.assertTrue(EndSessionFlowTest.isSessionValid(sessionManager, session));
+
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setScope(new Scope("openid"));
+        metadata.setIDTokenJWSAlg(idTokenHintSigAlg);
+        metadata.setIDTokenJWEAlg(idTokenHintEncAlg);
+        metadata.setIDTokenJWEEnc(idTokenHintEncMethod);
+        if (publicKey != null) {
+            metadata.setJWKSet(super.buildJWKSet(publicKey));
+        }
+        final FlowExecutionResult result;
+        try {
+            storeMetadataObject(storageService, clientId, clientSecret, metadata);
+            result = flowExecutor.launchExecution(flowId, null, externalContext);
+            removeMetadata(storageService, clientId);
+        } catch (final IOException e) {
+            Assert.fail();
+            return;
+        }
+
+        Assert.assertFalse(result.isEnded());
+        Assert.assertFalse(EndSessionFlowTest.isSessionValid(sessionManager, session));
+        
+        ((MockParameterMap) externalContext.getRequestParameterMap()).put("_eventId", "proceed");
+        final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+        Assert.assertEquals(response.getStatus(), 200);
+        Assert.assertTrue(result2.isEnded());
+    }
+
+    protected void assertEncryptedSignedJwt(final JWT jwt, final JWSAlgorithm jwsAlg, final JWEAlgorithm jweAlg,
+            final EncryptionMethod method, final String clientSecret, final PrivateKey privateKey,
+            final PublicKey publicKey, final PublicKey jwsValidationKey) {
+        assertSuccessIdTokenHintResponse(jwt.serialize(), jwsAlg, jweAlg, method, clientSecret, jwsValidationKey);
+    }
+
+    protected void assertSignedJwt(final JWT jwt, final JWSAlgorithm algorithm, final PublicKey publicKey,
+            final String clientSecret) {
+        assertSuccessIdTokenHintResponse(jwt.serialize(), algorithm, null, null, clientSecret, publicKey);
+    }
+
+    protected JWT obtainIdTokenHint(final String clientSecret, final PublicKey publicKey,
+            final PrivateKey signingKey, final JWSAlgorithm storedJwsAlgorithm, final JWEAlgorithm storedJweAlgorithm,
+            final EncryptionMethod storedJweMethod) {
+        final String clientId = encryptionOptional ? defaultClientId : defaultClientIdEncryptionEnforced;
+        final String payload = EndSessionFlowTest.getIdTokenHintPayload(issuer, subject, clientId, Instant.now().plusSeconds(300), Instant.now(), sessionId);
+        final JWT jwt = processJwsForIdTokenHint(storedJwsAlgorithm, payload, clientSecret, getSigningKey(storedJwsAlgorithm));
+        
+        try {
+            if (storedJweAlgorithm != null) {
+                if (publicKey != null) {
+                    final BasicJWKCredential credential = new BasicJWKCredential();
+                    credential.setPublicKey(publicKey);
+                    return createEncryptedJWT(jwt.serialize(), storedJweAlgorithm, storedJweMethod, credential,
+                        clientSecret);
+                } else if (clientSecret != null) {
+                    return createEncryptedJWT(jwt.serialize(), storedJweAlgorithm, storedJweMethod, null, clientSecret,
+                            false);
+                }
+            }
+        } catch (JOSEException | ParseException e) {
+            Assert.fail("Could not encrypt the JWT", e);
+        }
+        return jwt;
+    }
+
+    protected static JWT processJwsForIdTokenHint(final JWSAlgorithm storedJwsAlgorithm, final String payload,
+            final String clientSecret, final PrivateKey signingKey) {
+        try {
+            if (storedJwsAlgorithm == null) {
+                return createPrivateKeyJWT(JWTClaimsSet.parse(payload), (RSAPrivateKey) signingKey, JWSAlgorithm.RS256);
+            } else if (JWSAlgorithm.Family.EC.contains(storedJwsAlgorithm)) {
+                return createPrivateKeyJWT(JWTClaimsSet.parse(payload), (ECPrivateKey) signingKey, storedJwsAlgorithm);
+            } else if (JWSAlgorithm.Family.RSA.contains(storedJwsAlgorithm)) {
+                return createPrivateKeyJWT(JWTClaimsSet.parse(payload), (RSAPrivateKey) signingKey, storedJwsAlgorithm);
+            } else if (JWSAlgorithm.Family.HMAC_SHA.contains(storedJwsAlgorithm)) {
+                if (clientSecret == null) {
+                    return null;
+                }
+                return createSecretJWT(JWTClaimsSet.parse(payload), clientSecret, storedJwsAlgorithm);
+            }
+        } catch (JOSEException | ParseException e) {
+            Assert.fail(e.getMessage(), e);
+        }
+        return null;
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IdTokenHintJWSTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IdTokenHintJWSTest.java
new file mode 100644
index 00000000..d5b61698
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IdTokenHintJWSTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.flow;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.springframework.webflow.test.MockParameterMap;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.impl.StorageBackedSessionManager;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.security.DataSealerException;
+
+public class IdTokenHintJWSTest extends IssuedSignedJWTTest {
+
+    String issuer = "https://op.example.org";
+    String sessionId = "mockSessionId";
+    String subject = "mockSubject";
+
+    @Autowired
+    @Qualifier("shibboleth.SessionManager")
+    StorageBackedSessionManager sessionManager;
+
+    public IdTokenHintJWSTest() {
+        super(JWT_FETCHING_TYPE.ID_TOKEN_HINT, EndSessionFlowTest.FLOW_ID);
+    }
+
+    @Override @Test
+    public void testJwtSecurity_jwtSigAlgAndEncNotSpecified() throws Exception {
+        final JWT jwt = obtainJwt(null);
+        assertSuccessIdTokenHintResponse(jwt.serialize(), null, defaultClientSecret64B, null);
+    }
+
+    @Override
+    protected JWT obtainJwt(final JWSAlgorithm jwsAlgorithm) {
+        return obtainJwt(defaultClientSecret64B, jwsAlgorithm);
+    }
+    
+    @Override
+    protected JWT obtainJwt(final String clientSecret, final JWSAlgorithm jwsAlgorithm) {
+        return obtainIdTokenHint(clientSecret, jwsAlgorithm);
+    }
+
+    @Override
+    protected void assertNoJwtResponse(final String clientId, final String clientSecret,
+            final PublicKey publicKey, final JWSAlgorithm jwsAlgorithm, final JWEAlgorithm jweAlgorithm,
+            final EncryptionMethod method, final JWT_FETCHING_TYPE fetchingType) {
+        final JWT jwt = obtainIdTokenHint(defaultClientSecret64B, jwsAlgorithm);
+        assertErrorIdTokenHintResponse(jwt.serialize(), jwsAlgorithm, clientSecret, publicKey);
+    }
+
+    @Override
+    protected void assertExcludedAlgorithm(final String clientId, final String clientSecret, final PublicKey publicKey,
+            final JWSAlgorithm jwsAlgorithm) throws ParseException, DataSealerException, IOException {
+        final JWT jwt = obtainJwt(clientSecret, jwsAlgorithm);
+        assertErrorIdTokenHintResponse(jwt.serialize(), jwsAlgorithm, clientSecret, publicKey);
+    }
+
+    protected void assertErrorIdTokenHintResponse(final String IdTokenHint,
+            final JWSAlgorithm IdTokenHintSigAlg, final String clientSecret, final PublicKey publicKey) {
+        request.setMethod("GET");
+        EndSessionFlowTest.setRequestParameters(request, List.of(new Pair<>("id_token_hint", IdTokenHint)));
+        initializeThreadLocals();
+
+        final IdPSession session = EndSessionFlowTest.buildIdPSession(sessionManager, new OIDCRPSession.Builder()
+                .serviceId(defaultClientId)
+                .issuer(issuer)
+                .creationInstant(Instant.now())
+                .expirationInstant(Instant.now().plusSeconds(300))
+                .rootTokenIdentifier("mockRootId")
+                .sessionIdentifier(sessionId)
+                .subject(subject)
+                .build());
+        request.setCookies(response.getCookies());
+        Assert.assertTrue(EndSessionFlowTest.isSessionValid(sessionManager, session));
+
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setScope(new Scope("openid"));
+        metadata.setIDTokenJWSAlg(IdTokenHintSigAlg);
+        try {
+            metadata.setPostLogoutRedirectionURIs(Set.of(new URI("https://example.org/cb")));
+            storeMetadataObject(storageService, defaultClientId, clientSecret, metadata);
+            final FlowExecutionResult result = flowExecutor.launchExecution(flowId, null, externalContext);
+            removeMetadata(storageService, defaultClientId);
+            Assert.assertEquals(result.getOutcome().getId(), "ErrorView");        
+        } catch (final IOException | URISyntaxException e) {
+            Assert.fail();
+        }
+    }
+    
+    protected void assertSuccessIdTokenHintResponse(final String IdTokenHint,
+            final JWSAlgorithm IdTokenHintSigAlg, final String clientSecret, final PublicKey publicKey) {
+        request.setMethod("GET");
+        EndSessionFlowTest.setRequestParameters(request, List.of(new Pair<>("id_token_hint", IdTokenHint)));
+        initializeThreadLocals();
+
+        final IdPSession session = EndSessionFlowTest.buildIdPSession(sessionManager, new OIDCRPSession.Builder()
+                .serviceId(defaultClientId)
+                .issuer(issuer)
+                .creationInstant(Instant.now())
+                .expirationInstant(Instant.now().plusSeconds(300))
+                .rootTokenIdentifier("mockRootId")
+                .sessionIdentifier(sessionId)
+                .subject(subject)
+                .build());
+        request.setCookies(response.getCookies());
+        Assert.assertTrue(EndSessionFlowTest.isSessionValid(sessionManager, session));
+
+        final OIDCClientMetadata metadata = buildMetadataSkeleton();
+        metadata.setScope(new Scope("openid"));
+        metadata.setIDTokenJWSAlg(IdTokenHintSigAlg);
+        final FlowExecutionResult result;
+        try {
+            metadata.setPostLogoutRedirectionURIs(Set.of(new URI("https://example.org/cb")));
+            storeMetadataObject(storageService, defaultClientId, clientSecret, metadata);
+            result = flowExecutor.launchExecution(flowId, null, externalContext);
+            removeMetadata(storageService, defaultClientId);
+        } catch (final IOException | URISyntaxException e) {
+            Assert.fail();
+            return;
+        }
+        Assert.assertFalse(result.isEnded());
+        Assert.assertFalse(EndSessionFlowTest.isSessionValid(sessionManager, session));
+        
+        ((MockParameterMap) externalContext.getRequestParameterMap()).put("_eventId", "proceed");
+        final FlowExecutionResult result2 = flowExecutor.resumeExecution(result.getPausedKey(), externalContext);
+        Assert.assertEquals(response.getStatus(), 200);
+        Assert.assertTrue(result2.isEnded());
+    }
+
+    protected void assertSignedJwt(final JWT jwt, final JWSAlgorithm algorithm, final PublicKey publicKey,
+            final String clientSecret) {
+        assertSuccessIdTokenHintResponse(jwt.serialize(), algorithm, clientSecret, publicKey);
+    }
+
+    protected JWT obtainIdTokenHint(final String clientSecret, final JWSAlgorithm storedJwsAlgorithm) {
+        final String payload = EndSessionFlowTest.getIdTokenHintPayload(issuer, subject, defaultClientId, Instant.now().plusSeconds(300), Instant.now(), sessionId);
+
+        return processJwsForIdTokenHint(storedJwsAlgorithm, payload, clientSecret, getSigningKey(storedJwsAlgorithm));
+    }
+
+    protected static PrivateKey getSigningKey(final JWSAlgorithm jwsAlgorithm) {
+        if (JWSAlgorithm.ES256.equals(jwsAlgorithm)) {
+            return loadESSigningCredential().getPrivateKey();
+        } else if (JWSAlgorithm.ES384.equals(jwsAlgorithm)) {
+            return loadES384SigningCredential().getPrivateKey();
+        } else if (JWSAlgorithm.ES512.equals(jwsAlgorithm)) {
+            return loadES512SigningCredential().getPrivateKey();
+        }
+        return loadRSSigningCredential().getPrivateKey();
+    }
+
+    protected static JWT processJwsForIdTokenHint(final JWSAlgorithm storedJwsAlgorithm, final String payload,
+            final String clientSecret, final PrivateKey signingKey) {
+        try {
+            if (storedJwsAlgorithm == null) {
+                return createPrivateKeyJWT(JWTClaimsSet.parse(payload), (RSAPrivateKey) signingKey, JWSAlgorithm.RS256);
+            } else if (JWSAlgorithm.Family.EC.contains(storedJwsAlgorithm)) {
+                return createPrivateKeyJWT(JWTClaimsSet.parse(payload), (ECPrivateKey) signingKey, storedJwsAlgorithm);
+            } else if (JWSAlgorithm.Family.RSA.contains(storedJwsAlgorithm)) {
+                return createPrivateKeyJWT(JWTClaimsSet.parse(payload), (RSAPrivateKey) signingKey, storedJwsAlgorithm);
+            } else if (JWSAlgorithm.Family.HMAC_SHA.contains(storedJwsAlgorithm)) {
+                if (clientSecret == null) {
+                    return null;
+                }
+                return createSecretJWT(JWTClaimsSet.parse(payload), clientSecret, storedJwsAlgorithm);
+            }
+        } catch (JOSEException | ParseException e) {
+            Assert.fail(e.getMessage(), e);
+        }
+        return null;
+    }
+
+
+}
diff --git a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 429c71b2..6347a3cc 100644
--- a/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -121,6 +121,13 @@
                  </list>
             </property>
         </bean>
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdLogoutEncryptionEnforced">
+            <property name="profileConfigurations">
+                 <list>
+                     <bean parent="OIDC.Logout" p:encryptionOptional="false"/>
+                 </list>
+            </property>
+        </bean>
         <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdRefreshTokenRotation">
             <property name="profileConfigurations">
                  <list>

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


More information about the commits mailing list