[java-plugin-shibd-oidc] branch main updated: Update initiator/oidc flow

Phil Smart philip.smart at jisc.ac.uk
Fri Sep 5 13:46:10 UTC 2025


This is an automated email from the git hooks/post-receive script.

philsmart pushed a commit to branch main
in repository java-plugin-shibd-oidc.

View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-shibd-oidc.git;a=commit;h=bc6635c597c574862166b53f477870681663e0c2

The following commit(s) were added to refs/heads/main by this push:
     new bc6635c  Update initiator/oidc flow
bc6635c is described below

commit bc6635c597c574862166b53f477870681663e0c2
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Sep 5 14:46:08 2025 +0100

    Update initiator/oidc flow
    
     - Build flow to the end, encoding an appropriate
    authorization/authentication request
     - Copy in more RP classes (needs to be reviewed)
     - Improve flow tests, making the DDF input more appropriate for OIDC,
    add checks the DDF output is correct.
     - Configuration of the request is only via profile properties at the
    moment, needs input from the DDF.
---
 .../logic/RequestObjectRequiredAndSupported.java   | 100 ++++++
 ...WTClaimsSetFromRequestObjectLookupFunction.java |  86 +++++
 .../PayloadFromRequestObjectLookupFunction.java    |  88 ++++++
 .../navigate/RequestObjectTokenUpdateStrategy.java |  76 +++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  22 ++
 .../idp/flows/sp/initiator/oidc/oidc-beans.xml     | 161 ++++++++++
 .../idp/flows/sp/initiator/oidc/oidc-flow.xml      |  44 ++-
 .../shibboleth/idp/module/conf/sp/oidc.properties  |   6 +-
 .../sp/oidc/flows/OIDCAuthenticationFlowTest.java  | 127 +++++++-
 ...DCEnvironmentApplicationContextInitializer.java |   2 +-
 ...roviderMetadataFileBasedCredentialResolver.java | 105 +++++++
 .../test/resources/logback-webauthn-flow-test.xml  |  25 --
 .../resources/metadata/openid-configuration.json   |   4 +
 .../idp/module/conf/sp/oidc-test-agents.xml        |  55 +++-
 .../idp/module/conf/sp/oidc-test.properties}       |   7 +-
 .../credentials/op/global-provider-test-jwks.json  |  14 +
 .../net/shibboleth/sp/oidc-test-beans.xml          |   7 +
 .../messaging/impl/AddRequestedClaimsHandler.java  |   2 +-
 ...tObjectSupportedSignatureSigningAlgorithms.java |  40 +++
 .../sp/oidc/profile/impl/BuildRequestObject.java   | 349 +++++++++++++++++++++
 20 files changed, 1256 insertions(+), 64 deletions(-)

diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/config/logic/RequestObjectRequiredAndSupported.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/config/logic/RequestObjectRequiredAndSupported.java
new file mode 100644
index 0000000..9ea19de
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/config/logic/RequestObjectRequiredAndSupported.java
@@ -0,0 +1,100 @@
+/*
+ * 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.sp.oidc.config.logic;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.logic.AbstractRelyingPartyPredicate;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * A predicate that tests whether a request object should be built based on what is configured in the profile 
+ * configuration and whether the OP supports it.  
+ */
+public class RequestObjectRequiredAndSupported extends AbstractRelyingPartyPredicate {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(RequestObjectRequiredAndSupported.class);
+    
+    /** Strategy that will return {@link OIDCProviderMetadata}. */
+    @Nonnull private 
+    Function<ProfileRequestContext, OIDCProviderMetadataContext> oidcProviderMetadataContextLookupStrategy;
+    
+    /** Constructor.*/
+    public RequestObjectRequiredAndSupported() {
+        oidcProviderMetadataContextLookupStrategy = new ChildContextLookup<>(OIDCProviderMetadataContext.class)
+                .compose(new ChildContextLookup<>(OIDCPeerEntityContext.class))
+                .compose(new InboundMessageContextLookup());
+    }
+    
+    /**
+     * Set the lookup strategy to use to locate the {@link OIDCProviderMetadataContext}.
+     * 
+     * @param strategy lookup function to use
+     */
+    public void setOidcProviderMetadataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,OIDCProviderMetadataContext> strategy) {
+
+        oidcProviderMetadataContextLookupStrategy =
+                Constraint.isNotNull(strategy, 
+                        "OidcProviderMetadataContextLookupStrategy lookup strategy cannot be null");
+    }
+
+    @Override
+    public boolean test(final ProfileRequestContext input) {
+        
+        boolean requestObjectRequestedFromConfig = false;        
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null && rpc.getProfileConfig() instanceof final OIDCAuthenticationRelyingPartyProfileConfiguration rpConfig) {
+            requestObjectRequestedFromConfig = rpConfig.isUseRequestObject(input);
+        }
+        
+        final OIDCProviderMetadataContext metadata = oidcProviderMetadataContextLookupStrategy.apply(input);
+        final OIDCProviderMetadata providerInformation = 
+                metadata != null ? metadata.getProviderInformation() : null;
+        if (providerInformation == null) {
+            // Should not happen at the time this predicate is called.
+            log.warn("OIDC Provider Metadata is not available, can not determine if request object is supported, "
+                    + "will not build request object");
+            return false;
+        }
+
+        final boolean isSupportedByOP = providerInformation.supportsRequestParam();
+        
+        final boolean requestedAndSupport =
+                requestObjectRequestedFromConfig && isSupportedByOP; 
+        
+        log.debug("Authentication RequestObject was enabled '{}', is supported by the OP '{}', "
+                + "will be used '{}'", requestObjectRequestedFromConfig, isSupportedByOP, requestedAndSupport);
+       
+        return true;//requestedAndSupport;
+
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/JWTClaimsSetFromRequestObjectLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/JWTClaimsSetFromRequestObjectLookupFunction.java
new file mode 100644
index 0000000..6a81d43
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/JWTClaimsSetFromRequestObjectLookupFunction.java
@@ -0,0 +1,86 @@
+/*
+ * 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.sp.oidc.context.navigate;
+
+import java.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * Extract the {@link Payload} from the Request Object inside the {@link OIDCAuthenticationRequest}. 
+ * The Payload must either be signed, or plain. The claims will not be available if the payload is still encrypted.
+ */
+public class JWTClaimsSetFromRequestObjectLookupFunction implements Function<MessageContext, JWTClaimsSet>{
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(JWTClaimsSetFromRequestObjectLookupFunction.class);
+    
+    /** Strategy used to locate the {@link OIDCAuthenticationRequest} to sign.  */
+    @Nonnull private Function<MessageContext, OIDCAuthenticationRequest> authenticationRequestLookupStrategy;
+    
+    /** Constructor.*/
+    public JWTClaimsSetFromRequestObjectLookupFunction() {
+        authenticationRequestLookupStrategy = mc -> {
+            if (mc.getMessage() instanceof final OIDCAuthenticationRequest request) {
+                return request;
+            }
+            return null;
+        };   
+    }
+    
+    /**
+     * Set the strategy used to locate the {@link OIDCAuthenticationRequest} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAuthenticationRequestLookupStrategy(
+            @Nonnull final Function<MessageContext, OIDCAuthenticationRequest> strategy) {
+        authenticationRequestLookupStrategy =
+                Constraint.isNotNull(strategy, "AuthenticationRequestLookupStrategy lookup strategy cannot be null");
+    }
+
+    @Override
+    @Nullable public JWTClaimsSet apply(@Nullable final MessageContext messageContext) {
+        if (messageContext == null) {
+            return null;
+        }
+        final OIDCAuthenticationRequest authnRequest = authenticationRequestLookupStrategy.apply(messageContext);
+        final JWT requestObject = authnRequest != null ? authnRequest.getRequestObject() : null;
+        if (requestObject == null) {
+            return null;
+        }
+        
+        try {
+            return requestObject.getJWTClaimsSet();
+        } catch (final ParseException e) {
+            log.debug("Error parsing JWT Claims Set of the RequestObject", e);
+            return null;
+        }
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/PayloadFromRequestObjectLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/PayloadFromRequestObjectLookupFunction.java
new file mode 100644
index 0000000..bd5e1ca
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/PayloadFromRequestObjectLookupFunction.java
@@ -0,0 +1,88 @@
+/*
+ * 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.sp.oidc.context.navigate;
+
+import java.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * Extract the {@link Payload} from the Request Object inside the {@link OIDCAuthenticationRequest}. 
+ * The Payload must either be signed, or plain. The payload will not be available if still encrypted.
+ */
+public class PayloadFromRequestObjectLookupFunction implements Function<MessageContext, Payload>{
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(PayloadFromRequestObjectLookupFunction.class);
+    
+    /** Strategy used to locate the {@link OIDCAuthenticationRequest} to sign.  */
+    @Nonnull private Function<MessageContext, OIDCAuthenticationRequest> authenticationRequestLookupStrategy;
+    
+    /** Constructor.*/
+    public PayloadFromRequestObjectLookupFunction() {
+        authenticationRequestLookupStrategy = mc -> {
+            if (mc.getMessage() instanceof final OIDCAuthenticationRequest request) {
+                return request;
+            }
+            return null;
+        };   
+    }
+    
+    /**
+     * Set the strategy used to locate the {@link OIDCAuthenticationRequest} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAuthenticationRequestLookupStrategy(
+            @Nonnull final Function<MessageContext, OIDCAuthenticationRequest> strategy) {
+        authenticationRequestLookupStrategy =
+                Constraint.isNotNull(strategy, "AuthenticationRequestLookupStrategy lookup strategy cannot be null");
+    }
+
+    @Override
+    @Nullable public Payload apply(@Nullable final MessageContext messageContext) {
+        if (messageContext == null) {
+            return null;
+        }
+        final OIDCAuthenticationRequest authnRequest = authenticationRequestLookupStrategy.apply(messageContext);
+        final JWT requestObject = authnRequest != null ? authnRequest.getRequestObject() : null;
+        if (requestObject instanceof final SignedJWT signedJwt) {
+            return new Payload(signedJwt);
+        } else if (requestObject instanceof PlainJWT) {
+            try {
+                return new Payload(requestObject.getJWTClaimsSet().getClaims());
+            } catch (final ParseException e) {
+                log.error("Unable to convert plaintext JWT to claims set", e);
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/RequestObjectTokenUpdateStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/RequestObjectTokenUpdateStrategy.java
new file mode 100644
index 0000000..902e612
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/RequestObjectTokenUpdateStrategy.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.context.navigate;
+
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWT;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** Consumer that adds the {@link JWT} back to the Request Object in the {@link OIDCAuthenticationRequest}.*/
+public class RequestObjectTokenUpdateStrategy implements BiConsumer<JWT, MessageContext> {   
+    
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(RequestObjectTokenUpdateStrategy.class);
+    
+    /** Strategy used to locate the {@link OIDCAuthenticationRequest} to sign.  */
+    @Nonnull private Function<MessageContext, OIDCAuthenticationRequest> authenticationRequestLookupStrategy;
+    
+    /** Constructor.*/
+    public RequestObjectTokenUpdateStrategy() {
+        authenticationRequestLookupStrategy = mc -> {
+            if (mc.getMessage() instanceof final OIDCAuthenticationRequest request) {
+                return request;
+            }
+            return null;
+        };   
+    }
+    
+    /**
+     * Set the strategy used to locate the {@link OIDCAuthenticationRequest} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAuthenticationRequestLookupStrategy(
+            @Nonnull final Function<MessageContext, OIDCAuthenticationRequest> strategy) {
+        authenticationRequestLookupStrategy =
+                Constraint.isNotNull(strategy, "AuthenticationRequestLookupStrategy lookup strategy cannot be null");
+    }
+
+    @Override
+    public void accept(@Nullable final JWT jwt, @Nullable final MessageContext messageContext) {
+        if (messageContext == null) {
+            return;
+        }
+        final OIDCAuthenticationRequest authnRequest = authenticationRequestLookupStrategy.apply(messageContext);
+        if (authnRequest != null) {
+            authnRequest.setRequestObject(jwt);
+        } else {
+            log.warn("Unable to set JWT on OIDC authentication request, request was null");
+        }
+    }
+
+}
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index a6eb653..83b75b0 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -12,6 +12,28 @@
                            http://www.springframework.org/schema/integration/ip https://www.springframework.org/schema/integration/ip/spring-integration-ip.xsd"
 
     default-init-method="initialize" default-destroy-method="destroy">
+    
+    <!-- Functions use by the flow and global beans TODO: IS there a better place for these -->
+
+    <bean id="shibboleth.ChildLookup.OIDCProviderMetadataContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext) }" />
+        
+    <bean id="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <ref bean="shibboleth.ChildLookup.OIDCProviderMetadataContext" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <ref bean="shibboleth.ChildLookup.OIDCPeerEntityFromOutbound" />
+        </constructor-arg>
+    </bean>
+    
+    <bean id="shibboleth.ChildLookup.OIDCPeerEntityFromOutbound" parent="shibboleth.Functions.Compose"
+        c:g-ref="shibboleth.ChildLookup.OIDCPeerEntityContext" c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+        
+    <bean id="shibboleth.ChildLookup.OIDCPeerEntityContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext) }" />
 
 
     <!-- OpenID Provider information resolver service beans. -->
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
index 7fae032..60891e9 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
@@ -126,6 +126,167 @@
         p:callbackServletPath="sp/callback"
         p:allowedOrigins="%{sp.oidc.redirecturl.allowedOrigins:}"
         class="net.shibboleth.sp.oidc.profile.impl.DefaultRedirectUriCreationFunction" />
+        
+    <bean id="RequestObjectRequiredAndSupportedPredicate" scope="prototype"
+        class="net.shibboleth.sp.oidc.config.logic.RequestObjectRequiredAndSupported" />
+    
+    
+    <bean id="PopulateRequestObjectSignatureSigningParameters" scope="prototype"
+        class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters"
+        c:strategy-ref="shibboleth.MessageContextLookup.Inbound" p:noResultIsError="true"
+        p:configurationLookupStrategy-ref="RequestObjectSignatureSigningConfigurationLookup"
+        p:providerMetadataContextLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound"
+        p:signatureSigningParametersResolver-ref="RequestObjectSignatureSigningParametersResolver">
+            <property name="activationCondition">
+                <bean id="SignRequestObjectProxyCondition"
+                    class="net.shibboleth.oidc.profile.config.logic.SignRequestObjectPredicate"
+                    p:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty" />
+            </property>
+    </bean>
+    
+    <bean id="RequestObjectSignatureSigningParametersResolver" scope="prototype"
+        class="net.shibboleth.oidc.security.jose.impl.RelyingPartySigningParametersResolver"
+        p:providerMetadataAlgorithmLookupStrategy-ref="RequestObjectSupportedSignatureSigningAlgorithms" />
+        
+    <bean id="RequestObjectSupportedSignatureSigningAlgorithms" scope="prototype"
+        class="net.shibboleth.sp.oidc.metadata.impl.RequestObjectSupportedSignatureSigningAlgorithms" />
+    
+    
+    <bean id="RequestObjectSignatureSigningConfigurationLookup" lazy-init="true" scope="prototype"
+        class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
+        
+    
+    <!-- if the activation condition succeeds, encryption is not optional -->
+    <bean id="PopulateRequestObjectEncryptionParameters"
+        class="net.shibboleth.oidc.profile.impl.PopulateJWTEncryptionParameters" scope="prototype"
+        p:encryptionOptional="false"
+        p:forFriendlyName="Request Object"
+        p:configurationLookupStrategy-ref="RequestObjectEncryptionConfigurationLookup"
+        p:providerMetadataContextLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound"
+        p:encryptionParametersResolver-ref="EncryptionParametersResolver">
+        <property name="activationCondition">
+            <bean id="EncryptRequestObjectCondition"
+                class="net.shibboleth.oidc.profile.config.logic.EncryptRequestObjectPredicate"
+                p:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty" />
+        </property>
+    
+    </bean>
+    
+    <bean id="EncryptionParametersResolver" scope="prototype"
+        class="net.shibboleth.oidc.security.jose.impl.DefaultEncryptionParametersResolver">
+        <property name="keyTransportEncryptionAlgorithmsLookupStrategy">
+            <bean
+                class="net.shibboleth.oidc.security.jose.impl.ProviderMetadataKeyTransportEncryptionAlgorithmsLookupStrategy">
+                <constructor-arg>
+                    <bean
+                        class="net.shibboleth.oidc.profile.config.navigate.ProviderMetadataStringValuesLookupFunction" 
+                        c:keyName="request_object_encryption_alg_values_supported"/>
+                </constructor-arg>
+            </bean>
+        </property>
+        <property name="dataEncryptionAlgorithmsLookupStrategy">
+            <bean class="net.shibboleth.oidc.security.jose.impl.ProviderMetadataDataEncryptionAlgorithmsLookupStrategy">
+                <constructor-arg>
+                    <bean
+                        class="net.shibboleth.oidc.profile.config.navigate.ProviderMetadataStringValuesLookupFunction" 
+                        c:keyName="request_object_encryption_enc_values_supported"/>
+                </constructor-arg>
+            </bean>
+        </property>
+    </bean>
+    
+    <bean id="RequestObjectEncryptionConfigurationLookup" lazy-init="true" scope="prototype"
+        class="net.shibboleth.oidc.profile.config.navigate.JWTEncryptionConfigurationLookupFunction" />
+    
+    <bean id="BuildRequestObject" class="net.shibboleth.sp.oidc.profile.impl.BuildRequestObject"
+        scope="prototype"
+        p:claimsSetIsValidPredicate="#{getObject('shibboleth.oidc.RequestObjectClaimsSetIsValidPredicate')}"
+        p:requestObjectToBeSignedPredicate-ref="SignRequestObjectCondition" 
+        p:customClaimsStrategy="#{getObject('shibboleth.oidc.CustomRequestObjectClaimsStrategy')}"/>
+        
+    <bean id="SignRequestObjectCondition" scope="prototype"
+        class="net.shibboleth.oidc.profile.config.logic.SignRequestObjectPredicate"
+        p:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty" />
+        
+    <bean id="HandleOutboundMessage"
+            class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:messageHandler-ref="PreEncodeMessageHandler"
+            c:executionDirection="OUTBOUND">
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
+        </property>
+    </bean>
+    
+    <!-- TODO Might not need all of these in the preencode step if we can add the state before...but signing and encrypting the RO still might work here -->
+    <bean id="PreEncodeMessageHandler" class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain"
+        scope="prototype">
+        <property name="handlers">
+            <list>
+                <bean id="AddState" class="net.shibboleth.sp.oidc.messaging.impl.AddStateHandler"
+                    scope="prototype"
+                    p:stateGenerationStrategy="#{getObject('shibboleth.authn.oidc.rp.StateGenerationStrategy')}" />
+
+                <bean id="BuildPlainRequestObjectJWT"
+                    class="net.shibboleth.sp.oidc.messaging.impl.BuildPlainRequestObjectJWT"
+                    scope="prototype" />
+
+                 <bean id="SignRequestObject" class="net.shibboleth.oidc.security.impl.SignJWTHandler"
+                    scope="prototype" p:logName="RequestObject">
+                    <property name="claimsToSignLookupStrategy">
+                        <bean
+                            class="net.shibboleth.sp.oidc.context.navigate.JWTClaimsSetFromRequestObjectLookupFunction" />
+                    </property>
+                    <property name="jwtUpdateConsumer">
+                        <bean
+                            class="net.shibboleth.sp.oidc.context.navigate.RequestObjectTokenUpdateStrategy" />
+                    </property>
+                </bean> 
+
+                <bean id="EncryptRequestObject"
+                    class="net.shibboleth.oidc.security.impl.EncryptJWTHandler" scope="prototype"
+                    p:logName="RequestObject">
+                    <property name="payloadToEncryptLookupStrategy">
+                        <bean
+                            class="net.shibboleth.sp.oidc.context.navigate.PayloadFromRequestObjectLookupFunction" />
+                    </property>
+                    <property name="jwtUpdateConsumer">
+                        <bean
+                            class="net.shibboleth.sp.oidc.context.navigate.RequestObjectTokenUpdateStrategy" />
+                    </property>
+                </bean>
+                 <bean id="SetAuthenticationRequestTime"
+                    class="net.shibboleth.sp.oidc.messaging.impl.SetAuthenticationRequestTimeHandler" scope="prototype"/>
+            </list>
+        </property>
+    </bean>
+    
+    <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
+        p:createOutputObjects="true"
+        p:messageEncoderFactory-ref="messageEncoderFactory" />
+        
+    <!-- Message Encoder factory is a prototype to allow reuse of the encoders -->
+    <bean id="messageEncoderFactory"
+        class="net.shibboleth.oidc.profile.impl.AuthenticationRequestMessageEncoderFactory" scope="prototype"
+        c:encoders-ref="shibboleth.oidc.AuthenticationRequestEncoders" />
+
+    <!-- List must itself be a prototype so new encoders are created per request -->
+    <util:list id="shibboleth.oidc.AuthenticationRequestEncoders" scope="prototype">
+        <ref bean="HTTPRedirectAuthnRequestEncoder" />
+        <ref bean="HTTPPostAuthnRequestEncoder" />
+    </util:list>
+
+    <bean id="HTTPRedirectAuthnRequestEncoder"
+        class="net.shibboleth.oidc.profile.encoding.impl.HTTPRedirectAuthnRequestEncoder" init-method=""
+        scope="prototype" p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
+        p:authorizationParamsAreValidPredicate="#{getObject('%{sp.oidc.AuthzParamsValidPredicate:}'.trim())}" />
+
+    <bean id="HTTPPostAuthnRequestEncoder"
+        class="net.shibboleth.oidc.profile.encoding.impl.HTTPPostAuthnRequestEncoder" init-method="" scope="prototype"
+        p:velocityEngine-ref="shibboleth.VelocityEngine" p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
+        p:authorizationParamsAreValidPredicate="#{getObject('%{sp.oidc.AuthzParamsValidPredicate:}'.trim())}" 
+        p:cSPDigester="#{%{idp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
+        p:cSPNonceGenerator="#{%{idp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}"/>
+    
    
 
 </beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml
index 90fc4fd..6385fa7 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml
@@ -19,25 +19,51 @@
         <evaluate expression="InitializeOAuth2ClientContext" />
         <evaluate expression="InitializeAuthorizationRequest" />
         
-        <!-- <evaluate expression="PopulateRequestSignatureSigningParameters" />
-        <evaluate expression="PopulateEncryptionParameters" />
+        <!-- SAML<evaluate expression="PopulateRequestSignatureSigningParameters" />
+        SAML<evaluate expression="PopulateEncryptionParameters" />
         
-        <evaluate expression="AddAuthnRequest" />
         <evaluate expression="EncryptNameIDs" /> -->
         
         <evaluate expression="BuildAuthenticationRequest"/>
-        
-        <!-- 
-        <evaluate expression="HandleOutboundMessage" />
-        <evaluate expression="IssueCorrelationCookie" />
-        <evaluate expression="EncodeMessage" /> -->
         <evaluate expression="'proceed'" />
 
-        <transition on="proceed" to="proceed" />
+        <transition on="proceed" to="RequestObjectRequiredAndSupported" />
        <!--  Remap any other events into a fall-through to the next flow. -->
         <transition to="ReselectFlow" />
     </action-state>
     
+     <!-- Is a request object required by the configuration, and does the OP support it? -->
+    <decision-state id="RequestObjectRequiredAndSupported">
+        <if test="RequestObjectRequiredAndSupportedPredicate.test(opensamlProfileRequestContext)"
+            then="GenerateRequestObject" 
+            else="BuildOutboundMessage" />
+    </decision-state>
+
+    <action-state id="GenerateRequestObject">
+        <evaluate expression="PopulateRequestObjectSignatureSigningParameters" />
+        <evaluate expression="PopulateRequestObjectEncryptionParameters" />
+        <evaluate expression="BuildRequestObject" />
+        <!-- 
+            We can not sign and encrypt the RO here (we need state from WF execution key). 
+            That is left to the preEncodeMessageHandlers. 
+        -->
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="BuildOutboundMessage" />
+        <!--  Remap any other events into a fall-through to the next flow. -->
+        <transition to="ReselectFlow" />    
+    </action-state>
+    
+    <action-state id="BuildOutboundMessage">    
+        <evaluate expression="HandleOutboundMessage" />
+        <!-- <evaluate expression="IssueCorrelationCookie" /> -->
+        <evaluate expression="EncodeMessage" />
+        <evaluate expression="'proceed'" />
+         
+        <transition on="proceed" to="proceed" />
+        <!--  Remap any other events into a fall-through to the next flow. -->
+        <transition to="ReselectFlow" />    
+    </action-state>
+    
     <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
     <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml" />
 
diff --git a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
index a044e23..ec57c50 100644
--- a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
+++ b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
@@ -17,5 +17,9 @@ sp.oidc.signing.rsa.enc.key = %{idp.home}/credentials/sp/sp-encryption-rsa.jwk
 #sp.oidc.encryption.key.2 = %{idp.home}/credentials/sp/sp-encryption-old.key
 #sp.oidc.encryption.cert.2 = %{idp.home}/credentials/sp/sp-encryption-old.crt
 
-sp.oidc.redirecturl.allowedOrigins = http://localhost
+#sp.oidc.redirecturl.allowedOrigins = http://localhost
+
+## TEST ENC FOR NOW from profile config
+#idp.oidc.requestobject.encrypted = true
+
 
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java
index 17f5bf3..6b69dc0 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCAuthenticationFlowTest.java
@@ -15,20 +15,34 @@
 package net.shibboleth.sp.oidc.flows;
 
 import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.binding.SAMLBindingSupport;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.web.WebAppConfiguration;
 import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
+
+import net.shibboleth.sp.context.AgentRequestContext;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.flows.AbstractSPFlowTest;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
+import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
 import net.shibboleth.sp.profile.InitiatorConstants;
 import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.profile.impl.IssueCorrelationCookie;
 
 /**
  *
@@ -48,15 +62,10 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
 
     /** Resource URL. */
     @Nonnull public static final byte[] RESOURCE_URL = "https://sp.example.org/secure".getBytes(StandardCharsets.UTF_8);
+    
+    /** REDIRECT URI. */
+    @Nonnull public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/callback";
 
-    /** ACS URL. */
-    @Nonnull public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/ACS";
-
-    /** POST ACS URL. */
-    @Nonnull public static final String POST_RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/SAML2/POST";
-
-    /** Artifact ACS URL. */
-    @Nonnull public static final String ARTIFACT_RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/SAML2/Artifact";
 
     /** Constructor. */
     protected OIDCAuthenticationFlowTest() {
@@ -67,9 +76,10 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
      * Basic flow test, mainly for TDD.
      * 
      * @throws IOException on error
+     * @throws MessageDecodingException 
      */
     @Test
-    public void testFlow() throws IOException {
+    public void testFlow() throws IOException, MessageDecodingException {
         setDefaultAuth();
         
         final DDF input = new DDF(null).structure();
@@ -81,7 +91,104 @@ public class OIDCAuthenticationFlowTest extends AbstractSPFlowTest {
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertFlowExecutionResult(result, FLOW_ID);
         assertFlowExecutionOutcome(result.getOutcome());
-        //assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
+        
+        final AuthorizationRequest req = validateOutputMessage(result);
+    }
+    
+    /**
+     * Decode an encoded response and run sanity checks against it.
+     * 
+     * @param result flow execution result
+     * 
+     * @return the authentication/authorization request object
+     * 
+     * @throws MessageDecodingException
+     */
+    @Nonnull private AuthorizationRequest validateOutputMessage(@Nonnull final FlowExecutionResult result)
+            throws MessageDecodingException {
+        final ProfileRequestContext prc = retrieveProfileRequestContext(result);
+        assert prc != null;
+        final AgentRequestContext arc = prc.ensureSubcontext(AgentRequestContext.class);
+        final DDF input = arc.getInput();
+        final DDF output = arc.getOutput();
+
+        assert output != null;
+        Assert.assertTrue(output.isstruct());
+        final DDF http = output.getmember(RemotedHttpServletResponse.STRUCTURE_NAME);
+        Assert.assertTrue(http.isstruct());
+        
+        final AuthorizationRequest authnRequest;
+        final byte[] redirect = http.getmember(RemotedHttpServletResponse.REDIRECT).unsafe_string();
+        if (redirect != null) {
+            final String redirectURL = new String(redirect, StandardCharsets.UTF_8);
+            authnRequest = decodeRedirect(redirectURL,
+                    input != null ? input.getmember(SPConstants.STATE).string() : null);
+        } else {
+            //TODO what to pull out if in the POST body
+            final byte[] body = http.getmember("response.data").unsafe_string();
+            Assert.assertNotNull(body);
+            // Not trivial to consider parsing the form, so just bypass that step.
+            final Object oidc = prc.ensureOutboundMessageContext().ensureMessage();
+            assert oidc instanceof AuthenticationRequest;
+            authnRequest = (AuthenticationRequest) oidc;
+            Assert.assertEquals(SAMLBindingSupport.getRelayState(prc.ensureOutboundMessageContext()),
+                    input != null ? input.getmember(SPConstants.STATE).string() : null);
+        }
+        
+        assert authnRequest != null;
+        Assert.assertNotNull(authnRequest.getClientID());
+        Assert.assertNotNull(authnRequest.getScope());
+        Assert.assertTrue(authnRequest.getScope().contains("openid"));
+        
+        if (input != null) {
+            boolean foundCorrelationCookie = false;
+            for (final DDF header : http.getmember(RemotedHttpServletResponse.HEADERS)) {
+                if ("Set-Cookie".equals(header.name())) {
+                    final String cookie = header.string();
+                    assert cookie != null;
+                    if (cookie.startsWith("__Host-" + IssueCorrelationCookie.DEFAULT_COOKIE_PREFIX)) {
+                        //TODO our use of the correlation cookie
+                        
+//                        final Boolean passive = authnRequest.isPassive();
+//                        final String passiveDelim = passive ? "=T:" : "=F:";
+//                        Assert.assertEquals(cookie,
+//                                "__Host-_shibsp_req_" + input.getmember(SPConstants.STATE).string() + passiveDelim + authnRequest.getID()
+//                                    + "; HttpOnly=true; Path=/; SameSite=None; Secure=true");
+                        foundCorrelationCookie = true;
+                    }
+                }
+            }
+           //TODO Not Set yet:  Assert.assertTrue(foundCorrelationCookie);
+        }
+        
+//        final NameIDPolicy pol = authnRequest.getNameIDPolicy();
+//        assert pol != null;
+//        assertTrue(pol.getAllowCreate());
+//        Assert.assertEquals(pol.getFormat(), format);
+        
+        return authnRequest;
+    }
+    
+    /**
+     * Decodes an OAuth authorization message encoded via HTTP-Redirect binding.
+     * 
+     * @param url the encoded redirect
+     * @param relayState RelayState to check for TODO state?
+     * 
+     * @return decoded message
+     * @throws MessageDecodingException 
+     */
+    @Nonnull protected AuthorizationRequest decodeRedirect(@Nullable final String url, @Nullable final String relayState)
+            throws MessageDecodingException {
+        if (url == null) {
+            throw new MessageDecodingException("URl is null");
+        }
+        try {
+            return AuthorizationRequest.parse(new URI(url));
+        } catch (final ParseException | URISyntaxException e) {
+            throw new MessageDecodingException("No message, or incorrect type.", e);
+        }
+
     }
 
 }
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
index c4a67a2..0c5c9fc 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
@@ -47,7 +47,7 @@ public class TestSPOIDCEnvironmentApplicationContextInitializer
         mock.setProperty("sp.stateToken.Manager","shibboleth.CookieStateTokenManager");
         //mock.setProperty("idp.service.logging.resource", "/logback-webauthn-flow-test.xml");
         mock.setProperty("idp.additionalProperties",
-                "/conf/ldap.properties, /conf/saml-nameid.properties, /conf/services.properties, /conf/admin/admin.properties, /conf/authn/authn.properties, /conf/c14n/subject-c14n.properties, /credentials/secrets.properties, /conf/sp/sp.properties, /conf/sp/oidc.properties");
+                "/conf/ldap.properties, /conf/saml-nameid.properties, /conf/services.properties, /conf/admin/admin.properties, /conf/authn/authn.properties, /conf/c14n/subject-c14n.properties, /credentials/secrets.properties, /conf/sp/sp.properties, /conf/sp/oidc.properties,  /conf/sp/oidc-test.properties");
         applicationContext.getEnvironment().getPropertySources().addFirst(mock);
         log.info("Prepending properties '{}'", mock.getSource());
     }
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/metadata/ProviderMetadataFileBasedCredentialResolver.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/metadata/ProviderMetadataFileBasedCredentialResolver.java
new file mode 100644
index 0000000..badaec2
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/metadata/ProviderMetadataFileBasedCredentialResolver.java
@@ -0,0 +1,105 @@
+/*
+ * 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.sp.oidc.metadata;
+
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashSet;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+import org.springframework.util.StreamUtils;
+
+import com.nimbusds.jose.jwk.JWKSet;
+
+import net.shibboleth.oidc.security.credential.impl.BasicJOSEObjectCredentialResolver;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.component.InitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.resolver.CriteriaSet;
+import net.shibboleth.shared.resolver.ResolverException;
+import net.shibboleth.shared.resource.Resource;
+
+/**
+ * A credential resolver used for testing that returns the same collection of credentials no matter what metadata
+ * is provided.
+ */
+public class ProviderMetadataFileBasedCredentialResolver extends BasicJOSEObjectCredentialResolver
+        implements InitializableComponent {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(ProviderMetadataFileBasedCredentialResolver.class);
+
+    /** Initialization flag. */
+    private boolean isInitialized;
+
+    /** The resource that holds the JWKS. */
+    @Nonnull
+    private final Resource jwkSetResource;
+    
+    /** The contents of the JWKS.*/
+    @Nonnull private final JWKSet cachedSet;
+
+
+    /**
+    * Constructor.
+    *
+    * @param jwkSet the resource holding the JWK key sets.
+    */
+    public ProviderMetadataFileBasedCredentialResolver(
+        @Nonnull @ParameterName(name="jwkSet") final Resource jwkSet) {
+        jwkSetResource = Constraint.isNotNull(jwkSet, "JWKS can not be null");
+        
+        if (!jwkSetResource.isFile()) {
+            throw new IllegalArgumentException("JWKS must be a file");
+        }
+        try (var inputStream = jwkSet.getInputStream()) {
+            final String jwkSetAsString = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
+            cachedSet = JWKSet.parse(jwkSetAsString);
+            assert cachedSet != null;
+        } catch (final Exception e) {
+            throw new IllegalArgumentException("JWKS resource can not be read", e);
+        }
+        
+    }
+
+
+    /** {@inheritDoc} */
+    public boolean isInitialized() {
+        return isInitialized;
+    }
+
+    /** {@inheritDoc} */
+    public void initialize() throws ComponentInitializationException {
+        isInitialized = true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull
+    protected Iterable<Credential> resolveFromSource(@Nullable final CriteriaSet criteriaSet) throws ResolverException {
+        log.debug("Getting JWKs from cached set in '{}'",jwkSetResource.getDescription());
+        final LinkedHashSet<Credential> credentials = new LinkedHashSet<>(1);
+        populateCredentialsFromKeySet(cachedSet, credentials);
+        return credentials;
+    }
+
+
+
+}
diff --git a/sp-oidc-conf-impl/src/test/resources/logback-webauthn-flow-test.xml b/sp-oidc-conf-impl/src/test/resources/logback-webauthn-flow-test.xml
deleted file mode 100644
index d71e445..0000000
--- a/sp-oidc-conf-impl/src/test/resources/logback-webauthn-flow-test.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<configuration>
-
-    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
-        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-            <pattern>%level [%logger:%line] - %msg%n</pattern>
-            <charset>UTF-8</charset>
-        </encoder>
-    </appender>
-
-    <root>
-        <level value="WARN" />
-        <appender-ref ref="STDOUT" />
-    </root>
-    
-    <logger name="net.shibboleth.idp.plugin.authn" level="TRACE" additivity="false">
-        <appender-ref ref="STDOUT" />
-    </logger>
-     
-    <logger name="org.springframework.webflow" level="INFO" additivity="false">
-        <appender-ref ref="STDOUT" />
-    </logger>
-    
-</configuration>
\ No newline at end of file
diff --git a/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json b/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json
index 8f2f888..d04036c 100644
--- a/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json
+++ b/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json
@@ -5,6 +5,8 @@
 "userinfo_endpoint": "https://openidconnect.op.example.org/v1/userinfo",
 "revocation_endpoint": "https://oauth2.op.example.org/revoke",
 "jwks_uri": "https://op.example.org/oauth2/v3/certs",
+"request_parameter_supported" : true,
+"request_object_signing_alg_values_supported" : ["RS256"],
 "response_types_supported": [
 "code",
 "token",
@@ -15,6 +17,8 @@
 "code token id_token",
 "none"
 ],
+"request_object_encryption_enc_values_supported" : ["A128CBC-HS256"],
+"request_object_encryption_alg_values_supported" : ["RSA-OAEP-256"],
 "subject_types_supported": [
 "public"
 ],
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml
index c485d43..e0d1b85 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml
@@ -65,24 +65,47 @@
     </util:list>
 
     <util:list id="test.ProfileConfigurations">
-        <ref bean="OIDC.SSO" />
-<!--         <ref bean="SAML2.ECP" />
-        <ref bean="SAML2.Logout" /> -->
-    </util:list>
-
-    <util:list id="test.responseBindingProfileConfigurations">
-        <!-- <bean parent="OIDC.SSO" p:responseBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" /> -->
-        <bean parent="OIDC.SSO"/>
-<!--         <ref bean="SAML2.ECP" />
-        <ref bean="SAML2.Logout" /> -->
+        <bean parent="OIDC.SSO" p:securityConfiguration-ref="testSecConfig"/>
     </util:list>
+    
+    <!-- 
+        Overrides the default security configuration to provide an encryption credential resolver
+        that loads JWKs from a local file instead of retrieving them from the issuer’s JWKS URI.
+        This simplifies testing by avoiding reliance on an external JWKS endpoint. We also constraint
+        the algorithms used, to simplify baseline tests.
+     -->
+    <bean id="testSecConfig" parent="shibboleth.oidc.DefaultSecurityConfiguration">
+      <property name="jwtEncryptionConfiguration">
+        <bean id="dummy.oidc.EncryptionConfiguration" parent="dummy.oidc.BasicEncryptionConfiguration"
+            p:contentEncryptionKeyCredentialResolver-ref="defaultOIDCContentEncryptionKeyCredentialResolver">
+            <property name="keyTransportEncryptionAlgorithms">
+                <list>
+                    <util:constant
+                        static-field="net.shibboleth.oidc.jwa.support.KeyManagementConstants.ALGO_ID_ALG_RSA_OAEP_256" />
+                </list>
+            </property>
+            <property name="dataEncryptionAlgorithms">
+                <list>
+                    <util:constant
+                        static-field="net.shibboleth.oidc.jwa.support.EncryptionConstants.ALGO_ID_ENC_ALG_A128CBC_HS256" />
+                </list>
+            </property>
+            <property name="KEKCredentialResolver">
+                <bean id="defaultOIDCKeyEncryptionCredentialResolver"
+                    class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
+                    <constructor-arg>
+                        <list>
+                            <bean id="ProviderMetadataFileBasedCredentialResolver"
+                                class="net.shibboleth.sp.oidc.metadata.ProviderMetadataFileBasedCredentialResolver"
+                                c:jwkSet="classpath:/net/shibboleth/idp/module/credentials/op/global-provider-test-jwks.json" />
+                        </list>
+                    </constructor-arg>
+                </bean>
+            </property>
+        </bean>
+      </property>
+    </bean>
 
-    <util:list id="test.featureBlockingProfileConfigurations">
-        <!-- <bean parent="OIDC.SSO" p:disallowedFeatures="0x1F" /> -->
-        <bean parent="OIDC.SSO" />
-<!--         <ref bean="SAML2.ECP" />
-        <ref bean="SAML2.Logout" /> -->
-    </util:list>
 
     <!-- ============ Profile defaults ============ -->
     
diff --git a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test.properties
similarity index 85%
copy from sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
copy to sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test.properties
index a044e23..bff09e5 100644
--- a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test.properties
@@ -1,4 +1,4 @@
-# OIDC-specific RP settings
+# OIDC-specific RP test settings
 
 # Settings for RP public/private signing and encryption key(s)
 # During decryption key rollover, point the ".2" properties at a second
@@ -19,3 +19,8 @@ sp.oidc.signing.rsa.enc.key = %{idp.home}/credentials/sp/sp-encryption-rsa.jwk
 
 sp.oidc.redirecturl.allowedOrigins = http://localhost
 
+## TEST ENC FOR NOW from profile config
+idp.oidc.requestobject.encrypted = true
+
+#idp.oidc.encryption.config = dummy.oidc.EncryptionConfiguration
+
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/op/global-provider-test-jwks.json b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/op/global-provider-test-jwks.json
new file mode 100644
index 0000000..a2b5201
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/op/global-provider-test-jwks.json
@@ -0,0 +1,14 @@
+{
+  "keys": [
+    {
+      "kty": "RSA",
+      "kid": "test-rsa",
+      "use": "enc",
+      "alg": "RSA-OAEP-256",
+      "n": "sXchX0m6hD-0WWLHH7CBmUdZqcmULVqfSTQO1Gp6Tw1BaZs6w3Q7Y61EuyyQyRvnYp9TF9A36X8D6kZVjs8QyRCPXJ1z4CWe5Vd4VqSczNhbtNmoqHmbf_tnk3K4KcfHdOiG0fKtTe1sVHZp-srOjApXso2MXMCCrbvw8p9l0j8zUy2S1yrTtT6O4nD_f8RDN2y0x3Xkqf0J1Ae0uPCZ_P8qZf-ryjYlgHu41jTUGDhz9aEuZeq_0UZZKxPoJWh8T_Gsl_sEm9qMPjG-d5rORjQZ-4qv0FvAk3Q3FksznGd7eh1olPYCDnZzBjO0xk4LWhP2XBBij5zUgcqAnYKw",
+      "e": "AQAB"
+    }
+  ]
+}
+    
+    
\ No newline at end of file
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
index 2ee0182..2ec55a8 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
@@ -30,5 +30,12 @@
         c:entityCertificate-ref="dummy.idp.X509Certificate"
         c:privateKey-ref="dummy.idp.PrivateKey"
         p:entityId="https://idp.example.org" />
+     
+    <bean id="dummy.oidc.BasicEncryptionConfiguration" abstract="true"
+        class="net.shibboleth.oidc.security.jose.impl.BasicEncryptionConfiguration"
+        p:includedAlgorithms="#{getObject('shibboleth.oidc.IncludedEncryptionAlgorithms')}"
+        p:excludedAlgorithms="#{getObject('shibboleth.oidc.ExcludedEncryptionAlgorithms')}" />
+
+      
 
 </beans>
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddRequestedClaimsHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddRequestedClaimsHandler.java
index 8e65bde..9cd6a3e 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddRequestedClaimsHandler.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddRequestedClaimsHandler.java
@@ -37,7 +37,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
  * <p>The claims are added from a customizable strategy/hook. No additional claims are provided by default.</p>
  * 
  * <p>Also records in the request whether the upstream OP supports the claims parameter, for later inspection by 
- * downstream components that only access to the request e.g. an encoder.</p>
+ * downstream components that only has access to the request e.g. an encoder.</p>
  */
 public class AddRequestedClaimsHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
     
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/metadata/impl/RequestObjectSupportedSignatureSigningAlgorithms.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/metadata/impl/RequestObjectSupportedSignatureSigningAlgorithms.java
new file mode 100644
index 0000000..37c7a4d
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/metadata/impl/RequestObjectSupportedSignatureSigningAlgorithms.java
@@ -0,0 +1,40 @@
+/*
+ * 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.sp.oidc.metadata.impl;
+
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+/**
+ * Pull out the request object supported signature signing algorithms from the metadata.
+ */
+public class RequestObjectSupportedSignatureSigningAlgorithms implements Function<OIDCProviderMetadata, List<String>>{
+
+    @Override
+    @Nullable public List<String> apply(@Nullable final OIDCProviderMetadata metadata) {
+        
+        if (metadata == null || metadata.getRequestObjectJWSAlgs() == null) {
+            return null;
+        }
+        return metadata.getRequestObjectJWSAlgs()
+                .stream().map(JWSAlgorithm::getName).toList();
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/BuildRequestObject.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/BuildRequestObject.java
new file mode 100644
index 0000000..5fc353b
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/BuildRequestObject.java
@@ -0,0 +1,349 @@
+/*
+ * 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.sp.oidc.profile.impl;
+
+import java.time.Duration;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.oauth2.sdk.ResponseMode;
+import com.nimbusds.oauth2.sdk.id.Audience;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.claims.ACR;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.oidc.profile.encoding.AuthenticationContextClassReferenceSupport;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/**
+ * Action that creates a Request Object {@link JWT}, and sets it to the work context
+ * {@link OIDCAuthenticationRequest} located under {@link ProfileRequestContext#getOutboundMessageContext()}.
+ * 
+ * <p>Note, some parameters are set downstream in the flow before the request object is signed and or encrypted. 
+ * These parameters are only available to the HTTP Controller e.g. state, and must be set during the external
+ * authentication redirect.</p>
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
+ * @pre <pre>ProfileRequestContext.getOutboundMessageContext().getMessage() 
+ * instance of OIDCAuthenticationRequest.class</pre>
+ * @post Add a JWT request object to the in-flight authentication request 
+ */
+public class BuildRequestObject extends AbstractProfileAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(BuildRequestObject.class);
+    
+    /** Lookup strategy to locate the OpenID Provider metadata to use.*/
+    @Nonnull private Function<ProfileRequestContext, OIDCProviderMetadataContext> providerMetadataLookupStrategy;
+    
+    /** Lookup function for relying party context. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+    
+    /** A hook to allow additional checking of the request object claims after it is built.*/
+    @Nonnull private Predicate<ClaimsSet> claimsSetIsValidPredicate;
+    
+    /** A strategy hook to add custom claims to the claims set.*/
+    @Nonnull private BiConsumer<ProfileRequestContext, ClaimsSet> customClaimsStrategy;
+    
+    /** 
+     * Is the request object going to be signed? if so the 'iss' and 'aud' claims will be set.
+     * Defaults to always true, as it is permissible that both 'iss' and 'aud' claim can exist in 
+     * plain request objects.
+     */
+    @Nonnull private Predicate<ProfileRequestContext> requestObjectToBeSignedPredicate;
+    
+    /** OIDC authentication request built by the IdP. */
+    @NonnullBeforeExec private OIDCAuthenticationRequest authnRequest;
+    
+    /** OpenID Provider metadata .*/
+    @NonnullBeforeExec private OIDCProviderMetadata providerMetadata;
+    
+    /** Constructor.*/
+    public BuildRequestObject() {
+        claimsSetIsValidPredicate = PredicateSupport.alwaysTrue();
+        requestObjectToBeSignedPredicate = PredicateSupport.alwaysTrue();
+        
+        providerMetadataLookupStrategy = new ChildContextLookup<>(OIDCProviderMetadataContext.class).compose(
+                new ChildContextLookup<>(OIDCPeerEntityContext.class).compose(
+                        new OutboundMessageContextLookup()));
+        
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+        // Create a no-op consumer
+        customClaimsStrategy = (prc, set) -> {};
+    }
+    
+    /**
+     * Set a bi-consumer hook to add custom claims to the request object.
+     * 
+     * @param strategy The custom claims strategy to set.
+     * 
+     * @since 2.1.0
+     */
+    public void setCustomClaimsStrategy(@Nullable final BiConsumer<ProfileRequestContext, ClaimsSet> strategy) {
+        checkSetterPreconditions();
+        if (strategy != null) {
+            customClaimsStrategy = strategy;
+        }
+    }
+    
+    
+    /**
+     * Set the strategy used to locate the {@link RelyingPartyContext} associated with a given
+     * {@link ProfileRequestContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+    	checkSetterPreconditions();
+        
+        relyingPartyContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the lookup strategy to locate the OpenID providers metadata.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setProviderMetadataLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCProviderMetadataContext> strategy) {
+    	checkSetterPreconditions();
+        
+        providerMetadataLookupStrategy = 
+                Constraint.isNotNull(strategy,"Provider metadata lookup strategy can not be null");
+    }
+    
+    /**
+     * Set a hook that allows the built request object to be validated before it is used.
+     * This is run in addition too, but before, the built in validation taken from the specification.
+     * If this returns false, the built in validation is not run, and validation fails.
+     * 
+     * @param predicate the hook to run
+     */
+    public void setClaimsSetIsValidPredicate(@Nullable final Predicate<ClaimsSet> predicate) {
+    	checkSetterPreconditions();
+        
+        if (predicate != null) {
+            claimsSetIsValidPredicate = predicate;
+        }
+    }
+    
+    /**
+     * Set a predicate to determine if the request object will be 'eventually' signed. If so,
+     * the 'iss' and 'aud' claims will be set into the request object.
+     * 
+     * @param predicate the predicate
+     */
+    public void setRequestObjectToBeSignedPredicate(@Nullable final Predicate<ProfileRequestContext> predicate) {
+    	checkSetterPreconditions();
+        
+        if (predicate != null) {
+            requestObjectToBeSignedPredicate = predicate;
+        }
+    }
+    
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+       
+        final MessageContext outboundMsgContext = profileRequestContext.getOutboundMessageContext();
+        if (outboundMsgContext == null) {
+            log.error("{} Outbound message context was null", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        } 
+        if (!(outboundMsgContext.getMessage() instanceof OIDCAuthenticationRequest)) {
+            log.error("{} Outbound message was not an authentication request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        authnRequest = (OIDCAuthenticationRequest) outboundMsgContext.getMessage();
+        if (authnRequest == null) {
+            log.error("{} No authentication request found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        final OIDCProviderMetadataContext providerMetadataContext = 
+                providerMetadataLookupStrategy.apply(profileRequestContext);
+        if (providerMetadataContext == null) {
+            log.error("{} No provider metadata context found for peer", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        providerMetadata = providerMetadataContext.getProviderInformation();
+        if (providerMetadata == null) {
+            log.error("{} No provider metadata found for peer", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+
+    //TODO maybe we could share building of a request object or query params (from AbstractOIDCMessageEncoder)
+    // in some way
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        log.debug("{} Building a plain RequestObject JWT", getLogPrefix());        
+        
+        final ClaimsSet requestObjectClaims = new ClaimsSet();     
+        
+        if (requestObjectToBeSignedPredicate.test(profileRequestContext)) {
+            if (providerMetadata.getIssuer() != null) {
+                requestObjectClaims.setAudience(
+                        new Audience(providerMetadata.getIssuer().getValue()));                
+            } else {
+                // Should never happen
+                log.error("{} Signed RequestObject requires 'iss' claim, which is currently null",
+                        getLogPrefix());
+                ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+                return;
+            }
+            requestObjectClaims.setIssuer(new Issuer(authnRequest.getClientID().getValue())); 
+        }        
+        
+        requestObjectClaims.setClaim("client_id", authnRequest.getClientID().toString());  
+
+        setClaimIfPresent(requestObjectClaims, "nonce", authnRequest.getNonce());        
+        setClaimIfPresent(requestObjectClaims, "response_type", authnRequest.getResponseType());
+        
+        // Only set the response_mode if not equal to the default for that response_type
+        final ResponseMode responseMode = authnRequest.getDefaultResponseMode();
+        if (responseMode != null && !responseMode.equals(authnRequest.getResponseMode())){
+            requestObjectClaims.setClaim("response_mode", authnRequest.getResponseMode());
+        }
+        
+        setClaimIfPresent(requestObjectClaims, "redirect_uri", authnRequest.getRedirectURI());
+        setClaimIfPresent(requestObjectClaims,"scope", authnRequest.getScope());
+        setClaimIfPresent(requestObjectClaims, "max_age", authnRequest.getMaxAge()); 
+        setClaimIfPresent(requestObjectClaims, "login_hint", authnRequest.getLoginHint()); 
+        setClaimIfPresent(requestObjectClaims, "prompt", authnRequest.getPrompt());
+        setClaimIfPresent(requestObjectClaims, "display", authnRequest.getDisplay());
+        
+        if (authnRequest.providerSupportsClaimsParameter()) {
+            // Build the ACRs if set before adding the 'claims' claim
+            AuthenticationContextClassReferenceSupport.buildACRClaimsRequest(authnRequest);
+            if (authnRequest.getRequestedClaims() != null) {
+                requestObjectClaims.setClaim("claims", authnRequest.getRequestedClaims());
+            }
+        } else if (!authnRequest.getAcrs().isEmpty()) {        
+            // Only add ACR values as acr_values if the provider does not support the 'claims' claim.             
+            final String acrString = String.join(" ", authnRequest.getAcrs()
+                    .stream()
+                    .map(ACR::getValue).toList());
+            requestObjectClaims.setClaim("acr_values", acrString);
+        }
+        
+        // Add custom claims from the claims hook
+        customClaimsStrategy.accept(profileRequestContext, requestObjectClaims);
+        
+        // Validate the request object
+        if (!validateRequestObject(profileRequestContext, requestObjectClaims)) {
+            log.error("{} RequestObject claims are not valid", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return;
+        }
+        
+        if (log.isDebugEnabled()) {
+            log.debug("{} Setting the RequestObject claims: {}", getLogPrefix(),
+                    requestObjectClaims.toJSONString());
+        }
+        
+        // Create the claim first, can be signed and encrypted as a JWT later        
+        authnRequest.setRequestObjectClaimsSet(requestObjectClaims);
+               
+    }
+    
+    /**
+     * Set the claim onto the claims set if not {@code null}. Calls toString on each value, assuming it
+     * will produce the correct value.
+     * 
+     * @param claims the claims set
+     * @param claimName the claim name
+     * @param claim the claim
+     */
+    private void setClaimIfPresent(
+            @Nonnull final ClaimsSet claims, @Nonnull final String claimName, @Nullable final Object claim) {
+        if (claim instanceof final Duration duration) {
+            // Convert to seconds
+            claims.setClaim(claimName, duration.toSeconds());
+        } else if (claim != null) {
+            claims.setClaim(claimName, claim.toString());
+        }
+    }
+    
+    /**
+     * Ensure the request object is valid by assessing the claims are correct.
+     * 
+     * @param profileRequestContext the profile request context
+     * @param requestObjectClaims the claims of the request object
+     * 
+     * @return true if the request object claims are valid, false otherwise
+     */
+    private boolean validateRequestObject(@Nonnull final ProfileRequestContext profileRequestContext, 
+            @Nonnull final ClaimsSet requestObjectClaims) {
+        
+        if (!claimsSetIsValidPredicate.test(requestObjectClaims)) {
+            return false;
+        }
+       
+        if (requestObjectToBeSignedPredicate.test(profileRequestContext)) {
+            if (requestObjectClaims.getClaim("iss") == null) {
+                return false;
+            }
+            if (requestObjectClaims.getClaim("aud") == null) {
+                return false;
+            }
+            
+        }
+        return true;
+    }
+
+}

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


More information about the commits mailing list