[java-plugin-shibd-oidc] branch main updated: Fill basic flow up to building a basic authn request

Phil Smart philip.smart at jisc.ac.uk
Mon Aug 25 11:14:21 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=9e3b13b8b20264395c1c7c42e4a9fc04d8c04034

The following commit(s) were added to refs/heads/main by this push:
     new 9e3b13b  Fill basic flow up to building a basic authn request
9e3b13b is described below

commit 9e3b13b8b20264395c1c7c42e4a9fc04d8c04034
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Mon Aug 25 12:14:18 2025 +0100

    Fill basic flow up to building a basic authn request
    
     - A quick and dirty effort to get to the request building stage. Many
    settings are config options are comming from the wrong location (e.g.
    should be from the DDF and Application etc).
     - Very much a WIP
     - Need to think about where the action classes should live in the long
    term.
---
 sp-oidc-api/pom.xml                                |  31 ++-
 .../config/navigate/RedirectUriLookupFunction.java |  50 ++++
 .../sp/oidc/context/OAuth2ClientContext.java       |  91 +++++++
 sp-oidc-conf-impl/pom.xml                          |   5 -
 .../idp/flows/sp/initiator/oidc/oidc-beans.xml     | 276 +++++++--------------
 .../idp/flows/sp/initiator/oidc/oidc-flow.xml      |  26 +-
 .../module/conf/oidc-metadata-providers-system.xml |   3 +
 .../shibboleth/idp/module/conf/sp/oidc.properties  |   2 +
 .../resources/metadata/openid-configuration.json   |  55 ++++
 .../shibboleth/idp/module/conf/sp/sp.properties    |   1 +
 ...CAuthenticationRequestActionMessageHandler.java | 254 +++++++++++++++++++
 ...actOIDCAuthenticationRequestMessageHandler.java |  94 +++++++
 ...uthenticationContextClassReferencesHandler.java |  81 ++++++
 .../sp/oidc/messaging/impl/AddDisplayHandler.java  |  55 ++++
 .../oidc/messaging/impl/AddEndpointURIHandler.java |  50 ++++
 .../impl/AddForceAuthenticationHandler.java        |  59 +++++
 .../oidc/messaging/impl/AddLoginHintHandler.java   |  45 ++++
 .../sp/oidc/messaging/impl/AddMaxAgeHandler.java   |  47 ++++
 .../sp/oidc/messaging/impl/AddNonceHandler.java    |  74 ++++++
 .../impl/AddPCKECodeVerifierAndChallenge.java      | 130 ++++++++++
 .../impl/AddPassiveAuthenticationHandler.java      |  60 +++++
 .../oidc/messaging/impl/AddRedirectURIHandler.java |  82 ++++++
 .../messaging/impl/AddRequestedClaimsHandler.java  |  93 +++++++
 .../impl/AddResponseTypeAndModeHandler.java        | 204 +++++++++++++++
 .../sp/oidc/messaging/impl/AddScopesHandler.java   |  50 ++++
 .../sp/oidc/messaging/impl/AddStateHandler.java    |  91 +++++++
 .../messaging/impl/BuildPlainRequestObjectJWT.java |  56 +++++
 .../impl/SetAuthenticationRequestTimeHandler.java  |  40 +++
 .../sp/oidc/messaging/impl/package-info.java       |   8 +-
 .../ApplicationMetadataResolverLookupFunction.java |  80 ++++++
 .../impl/OIDCProviderMetadataLookupHandler.java    |  45 ++--
 .../impl/DefaultRedirectUriCreationFunction.java   | 247 ++++++++++++++++++
 .../impl/InitializeAuthorizationRequest.java       | 121 +++++++++
 .../impl/InitializeOAuth2ClientContext.java        | 174 +++++++++++++
 .../impl/InitializeOutboundMessageContext.java     | 161 ++++++++++++
 .../InitializeRelyingPartyContextFromOIDCPeer.java | 178 +++++++++++++
 .../sp/oidc/profile/impl/OIDCSupport.java          |  75 ++++++
 .../impl/PrepareOIDCInboundMessageContext.java     |   2 +-
 38 files changed, 2961 insertions(+), 235 deletions(-)

diff --git a/sp-oidc-api/pom.xml b/sp-oidc-api/pom.xml
index 189c2da..3a0d4d8 100644
--- a/sp-oidc-api/pom.xml
+++ b/sp-oidc-api/pom.xml
@@ -28,11 +28,13 @@
             <artifactId>sp-server-api</artifactId>
             <scope>provided</scope>
         </dependency>
-       <!-- <dependency>
-            <groupId>${oidc-common.groupId}</groupId>
-            <artifactId>oidc-common-profile-api</artifactId>
+        
+        <dependency>
+            <groupId>${shib-profile.groupId}</groupId>
+            <artifactId>shib-profile-api</artifactId>
             <scope>provided</scope>
-        </dependency>-->
+        </dependency>
+        
         <dependency>
             <groupId>${shib-attribute.groupId}</groupId>
             <artifactId>shib-attribute-api</artifactId>
@@ -48,6 +50,27 @@
             <artifactId>shib-metadata-api</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>${shib-metadata.groupId}</groupId>
+            <artifactId>shib-metadata-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>${oidc-common.groupId}</groupId>
+            <artifactId>oidc-common-profile-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${oidc-common.groupId}</groupId>
+            <artifactId>oidc-common-metadata-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${oidc-common.groupId}</groupId>
+            <artifactId>oidc-common-metadata-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
         
         <!-- Test Dependencies -->
     </dependencies>
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/config/navigate/RedirectUriLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/config/navigate/RedirectUriLookupFunction.java
new file mode 100644
index 0000000..c0d9a98
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/config/navigate/RedirectUriLookupFunction.java
@@ -0,0 +1,50 @@
+/*
+ * 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.navigate;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+
+/**
+ * Pull a redirect_uri from the Relying Party profile config if present. Returns {@literal null} otherwise. 
+ */
+public class RedirectUriLookupFunction extends AbstractRelyingPartyLookupFunction<URI> {
+
+    @Override
+    @Nullable public URI apply(@Nullable final ProfileRequestContext input) {
+        final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+        if (rpc != null) {
+            final ProfileConfiguration pc = rpc.getProfileConfig();
+            if (pc instanceof OIDCAuthenticationRelyingPartyProfileConfiguration config) {
+                try {
+                    final String uriString = config.getRedirectUriOverride(input);
+                    return uriString != null ? new URI(uriString): null;
+                } catch (final URISyntaxException e) {
+                    return null;
+                }
+            } 
+        }        
+        return null;
+    }
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/OAuth2ClientContext.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/OAuth2ClientContext.java
new file mode 100644
index 0000000..41abd79
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/OAuth2ClientContext.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+import java.net.URI;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A context to store information pertaining to the OAuth2 client (Relying Party) to use in communication
+ * with a OpenID Provider.
+ * 
+ * <p>Typically a subcontext under {@link OIDCPeerEntityContext}, as it relates to the client
+ * associated pairwise with the upstream OP peer.</p>
+ * 
+ * @parent {@link OIDCPeerEntityContext}
+ * @added During an OAuth 2.0 authentication request attempt
+ */
+public class OAuth2ClientContext extends BaseContext {
+    
+    /** The client_id.*/
+    @Nullable private String clientId;
+    
+    
+    /** An redirect URI that should take preference over any automatically computed.*/
+    @Nullable private URI redirectUriOverride;
+    
+   
+    /**
+     * Set the redirect_uri to use in place of any other.
+     * 
+     * @param override the redirect_uri
+     * 
+     * @return this
+     */
+    public OAuth2ClientContext setRedirectUriOverride(@Nullable final URI override) {
+        redirectUriOverride = override;
+        return this;
+    }
+    
+    /**
+     * Get the redirect_uri which should be used in place of any other.
+     * 
+     * @return the redirect_uri
+     */
+    public URI getRedirectUriOverride() {
+        return redirectUriOverride;
+    }
+    
+    /**
+     * Set the client_id.
+     * 
+     * @param id the client_id
+     * 
+     * @return this
+     */
+    public OAuth2ClientContext setClientId(@Nonnull @NotEmpty final String id) {
+        clientId = Constraint.isNotEmpty(id, "ClientID can not be null or empty");
+        return this;
+    }
+    
+    /**
+     * Get the client_id.
+     * 
+     * @return the client_id
+     */
+    public String getClientId() {
+        return clientId;
+    }
+    
+
+}
diff --git a/sp-oidc-conf-impl/pom.xml b/sp-oidc-conf-impl/pom.xml
index da38193..abb43c1 100644
--- a/sp-oidc-conf-impl/pom.xml
+++ b/sp-oidc-conf-impl/pom.xml
@@ -78,11 +78,6 @@
             <scope>provided</scope>
         </dependency>
         
-        <!-- TODO: should we pull config from here <dependency>
-            <groupId>${oidc-config.groupId}</groupId>
-            <artifactId>idp-plugin-oidc-config-impl</artifactId>
-            <scope>provided</scope>
-        </dependency>-->
         <dependency>
             <groupId>${oidc-common.groupId}</groupId>
             <artifactId>oidc-common-crypto-impl</artifactId>
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 29a167c..7fae032 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
@@ -9,9 +9,16 @@
     
     
     <!-- Some of these are common beans -->
-        <bean id="WebFlowInboundMessageHandlerAdaptor"
+    <bean id="WebFlowInboundMessageHandlerAdaptor"
         class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype" abstract="true"
         c:executionDirection="INBOUND" />
+    
+    <bean id="WebFlowOutboundMessageHandlerAdaptor"
+        class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype" abstract="true"
+        c:executionDirection="OUTBOUND" />
+    
+    <util:constant id="shiibboleth.sp.oidc.ProfileId"
+        static-field="net.shibboleth.oidc.profile.config.OIDCSSOProfileConfiguration.PROFILE_ID" />
     <!-- end -->
     
     
@@ -23,209 +30,102 @@
         
         <!-- TODO Copied over -->
     <bean id="PrepareInboundMessageContext" 
-        class="net.shibboleth.sp.oidc.impl.PrepareOIDCInboundMessageContext" scope="prototype"
-        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+        class="net.shibboleth.sp.oidc.profile.impl.PrepareOIDCInboundMessageContext" scope="prototype"
         p:identifierLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
     </bean>
     
-    <bean id="OIDCProviderMetadataLookup" parent="WebFlowInboundMessageHandlerAdaptor">
+    <bean id="ProviderMetadataLookup" parent="WebFlowInboundMessageHandlerAdaptor">
         <constructor-arg name="messageHandler"> <!-- TODO Copied over -->
             <bean class="net.shibboleth.sp.oidc.metadata.impl.OIDCProviderMetadataLookupHandler"
                 scope="prototype">
-                <!-- <property name="providerMetadataResolver">
-                    <ref bean="shibboleth.authn.oidc.rp.ProviderMetadataResolver" />
-                </property> -->
+                <property name="ProviderMetadataResolverLookupStrategy">
+                    <bean class="net.shibboleth.sp.oidc.metadata.impl.ApplicationMetadataResolverLookupFunction" /> <!-- TODO Needs to be core-sp version -->
+                </property>
             </bean>
         </constructor-arg>
     </bean>
-
-    <!-- <util:constant id="shiibboleth.sp.ProfileId"
-        static-field="net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration.PROFILE_ID" />
-
-    <import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml" />
-
     
-
-    <bean id="PrepareInboundMessageContext"
-            class="net.shibboleth.idp.saml.session.impl.PrepareInboundMessageContext" scope="prototype"
-            p:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple" />
-
-    <bean id="InitializeOutboundMessageContext"
+    <bean id="InitializeRelyingPartyContextFromOIDCPeer"
+        class="net.shibboleth.sp.oidc.profile.impl.InitializeRelyingPartyContextFromOIDCPeer" scope="prototype" />
+        
+    <bean id="SelectRelyingPartyConfiguration"
+        class="net.shibboleth.sp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype" />
+        
+    <bean id="SelectProfileConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+        p:profileId-ref="shiibboleth.sp.oidc.ProfileId" />
+        
+<!--     <bean id="InitializeOutboundMessageContext"
         class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
-        p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" />
-
-    <bean id="InitializeMessageChannelSecurityContext" 
-        class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity" scope="prototype"
-        p:confidentialityActive="false"
-        p:integrityActive="false" />
-
-    <util:constant id="shibboleth.EndpointType"
-        static-field="org.opensaml.saml.saml2.metadata.SingleSignOnService.DEFAULT_ELEMENT_NAME" />
-
-    <util:list id="OutgoingSAML2SPRequestBindings">
-        <ref bean="shibboleth.Binding.SAML2Redirect" />
-        <ref bean="shibboleth.Binding.SAML2POST" />
-        <ref bean="shibboleth.Binding.SAML2POSTSimpleSign" />
-    </util:list>
-
-    <bean id="OutgoingSAML2SPRequestBindingsStrategy" parent="shibboleth.Functions.Constant"
-        c:_0-ref="OutgoingSAML2SPRequestBindings" />
-
-    <bean id="PopulateBindingAndEndpointContexts"
-        class="net.shibboleth.idp.saml.profile.impl.PopulateBindingAndEndpointContexts" scope="prototype"
-        p:endpointResolver-ref="shibboleth.EndpointResolver"
-        p:endpointType-ref="shibboleth.EndpointType"
-        p:bindingDescriptorsLookupStrategy-ref="OutgoingSAML2SPRequestBindingsStrategy"
-        p:artifactImpliesSecureChannel="%{sp.artifact.secureChannel:true}" />
-
-    <bean id="PopulateRequestSignatureSigningParameters"
-            class="org.opensaml.saml.common.profile.impl.PopulateSignatureSigningParameters" scope="prototype"
-            p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
-            p:signatureSigningParametersResolver-ref="shibboleth.SignatureSigningParametersResolver"
-            p:noResultIsError="false">
-        <property name="activationCondition">
-            <bean class="net.shibboleth.saml.profile.config.logic.SignRequestsPredicate"
-                p:honorMetadata="%{sp.saml.honorWantAuthnRequestsSigned:true}" />
-        </property>
-    </bean>
-
-    <bean id="PopulateEncryptionParameters"
-        class="net.shibboleth.idp.saml.saml2.profile.impl.PopulateEncryptionParameters" scope="prototype"
-        p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
-        p:encryptionParametersResolver-ref="shibboleth.EncryptionParametersResolver"
-        p:protocol-ref="shibboleth.MetadataLookup.Protocol"
-        p:role-ref="shibboleth.MetadataLookup.Role" />
-
-    <util:map id="InboundSAML2BindingMap">
-        <entry key="POST">
-            <util:constant static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML2_POST_BINDING_URI" />
-        </entry>
-        <entry key="POST-SimpleSign">
-            <util:constant static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML2_POST_SIMPLE_SIGN_BINDING_URI" />
-        </entry>
-        <entry key="Artifact">
-            <util:constant static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML2_ARTIFACT_BINDING_URI" />
-        </entry>
-    </util:map>
-
-    <bean id="AddAuthnRequest"
-            class="net.shibboleth.sp.saml.saml2.profile.impl.AddAuthnRequest" scope="prototype"
-            p:overwriteExisting="true"
-            p:nameIDLookupStrategy="#{getObject('%{sp.authn.SAML.NameIDLookupStrategy:}'.trim())}"
-            p:inboundBindingMap-ref="InboundSAML2BindingMap">
-        <property name="identifierGeneratorLookupStrategy">
-            <bean class="net.shibboleth.profile.config.navigate.IdentifierGenerationStrategyLookupFunction"
-                p:defaultIdentifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy" />
-        </property>
-    </bean>
-    
-    Default formats not to encrypt.
-    <util:set id="DefaultPlaintextNameIDFormats">
-        <util:constant static-field="org.opensaml.saml.saml2.core.NameIDType.ENTITY" />
-    </util:set>
-
-    <bean id="EncryptNameIDs"
-            class="org.opensaml.saml.saml2.profile.impl.EncryptNameIDs" scope="prototype"
-            p:excludedFormats="#{getObject('shibboleth.PlaintextNameIDFormats') ?: getObject('DefaultPlaintextNameIDFormats')}"
-            p:recipientLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
-        <property name="encryptionContextLookupStrategy">
-            <bean parent="shibboleth.Functions.Compose"
-                c:g-ref="shibboleth.ChildLookup.EncryptionParameters"
-                c:f-ref="shibboleth.ChildLookup.RelyingParty" />
-        </property>
-    </bean>
+        p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" /> -->
+        
+   <!-- TODO, self context -->
+   <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.sp.oidc.profile.impl.InitializeOutboundMessageContext"
+        scope="prototype" />
 
-    <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>
+    <bean id="InitializeOAuth2ClientContext" scope="prototype"
+        class="net.shibboleth.sp.oidc.profile.impl.InitializeOAuth2ClientContext"/>
+        
+    <bean id="InitializeAuthorizationRequest"
+        class="net.shibboleth.sp.oidc.profile.impl.InitializeAuthorizationRequest" scope="prototype"/>
+        
+    <!-- Construct a suitable outbound authentication request -->
     
-    <bean id="PreEncodeMessageHandler"
-            class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
-        <property name="handlers">
-            <list>
-                <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
-                    <property name="functionLookupStrategy">
-                        <bean class="net.shibboleth.saml.profile.config.navigate.messaging.MessageHandlerLookupFunction" />
-                    </property>
-                </bean>
-                <bean class="org.opensaml.saml.common.binding.impl.SAMLOutboundDestinationHandler" scope="prototype"/>
-                <bean class="org.opensaml.saml.common.binding.security.impl.EndpointURLSchemeSecurityHandler" scope="prototype"/>
-                <bean class="org.opensaml.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler" scope="prototype">
-                    <property name="activationCondition">
-                        <bean parent="shibboleth.Conditions.NOT">
-                            <constructor-arg>
-                                <bean class="org.opensaml.saml.common.messaging.logic.SignatureCapableBindingPredicate" />
-                            </constructor-arg>
-                        </bean>
-                    </property>
-                </bean>
-            </list>
-        </property>
-    </bean>
-
-    <bean id="IssueCorrelationCookie" class="net.shibboleth.sp.profile.impl.IssueCorrelationCookie" scope="prototype"
-        p:cookieManager-ref="shibboleth.RemotedCookieManager"
-        p:cookiePrefix="%{sp.correlation.cookiePrefix:__Host-_shibsp_req_}"
-        p:createOutputObjects="true"
-        p:errorFatal="%{sp.stateToken.errorsFatal:false}"
-        p:requestIDLookupStrategy-ref="RequestIDStrategy"
-        p:passiveRequestPredicate-ref="PassivePredicate" />
-
-    <bean id="RequestIDStrategy" parent="shibboleth.Functions.Compose">
-        <constructor-arg name="g">
-            <bean class="org.opensaml.saml.common.messaging.context.navigate.SAMLMessageInfoContextIDFunction" />
-        </constructor-arg>
-        <constructor-arg name="f">
-            <bean parent="shibboleth.Functions.Compose"
-                c:g-ref="shibboleth.ChildLookupOrCreate.SAMLMessageInfoContext"
-                c:f-ref="shibboleth.MessageContextLookup.Outbound" />
-        </constructor-arg>
-    </bean>
-
-    <bean id="PassivePredicate" class="net.shibboleth.shared.logic.PredicateSupport" factory-method="fromFunction">
+    <!-- TODO Lots of this is comming from the profile config but might need to come from elsewhere e.g. the Application or DDF -->
+    <bean id="BuildAuthenticationRequest" parent="WebFlowOutboundMessageHandlerAdaptor" scope="prototype">
         <constructor-arg>
-            <bean class="net.shibboleth.idp.saml.audit.impl.IsPassiveAuditExtractor">
-                <constructor-arg>
-                    <bean parent="shibboleth.Functions.Compose"
-                        c:g-ref="shibboleth.MessageLookup.AuthnRequest"
-                        c:f-ref="shibboleth.MessageContextLookup.Outbound" />
-                </constructor-arg>
+            <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+                <property name="handlers">
+                    <list>
+                        <bean id="AddResponseTypeAndMode" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddResponseTypeAndModeHandler"/> 
+                        <bean id="AddMaxAge" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddMaxAgeHandler"/> 
+                        <bean id="AddDisplay" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddDisplayHandler"/>
+                        <bean id="AddScopes" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddScopesHandler"/>
+                        <bean id="AddNonce" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddNonceHandler"/>
+                        <bean id="AddEndpointURI" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddEndpointURIHandler"/>
+                        <bean id="AddLoginHintHandler" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddLoginHintHandler"/>
+                        <bean id="AddRequestedClaims" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddRequestedClaimsHandler"
+                            p:requestedClaimsHook="#{getObject('shibboleth.authn.oidc.rp.RequestedClaimsHook')}" />
+                        <bean id="AddPCKECodeVerifierAndChallenge" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddPCKECodeVerifierAndChallenge"/>
+                        <bean id="AddRedirectURI" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddRedirectURIHandler"
+                            p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+                            p:redirectUriCreationStrategy="#{getObject('shibboleth.authn.oidc.rp.RedirectUriCreationStrategy') ?: 
+                                getObject('DefaultRedirectUriCreationStrategy')}" />
+                        <bean id="AddAuthenticationContextClassReferences" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddAuthenticationContextClassReferencesHandler"/>
+                        <bean id="AddForceAuthentication" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddForceAuthenticationHandler" />
+                        <!-- 
+                            Set passive authentication, prompt=none. Run after forced authentication (prompt=login) so this will take precedence 
+                            but the max_age value will be left (if forced authentication is used).
+                         -->
+                        <bean id="AddPassiveAuthentication" scope="prototype"
+                            class="net.shibboleth.sp.oidc.messaging.impl.AddPassiveAuthenticationHandler" />
+                    </list>
+                </property>
             </bean>
         </constructor-arg>
-        <constructor-arg>
-            <ref bean="shibboleth.Conditions.FALSE" />
-        </constructor-arg>
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
+        </property>
     </bean>
-            
-    <bean id="messageEncoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory" />
-
-    <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
-        p:createOutputObjects="true"
-        p:messageEncoderFactory-ref="messageEncoderFactory" />
-
-    Override IdP's encoders to supply the DDF-backed servlet supplier.
-
-    <bean id="shibboleth.Encoders.SAML2RedirectEncoder"
-          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPRedirectDeflateEncoder" scope="prototype" init-method=""
-          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier" />
-
-    <bean id="shibboleth.Encoders.SAML2PostEncoder"
-          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostEncoder" scope="prototype" init-method=""
-          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
-          p:velocityEngine-ref="shibboleth.VelocityEngine"
-          p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
-          p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
-
-    <bean id="shibboleth.Encoders.SAML2PostSimpleSignEncoder"
-          class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostSimpleSignEncoder" scope="prototype" init-method=""
-          p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
-          p:velocityEngine-ref="shibboleth.VelocityEngine"
-          p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
-          p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" /> -->
+    
+    <!--  TODO, this needs to come from the agent config? -->
+    <bean id="DefaultRedirectUriCreationStrategy" scope="prototype"
+        p:callbackServletPath="sp/callback"
+        p:allowedOrigins="%{sp.oidc.redirecturl.allowedOrigins:}"
+        class="net.shibboleth.sp.oidc.profile.impl.DefaultRedirectUriCreationFunction" />
+   
 
 </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 466dd4b..90fc4fd 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
@@ -2,30 +2,32 @@
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
     parent="sp/initiator">
-
-    <!-- TODO. OIDC does not have the same session initiation mechanisms as SAML, so we will add our own -->
     
     <action-state id="OIDCSessionInitiator">
         <evaluate expression="ValidateSessionInitiatorRequest" />
         <evaluate expression="PrepareInboundMessageContext" /> <!-- needs session and logout support -->
-        <evaluate expression="OIDCProviderMetadataLookup" />
-        <!-- <evaluate expression="SAMLProtocolAndRole" />
-        <evaluate expression="SAMLMetadataLookup" />
+        <!-- <evaluate expression="SAMLProtocolAndRole" /> -->
+        <evaluate expression="ProviderMetadataLookup" />
         
-        <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
+        <evaluate expression="InitializeRelyingPartyContextFromOIDCPeer" />
         <evaluate expression="SelectRelyingPartyConfiguration" />
         <evaluate expression="SelectProfileConfiguration" />
         
         <evaluate expression="InitializeOutboundMessageContext" />
-        <evaluate expression="InitializeMessageChannelSecurityContext" />
-        <evaluate expression="PopulateBindingAndEndpointContexts" />
-
-        <evaluate expression="PopulateRequestSignatureSigningParameters" />
+        <!-- <evaluate expression="InitializeMessageChannelSecurityContext" /> -->
+        <!-- <evaluate expression="PopulateBindingAndEndpointContexts" /> -->
+        <evaluate expression="InitializeOAuth2ClientContext" />
+        <evaluate expression="InitializeAuthorizationRequest" />
+        
+        <!-- <evaluate expression="PopulateRequestSignatureSigningParameters" />
         <evaluate expression="PopulateEncryptionParameters" />
         
         <evaluate expression="AddAuthnRequest" />
-        <evaluate expression="EncryptNameIDs" />
-
+        <evaluate expression="EncryptNameIDs" /> -->
+        
+        <evaluate expression="BuildAuthenticationRequest"/>
+        
+        <!-- 
         <evaluate expression="HandleOutboundMessage" />
         <evaluate expression="IssueCorrelationCookie" />
         <evaluate expression="EncodeMessage" /> -->
diff --git a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/oidc-metadata-providers-system.xml b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/oidc-metadata-providers-system.xml
index 96e7c81..5092369 100644
--- a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/oidc-metadata-providers-system.xml
+++ b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/oidc-metadata-providers-system.xml
@@ -10,6 +10,9 @@
     default-init-method="initialize" default-destroy-method="destroy" default-lazy-init="true">
 
     <!-- Loaded by the postconfig.xml file as global beans -->
+    
+    <!-- TODO bean IDs are all wrong -->
+    <!-- TODO is this in the correct place in the tree -->
 
     <bean id="shibboleth.authn.oidc.rp.ProviderMetadataProvider" lazy-init="false"
         class="net.shibboleth.oidc.metadata.ProviderMetadataProviderContainer"
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 bc3174e..a044e23 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,3 +17,5 @@ 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
+
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
new file mode 100644
index 0000000..8f2f888
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json
@@ -0,0 +1,55 @@
+{
+"issuer": "https://op.example.org",
+"authorization_endpoint": "https://op.example.org/o/oauth2/v2/auth",
+"token_endpoint": "https://oauth2.op.example.org/token",
+"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",
+"response_types_supported": [
+"code",
+"token",
+"id_token",
+"code token",
+"code id_token",
+"token id_token",
+"code token id_token",
+"none"
+],
+"subject_types_supported": [
+"public"
+],
+"id_token_signing_alg_values_supported": [
+"RS256"
+],
+"scopes_supported": [
+"openid",
+"email",
+"profile"
+],
+"token_endpoint_auth_methods_supported": [
+"client_secret_post",
+"client_secret_basic"
+],
+"claims_supported": [
+"aud",
+"email",
+"email_verified",
+"exp",
+"family_name",
+"given_name",
+"iat",
+"iss",
+"locale",
+"name",
+"picture",
+"sub"
+],
+"code_challenge_methods_supported": [
+"plain",
+"S256"
+],
+"grant_types_supported": [
+"authorization_code",
+"refresh_token"
+]
+}
\ No newline at end of file
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
index ebc1e04..4bd9eda 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
@@ -32,3 +32,4 @@ sp.application.tokenConsumers = saml2/artifact, saml2/post, saml2/post-simplesig
 #sp.stateToken.Manager = shibboleth.StorageStateTokenManager
 # Controls storage back-end for storage-based state tokens
 #sp.stateToken.StorageService = shibboleth.StorageService
+
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AbstractOIDCAuthenticationRequestActionMessageHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AbstractOIDCAuthenticationRequestActionMessageHandler.java
new file mode 100644
index 0000000..fd97208
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AbstractOIDCAuthenticationRequestActionMessageHandler.java
@@ -0,0 +1,254 @@
+/*
+ * 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.messaging.impl;
+
+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.messaging.handler.AbstractMessageHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
+import org.slf4j.Logger;
+
+import com.google.common.base.Predicates;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+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.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/** 
+ * An abstract message handler that runs inside an {@link WebFlowMessageHandlerAdaptor}
+ * that provides functions to make available various OIDC contexts.
+ * 
+ * <p>The {@link MessageContext} will either be INBOUND or OUTBOUND depending on the direction defined
+ * by the calling {@link WebFlowMessageHandlerAdaptor} action.</p>
+ */
+public abstract class AbstractOIDCAuthenticationRequestActionMessageHandler extends AbstractMessageHandler {    
+    
+    /** Lookup function for parent ProfileRequestContext. */
+    @Nonnull private static final ParentProfileRequestContextLookup<MessageContext> PRC_LOOKUP
+        = new ParentProfileRequestContextLookup<>();
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = 
+            LoggerFactory.getLogger(AbstractOIDCAuthenticationRequestActionMessageHandler.class);
+    
+    /** Lookup strategy to locate the OpenID Provider metadata to use.*/
+    @Nonnull private Function<MessageContext, OIDCProviderMetadataContext> providerMetadataLookupStrategy;
+    
+    /** Lookup function for relying party context. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+        
+    /** OIDC authentication request built by the IdP. */
+    @NonnullBeforeExec private OIDCAuthenticationRequest authnRequest;
+    
+    /** OpenID Provider metadata .*/
+    @NonnullBeforeExec private OIDCProviderMetadata providerMetadata;
+    
+    /** Applicable profile configuration. */
+    @NonnullBeforeExec private OIDCAuthenticationRelyingPartyProfileConfiguration profileConfiguration;
+    
+    /** Current HTTP request, if available. */
+    @Nullable private NonnullSupplier<HttpServletRequest> httpServletRequestSupplier;
+    
+    /** Constructor.*/
+    protected AbstractOIDCAuthenticationRequestActionMessageHandler() {
+        providerMetadataLookupStrategy = new ChildContextLookup<>(OIDCProviderMetadataContext.class).compose(
+                new ChildContextLookup<>(OIDCPeerEntityContext.class));
+        
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+    }
+    
+    /**
+     * Set the HTTP servlet request supplier.
+     * 
+     * @param requestSupplier the supplier.
+     */
+    public void setHttpServletRequestSupplier(@Nullable final NonnullSupplier<HttpServletRequest> requestSupplier) {
+    	checkSetterPreconditions();
+
+        httpServletRequestSupplier = requestSupplier;
+    }
+    
+    /**
+     * Get the HTTP servlet request supplier.
+     * 
+     * @return the HTTP servlet request supplier
+     */
+    @Nullable public HttpServletRequest getHttpServletRequest() {
+        if (httpServletRequestSupplier != null) {
+            return httpServletRequestSupplier.get();
+        }        
+        return null;
+    }
+
+    /**
+     * Get the supplier for  HTTP request if available.
+     *
+     * @return current HTTP request
+     */
+    @Nullable public NonnullSupplier<HttpServletRequest> getHttpServletRequestSupplier() {
+        return httpServletRequestSupplier;
+    }
+
+    /**
+     * Returns the authentication request build by this IdP. Should never be 
+     * {@code null} after after {@code doPreExecute} has been called.
+     * 
+     * @return the authentication request.
+     */
+    @NonnullBeforeExec protected OIDCAuthenticationRequest getAuthenticationRequest() {
+        return authnRequest;
+    }
+    
+    /**
+     * Returns the profile configuration associated with this request. Should never be 
+     * {@code null} after {@code doPreExecute} has been called.
+     * 
+     * @return the profile configuration
+     */
+    @NonnullBeforeExec 
+    protected OIDCAuthenticationRelyingPartyProfileConfiguration getProfileConfiguration() {
+        return profileConfiguration;
+    }
+
+    
+    /**
+     * Returns the OpenID Provider metadata. Should never be {@code null} after
+     * after {@code doPreExecute} has been called.
+     * 
+     * @return The provider metadata context.
+     */
+    @NonnullBeforeExec protected OIDCProviderMetadata getProviderMetadata() {
+        return providerMetadata;
+    }
+    
+    /**
+     * 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<MessageContext, OIDCProviderMetadataContext> strategy) {
+    	checkSetterPreconditions();
+        
+        providerMetadataLookupStrategy = 
+                Constraint.isNotNull(strategy,"Provider metadata lookup strategy can not be null");
+    }
+    
+    
+    /**
+     * Adapt a {@link ProfileRequestContext} function to a {@link MessageContext} function via composing
+     * with a lookup function.
+     * 
+     * @param function the profile request context function
+     * @return the message context function
+     * 
+     * @param <T> the output type of the functions
+     */
+    @Nullable protected <T> Function<MessageContext, T> adapt(
+            @Nullable final Function<ProfileRequestContext, T> function) {
+        if (function == null) {
+            return null;
+        }
+        return function.compose(PRC_LOOKUP);
+    }
+    
+    /**
+     * Lookup the profile request context from the given message context using the function {@code PRC_LOOKUP}.
+     * 
+     * @param messageContext the message context to find the profile request context from
+     * @return the profile request context, or {@literal null} if not found
+     */
+    @Nullable protected ProfileRequestContext lookupProfileRequestContext(
+            @Nonnull final MessageContext messageContext) {
+        return PRC_LOOKUP.apply(messageContext);
+    }
+    
+    /**
+     * Adapt a {@link ProfileRequestContext} predicate into a {@link MessageContext} predicate via composing
+     * with a lookup function.
+     * 
+     * @param predicate the profile request context predicate
+     * @return the message context predicate
+     */
+    @Nullable protected Predicate<MessageContext> adapt(@Nullable final Predicate<ProfileRequestContext> predicate) {
+        if (predicate == null) {
+            return null;
+        }
+        return Predicates.compose(predicate::test, PRC_LOOKUP::apply);
+    }
+    
+    @Override
+    protected boolean doPreInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+
+        if (!(messageContext.getMessage() instanceof OIDCAuthenticationRequest)) {
+            throw new MessageHandlerException("Message was not an authentication request");
+        }
+        authnRequest = (OIDCAuthenticationRequest) messageContext.getMessage();
+        
+        final OIDCProviderMetadataContext providerMetadataContext = 
+                        providerMetadataLookupStrategy.apply(messageContext);
+        if (providerMetadataContext == null) {
+            throw new MessageHandlerException("No provider metadata context found for peer");
+        }
+        providerMetadata = providerMetadataContext.getProviderInformation();
+        if (providerMetadata == null) {
+            throw new MessageHandlerException("No provider metadata context found for peer");
+        }
+        final var adaptedFunction = adapt(relyingPartyContextLookupStrategy);
+        assert adaptedFunction != null;
+        final RelyingPartyContext rpCtx = adaptedFunction.apply(messageContext);
+        if (rpCtx != null && rpCtx.getConfiguration() != null &&
+                rpCtx.getProfileConfig() instanceof final OIDCAuthenticationRelyingPartyProfileConfiguration rpConfig) {
+            profileConfiguration = rpConfig;
+        }
+        if (profileConfiguration == null) {
+            throw new MessageHandlerException("Profile configuration could not found");
+        }
+        
+        return super.doPreInvoke(messageContext);
+    }
+    
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AbstractOIDCAuthenticationRequestMessageHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AbstractOIDCAuthenticationRequestMessageHandler.java
new file mode 100644
index 0000000..a8771f8
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AbstractOIDCAuthenticationRequestMessageHandler.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.messaging.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.AbstractMessageHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * An abstract message handler that makes available the {@link OIDCAuthenticationRequest}.
+ */
+public abstract class AbstractOIDCAuthenticationRequestMessageHandler extends AbstractMessageHandler {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractOIDCAuthenticationRequestMessageHandler.class);
+    
+    /** Strategy used to locate the {@link OIDCAuthenticationRequest}.  */
+    @Nonnull private Function<MessageContext, OIDCAuthenticationRequest> authenticationRequestLookupStrategy;
+    
+    
+    /** The stashed {@link OIDCAuthenticationRequest}.*/
+    @NonnullBeforeExec private OIDCAuthenticationRequest authnRequest;
+    
+    /** Constructor.*/
+    protected AbstractOIDCAuthenticationRequestMessageHandler() {
+        
+        authenticationRequestLookupStrategy = mc -> {
+            if (mc.getMessage() instanceof final OIDCAuthenticationRequest request) {
+                return request;
+            }
+            return null;
+        };
+    }
+    
+    /**
+     * Get the authentication request.
+     * 
+     * @return the authentication request
+     */
+    @NonnullBeforeExec protected OIDCAuthenticationRequest getAuthenticationRequest() {
+        return authnRequest;
+    }
+
+    /**
+     * Set the strategy used to locate the {@link OIDCAuthenticationRequest} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAuthenticationRequestLookupStrategy(
+            @Nonnull final Function<MessageContext, OIDCAuthenticationRequest> strategy) {
+    	checkSetterPreconditions();
+
+        authenticationRequestLookupStrategy =
+                Constraint.isNotNull(strategy, "AuthenticationRequestLookupStrategy lookup strategy cannot be null");
+    }
+    
+    @Override
+    protected boolean doPreInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+
+        authnRequest = authenticationRequestLookupStrategy.apply(messageContext);
+        if (authnRequest == null) {
+            log.debug("{} OIDC authentication request is null", getLogPrefix());
+            throw new MessageHandlerException("OIDC authentication request is null");
+        }
+        
+        return super.doPreInvoke(messageContext);
+    }
+
+    
+    
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddAuthenticationContextClassReferencesHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddAuthenticationContextClassReferencesHandler.java
new file mode 100644
index 0000000..50410c4
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddAuthenticationContextClassReferencesHandler.java
@@ -0,0 +1,81 @@
+/*
+ * 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.messaging.impl;
+
+import java.security.Principal;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.claims.ACR;
+
+import net.shibboleth.oidc.profile.config.navigate.ProxyAwareDefaultOIDCAuthenticationContextClassRequestLookupFunction;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * A message handler that adds any authentication context class references from the those derived from the
+ * profile config - which may be proxied and mapped from the original request.
+ */
+public class AddAuthenticationContextClassReferencesHandler 
+                extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddAuthenticationContextClassReferencesHandler.class);
+    
+    @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {
+        
+        final List<ACR> acrs = buildRequestedAuthnContext(lookupProfileRequestContext(messageContext));  
+        if (acrs != null) {
+            log.trace("{} setting ACRs to '{}' ", getLogPrefix(), acrs);
+            getAuthenticationRequest().setAcrs(acrs);
+        } else {
+            log.trace("{} no ACRs requested", getLogPrefix());
+        }
+    }
+    
+    /**
+    * Build a list of {@link ACR}s if warranted. Converted from any default authentication method {@link Principal}s.
+    * 
+    * <p>By default for this proxy case, the authentication methods are mapped from the upstream request
+    * by the default authentication methods e.g. using the 
+    * {@link ProxyAwareDefaultOIDCAuthenticationContextClassRequestLookupFunction}.</p>
+    * 
+    * @param profileRequestContext current profile request context
+    * 
+    * @return the list of ACRs. 
+    */
+   @Nullable private List<ACR> buildRequestedAuthnContext(
+           @Nullable final ProfileRequestContext profileRequestContext) {
+       
+       final List<Principal> principals = getProfileConfiguration()
+               .getDefaultAuthenticationMethods(profileRequestContext);
+       if (principals.isEmpty()) {
+           return null;
+       }
+       
+       return principals.stream()
+           .map(p -> new ACR(p.getName())).toList();
+   }
+    
+    
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddDisplayHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddDisplayHandler.java
new file mode 100644
index 0000000..2bb6683
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddDisplayHandler.java
@@ -0,0 +1,55 @@
+/*
+ * 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.messaging.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.Display;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+
+/** 
+ * Message handler that adds the optional 'display' request parameter if manually set on the profile configuration. 
+ * 
+ * @since 2.1.0
+ */
+public class AddDisplayHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddDisplayHandler.class);
+
+    @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {   
+       
+       final String display = getProfileConfiguration().getDisplay(lookupProfileRequestContext(messageContext));
+       if (StringSupport.trimOrNull(display) != null) {           
+           try {
+               getAuthenticationRequest().setDisplay(Display.parse(display));
+               log.trace("Adding 'display' request parameter value '{}'", display);
+            } catch (final ParseException e) {
+                throw new MessageHandlerException("Unable to add a 'display' value of: " + display);
+            }
+       }
+  
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddEndpointURIHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddEndpointURIHandler.java
new file mode 100644
index 0000000..0606dfa
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddEndpointURIHandler.java
@@ -0,0 +1,50 @@
+/*
+ * 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.messaging.impl;
+
+import java.net.URI;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/** 
+ * A message handler that adds the authorization endpoint URI from the providers metadata 
+ * to the under constructions authentication request. If an authorization endpoint does
+ * not exist, an exception is thrown.
+ */
+public class AddEndpointURIHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddEndpointURIHandler.class);
+    
+    @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {
+        
+        final URI authzEndpoint = getProviderMetadata().getAuthorizationEndpointURI();
+        if (authzEndpoint == null) {
+            throw new MessageHandlerException("OAuth 2.0 Authorization Endpoint URI not found in provider metadata");
+        }
+        
+        getAuthenticationRequest().setEndpointURI(authzEndpoint);
+        log.trace("{} Added authorization endpoint '{}' to authentication request",getLogPrefix(),
+                getAuthenticationRequest().getEndpointURI());
+    }
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddForceAuthenticationHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddForceAuthenticationHandler.java
new file mode 100644
index 0000000..6b4f0a6
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddForceAuthenticationHandler.java
@@ -0,0 +1,59 @@
+/*
+ * 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.messaging.impl;
+
+import java.time.Duration;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.Prompt;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * An action that sets the 'prompt' parameter to 'login' and max_age to 0 seconds, iff force authn was requested by the 
+ * upstream SP (or is overridden in the profile config).
+ */
+public class AddForceAuthenticationHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddForceAuthenticationHandler.class);
+    
+    @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {
+        
+        if (getProfileConfiguration().isForceAuthn(lookupProfileRequestContext(messageContext))) {
+            log.trace("{} Setting prompt=login and max_age=0 (ForceAuthn) for OIDC AuthnRequest", getLogPrefix());
+            try {
+                getAuthenticationRequest().setPrompt(Prompt.parse(Prompt.Type.LOGIN.toString()));
+                getAuthenticationRequest().setMaxAge(Duration.ofSeconds(0));
+            } catch (final ParseException e) {
+                // This should never happen
+                throw new MessageHandlerException("Unable to honour force-authn, "
+                        + "setting prompt to force-login as failed", e);
+            }
+        } else {
+            log.trace("{} No ForceAuthn requirement, so no prompt or max_age set", getLogPrefix());
+        }
+    }
+    
+    
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddLoginHintHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddLoginHintHandler.java
new file mode 100644
index 0000000..c4482bc
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddLoginHintHandler.java
@@ -0,0 +1,45 @@
+/*
+ * 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.messaging.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/** Message handler that adds the login_hint parameter based on any defined in the profile configuration.*/
+public class AddLoginHintHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddLoginHintHandler.class);
+
+    @Override
+    protected void doInvoke(final MessageContext messageContext) throws MessageHandlerException {
+        
+        final String loginHint = 
+                getProfileConfiguration().getLoginHint(lookupProfileRequestContext(messageContext));
+        
+        if (loginHint != null) {
+            log.trace("{} Added login_hint parameter '{}'", getLogPrefix(), loginHint);
+            getAuthenticationRequest().setLoginHint(loginHint);
+        }
+        
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddMaxAgeHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddMaxAgeHandler.java
new file mode 100644
index 0000000..edfc045
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddMaxAgeHandler.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.sp.oidc.messaging.impl;
+
+import java.time.Duration;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/** Message handler that adds the max_age parameter based on any defined in the profile configuration.*/
+public class AddMaxAgeHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddMaxAgeHandler.class);
+
+    @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {   
+       
+        final Duration maxAge = 
+                getProfileConfiguration().getMaxAuthenticationAge(lookupProfileRequestContext(messageContext));
+        
+        if (maxAge != null) {
+            log.trace("{} Added max_age parameter '{}'", getLogPrefix(), maxAge);
+            getAuthenticationRequest().setMaxAge(maxAge);
+        }
+  
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddNonceHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddNonceHandler.java
new file mode 100644
index 0000000..948a0b0
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddNonceHandler.java
@@ -0,0 +1,74 @@
+/*
+ * 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.messaging.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.Nonce;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.oidc.profile.impl.OIDCSupport;
+
+/** 
+ * A message handler that adds a nonce from a lookup strategy to the authentication request.
+ * The injected nonce generation strategy could generate a {@literal null} nonce.
+ */
+public class AddNonceHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddNonceHandler.class);
+    
+    /** Strategy used to generate a nonce. Could generate a {@literal null} nonce.*/
+    @Nonnull private Function<ProfileRequestContext, Nonce> nonceGenerationStrategy;
+    
+    /** Constructor.*/
+    public AddNonceHandler() {
+        // Simple strategy that uses a secure random implementation to generate a nonce of length 16
+        nonceGenerationStrategy = prc -> new Nonce(OIDCSupport.generateNonce(16));
+    }
+    
+    /**
+     * Set the nonce generation strategy to use.
+     * 
+     * @param strategy the strategy
+     */
+    public void setNonceGenerationStrategy(@Nonnull final Function<ProfileRequestContext, Nonce> strategy) {
+    	checkSetterPreconditions();
+        
+        nonceGenerationStrategy = Constraint.isNotNull(strategy, "Nonce generation strategy can not be null");
+    }
+    
+    @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {
+        
+        
+        getAuthenticationRequest().setNonce(
+                nonceGenerationStrategy.apply(lookupProfileRequestContext(messageContext)));
+        
+        log.trace("{} Added nonce '{}' to authentication request",getLogPrefix(),
+                getAuthenticationRequest().getNonce());
+    }
+    
+    
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddPCKECodeVerifierAndChallenge.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddPCKECodeVerifierAndChallenge.java
new file mode 100644
index 0000000..19d5dd0
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddPCKECodeVerifierAndChallenge.java
@@ -0,0 +1,130 @@
+/*
+ * 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.messaging.impl;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.profile.core.OAuthAuthorizationRequest.CodeChallengeMethod;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Create an OAuth 2.0 PCKE code_verifier to use in the token request, and derives a code_challenge for immediate use in
+ * the authorization request.
+ * 
+ * @since 2.1.0
+ */
+public class AddPCKECodeVerifierAndChallenge extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddPCKECodeVerifierAndChallenge.class);
+
+    @Override
+    protected void doInvoke(final MessageContext messageContext) throws MessageHandlerException {
+        
+        if (getProfileConfiguration().isForcePKCE(lookupProfileRequestContext(messageContext))) {
+            
+            log.trace("{} PKCE enabled, adding code_challenge to request", getLogPrefix());
+            
+            final CodeChallengeMethod method = 
+                    getProfileConfiguration().isAllowPKCEPlain(lookupProfileRequestContext(messageContext)) 
+                    ? CodeChallengeMethod.PLAIN :  CodeChallengeMethod.S256;
+            
+            final String codeVerifier = generateCodeVerifier(32);     
+            final String challenge = computeCodeChallenge(codeVerifier, method);
+            
+            if (log.isTraceEnabled()) {
+                log.trace("{} Created code verifier '...{}'", getLogPrefix(), 
+                        codeVerifier.substring(challenge.length()-3));
+                log.trace("{} Derived code challenge '...{}'", getLogPrefix(), 
+                        challenge.substring(challenge.length()-3));
+            }
+            getAuthenticationRequest().setCodeVerifier(codeVerifier);
+            getAuthenticationRequest().setCodeChallenge(challenge);
+            getAuthenticationRequest().setCodeChallengeMethod(method);
+
+        } else {            
+            log.trace("{} PKCE not enabled", getLogPrefix());            
+        }        
+    }
+    
+    /**
+     * Generates a code_verifier for use during Proof Key for Code Exchange. The generated bytes are base64 URL 
+     * encoded before they are returned. 
+     *  
+     * @param length the byte length of the code_verifier. Must be at least 32 bytes long (RFC7636 section 7.1).
+     * 
+     * @return the base64 URL encoded coder_verifier value.
+     * 
+     * @throws MessageHandlerException if there is an error generating the verifier. 
+     */
+    @Nonnull private static String generateCodeVerifier(@Nonnull final Integer length) throws MessageHandlerException {
+        if (length < 32) {
+            throw new MessageHandlerException("PKCE coder_verifier must be at least 32 bytes long");
+        }
+        try {
+            final SecureRandom secureRandom = new SecureRandom();
+            final byte[] verifierInBytes = new byte[length];
+            secureRandom.nextBytes(verifierInBytes);
+            return Base64Support.encodeURLSafe(verifierInBytes);
+        } catch (final Exception e) {
+            throw new MessageHandlerException(e);            
+        }
+    }
+    
+    /**
+     * Compute the code_challenge from the code_verifier. If the {@link CodeChallengeMethod#PLAIN} method is used, the
+     * codeVerifier is returned directly. If the {@link CodeChallengeMethod#S256} method is used, the bytes of the
+     * codeVerifier are SHA-256 hashed and base 64 URL encoded before being returned. 
+     * 
+     * @param codeVerifier the code_verifier to compute the code_challenge from
+     * @param method the code_challenge_method
+     * 
+     * @return the computed code_challenge
+     * 
+     * @throws MessageHandlerException on error computing the code_challenge
+     */
+    @Nonnull @NotEmpty private String computeCodeChallenge(@Nonnull @NotEmpty final String codeVerifier, 
+            @Nonnull final CodeChallengeMethod method) throws MessageHandlerException {
+        
+        if (method == CodeChallengeMethod.PLAIN) {
+            return codeVerifier;
+        }
+        try {            
+            final MessageDigest md = MessageDigest.getInstance("SHA-256");
+            final byte[] hash = md.digest(codeVerifier.getBytes());
+            assert hash != null;
+            return Base64Support.encodeURLSafe(hash);
+            
+        } catch (final NoSuchAlgorithmException | EncodingException e) {
+            throw new MessageHandlerException("Unable to compute code_challenge", e);
+        }
+
+        
+        
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddPassiveAuthenticationHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddPassiveAuthenticationHandler.java
new file mode 100644
index 0000000..2a4e63b
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddPassiveAuthenticationHandler.java
@@ -0,0 +1,60 @@
+/*
+ * 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.messaging.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.Prompt;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * A message handler that sets the 'prompt' parameter to 'none' if passive authentication has been requested by the
+ * SP.
+ */
+public class AddPassiveAuthenticationHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddPassiveAuthenticationHandler.class);
+    
+    @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {
+        
+        final ProfileRequestContext prc = lookupProfileRequestContext(messageContext);
+        boolean isPassive = false;
+        if (prc != null && prc.getParent() instanceof final AuthenticationContext authnContext) {
+            isPassive = authnContext.isPassive();
+        }
+        if (isPassive) {
+            log.trace("{} Setting 'prompt=none' for OIDC AuthnRequest", getLogPrefix());
+            try {
+                getAuthenticationRequest().setPrompt(Prompt.parse(Prompt.Type.NONE.toString()));
+            } catch (final ParseException e) {
+              // This should never happen
+              throw new MessageHandlerException("Unable to honour passive authentication requirement, "
+                      + "setting prompt to 'none' has failed", e);
+          }
+        } else {
+            log.trace("{} No passive authentication requirement, so prompt=none has not been set", getLogPrefix());
+        }
+    }
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddRedirectURIHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddRedirectURIHandler.java
new file mode 100644
index 0000000..8955161
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddRedirectURIHandler.java
@@ -0,0 +1,82 @@
+/*
+ * 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.messaging.impl;
+
+import java.net.URI;
+import java.util.function.BiFunction;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * A message handler that adds a redirect_uri to the authentication request.
+ */
+public class AddRedirectURIHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddRedirectURIHandler.class);
+    
+    /** Function to create a suitable redirect URI from the given servlet request and profile request context.*/
+    @NonnullAfterInit private BiFunction<HttpServletRequest, ProfileRequestContext, URI> redirectUriCreationStrategy;
+        
+    /**
+     * Set the creation strategy used to compute or lookup a redirect URI.
+     * 
+     * @param strategy the creation strategy
+     */
+    public void setRedirectUriCreationStrategy(
+            @Nullable final BiFunction<HttpServletRequest, ProfileRequestContext, URI> strategy) {
+    	checkSetterPreconditions();
+        
+        if (strategy != null) {
+            redirectUriCreationStrategy = 
+                Constraint.isNotNull(strategy, "RedirectURI creation lookup strategy cannot be null");
+        }
+    }
+    
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (redirectUriCreationStrategy == null) {
+            throw new ComponentInitializationException("redirectUriCreationStrategy cannot be null");
+        }
+    }
+
+    @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {   
+       
+        final URI redirectUri = 
+                redirectUriCreationStrategy.apply(getHttpServletRequest(), lookupProfileRequestContext(messageContext));
+        if (redirectUri == null) {
+            throw new MessageHandlerException("Redirect URI could not be located or created using the strategy");
+        }
+        log.trace("{} Created redirect_uri '{}'", getLogPrefix(), redirectUri);
+        getAuthenticationRequest().setRedirectURI(redirectUri);
+  
+    }
+
+}
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
new file mode 100644
index 0000000..8e65bde
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddRequestedClaimsHandler.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.messaging.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
+
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+
+/** 
+ * A message handler that adds requested claims to the under constructions authentication request.
+ * 
+ * <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>
+ */
+public class AddRequestedClaimsHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddRequestedClaimsHandler.class);
+    
+    /** A hook that creates requested claims JSON Object from the profile request object.*/
+    @Nonnull private Function<ProfileRequestContext, OIDCClaimsRequest> requestedClaimsHook;
+    
+    /** Constructor.*/
+    public AddRequestedClaimsHandler() {
+        requestedClaimsHook = FunctionSupport.constant(null);
+    }
+    
+    /**
+     * Set the hook that generates a requested claims JSON Object from the given profile request object.
+     * 
+     * @param hook the hook
+     */
+    public void setRequestedClaimsHook(@Nullable final Function<ProfileRequestContext, OIDCClaimsRequest> hook) {
+    	checkSetterPreconditions();
+
+        if (hook != null) {
+            requestedClaimsHook = hook;
+        }
+    }
+    
+    @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {
+        
+        // Stash whether the OP supports the claims parameter for later introspection
+        getAuthenticationRequest().setProviderSupportsClaimsParameter(getProviderMetadata().supportsClaimsParam());
+        
+        if (!getProviderMetadata().supportsClaimsParam()) {
+            log.trace("{} OpenID Provider does not support the 'claims' parameter", getLogPrefix());
+            return;
+        }
+        
+        final OIDCClaimsRequest requestedClaims = 
+                requestedClaimsHook.apply(lookupProfileRequestContext(messageContext));
+        if (requestedClaims != null) {
+            getAuthenticationRequest().setRequestedClaims(requestedClaims);
+            log.trace("{} Added requested claims '{}' to the authentication request",getLogPrefix(), requestedClaims);
+        } else {
+            log.trace("{} No individual claims requested", getLogPrefix());
+        }
+        
+        
+    }
+    
+    
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddResponseTypeAndModeHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddResponseTypeAndModeHandler.java
new file mode 100644
index 0000000..e19585a
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddResponseTypeAndModeHandler.java
@@ -0,0 +1,204 @@
+/*
+ * 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.messaging.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.ResponseMode;
+import com.nimbusds.oauth2.sdk.ResponseType;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A message handler that populates the authentication request response_mode and response_type from various strategies, 
+ * by default from the profile configuration.
+ * 
+ *  <p>Unless explicitly set, the default response_mode for the specified response_type will be used.</p> 
+ */
+public class AddResponseTypeAndModeHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddResponseTypeAndModeHandler.class);
+    
+    
+    @Override protected void doInvoke(@Nonnull final MessageContext messageContext) 
+                throws MessageHandlerException {
+        
+        final String responseTypeFromProfile = 
+                getProfileConfiguration().getResponseType(lookupProfileRequestContext(messageContext)); 
+        final ResponseType responseType = parseResponseType(responseTypeFromProfile);
+        if (responseType == null){
+            throw new MessageHandlerException("response_type '"+responseTypeFromProfile+"' is not supported");
+        }
+        // Check is supported by the provider
+        checkProviderSupportsResponseType(responseType);
+       
+        final String responseModeFromProfile = 
+                getProfileConfiguration().getResponseMode(lookupProfileRequestContext(messageContext)); 
+        final ResponseMode responseModeOverride = parseResponseMode(responseModeFromProfile);
+        log.trace("{} response mode override from profile config '{}', parsed as '{}'",getLogPrefix(), responseModeFromProfile, 
+                responseModeOverride);
+                
+        final ResponseMode compatibleMode = ResponseMode.resolve(null, responseType);
+        if (compatibleMode == null) {
+            throw new MessageHandlerException("A compatible response_mode for response_type "
+                    + "'"+responseType+"' could not be found");
+        }
+        
+        checkProviderSupportsResponseMode(compatibleMode);
+        
+        log.trace("{} Compatible response_mode '{}' resolved from response_type '{}'", getLogPrefix(), compatibleMode, 
+                responseTypeFromProfile);    
+        getAuthenticationRequest().setDefaultResponseMode(compatibleMode);
+            
+        if (responseModeOverride != null && !responseModeOverride.equals(compatibleMode)) {
+            log.debug("{} response_mode override '{}' exists in the profile configuration and is different than the"
+                    + " default mode '{}' for response_type '{}'",
+                    getLogPrefix(), responseModeFromProfile, compatibleMode, responseType);  
+            
+            checkProviderSupportsResponseMode(responseModeOverride);
+            getAuthenticationRequest().setResponseMode(responseModeOverride);
+            
+        } else {
+            getAuthenticationRequest().setResponseMode(compatibleMode);
+        }
+        
+        getAuthenticationRequest().setResponseType(responseType); 
+        log.trace("{} response_type '{}' selected", getLogPrefix(), getAuthenticationRequest().getResponseType());
+        log.trace("{} response_mode '{}' selected", getLogPrefix(), getAuthenticationRequest().getResponseMode());
+            
+    }
+    
+    /**
+     * Check the OpenID Provider supports the response_type from its metadata value response_types_supported. Throws
+     * an exception if not.
+     * 
+     * @param responseType the response_type to check is supported
+     * 
+     * @throws MessageHandlerException if the OpenID Provider does not support the response_type
+     */
+    private void checkProviderSupportsResponseType(@Nonnull final ResponseType responseType) 
+            throws MessageHandlerException {
+        final List<ResponseType> responseTypesSupported = getProviderMetadata().getResponseTypes();
+        if (responseTypesSupported == null) {
+            throw new MessageHandlerException("OpenID Provider has not specified supported response types "
+                    + "(response_types_supported). It MUST.");
+        }
+        if (!responseTypesSupported.contains(responseType)) {
+            throw new MessageHandlerException("OpenID Provider does not support chosen response_type: "
+                    + responseType.toString());
+        }
+    }
+    
+    /**
+     * Check the OpenID Provider supports the response_mode from its metadata value response_modes_supported. Throws
+     * an exception if not.
+     * 
+     * @param responseMode the response_mode to check is supported
+     * 
+     * @throws MessageHandlerException if the OpenID Provider does not support the response_mode
+     */
+    private void checkProviderSupportsResponseMode(@Nonnull final ResponseMode responseMode) 
+            throws MessageHandlerException {
+        final List<ResponseMode> responseModesSupportedOP = getProviderMetadata().getResponseModes();
+        final List<ResponseMode> responseModesSupported = new ArrayList<>(2);
+        if (responseModesSupportedOP == null || responseModesSupportedOP.isEmpty()) {
+            // Add the defaults from the specification
+            responseModesSupported.add(ResponseMode.QUERY);
+            responseModesSupported.add(ResponseMode.FRAGMENT);
+        } else {
+            responseModesSupportedOP.forEach(responseModesSupported::add);
+        }
+        if (!responseModesSupported.contains(responseMode)) {
+            throw new MessageHandlerException("OpenID Provider does not support chosen response_mode: "
+                    + responseMode.toString());
+        }
+        
+    }
+    
+    /**
+     * Parse the response_type into a known {@link ResponseType}.
+     * 
+     * @param responseTypeFromProfile the response_type as a string
+     * 
+     * @return the parsed {@link ResponseType}, or {@literal null} if the input type is unknown
+     */
+    @Nullable private ResponseType parseResponseType(@Nullable final String responseTypeFromProfile) {
+        
+        if (responseTypeFromProfile == null) {
+            return null;
+        }
+
+        if (responseTypeFromProfile.equals(ResponseType.CODE.toString())) {
+            return ResponseType.CODE;
+        } else if (responseTypeFromProfile.equals(ResponseType.CODE_IDTOKEN.toString())) {
+            return ResponseType.CODE_IDTOKEN; 
+        } else if (responseTypeFromProfile.equals(ResponseType.CODE_IDTOKEN_TOKEN.toString())) {
+            return ResponseType.CODE_IDTOKEN_TOKEN;
+        } else if (responseTypeFromProfile.equals(ResponseType.CODE_TOKEN.toString())) {
+            return ResponseType.CODE_TOKEN;
+        } else if (responseTypeFromProfile.equals(ResponseType.IDTOKEN.toString())) {
+            return ResponseType.IDTOKEN;
+        } else if (responseTypeFromProfile.equals(ResponseType.IDTOKEN_TOKEN.toString())) {
+            return ResponseType.IDTOKEN_TOKEN;
+        } else {
+            return null;
+        }
+    }
+    
+ // Checkstyle: ReturnCount OFF
+    /**
+     * Parse the response_mode into a known {@link ResponseMode}.
+     * 
+     * @param responseModeFromProfile the response_mode as a string
+     * 
+     * @return the parsed {@link ResponseMode}, or {@literal null} if the input type is unknown
+     */
+    @Nullable private ResponseMode parseResponseMode(@Nullable final String responseModeFromProfile) {
+        
+        if (responseModeFromProfile == null) {
+            return null;
+        }
+        
+        if (responseModeFromProfile.equals(ResponseMode.FORM_POST.getValue())) {
+            return ResponseMode.FORM_POST;
+        } else if (responseModeFromProfile.equals(ResponseMode.FORM_POST_JWT.getValue())) {
+            return ResponseMode.FORM_POST_JWT;
+        } else if (responseModeFromProfile.equals(ResponseMode.QUERY.getValue())) {
+            return ResponseMode.QUERY;
+        } else if (responseModeFromProfile.equals(ResponseMode.FRAGMENT.getValue())) {
+            return ResponseMode.FRAGMENT;
+        } else if (responseModeFromProfile.equals(ResponseMode.FRAGMENT_JWT.getValue())) {
+            return ResponseMode.FRAGMENT_JWT;
+        } else if (responseModeFromProfile.equals(ResponseMode.JWT.getValue())) {
+            return ResponseMode.JWT;
+        } else if (responseModeFromProfile.equals(ResponseMode.QUERY_JWT.getValue())) {
+            return ResponseMode.QUERY_JWT;
+        } else {
+            return null;
+        }
+    }
+ // Checkstyle: ReturnCount ON
+    
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddScopesHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddScopesHandler.java
new file mode 100644
index 0000000..97794db
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddScopesHandler.java
@@ -0,0 +1,50 @@
+/*
+ * 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.messaging.impl;
+
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/** 
+ * A message handler that adds the scopes from the profile request object to the authentication request.
+ */
+public class AddScopesHandler extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddScopesHandler.class);
+    
+    @Override
+    protected void doInvoke(@Nonnull final MessageContext messageContext) 
+            throws MessageHandlerException {
+        
+        final Set<String> scopes = getProfileConfiguration().getScopes(lookupProfileRequestContext(messageContext));
+        if (scopes != null && !scopes.isEmpty()) {
+            scopes.forEach(s -> getAuthenticationRequest().getScope().add(s));
+        }
+        log.trace("{} Added scopes '{}' to authentication request",getLogPrefix(), 
+                getAuthenticationRequest().getScope());
+    }
+    
+    
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddStateHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddStateHandler.java
new file mode 100644
index 0000000..a9067a3
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/AddStateHandler.java
@@ -0,0 +1,91 @@
+/*
+ * 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.messaging.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.id.State;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.oidc.profile.impl.OIDCSupport;
+
+/** 
+ * Add state to the authentication request URL and the request object claims (if present).
+ * By default this is generated by concatenating the Hex value of the spring webflow execution 
+ * key with a secure random 32 character nonce. 
+ * */
+public class AddStateHandler extends AbstractOIDCAuthenticationRequestMessageHandler {
+    
+    /** The 'state' claim name.*/
+    @Nonnull private static final String STATE_CLAIM = "state";
+
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddStateHandler.class);
+    
+    /** Function to create a suitable 'state' to add to the authentication request.*/
+    @Nonnull private Function<MessageContext, String> stateGenerationStrategy;
+    
+    /** Constructor.*/
+    public AddStateHandler() {
+        // By default, generate state a 32 character nonce.
+        stateGenerationStrategy = msg -> OIDCSupport.generateNonce(32);
+    }
+    
+    /**
+     * Set the creation strategy used to compute the 'state'.
+     * 
+     * @param strategy the creation strategy
+     */
+    public void setStateGenerationStrategy(
+            @Nullable final  Function<MessageContext, String> strategy) {
+    	checkSetterPreconditions();
+        
+        if (strategy != null) {
+            stateGenerationStrategy = strategy;
+        }
+        
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+        
+        final String stateString = stateGenerationStrategy.apply(messageContext);        
+        if (stateString == null) {
+            throw new MessageHandlerException("Generated state was null");
+        }
+        log.trace("{} Generated state '{}'", getLogPrefix(), stateString);
+        final State state = new State(stateString);
+        
+        // Add to outer request
+        getAuthenticationRequest().setState(state);
+        
+        // Add to Request Object if exists
+        final ClaimsSet claims = getAuthenticationRequest().getRequestObjectClaimsSet();
+        if (claims != null) {            
+            log.trace("{} Adding state to JWT RequestObject", getLogPrefix());
+            claims.setClaim(STATE_CLAIM, state);           
+        }               
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/BuildPlainRequestObjectJWT.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/BuildPlainRequestObjectJWT.java
new file mode 100644
index 0000000..313861f
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/BuildPlainRequestObjectJWT.java
@@ -0,0 +1,56 @@
+/*
+ * 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.messaging.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/** 
+ * If the Request Object claims are present in the authentication request, convert them 
+ * into a JWTClaimsSet inside a PlainJWT. 
+ */
+public class BuildPlainRequestObjectJWT extends AbstractOIDCAuthenticationRequestMessageHandler {
+
+    /** Logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(BuildPlainRequestObjectJWT.class);
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {
+
+        final ClaimsSet requestObjectClaims = getAuthenticationRequest().getRequestObjectClaimsSet();
+        if (requestObjectClaims == null) {
+            log.trace("{} RequestObject claims are not present, request object JWT skipped", getLogPrefix());
+            return;
+        }
+        try {
+            getAuthenticationRequest().setRequestObject(new PlainJWT(requestObjectClaims.toJWTClaimsSet()));
+            log.trace("{} Built Plain JWT RequestObject from claims", getLogPrefix());
+        } catch (final ParseException e) {
+            throw new MessageHandlerException("Unable to generate request object JWT", e);
+        }
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/SetAuthenticationRequestTimeHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/SetAuthenticationRequestTimeHandler.java
new file mode 100644
index 0000000..13d802f
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/SetAuthenticationRequestTimeHandler.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.messaging.impl;
+
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/** Handler that adds the authentication request time to the authentication request.*/
+public class SetAuthenticationRequestTimeHandler extends AbstractOIDCAuthenticationRequestMessageHandler {
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SetAuthenticationRequestTimeHandler.class);
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInvoke(@Nonnull final MessageContext messageContext) throws MessageHandlerException {        
+        getAuthenticationRequest().setAuthnRequestTime(Instant.now());              
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/Example.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/package-info.java
similarity index 86%
rename from sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/Example.java
rename to sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/package-info.java
index 3a5e335..91d9f47 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/Example.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/messaging/impl/package-info.java
@@ -12,11 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.sp.oidc;
-
 /**
- *
+ * Package that contains message handlers.
  */
-public class Example {
-
-}
+package net.shibboleth.sp.oidc.messaging.impl;
\ No newline at end of file
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/metadata/impl/ApplicationMetadataResolverLookupFunction.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/metadata/impl/ApplicationMetadataResolverLookupFunction.java
new file mode 100644
index 0000000..bf164fa
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/metadata/impl/ApplicationMetadataResolverLookupFunction.java
@@ -0,0 +1,80 @@
+/*
+ * 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.io.IOException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.saml.metadata.resolver.MetadataResolver;
+import org.slf4j.Logger;
+import org.springframework.core.io.ClassPathResource;
+
+import net.shibboleth.oidc.metadata.ProviderMetadataResolver;
+import net.shibboleth.oidc.metadata.impl.FilesystemProviderMetadataResolver;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.Application;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.profile.context.navigate.messaging.AbstractAgentRequestLookupFunction;
+
+/**
+ * Locates the {@link MetadataResolver} associated with the {@link Application} making an agent request,
+ * and wraps it in a {@link ProviderMetadataResolver}.
+ * 
+ * TODO I think this will eventually exist in the SP core and be more generic.
+ */
+public class ApplicationMetadataResolverLookupFunction
+        extends AbstractAgentRequestLookupFunction<ProviderMetadataResolver> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ApplicationMetadataResolverLookupFunction.class);
+    
+    /** {@inheritDoc} */
+    @Nullable public ProviderMetadataResolver apply(@Nullable final MessageContext input) {
+        final AgentRequestContext arc = getAgentRequestContext(input);
+        if (arc != null) {
+            final Application application = arc.getApplication();
+            if (application != null) {
+                try {
+//                    final ReloadingProviderMetadataProvider metadataResolver =
+//                            new ReloadingProviderMetadataProvider(application.getMetadataResolver());
+//                    metadataResolver.setId(application.getId() + " MetadataResolver");
+//                    metadataResolver.initialize();
+//                    
+//                    final ProviderMetadataResolver roleResolver =
+//                            new ProviderMetadataResolver(metadataResolver);
+//                    roleResolver.initialize();
+                    
+                    // FIXME, this will need changing over once supported to take from Application
+                    final FilesystemProviderMetadataResolver fsr = 
+                            new FilesystemProviderMetadataResolver(
+                                    new ClassPathResource("metadata/openid-configuration.json"));
+                    fsr.setId(application.getId() + " MetadataResolver");
+                    fsr.initialize();
+                    
+                    return fsr;
+                } catch (final IOException | ComponentInitializationException e) {
+                    log.error("Exception wrapping Application-supplied MetadataResolver for use", e);
+                }
+            }
+        }
+        
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/metadata/impl/OIDCProviderMetadataLookupHandler.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/metadata/impl/OIDCProviderMetadataLookupHandler.java
index ed40bf3..7fe7e6e 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/metadata/impl/OIDCProviderMetadataLookupHandler.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/metadata/impl/OIDCProviderMetadataLookupHandler.java
@@ -36,9 +36,8 @@ import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
 import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
 import net.shibboleth.oidc.profile.messaging.context.AbstractOIDCEntityContext;
 import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
-import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
 import net.shibboleth.shared.primitive.LoggerFactory;
 import net.shibboleth.shared.resolver.CriteriaSet;
 import net.shibboleth.shared.resolver.ResolverException;
@@ -54,17 +53,15 @@ import net.shibboleth.shared.resolver.ResolverException;
  * then its data will be re-used.
  * </p>
  * 
- * 
- * TODO, this comes from the agent service?
  */
 public class OIDCProviderMetadataLookupHandler extends AbstractMessageHandler {
     
     /** Logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(OIDCProviderMetadataLookupHandler.class);
     
-    /** Resolver used to look up OIDC provider information. */
-    @NonnullAfterInit private ProviderMetadataResolver providerResolver;
-
+    /** Resolver used to look up OIDC metadata. */
+    @Nonnull private Function<MessageContext,ProviderMetadataResolver> metadataResolverLookupStrategy;
+    
     /** Strategy to resolve the context class to add the resolved metadata too.*/
     @Nonnull private Function<MessageContext,? extends AbstractOIDCEntityContext> contextClassLookupStrategy;
     
@@ -74,6 +71,7 @@ public class OIDCProviderMetadataLookupHandler extends AbstractMessageHandler {
     /** Constructor.*/
     public OIDCProviderMetadataLookupHandler() {
         contextClassLookupStrategy = new ChildContextLookup<>(OIDCPeerEntityContext.class);
+        metadataResolverLookupStrategy = FunctionSupport.constant(null);
     }
     
     /** Set the context class lookup strategy.
@@ -107,23 +105,32 @@ public class OIDCProviderMetadataLookupHandler extends AbstractMessageHandler {
    public void setProviderMetadataResolver(@Nonnull final ProviderMetadataResolver resolver) {
 	   checkSetterPreconditions();
 
-       providerResolver = Constraint.isNotNull(resolver, "ProviderMetadataResolver cannot be null");
+	   metadataResolverLookupStrategy = FunctionSupport.constant(
+               Constraint.isNotNull(resolver, "ProviderMetadataResolver cannot be null"));
    }
    
-   /** {@inheritDoc} */
-   @Override
-   protected void doInitialize() throws ComponentInitializationException {
-       super.doInitialize();
-
-       if (providerResolver == null) {
-           throw new ComponentInitializationException("ProviderMetadataResolver cannot be null");
-       }
-
+   /**
+    * Set the lookup strategy for the {@link ProviderMetadataResolver} to use.
+    * 
+    * @param strategy lookup strategy
+    * 
+    */
+   public void setProviderMetadataResolverLookupStrategy(
+           @Nonnull final Function<MessageContext,ProviderMetadataResolver> strategy) {
+       checkSetterPreconditions();
+       metadataResolverLookupStrategy =
+               Constraint.isNotNull(strategy, "ProviderMetadataResolver lookup strategy cannot be null");
    }
 
     @Override
     protected void doInvoke(final MessageContext messageContext) throws MessageHandlerException {
-        ifNotInitializedThrowUninitializedComponentException();
+        checkComponentActive();
+        
+        final ProviderMetadataResolver metadataResolver = metadataResolverLookupStrategy.apply(messageContext);
+        if (metadataResolver == null) {
+            log.error("{} No MetadataResolver available", getLogPrefix());
+            return;
+        }
         
         final AbstractOIDCEntityContext entityCtx = contextClassLookupStrategy.apply(messageContext);
         final String id = entityCtx != null ? entityCtx.getIdentifier() : null;
@@ -145,7 +152,7 @@ public class OIDCProviderMetadataLookupHandler extends AbstractMessageHandler {
         final IssuerIDCriterion issuerCriterion = new IssuerIDCriterion(new Issuer(entityCtx.getIdentifier()));
         final CriteriaSet criteria = new CriteriaSet(issuerCriterion);
         try {
-            final OIDCProviderMetadata issuerMetadata = providerResolver.resolveSingle(criteria);
+            final OIDCProviderMetadata issuerMetadata = metadataResolver.resolveSingle(criteria);
             if (issuerMetadata == null) {
                 log.debug("{} No provider metadata returned for {}",getLogPrefix(), entityCtx.getIdentifier());
                 return;
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultRedirectUriCreationFunction.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultRedirectUriCreationFunction.java
new file mode 100644
index 0000000..0af2c35
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultRedirectUriCreationFunction.java
@@ -0,0 +1,247 @@
+/*
+ * 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.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.core5.net.URIBuilder;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.sp.oidc.context.OAuth2ClientContext;
+
+/**
+ *  Constructive, pure, function that returns a redirect_uri from one of (ordered):
+ *  <ol>
+ *      <li>A pre-registered redirect_uri on the {@link OAuth2ClientContext#getRedirectUriOverride()}. 
+ *      Or, if none are pre-registered;
+ *      <li>Derived from the HTTP Servlet request server parameters, checking the origin
+ *          against an allowed set of origins - to prevent Host header injection.
+ *   </ol>
+ *  
+ *  <p>Returns null if one can not be constructed.</p>
+ *  
+ *  <p>Is thread-safe and immutable</p> 
+ */
+ at ThreadSafeAfterInit
+public class DefaultRedirectUriCreationFunction extends AbstractIdentifiableInitializableComponent
+                        implements BiFunction<HttpServletRequest, ProfileRequestContext, URI> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultRedirectUriCreationFunction.class);
+    
+    /** The strategy used to lookup or create the {@link OAuth2ClientContext} for storing the client_id.*/
+    @Nonnull private Function<ProfileRequestContext, OAuth2ClientContext> oauth2ClientContextLookupStrategy;
+    
+    /** The path, excluding the context and servlet paths, to the RP callback handler.*/
+    @NonnullAfterInit @NotEmpty private String callbackServletPath;
+    
+    /** 
+     * A set of 'allowed' origins that can be used as the scheme, host, and port portion of the redirectURI.
+     * Can be null, if so a redirect_uri must be specified in the context tree.
+     */
+    @NonnullAfterInit private Set<String> allowedOrigins;
+    
+    /**
+     * Constructor.
+     */
+    public DefaultRedirectUriCreationFunction() {          
+        // Default under OIDCPeerEntityContext in the outbound context (create true) under the nested PRC.
+        oauth2ClientContextLookupStrategy = new ChildContextLookup<>(OAuth2ClientContext.class).compose(
+                new ChildContextLookup<>(OIDCPeerEntityContext.class).compose(
+                        new OutboundMessageContextLookup()));        
+    }
+    
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (StringSupport.trimOrNull(callbackServletPath) == null) {
+            throw new ComponentInitializationException("Callback servlet path can not be null");
+        }
+        if (allowedOrigins == null) {
+            allowedOrigins = Collections.emptySet();
+        }
+    }
+    
+    /**
+     * Set the path segment relative to the servlet path of the callback endpoint.
+     * 
+     * @param path the callback servlet path
+     */
+    public void setCallbackServletPath(@Nonnull @NotEmpty final String path) {
+    	checkSetterPreconditions();
+
+        callbackServletPath = Constraint.isNotEmpty(path, "callbackServletPath can not be null");
+    }
+    
+    /**
+     * Set the allowed origins to use if a redirect_uri is computed.
+     * 
+     * @param origins the origins
+     */
+    public void setAllowedOrigins(@Nullable final Set<String> origins) {
+    	checkSetterPreconditions();
+        
+        if (origins == null) {
+            allowedOrigins = Collections.emptySet();
+        }
+        allowedOrigins = Collections.unmodifiableSet(origins);
+    }
+
+    
+    /**
+     * Set the strategy to lookup the {@link OAuth2ClientContext} 
+     * from the {@link ProfileRequestContext}.
+     * 
+     * @param strgy the strategy.
+     */
+    public void setOAuth2ClientContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OAuth2ClientContext> strgy) {        
+    	checkSetterPreconditions();
+
+        oauth2ClientContextLookupStrategy = Constraint.isNotNull(strgy, 
+                "OAuth2 client context lookup strategy cannot be null");
+    }
+
+    @Override
+    @Nullable public URI apply(@Nullable final HttpServletRequest request, @Nullable final ProfileRequestContext prc) {
+        
+        final OAuth2ClientContext context = oauth2ClientContextLookupStrategy.apply(prc);
+        if (context == null) {
+            log.warn("Could not locate the OAuth2 Client Context, can not compute redirect_uri");
+            return null;
+        }
+        if (request == null) {
+            log.warn("HttpServletRequest was unavailable, can not compute redirect_uri");
+            return null;
+        }
+        if (context.getRedirectUriOverride() != null) {
+            return context.getRedirectUriOverride();
+        } 
+        
+        // Should be caught upstream, but dbl check.
+        if (allowedOrigins.isEmpty()) {
+            log.warn("Can not compute redirect_uri if allowed origins is empty");
+            return null;
+        }
+        
+        
+        try {
+            final String scheme = request.getScheme();
+            assert scheme != null;
+            final String serverName = request.getServerName();
+            assert serverName != null;
+            
+            final URI redirectUri = buildURIIgnoreDefaultPorts(scheme,
+                    serverName,
+                    request.getServerPort(),
+                    request.getContextPath()+request.getServletPath()+callbackServletPath);
+            
+            final String origin = buildOrigin(redirectUri);
+            if (!allowedOrigins.contains(origin)) {
+                log.warn("The 'origin' of the computed redirect_uri ('{}') is not allowed. If permissible, add it "
+                        + "to the allowed origins property.",origin);
+                return null;
+            }
+            return redirectUri;
+        } catch (final URISyntaxException e) {
+            log.warn("Unable to create redirect_uri for OIDC authentication request", e);
+            return null;
+        }
+    }
+    
+    /**
+     * Builds the 'origin' (see RFC 6454) from given URI. Omits default or unknown ports. 
+     *  
+     * @param uri the URI to build the origin from
+     * @return the origin
+     * @throws URISyntaxException if there is an error getting information from the URI.
+     */
+    @Nonnull private String buildOrigin(@Nonnull final URI uri) throws URISyntaxException {    
+        if (uri.getPort() == -1) {
+            //is the default port (or is not defined), do not include
+            final String uriAsString = new URI(String.format("%s://%s", uri.getScheme(),uri.getHost())).toString();
+            assert uriAsString != null;
+            return uriAsString;
+        } else {
+            final String uriAsString =
+                    new URI(String.format("%s://%s:%s", uri.getScheme(),uri.getHost(),uri.getPort())).toString();
+            assert uriAsString != null;
+            return uriAsString;
+        }
+
+    }
+    
+    /**
+     * Build a {@link URI} from the given parameters. If the scheme is either
+     * 'http' or 'https' with their respective default port, the port is set to -1.
+     * 
+     * @param scheme the scheme
+     * @param host the hostname
+     * @param port the port
+     * @param path the path
+     * 
+     * @return a fully built URI from the given parameters.
+     * 
+     * @throws URISyntaxException if the URI can not be constructed.
+     */
+    @Nonnull private final URI buildURIIgnoreDefaultPorts(@Nonnull final String scheme, 
+            @Nonnull final String host, final int port, 
+            @Nonnull final String path) throws URISyntaxException {
+        
+        int usedPort = port;
+        if ("http".equalsIgnoreCase(scheme)) {
+            // ignore port iff using the default http port
+            if (port == 80) {
+                usedPort = -1;
+            }
+        } else if ("https".equalsIgnoreCase(scheme)) {
+            // ignore port iff using the default https port
+            if (port == 443) {
+                usedPort = -1;
+            }
+        }
+        final URI builtUri = new URIBuilder()
+                .setScheme(scheme)
+                .setHost(host)
+                .setPort(usedPort)
+                .setPath(path)
+                .build();
+        assert builtUri != null;
+        return builtUri;
+    }
+    
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeAuthorizationRequest.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeAuthorizationRequest.java
new file mode 100644
index 0000000..669a45d
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeAuthorizationRequest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.oidc.context.OAuth2ClientContext;
+
+/**
+ * An action that creates an {@link OIDCAuthenticationRequest} shell to populate in future steps,
+ * and sets it to the outbound message context.
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * @post Add an {@link OIDCAuthenticationRequest} as the message of the outbound context.
+ */
+public class InitializeAuthorizationRequest extends AbstractProfileAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(InitializeAuthorizationRequest.class); 
+    
+    /** The strategy used to lookup or create the {@link OAuth2ClientContext} for storing the client_id.*/
+    @Nonnull 
+    private Function<ProfileRequestContext, OAuth2ClientContext> oauth2ClientContextLookupStrategy;
+    
+    /** The stashed OAuth2 client context.*/
+    @NonnullBeforeExec private OAuth2ClientContext oauth2ClientContext;
+    
+    /** The stashed outbound message context. */
+    @NonnullBeforeExec private MessageContext outMessageContext;
+    
+    /** Constructor.*/
+    public InitializeAuthorizationRequest() {        
+        // Default under OIDCPeerEntityContext in the outbound context (create true).
+        oauth2ClientContextLookupStrategy = new ChildContextLookup<>(OAuth2ClientContext.class, true).compose(
+                new ChildContextLookup<>(OIDCPeerEntityContext.class).compose(
+                        new OutboundMessageContextLookup()));
+    }
+    
+    /**
+     * Set the strategy to lookup the {@link OAuth2ClientContext} 
+     * from the {@link ProfileRequestContext}.
+     * 
+     * @param strgy the strategy.
+     */
+    public void setOAuth2ClientContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OAuth2ClientContext> strgy) {
+    	checkSetterPreconditions();
+
+        oauth2ClientContextLookupStrategy = Constraint.isNotNull(strgy, 
+                "OAuth2 client context lookup strategy cannot be null");
+    }
+    
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+             
+        oauth2ClientContext = oauth2ClientContextLookupStrategy.apply(profileRequestContext);
+        if (oauth2ClientContext == null) {
+            log.error("{} OAuth2 client context not found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        
+        outMessageContext = profileRequestContext.getOutboundMessageContext();
+        if (outMessageContext == null) {
+            log.error("{} Outbound message context not found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        
+        return true;
+    }
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        super.doExecute(profileRequestContext);
+        
+        final OIDCAuthenticationRequest authRequest = 
+                new OIDCAuthenticationRequest(new ClientID(oauth2ClientContext.getClientId()));
+        
+        outMessageContext.setMessage(authRequest);
+        log.debug("{} Adding shell OIDC authentication request to outbound context for client '{}'", getLogPrefix(), 
+                oauth2ClientContext.getClientId());
+       
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeOAuth2ClientContext.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeOAuth2ClientContext.java
new file mode 100644
index 0000000..d27de46
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeOAuth2ClientContext.java
@@ -0,0 +1,174 @@
+/*
+ * 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.net.URI;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationRelyingPartyProfileConfiguration;
+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.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.sp.oidc.config.navigate.RedirectUriLookupFunction;
+import net.shibboleth.sp.oidc.context.OAuth2ClientContext;
+
+/**
+ * An {@link AbstractProfileAction action} that resolves the client identifier and redirect URI for the chosen 
+ * provider (issuer). 
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @post Add the clientId and redirect URI to the {@link OAuth2ClientContext}
+ */
+// TODO this mostly pulls in things from the profile config and stores them, it could just come from the profile later
+public class InitializeOAuth2ClientContext extends AbstractProfileAction {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(InitializeOAuth2ClientContext.class);
+    
+    /** The strategy used to lookup or create the {@link OAuth2ClientContext} for storing the client_id.*/
+    @Nonnull private Function<ProfileRequestContext, OAuth2ClientContext> oauth2ClientContextLookupStrategy;
+    
+    /** The stashed OAuth2 client context.*/
+    @NonnullBeforeExec private OAuth2ClientContext oauth2ClientContext;
+
+    /** A redirect_uri lookup strategy which can pull out an override redirect_uri from the profile request context.*/
+    @Nonnull private  Function<ProfileRequestContext, URI> redirectUriOverrideLookupStrategy;
+    
+    /** Lookup function for relying party context. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+    
+    /** Applicable stashed profile configuration. */
+    @NonnullBeforeExec private OIDCAuthenticationRelyingPartyProfileConfiguration profileConfiguration;
+    
+    /** Constructor.*/
+    public InitializeOAuth2ClientContext() {       
+        // Default under OIDCPeerEntityContext in the outbound context (create true).
+        oauth2ClientContextLookupStrategy = new ChildContextLookup<>(OAuth2ClientContext.class, true).compose(
+                new ChildContextLookup<>(OIDCPeerEntityContext.class).compose(
+                        new OutboundMessageContextLookup()));
+        
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+        
+        redirectUriOverrideLookupStrategy = new RedirectUriLookupFunction();
+    }
+    
+    /**
+     * Set lookup strategy for relying party context.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+        relyingPartyContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the redirect_uri lookup strategy to locate an explicitly set redirect_uri.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setRedirectUriOverrideLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, URI> strategy) {
+    	checkSetterPreconditions();
+        
+        redirectUriOverrideLookupStrategy = 
+                Constraint.isNotNull(strategy, "Redirect URI lookup strategy can not be null");
+    }
+    
+    /**
+     * Set the strategy to lookup the {@link OAuth2ClientContext} 
+     * from the {@link ProfileRequestContext}.
+     * 
+     * @param strgy the strategy.
+     */
+    public void setOAuth2ClientContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OAuth2ClientContext> strgy) {
+    	checkSetterPreconditions();
+
+        oauth2ClientContextLookupStrategy = Constraint.isNotNull(strgy, 
+                "OAuth2 client context lookup strategy cannot be null");
+    }
+    
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        oauth2ClientContext = oauth2ClientContextLookupStrategy.apply(profileRequestContext);
+        if (oauth2ClientContext == null) {
+            log.error("{} No OAuth2 client context found or created", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        final RelyingPartyContext rpCtx = relyingPartyContextLookupStrategy.apply(profileRequestContext);     
+        if (rpCtx != null && rpCtx.getConfiguration() != null &&
+                rpCtx.getProfileConfig() instanceof final OIDCAuthenticationRelyingPartyProfileConfiguration rpConfig) {
+            profileConfiguration = rpConfig;
+        }
+        if (profileConfiguration == null) {
+            log.error("{} OIDCAuthorizationConfiguration not found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        
+        return true;        
+    }
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        super.doExecute(profileRequestContext);
+        
+        //TODO from the agent Application?
+        final String clientId = "test-client"; //profileConfiguration.getClientId(profileRequestContext);
+        if (StringSupport.trimOrNull(clientId) == null) {
+            log.error("{} No client_id found from the profile configuration", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CONFIG);
+            return;
+        }
+        assert clientId != null;
+        oauth2ClientContext.setClientId(clientId);
+        
+        final URI redirectUri = redirectUriOverrideLookupStrategy.apply(profileRequestContext);
+        if (redirectUri != null) {
+            log.debug("{} Redirect_uri has been explicitly set as '{}'", getLogPrefix(), redirectUri);
+            oauth2ClientContext.setRedirectUriOverride(redirectUri);
+        }             
+        log.debug("{} Initialized OAuth2 Client Context for client '{}'", getLogPrefix(), clientId);
+       
+    }
+    
+    
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeOutboundMessageContext.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeOutboundMessageContext.java
new file mode 100644
index 0000000..57bfb65
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeOutboundMessageContext.java
@@ -0,0 +1,161 @@
+/*
+ * 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.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.BaseContext;
+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.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
+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.primitive.LoggerFactory;
+
+/** 
+ * Initialize an outbound message context with an OIDC peer entity context ready for an authorization/authentication 
+ * request to be built.
+ * 
+ * TODO self context
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @post ProfileRequestContext.getOutboundMessageContext(msgCtx != null
+ * 
+ */
+public class InitializeOutboundMessageContext extends AbstractProfileAction {
+    
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(InitializeOutboundMessageContext.class); 
+    
+    /** The {@link OIDCPeerEntityContext} to base the outbound context on. */
+    @NonnullBeforeExec private OIDCPeerEntityContext peerEntityCtx;
+            
+    /** Strategy function to lookup the {@link OIDCMetadataContext} that represents this client during
+     * communication with the given OIDC peer. */
+    @Nonnull 
+    private Function<ProfileRequestContext, OIDCMetadataContext> oidcClientMetadataCtxLookupStrategy;
+    
+    /**
+     * Strategy used to locate the {@link RelyingPartyContext} associated with a given {@link ProfileRequestContext}.
+     */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyCtxLookupStrategy;
+
+    
+    /**
+     * Constructor.
+     */
+    public InitializeOutboundMessageContext() {
+        oidcClientMetadataCtxLookupStrategy = new ChildContextLookup<>(OIDCMetadataContext.class).compose(
+                new InboundMessageContextLookup());    
+        relyingPartyCtxLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+    }
+    
+    /**
+     * Set the strategy to lookup the {@link OIDCMetadataContext} 
+     * from the {@link ProfileRequestContext}.
+     * 
+     * @param strgy What to set.
+     */
+    public void setOIDCClientMetadataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCMetadataContext> strgy) {
+    	checkSetterPreconditions();
+
+        oidcClientMetadataCtxLookupStrategy = Constraint.isNotNull(strgy, "Injected Metadata Strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to locate the {@link RelyingPartyContext} associated with a given
+     * {@link ProfileRequestContext}.
+     * 
+     * @param strategy strategy used to locate the {@link RelyingPartyContext} associated with a given
+     *            {@link ProfileRequestContext}
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyContext> strategy) {
+    	checkSetterPreconditions();
+
+        relyingPartyCtxLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+    
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        final RelyingPartyContext relyingPartyCtx = relyingPartyCtxLookupStrategy.apply(profileRequestContext);
+        if (relyingPartyCtx == null) {
+            log.error("{} No relying party context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        final BaseContext identifyingCtx = relyingPartyCtx.getRelyingPartyIdContextTree();
+        if (!(identifyingCtx instanceof OIDCPeerEntityContext)) {
+            log.debug("{} No OIDC peer entity context found via relying party context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            return false;
+        }
+
+        peerEntityCtx = (OIDCPeerEntityContext) identifyingCtx;
+        
+        return true;
+        
+    }
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        super.doExecute(profileRequestContext);
+        
+        final MessageContext msgCtx = new MessageContext();
+        profileRequestContext.setOutboundMessageContext(msgCtx);
+        
+        //TODO self context for info about RP?
+        
+        final OIDCPeerEntityContext outboundPeerContext = msgCtx.ensureSubcontext(OIDCPeerEntityContext.class);
+        outboundPeerContext.setIdentifier(peerEntityCtx.getIdentifier());
+        
+        final OIDCProviderMetadataContext inboundProviderMetadataCtx = 
+                peerEntityCtx.getSubcontext(OIDCProviderMetadataContext.class);
+        
+        if (inboundProviderMetadataCtx != null) {
+            final OIDCProviderMetadataContext outboundMetadataCtx = 
+                    outboundPeerContext.ensureSubcontext(OIDCProviderMetadataContext.class);
+            // Pass a reference here? we do not need to mutate the metadata
+            outboundMetadataCtx.setProviderInformation(inboundProviderMetadataCtx.getProviderInformation());            
+        }
+
+        log.debug("{} Initialized outbound message context", getLogPrefix());
+
+
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeRelyingPartyContextFromOIDCPeer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeRelyingPartyContextFromOIDCPeer.java
new file mode 100644
index 0000000..9b035d1
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/InitializeRelyingPartyContextFromOIDCPeer.java
@@ -0,0 +1,178 @@
+/*
+ * 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.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
+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.primitive.LoggerFactory;
+
+/**
+ * Action that adds a {@link RelyingPartyContext} to the current {@link ProfileRequestContext} tree via a creation
+ * function. The context is populated via a lookup strategy to locate a {@link OIDCPeerEntityContext},
+ * by default via {@link ProfileRequestContext#getInboundMessageContext()}.
+ * 
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
+ * @post ProfileRequestContext.getSubcontext(RelyingPartyContext.class) != null with relying party id set.
+ */
+public class InitializeRelyingPartyContextFromOIDCPeer extends AbstractProfileAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(InitializeRelyingPartyContextFromOIDCPeer.class);
+
+    /** Strategy that will return or create a {@link RelyingPartyContext}. */
+    @Nonnull private Function<ProfileRequestContext, RelyingPartyContext> relyingPartyContextCreationStrategy;
+    
+    /** Strategy that will return {@link OIDCMetadataContext}. */
+    @Nonnull 
+    private Function<ProfileRequestContext, OIDCProviderMetadataContext> oidcProviderMetadataContextLookupStrategy;
+        
+    /** OIDC peer entity context to populate from. */
+    @NonnullBeforeExec private OIDCPeerEntityContext peerEntityCtx;
+    
+    /** Strategy used to look up the {@link OIDCPeerEntityContext} to draw from. */
+    @Nonnull private Function<ProfileRequestContext,OIDCPeerEntityContext> peerEntityContextLookupStrategy;
+    
+    /** The stashed identifier of the OIDC entity.*/
+    @NonnullBeforeExec private String peerIdentifier;
+    
+    /** Constructor. */
+    public InitializeRelyingPartyContextFromOIDCPeer() {
+        relyingPartyContextCreationStrategy = new ChildContextLookup<>(RelyingPartyContext.class, true);  
+        
+        oidcProviderMetadataContextLookupStrategy = new ChildContextLookup<>(OIDCProviderMetadataContext.class).compose(
+                new ChildContextLookup<>(OIDCPeerEntityContext.class).compose(
+                        new InboundMessageContextLookup()));  
+        
+        peerEntityContextLookupStrategy =
+                new ChildContextLookup<>(OIDCPeerEntityContext.class).compose(
+                        new InboundMessageContextLookup());
+    }
+    
+    /**
+     * Set the strategy used to look up the {@link OIDCPeerEntityContext} to draw from.
+     * 
+     * @param strategy strategy used to look up the {@link OIDCPeerEntityContext}
+     */
+    public void setPeerEntityContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,OIDCPeerEntityContext> strategy) {
+    	checkSetterPreconditions();
+
+        peerEntityContextLookupStrategy =
+                Constraint.isNotNull(strategy, "OIDCPeerEntityContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the strategy used to return or create the {@link RelyingPartyContext} .
+     * 
+     * @param strategy creation strategy
+     */
+    public void setRelyingPartyContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext, RelyingPartyContext> strategy) {
+    	checkSetterPreconditions();
+
+        relyingPartyContextCreationStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext creation strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to return the {@link OIDCProviderMetadataContext}.
+     * 
+     * @param strategy The lookup strategy.
+     */
+    public void setOidcProviderMetadataContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, OIDCProviderMetadataContext> strategy) {
+    	checkSetterPreconditions();
+
+        oidcProviderMetadataContextLookupStrategy =
+                Constraint.isNotNull(strategy, "OIDCMetadataContext lookup strategy cannot be null");
+    }
+    
+    
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            log.error("{} pre-execute failed", getLogPrefix());
+            return false;
+        }
+
+        peerEntityCtx = peerEntityContextLookupStrategy.apply(profileRequestContext);
+        if (peerEntityCtx == null) {
+            log.warn("{} Unable to locate OIDCPeerEntityContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        peerIdentifier = peerEntityCtx.getIdentifier();
+        if (peerIdentifier == null) {
+            log.warn("{} Unable to locate peer identifier", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+    
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final RelyingPartyContext rpContext = relyingPartyContextCreationStrategy.apply(profileRequestContext);
+        if (rpContext == null) {
+            log.error("{} Unable to locate or create RelyingPartyContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            return;
+        }
+
+        //TODO move these to strategies like the InitializeRelyingPartyContextFromSAMLPeer equiv.
+        
+        log.debug("{} Attaching RelyingPartyContext based on OIDC peer '{}'", getLogPrefix(),
+                peerEntityCtx.getIdentifier());
+        rpContext.setRelyingPartyId(peerEntityCtx.getIdentifier());
+        rpContext.setRelyingPartyIdContextTree(peerEntityCtx);
+        final OIDCProviderMetadataContext oidcContext = 
+                oidcProviderMetadataContextLookupStrategy.apply(profileRequestContext);
+        
+        if (oidcContext == null) {
+            log.trace("{} Unable to find OpenID Provider Metadata", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            return;
+        }
+
+        final var providerMetadata = oidcContext.getProviderInformation();
+        if (providerMetadata != null && 
+                peerIdentifier.equals(providerMetadata.getIssuer().getValue())) {
+            log.debug("{} Setting the RelyingPartyContext to 'verified'", getLogPrefix());
+            rpContext.setVerified(true);
+        }
+    }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java
new file mode 100644
index 0000000..3ad6057
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java
@@ -0,0 +1,75 @@
+/*
+ * 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.security.SecureRandom;
+
+import javax.annotation.Nonnull;
+
+import org.apache.commons.codec.binary.Hex;
+
+import net.shibboleth.shared.logic.Constraint;
+
+/** Support class for OIDC related implementations.*/
+public final class OIDCSupport {
+    
+    /** Private constructor.*/
+    private OIDCSupport() {
+        
+    }
+    
+    /**
+     * Generates a random identifier to be used as a nonce.
+     *  
+     * @param length the length of the parameter.
+     * 
+     * @return the randomly generated nonce value.
+     */
+    @Nonnull public static String generateNonce(@Nonnull final Integer length) {
+        final SecureRandom secureRandom = new SecureRandom();
+        final StringBuilder sb = new StringBuilder();
+        while(sb.length() < length){
+            sb.append(Integer.toHexString(secureRandom.nextInt()));
+        }
+        final String nonce = sb.toString().substring(0, length);
+        assert nonce != null;
+        return nonce;
+    }
+        
+    
+    /**
+     * <p>Generate a state parameter from a nonce component and an execution key component.</p>
+     * 
+     * <p>The nonce is separated from the key by a dot e.g. {@literal <nonce>.<keyHex>}.</p>
+     * 
+     *  <p>The nonce is assumed to be already encoded in its transmission format e.g. Hex. The key is
+     *  hex encoded before it is combined with the nonce. The result is assumed URL encoded e.g. inside
+     *  the allowed set of URI characters or, no character in the state is from the URI reserved set.</p>
+     * 
+     * @param nonce the nonce component. 
+     * @param key the key component. The key is hex encoded before it is added to the generated state.
+     * 
+     * @return the combined state component.
+     */
+    @Nonnull public static String generateState(@Nonnull final String nonce, @Nonnull final String key) {
+        Constraint.isNotNull(nonce, "NonceHex key can not be null");
+        Constraint.isNotNull(key, "Webflow execution key can not be null");
+        
+        final String keyHex = Hex.encodeHexString(key.getBytes());
+        return nonce+"."+keyHex;
+    }
+    
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/impl/PrepareOIDCInboundMessageContext.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java
similarity index 99%
rename from sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/impl/PrepareOIDCInboundMessageContext.java
rename to sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java
index c835dce..3f92aa5 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/impl/PrepareOIDCInboundMessageContext.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 
-package net.shibboleth.sp.oidc.impl;
+package net.shibboleth.sp.oidc.profile.impl;
 
 import java.util.function.Function;
 import java.util.function.Predicate;

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


More information about the commits mailing list