[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