[java-plugin-shibd-saml] 01/01: Migrate SAML SP code into separate plugin project.

Scott Cantor cantor.2 at osu.edu
Tue Aug 20 13:22:38 UTC 2024


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

scantor pushed a commit to branch main
in repository java-plugin-shibd-saml.

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

commit f47746eff3df236046fb19c5fb95e6cdefb8e22f
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Aug 20 09:22:34 2024 -0400

    Migrate SAML SP code into separate plugin project.
---
 .gitignore                                         |  14 +
 LICENSE.txt                                        | 202 ++++++++
 pom.xml                                            | 169 ++++++
 resources/checkstyle/checkstyle.xml                | 122 +++++
 sp-saml-api/.checkstyle                            |  10 +
 sp-saml-api/.gitignore                             |   2 +
 sp-saml-api/pom.xml                                |  51 ++
 .../saml2/profile/SAML2InitiatorConstants.java     |  30 ++
 .../config/BrowserSSOProfileConfiguration.java     |  67 +++
 .../profile/config/ECPProfileConfiguration.java    |  21 +
 .../profile/config/SAML2ProfileConfiguration.java  |  35 ++
 .../config/SingleLogoutProfileConfiguration.java   |  37 ++
 .../sp/saml/saml2/profile/config/package-info.java |  18 +
 .../sp/saml/saml2/profile/package-info.java        |  18 +
 sp-saml-bom/.gitignore                             |   1 +
 sp-saml-bom/pom.xml                                |  40 ++
 sp-saml-conf-impl/.gitignore                       |   2 +
 sp-saml-conf-impl/pom.xml                          | 278 ++++++++++
 .../net/shibboleth/sp/saml/conf/SAMLModule.java    |  38 ++
 .../net/shibboleth/sp/saml/conf/SAMLPlugin.java    |  49 ++
 .../java/net/shibboleth/sp/saml/conf/Version.java  |  46 ++
 .../net/shibboleth/sp/saml/conf/package-info.java  |  18 +
 .../idp/flows/sp/initiator/saml2/saml2-beans.xml   | 185 +++++++
 .../idp/flows/sp/initiator/saml2/saml2-flow.xml    |  39 ++
 .../sp/service/application/postconfig.xml          | 320 ++++++++++++
 .../services/net.shibboleth.idp.module.IdPModule   |   1 +
 .../services/net.shibboleth.idp.plugin.IdPPlugin   |   1 +
 .../shibboleth/idp/module/conf/sp/saml.properties  |   2 +
 .../net/shibboleth/sp/saml/conf/module.properties  |  14 +
 .../net/shibboleth/sp/saml/conf/plugin.properties  |   9 +
 .../flows/saml2/SAML2SessionInitiatorFlowTest.java | 429 ++++++++++++++++
 ...MLEnvironmentApplicationContextInitializer.java |  54 ++
 .../sp/saml/flows/saml2/package-info.java          |  18 +
 .../src/test/resources/logback-test.xml            |  17 +
 .../idp/module/conf/sp/metadata-providers.xml      | 108 ++++
 .../idp/module/conf/sp/saml-test-agents.xml        |  72 +++
 .../net/shibboleth/sp/saml-test-beans.xml          |  27 +
 sp-saml-dist/pom.xml                               | 111 ++++
 sp-saml-dist/src/main/assembly/assembly.xml        |  43 ++
 sp-saml-dist/src/main/enforcer/shibbolethKeys.gpg  | Bin 0 -> 55669 bytes
 sp-saml-dist/src/main/resources/bootstrap/keys.txt | 427 ++++++++++++++++
 sp-saml-impl/.checkstyle                           |  10 +
 sp-saml-impl/.gitignore                            |   2 +
 sp-saml-impl/pom.xml                               |  85 +++
 ...tSAML2ArtifactConsumerProfileConfiguration.java | 101 ++++
 ...SAML2AssertionConsumerProfileConfiguration.java | 158 ++++++
 .../impl/AbstractSAML2ProfileConfiguration.java    | 218 ++++++++
 .../impl/BrowserSSOProfileConfiguration.java       | 415 +++++++++++++++
 .../config/impl/ECPProfileConfiguration.java       |  26 +
 .../impl/SingleLogoutProfileConfiguration.java     | 194 +++++++
 .../saml2/profile/config/impl/package-info.java    |  18 +
 .../saml/saml2/profile/impl/AddAuthnRequest.java   | 569 +++++++++++++++++++++
 .../sp/saml/saml2/profile/impl/package-info.java   |  18 +
 .../AbstractSAML2ProfileConfigurationTest.java     |  58 +++
 .../impl/BrowserSSOProfileConfigurationTest.java   | 177 +++++++
 .../impl/SingleLogoutProfileConfigurationTest.java |  65 +++
 56 files changed, 5259 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bf94c29
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+# A simulation of Subversion default ignores, generated by reposurgeon.
+*~
+*.#*
+.*.swp
+.DS_store
+# Simulated Subversion default ignores end here
+# The contents of the svn:ignore property on the branch root.
+classpath\:
+test-output
+target
+.settings
+.project
+.classpath
+.vscode
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..c305f41
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,169 @@
+<!-- See LICENSE.txt file in the root directory of this repository for the copyright/license information. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    
+    <parent>
+        <groupId>net.shibboleth</groupId>
+        <artifactId>parent</artifactId>
+        <version>17.2.0-SNAPSHOT</version>
+    </parent>
+    
+    <groupId>net.shibboleth.sp</groupId>
+    <artifactId>sp-saml-parent</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>Shibboleth SP :: SAML Protocol Plugin</name>
+    <packaging>pom</packaging>
+    
+    <properties>
+        <shibboleth.projectName>java-plugin-shibd-saml</shibboleth.projectName>
+        <shib-shared.groupId>net.shibboleth</shib-shared.groupId>
+        <shib-shared.version>9.2.0-SNAPSHOT</shib-shared.version>
+        <opensaml.groupId>org.opensaml</opensaml.groupId>
+        <opensaml.version>5.2.0-SNAPSHOT</opensaml.version>
+        <shib-metadata.groupId>net.shibboleth</shib-metadata.groupId>
+        <shib-metadata.version>5.2.0-SNAPSHOT</shib-metadata.version>
+        <shib-attribute.groupId>net.shibboleth</shib-attribute.groupId>
+        <shib-attribute.version>5.2.0-SNAPSHOT</shib-attribute.version>
+        <shib-profile.groupId>net.shibboleth</shib-profile.groupId>
+        <shib-profile.version>5.2.0-SNAPSHOT</shib-profile.version>
+        <idp.groupId>net.shibboleth.idp</idp.groupId>
+        <idp.version>5.2.0-SNAPSHOT</idp.version>
+        <shibd.groupId>net.shibboleth.sp</shibd.groupId>
+        <shibd.version>0.0.1-SNAPSHOT</shibd.version>
+        <checkstyle.configLocation>${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+    </properties>
+    
+    <description>
+        IdP plugin to add SAML protocol support to the SP processing hub and agents.
+    </description>
+    
+    <modules>
+        <module>sp-saml-bom</module>
+        <module>sp-saml-conf-impl</module>
+        <module>sp-saml-api</module>
+        <module>sp-saml-impl</module>
+        <module>sp-saml-dist</module>
+    </modules>
+    
+    <dependencies>
+        <!-- Project wide Dependencies -->
+        <dependency>
+            <groupId>${slf4j.groupId}</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.findbugs</groupId>
+            <artifactId>jsr305</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>net.shibboleth</groupId>
+            <artifactId>shib-support</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.testng</groupId>
+            <artifactId>testng</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <dependencyManagement>
+        <dependencies>
+            <!-- Import Dependencies: Shib-Shared -->
+            <dependency>
+                <groupId>${shib-shared.groupId}</groupId>
+                <artifactId>shib-shared-bom</artifactId>
+                <version>${shib-shared.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!-- OpenSAML BOM for importing opensaml dependencies -->
+            <dependency>
+                <groupId>${opensaml.groupId}</groupId>
+                <artifactId>opensaml-bom</artifactId>
+                <version>${opensaml.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!-- Import Dependencies: Shib-Metadata/Attribute/Profile -->
+            <dependency>
+                <groupId>${shib-profile.groupId}</groupId>
+                <artifactId>shib-profile-bom</artifactId>
+                <version>${shib-profile.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>${shib-attribute.groupId}</groupId>
+                <artifactId>shib-attribute-bom</artifactId>
+                <version>${shib-attribute.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>${shib-metadata.groupId}</groupId>
+                <artifactId>shib-metadata-bom</artifactId>
+                <version>${shib-metadata.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!-- Shibboleth IdP BOM for importing IdP dependencies -->
+            <dependency>
+                <groupId>${idp.groupId}</groupId>
+                <artifactId>idp-bom</artifactId>
+                <version>${idp.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <!-- Shibboleth SP Hub BOM for importing SP depednencies -->
+            <dependency>
+                <groupId>${shibd.groupId}</groupId>
+                <artifactId>sp-bom</artifactId>
+                <version>${shibd.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifestEntries>
+                            <Automatic-Module-Name>${automatic.module.name}</Automatic-Module-Name>
+                            <Sealed>true</Sealed>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+             </plugin>
+        </plugins>
+
+         <pluginManagement>
+            <plugins>
+                 <plugin>
+                     <groupId>org.codehaus.mojo</groupId>
+                     <artifactId>exec-maven-plugin</artifactId>
+                     <version>3.0.0</version>
+                     <configuration>
+                         <mainClass>unused</mainClass>
+                         <skip>true</skip>
+                     </configuration>
+                 </plugin>
+             </plugins>
+        </pluginManagement>
+    </build>
+
+</project>
diff --git a/resources/checkstyle/checkstyle.xml b/resources/checkstyle/checkstyle.xml
new file mode 100644
index 0000000..e3f1203
--- /dev/null
+++ b/resources/checkstyle/checkstyle.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
+
+<!--
+    This configuration file was written by the eclipse-cs plugin configuration editor
+-->
+<!--
+    Checkstyle-Configuration: Shibboleth Checkstyle
+    Description: none
+-->
+<module name="Checker">
+  <property name="severity" value="warning"/>
+  <module name="TreeWalker">
+    <property name="tabWidth" value="4"/>
+    <module name="JavadocMethod"/>
+    <module name="JavadocType">
+        <property name="allowUnknownTags" value="true"/>
+    </module>
+    <module name="JavadocVariable"/>
+    <module name="JavadocStyle">
+      <property name="checkEmptyJavadoc" value="true"/>
+    </module>
+    <module name="ConstantName"/>
+    <module name="LocalFinalVariableName"/>
+    <module name="LocalVariableName"/>
+    <module name="MemberName"/>
+    <module name="MethodName"/>
+    <module name="PackageName"/>
+    <module name="ParameterName"/>
+    <module name="StaticVariableName"/>
+    <module name="TypeName"/>
+    <module name="AvoidStarImport"/>
+    <module name="IllegalImport"/>
+    <module name="RedundantImport"/>
+    <module name="UnusedImports"/>
+    <module name="MethodLength">
+      <property name="max" value="70"/>
+    </module>
+    <module name="ParameterNumber">
+      <property name="max" value="5"/>
+    </module>
+    <module name="EmptyForIteratorPad"/>
+    <module name="MethodParamPad"/>
+    <module name="ModifierOrder"/>
+    <module name="AvoidNestedBlocks"/>
+    <module name="LeftCurly"/>
+    <module name="NeedBraces"/>
+    <module name="RightCurly"/>
+    <module name="EmptyStatement"/>
+    <module name="EqualsHashCode"/>
+    <module name="HiddenField"/>
+    <module name="IllegalInstantiation"/>
+    <module name="InnerAssignment"/>
+    <module name="MissingSwitchDefault"/>
+    <module name="SimplifyBooleanExpression"/>
+    <module name="SimplifyBooleanReturn"/>
+    <module name="FinalClass"/>
+    <module name="HideUtilityClassConstructor"/>
+    <module name="VisibilityModifier"/>
+    <module name="ArrayTypeStyle"/>
+    <module name="UpperEll"/>
+    <module name="AnonInnerLength"/>
+    <module name="EmptyForInitializerPad"/>
+    <module name="CovariantEquals"/>
+    <module name="DefaultComesLast"/>
+    <module name="DeclarationOrder"/>
+    <module name="ExplicitInitialization"/>
+    <module name="FallThrough"/>
+    <module name="IllegalThrows"/>
+    <module name="MultipleVariableDeclarations"/>
+    <module name="PackageDeclaration"/>
+    <module name="ParameterAssignment"/>
+    <module name="ReturnCount">
+      <property name="max" value="8"/>
+      <property name="maxForVoid" value="8"/>
+    </module>
+    <module name="StringLiteralEquality"/>
+    <module name="SuperFinalize"/>
+    <module name="ArrayTrailingComma"/>
+    <module name="UnnecessaryParentheses"/>
+    <module name="MutableException"/>
+    <module name="ThrowsCount">
+      <property name="max" value="3"/>
+    </module>
+    <module name="CyclomaticComplexity"/>
+    <module name="TrailingComment"/>
+    <module name="EqualsAvoidNull"/>
+    <module name="ModifiedControlVariable"/>
+    <module name="FinalParameters">
+      <property name="tokens" value="METHOD_DEF,CTOR_DEF,LITERAL_CATCH"/>
+    </module>
+    <module name="FinalLocalVariable">
+      <property name="tokens" value="PARAMETER_DEF,VARIABLE_DEF"/>
+      <property name="validateEnhancedForLoopVariable" value="true"/>
+    </module>
+    <module name="SuppressionCommentFilter">
+      <property name="offCommentFormat" value="\bCheck[Ss]tyle:\s*([\w|]+)\s+OFF\b"/>
+      <property name="onCommentFormat" value="\bCheck[Ss]tyle:\s*([\w|]+)\s+ON\b"/>
+      <property name="checkFormat" value="$1"/>
+    </module>
+    <module name="MissingJavadocMethod"/>
+    <module name="MissingJavadocPackage"/>
+    <module name="MissingJavadocType"/>
+    <module name="InvalidJavadocPosition"/>
+  </module>
+  <module name="FileTabCharacter"/>
+  <module name="FileLength">
+    <property name="max" value="1000"/>
+  </module>
+  <module name="Header">
+    <property name="header" value="/*\n * Licensed to the University Corporation for Advanced Internet Development,\n * Inc. (UCAID) under one or more contributor license agreements.  See the\n * NOTICE file distributed with this work for additional information regarding\n * copyright ownership. The UCAID licenses this file to You under the Apache\n * License, Version 2.0 (the "License"); you may not use this file except in\n * compliance with the License.  You may obtain a cop [...]
+    <property name="fileExtensions" value="java"/>
+  </module>
+  <module name="JavadocPackage"/>
+  <module name="LineLength">
+    <property name="max" value="120"/>
+  </module>
+  <module name="SuppressionFilter">
+    <property name="file" value="resources/checkstyle-suppressions.xml"/>
+    <property name="optional" value="false"/>
+  </module>
+</module>
diff --git a/sp-saml-api/.checkstyle b/sp-saml-api/.checkstyle
new file mode 100644
index 0000000..1feabed
--- /dev/null
+++ b/sp-saml-api/.checkstyle
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false">
+  <local-check-config name="Shibboleth Checkstyle" location="/java-plugin-shibd/resources/checkstyle/checkstyle.xml" type="project" description="">
+    <additional-data name="protect-config-file" value="false"/>
+  </local-check-config>
+  <fileset name="all" enabled="true" check-config-name="Shibboleth Checkstyle" local="true">
+    <file-match-pattern match-pattern="." include-pattern="true"/>
+  </fileset>
+</fileset-config>
diff --git a/sp-saml-api/.gitignore b/sp-saml-api/.gitignore
new file mode 100644
index 0000000..1df25bf
--- /dev/null
+++ b/sp-saml-api/.gitignore
@@ -0,0 +1,2 @@
+/test-output
+/target
diff --git a/sp-saml-api/pom.xml b/sp-saml-api/pom.xml
new file mode 100644
index 0000000..6e935e5
--- /dev/null
+++ b/sp-saml-api/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- See LICENSE.txt file in the root directory of this repository for the copyright/license information. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>net.shibboleth.sp</groupId>
+        <artifactId>sp-saml-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+    
+    <artifactId>sp-saml-api</artifactId>
+    <description>SP SAML API.</description>
+    <name>Shibboleth SP :: SAML Protocol Plugin :: API</name>
+    <packaging>jar</packaging>
+    
+    <properties>
+        <checkstyle.configLocation>${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+        <automatic.module.name>net.shibboleth.sp.saml</automatic.module.name>
+    </properties>
+        
+    <dependencies>
+        <!-- Compile dependencies -->
+
+        <!-- Provided dependencies -->
+        <dependency>
+            <groupId>${shibd.groupId}</groupId>
+            <artifactId>sp-server-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${shib-profile.groupId}</groupId>
+            <artifactId>shib-saml-profile-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-attribute-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-metadata.groupId}</groupId>
+            <artifactId>shib-metadata-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <!-- Test Dependencies -->
+    </dependencies>
+
+</project>
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/SAML2InitiatorConstants.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/SAML2InitiatorConstants.java
new file mode 100644
index 0000000..7e74962
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/SAML2InitiatorConstants.java
@@ -0,0 +1,30 @@
+/*
+ * 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.saml.saml2.profile;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * Constants for session initiator operations.
+ */
+public final class SAML2InitiatorConstants {
+
+    /** Private constructor. */
+    private SAML2InitiatorConstants() {
+        
+    }
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
new file mode 100644
index 0000000..391a354
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
@@ -0,0 +1,67 @@
+/*
+ * 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.saml.saml2.profile.config;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.saml.profile.config.SAMLArtifactConsumerProfileConfiguration;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml.saml2.core.AuthnRequest;
+
+/** Configuration support for SP SAML 2.0 Browser SSO. */
+public interface BrowserSSOProfileConfiguration extends SAMLArtifactConsumerProfileConfiguration,
+        net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration {
+
+    /**
+     * Get the {@link AuthnContextClassRef} values to include in SAML request.
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return list of class references
+     */
+    @Nonnull @NonnullElements @NotLive @Unmodifiable List<String> getAuthnContextClassRefs(
+            @Nullable final ProfileRequestContext profileRequestContext);
+
+    /**
+     * Get the name identifier format to require via the SAML request.
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return required format
+     */
+    @Nullable String getNameIDFormat(@Nullable final ProfileRequestContext profileRequestContext);
+
+    
+    /**
+     * Get the SAML binding to insert into an {@link AuthnRequest} to control the response binding.
+     * 
+     * <p>Defaults tp {@link SAMLConstants#SAML2_POST_BINDING_URI}.</p>
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return response binding
+     */
+    @Nullable String getResponseBinding(@Nullable final ProfileRequestContext profileRequestContext);
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/ECPProfileConfiguration.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/ECPProfileConfiguration.java
new file mode 100644
index 0000000..6b921b0
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/ECPProfileConfiguration.java
@@ -0,0 +1,21 @@
+/*
+ * 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.saml.saml2.profile.config;
+
+/** Configuration support for SP SAML 2.0 ECP. */
+public interface ECPProfileConfiguration
+    extends BrowserSSOProfileConfiguration, net.shibboleth.saml.saml2.profile.config.ECPProfileConfiguration {
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SAML2ProfileConfiguration.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SAML2ProfileConfiguration.java
new file mode 100644
index 0000000..55f3f52
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SAML2ProfileConfiguration.java
@@ -0,0 +1,35 @@
+/*
+ * 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.saml.saml2.profile.config;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/** Common interface for SAML 2.0 SP profile configurations. */
+public interface SAML2ProfileConfiguration extends net.shibboleth.saml.saml2.profile.config.SAML2ProfileConfiguration {
+
+    /**
+     * Get the entityID assigned to this system.
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return our own entityID
+     */
+    @Nullable @NotEmpty String getEntityID(@Nullable final ProfileRequestContext profileRequestContext);
+    
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
new file mode 100644
index 0000000..4a1d087
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
@@ -0,0 +1,37 @@
+/*
+ * 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.saml.saml2.profile.config;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.ext.saml2aslo.Asynchronous;
+
+/** Configuration support for SP SAML 2.0 Single Logout. */
+public interface SingleLogoutProfileConfiguration
+        extends net.shibboleth.saml.saml2.profile.config.SingleLogoutProfileConfiguration {
+
+    /**
+     * Get whether to include the {@link Asynchronous} extension in requests.
+     * 
+     * <p>Defaults to true.</p>
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return true iff the extension should be included in requests
+     */
+    boolean isAsynchronous(@Nullable final ProfileRequestContext profileRequestContext);
+    
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/package-info.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/package-info.java
new file mode 100644
index 0000000..e97e075
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * Configuration of SAML support in SP.
+ */
+package net.shibboleth.sp.saml.saml2.profile.config;
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/package-info.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/package-info.java
new file mode 100644
index 0000000..c5fff8b
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * API classes for SAML 2.0 profiles.
+ */
+package net.shibboleth.sp.saml.saml2.profile;
\ No newline at end of file
diff --git a/sp-saml-bom/.gitignore b/sp-saml-bom/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/sp-saml-bom/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/sp-saml-bom/pom.xml b/sp-saml-bom/pom.xml
new file mode 100644
index 0000000..364be7c
--- /dev/null
+++ b/sp-saml-bom/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>net.shibboleth.sp</groupId>
+        <artifactId>sp-saml-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+
+    <name>Shibboleth SP SAML Protocol Plugin :: BOM</name>
+    <description>Bill of Materials</description>
+    <artifactId>sp-saml-bom</artifactId>
+    <packaging>pom</packaging>
+    
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>${project.groupId}</groupId>
+                <artifactId>sp-saml-api</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            
+            <dependency>
+                <groupId>${project.groupId}</groupId>
+                <artifactId>sp-saml-impl</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            
+            <dependency>
+                <groupId>${project.groupId}</groupId>
+                <artifactId>sp-saml-conf-impl</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+    
+</project>
diff --git a/sp-saml-conf-impl/.gitignore b/sp-saml-conf-impl/.gitignore
new file mode 100644
index 0000000..1df25bf
--- /dev/null
+++ b/sp-saml-conf-impl/.gitignore
@@ -0,0 +1,2 @@
+/test-output
+/target
diff --git a/sp-saml-conf-impl/pom.xml b/sp-saml-conf-impl/pom.xml
new file mode 100644
index 0000000..0fd61a4
--- /dev/null
+++ b/sp-saml-conf-impl/pom.xml
@@ -0,0 +1,278 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- See LICENSE.txt file in the root directory of this repository for the copyright/license information. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>net.shibboleth.sp</groupId>
+        <artifactId>sp-saml-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+    
+    <artifactId>sp-saml-conf-impl</artifactId>
+    <name>Shibboleth SP :: SAML Protocol Plugin :: Internal Configuration</name>
+    <description>SP SAML protocol plugin internal configuration.</description>
+    <packaging>jar</packaging>
+    
+    <properties>
+        <checkstyle.configLocation>${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+        <automatic.module.name>net.shibboleth.sp.saml.conf</automatic.module.name>
+    </properties>
+        
+    <dependencies>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>sp-saml-api</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>sp-saml-impl</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- For plugin/module classes. -->
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-admin-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Supports Eclipse class lookup from within Spring files. -->
+        <dependency>
+            <groupId>${shib-profile.groupId}</groupId>
+            <artifactId>shib-profile-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-profile.groupId}</groupId>
+            <artifactId>shib-profile-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-profile.groupId}</groupId>
+            <artifactId>shib-saml-profile-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-attribute-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-saml-attribute-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-attribute-filter-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-attribute-resolver-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-core-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-core-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-saml-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-saml-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-storage-impl</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-networking-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-security-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-velocity-spring</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    
+        <!-- Test Dependencies -->
+        <dependency>
+            <groupId>${shibd.groupId}</groupId>
+            <artifactId>sp-conf-impl</artifactId>
+            <version>${shibd.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${shibd.groupId}</groupId>
+            <artifactId>sp-conf-impl</artifactId>
+            <version>${shibd.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-conf-impl</artifactId>
+            <version>${idp.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-conf-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-schema</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-spring</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${shib-shared.groupId}</groupId>
+            <artifactId>shib-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${spring-webflow.groupId}</groupId>
+            <artifactId>spring-webflow</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring-webflow.groupId}</groupId>
+            <artifactId>spring-binding</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-beans</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-context</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-expression</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-jdbc</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-web</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-webmvc</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.codehaus.janino</groupId>
+            <artifactId>janino</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifestEntries>
+                            <Automatic-Module-Name>${automatic.module.name}</Automatic-Module-Name>
+                        </manifestEntries>
+                        <manifestSections>
+                            <manifestSection>
+                                <name>net/shibboleth/sp/</name>
+                                <manifestEntries>
+                                    <Implementation-Title>${project.artifactId}</Implementation-Title>
+                                    <Implementation-Version>${project.version}</Implementation-Version>
+                                    <Implementation-Vendor>shibboleth.net</Implementation-Vendor>
+                                </manifestEntries>
+                            </manifestSection>
+                        </manifestSections>
+                    </archive>
+                </configuration>
+             </plugin>
+         </plugins>
+    </build>
+
+</project>
diff --git a/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/SAMLModule.java b/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/SAMLModule.java
new file mode 100644
index 0000000..307cb7d
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/SAMLModule.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.saml.conf;
+
+import java.io.IOException;
+
+import net.shibboleth.idp.module.IdPModule;
+import net.shibboleth.idp.module.impl.PluginIdPModule;
+import net.shibboleth.profile.module.ModuleException;
+
+/**
+ * {@link IdPModule} implementation.
+ */
+public final class SAMLModule extends PluginIdPModule {
+
+    /**
+     * Constructor.
+     *  
+     * @throws ModuleException on error
+     * @throws IOException on error
+     */
+    public SAMLModule() throws IOException, ModuleException {
+        super(Version.getVersion(), SAMLModule.class);
+    }
+
+}
diff --git a/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/SAMLPlugin.java b/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/SAMLPlugin.java
new file mode 100644
index 0000000..3b90da7
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/SAMLPlugin.java
@@ -0,0 +1,49 @@
+/*
+ * 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.saml.conf;
+
+import java.io.IOException;
+
+import net.shibboleth.idp.module.IdPModule;
+import net.shibboleth.idp.plugin.impl.FirstPartyIdPPlugin;
+import net.shibboleth.profile.module.ModuleException;
+import net.shibboleth.profile.plugin.PluginException;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Details about the SP SAML protocol plugin.
+ */
+public class SAMLPlugin extends FirstPartyIdPPlugin {
+    
+    /**
+     * Constructor.
+     * 
+     * @throws IOException if the properties fail to load
+     * @throws PluginException if other errors occur
+     */
+    public SAMLPlugin() throws IOException, PluginException {
+        super(SAMLPlugin.class);
+        try {
+            final IdPModule module = new SAMLModule();
+            setEnableOnInstall(CollectionSupport.singleton(module));
+            setDisableOnRemoval(CollectionSupport.singleton(module));
+        } catch (final IOException e) {
+            throw e;
+        } catch (final ModuleException e) {
+            throw new PluginException(e);
+        }
+    }
+    
+}
diff --git a/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/Version.java b/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/Version.java
new file mode 100644
index 0000000..af96bf1
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/Version.java
@@ -0,0 +1,46 @@
+/*
+ * 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.saml.conf;
+
+import javax.annotation.Nullable;
+
+/** Class for getting and printing the version of the plugin. */
+public final class Version {
+
+    /** Plugin version. */
+    @Nullable private static final String VERSION = Version.class.getPackage().getImplementationVersion();
+
+    /** Constructor. */
+    private Version() {
+    }
+
+    /**
+     * Main entry point to program.
+     * 
+     * @param args command line arguments
+     */
+    public static void main(final String[] args) {
+        System.out.println(VERSION);
+    }
+
+    /**
+     * Get the version of the IdP.
+     * 
+     * @return version of the IdP
+     */
+    @Nullable public static String getVersion() {
+        return VERSION;
+    }
+}
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/package-info.java b/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/package-info.java
new file mode 100644
index 0000000..c15b9f5
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/java/net/shibboleth/sp/saml/conf/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * Module/plugin implementations for SP SAML support.
+ */
+package net.shibboleth.sp.saml.conf;
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
new file mode 100644
index 0000000..2caa461
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <bean id="ValidateSessionInitiatorRequest"
+        class="net.shibboleth.sp.profile.impl.ValidateSessionInitiatorRequest" scope="prototype" />
+
+    <bean id="PrepareInboundMessageContext"
+            class="net.shibboleth.idp.saml.session.impl.PrepareInboundMessageContext" scope="prototype"
+            p:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+
+    <bean id="WebFlowMessageHandlerAdaptor" abstract="true"
+        class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+        c:executionDirection="INBOUND" />
+
+    <bean id="SAMLProtocolAndRole" parent="WebFlowMessageHandlerAdaptor">
+        <constructor-arg name="messageHandler">
+            <bean class="org.opensaml.saml.common.binding.impl.SAMLProtocolAndRoleHandler" scope="prototype"
+                p:protocol-ref="shibboleth.MetadataLookup.Protocol"
+                p:role-ref="shibboleth.MetadataLookup.Role" />
+        </constructor-arg>
+    </bean>
+    
+    <util:constant id="shibboleth.MetadataLookup.Protocol"
+        static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML20P_NS" />
+
+    <util:constant id="shibboleth.MetadataLookup.Role"
+        static-field="org.opensaml.saml.saml2.metadata.IDPSSODescriptor.DEFAULT_ELEMENT_NAME" />
+    
+    <bean id="SAMLMetadataLookup" parent="WebFlowMessageHandlerAdaptor">
+        <constructor-arg name="messageHandler">
+            <bean class="org.opensaml.saml.common.binding.impl.SAMLMetadataLookupHandler" scope="prototype">
+                <property name="roleDescriptorResolverLookupStrategy">
+                    <bean class="net.shibboleth.sp.profile.context.navigate.messaging.ApplicationMetadataResolverLookupFunction" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+
+    <bean id="InitializeRelyingPartyContextFromSAMLPeer"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" 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="#{T(net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration).PROFILE_ID}" />
+
+    <bean id="AppAwareIssuerLookupFunction" class="net.shibboleth.sp.profile.context.navigate.IssuerLookupFunction" />
+
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
+        p:selfIdentityLookupStrategy-ref="AppAwareIssuerLookupFunction" />
+
+    <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" />
+
+    <bean id="AddAuthnRequest"
+            class="net.shibboleth.sp.saml.saml2.profile.impl.AddAuthnRequest" scope="prototype"
+            p:overwriteExisting="true"
+            p:issuerLookupStrategy-ref="AppAwareIssuerLookupFunction"
+            p:nameIDLookupStrategy="#{getObject('%{sp.authn.SAML.NameIDLookupStrategy:}'.trim())}">
+        <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>
+
+    <bean id="HandleOutboundMessage" parent="WebFlowMessageHandlerAdaptor"
+            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="PreEncodeMessageHandler"
+            class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
+        <property name="handlers">
+            <list>
+                <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.messaging.handler.impl.FunctionMessageHandler" scope="prototype"
+                    p:function="#{getObject('%{sp.SAML.outboundMessageHandlerFunction:}'.trim())}" />
+                <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="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}" />
+
+</beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml
new file mode 100644
index 0000000..9a413e9
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-flow.xml
@@ -0,0 +1,39 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="sp/initiator">
+
+    <action-state id="SAML2SessionInitiator">
+        <evaluate expression="ValidateSessionInitiatorRequest" />
+        <evaluate expression="PrepareInboundMessageContext" />
+        <evaluate expression="SAMLProtocolAndRole" />
+        <evaluate expression="SAMLMetadataLookup" />
+        
+        <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+        <evaluate expression="SelectProfileConfiguration" />
+        
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="InitializeMessageChannelSecurityContext" />
+        <!-- TODO: RelayState handling so it's populated on the inbound side... -->
+        <evaluate expression="PopulateBindingAndEndpointContexts" />
+
+        <evaluate expression="PopulateRequestSignatureSigningParameters" />
+        <evaluate expression="PopulateEncryptionParameters" />
+        
+        <evaluate expression="AddAuthnRequest" />
+        <evaluate expression="EncryptNameIDs" />
+
+        <evaluate expression="HandleOutboundMessage" />
+        <evaluate expression="EncodeMessage" />
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="proceed" />
+        <!-- Remap any other events into a fall-through to the next flow. -->
+        <transition to="ReselectFlow" />
+    </action-state>
+    
+    <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml" />
+
+</flow>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/application/postconfig.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/application/postconfig.xml
new file mode 100644
index 0000000..489e6fa
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/application/postconfig.xml
@@ -0,0 +1,320 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xmlns:c="http://www.springframework.org/schema/c"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+                           
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+
+    <context:annotation-config/>
+    
+    <!-- Import parent beans for metadata-driven configuration approach. -->
+<!--     <import resource="relying-party-mddriven.xml" /> -->
+        
+    <!--
+    Default Profile Configurations, also usable as parent beans in overrides.
+    
+    Each profile shows up twice, once as a basic bean and again as a child bean that adds
+    a suite of metadata-driven lookup strategies pre-injected.
+    -->
+    
+    <bean id="SAML2.SSO" lazy-init="true"
+        class="net.shibboleth.sp.saml.saml2.profile.config.impl.BrowserSSOProfileConfiguration"
+        p:securityConfiguration-ref="shibboleth.DefaultXMLSecurityConfiguration"
+        p:encryptionOptionalPredicate="%{sp.encryption.optional:false}" />
+
+    <util:constant id="SAML2.SSO.FEATURE_AUTHNCONTEXT"
+        static-field="net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration.FEATURE_AUTHNCONTEXT"/>
+
+    <util:constant id="SAML2.SSO.FEATURE_SCOPING"
+        static-field="net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration.FEATURE_SCOPING"/>
+
+    <util:constant id="SAML2.SSO.FEATURE_NAMEIDFORMAT"
+        static-field="net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration.FEATURE_NAMEIDFORMAT"/>
+
+    <util:constant id="SAML2.SSO.FEATURE_SPNAMEQUALIFIER"
+        static-field="net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration.FEATURE_SPNAMEQUALIFIER"/>
+
+    <util:constant id="SAML2.SSO.FEATURE_FORCEAUTHN"
+        static-field="net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration.FEATURE_FORCEAUTHN"/>
+
+    <bean id="SAML2.ECP" lazy-init="true"
+        class="net.shibboleth.sp.saml.saml2.profile.config.impl.ECPProfileConfiguration"
+        p:securityConfiguration-ref="shibboleth.DefaultXMLSecurityConfiguration"
+        p:encryptionOptionalPredicate="%{sp.encryption.optional:false}" />
+        
+    <bean id="SAML2.Logout" lazy-init="true"
+        class="net.shibboleth.sp.saml.saml2.profile.config.impl.SingleLogoutProfileConfiguration"
+        p:securityConfiguration-ref="shibboleth.DefaultXMLSecurityConfiguration"
+        p:encryptionOptionalPredicate="%{sp.encryption.optional:false}"
+        p:asynchronous="%{sp.logout.asynchronous:true}" />
+    
+    <!--
+    Security Configuration Defaults
+    
+    These settings establish the default security configurations for signatures and encryption
+    and loads the default credentials used.
+    -->
+
+    <bean id="shibboleth.DefaultXMLSecurityConfiguration" class="org.opensaml.xmlsec.config.BasicXMLSecurityConfiguration">
+        <property name="signatureSigningConfiguration">
+            <ref bean="#{'%{sp.signing.config:shibboleth.SigningConfiguration.SHA256}'.trim()}" />
+        </property>
+        
+        <property name="signatureValidationConfiguration">
+            <bean parent="shibboleth.BasicSignatureValidationConfiguration" 
+                p:signatureTrustEngine-ref="#{'%{sp.trust.signatures:shibboleth.ChainingSignatureTrustEngine}'.trim()}" />
+        </property>
+
+        <property name="clientTLSValidationConfiguration">
+            <bean parent="shibboleth.BasicClientTLSValidationConfiguration"
+                p:x509TrustEngine-ref="#{'%{sp.trust.certificates:shibboleth.ChainingX509TrustEngine}'.trim()}" />
+        </property>
+        
+        <property name="httpClientSecurityConfiguration">
+            <bean parent="shibboleth.BasicHttpClientSecurityConfiguration"
+                p:clientTLSCredential="#{getObject('shibboleth.DefaultClientTLSCredential')}"
+                p:TLSTrustEngine-ref="#{'%{sp.trust.httpClient.tls:shibboleth.ChainingX509TrustEngine}'.trim()}" />
+        </property>
+        
+        <property name="encryptionConfiguration">
+            <ref bean="#{'%{sp.encryption.config:shibboleth.EncryptionConfiguration.CBC}'.trim()}" />
+        </property>
+
+        <property name="decryptionConfiguration">
+            <bean parent="shibboleth.BasicDecryptionConfiguration"
+                p:KEKKeyInfoCredentialResolver-ref="defaultKeyEncryptionCredentialResolver"
+                p:encryptedKeyResolver-ref="defaultEncryptedKeyResolver" />
+        </property>
+    </bean>
+    
+    <!-- KeyInfoGeneration machinery. Former gets injected into the signing and encryption configs. -->
+    <bean id="NamedKeyInfoGeneratorManager"
+        class="org.opensaml.xmlsec.keyinfo.NamedKeyInfoGeneratorManager"
+        p:useDefaultManager="true" />
+    <bean id="DefaultKeyInfoGeneratorManager"
+        factory-bean="NamedKeyInfoGeneratorManager"
+        factory-method="getDefaultManager" />
+        
+    <!-- These install two default generator managers, for Credential and X509Credential. -->
+    <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+        p:targetObject-ref="DefaultKeyInfoGeneratorManager"
+        p:targetMethod="registerFactory">
+        <property name="arguments">
+            <list>
+                <ref bean="%{sp.security.basicKeyInfoFactory:shibboleth.BasicKeyInfoGeneratorFactory}" />
+            </list>
+        </property>
+    </bean>
+    <bean class="org.springframework.beans.factory.config.MethodInvokingBean"
+        p:targetObject-ref="DefaultKeyInfoGeneratorManager"
+        p:targetMethod="registerFactory">
+        <property name="arguments">
+            <list>
+                <ref bean="%{sp.security.x509KeyInfoFactory:shibboleth.X509KeyInfoGeneratorFactory}" />
+            </list>
+        </property>
+    </bean>
+    
+    <!-- The default generators, overrideable by inheriting from them and then setting the two properties. -->
+    <bean id="shibboleth.BasicKeyInfoGeneratorFactory" class="org.opensaml.xmlsec.keyinfo.impl.BasicKeyInfoGeneratorFactory"
+        p:emitPublicKeyValue="true"
+        p:emitKeyNames="true"  />
+    <bean id="shibboleth.X509KeyInfoGeneratorFactory" class="org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory"
+        p:emitEntityCertificate="true" />
+
+    <!-- Convenience beans for directly injecting SHA-256 or SHA-1 usage.  -->
+    
+    <bean id="shibboleth.SecurityConfiguration.SHA256" parent="shibboleth.DefaultSecurityConfiguration" lazy-init="true"
+        p:signatureSigningConfiguration-ref="shibboleth.SigningConfiguration.SHA256" />
+
+    <bean id="shibboleth.SecurityConfiguration.SHA1" parent="shibboleth.DefaultSecurityConfiguration" lazy-init="true"
+        p:signatureSigningConfiguration-ref="shibboleth.SigningConfiguration.SHA1" />
+
+    <!-- Actual signing configurations for SHA-256 and SHA-1. -->
+    
+    <bean id="shibboleth.SigningConfiguration.SHA256" parent="shibboleth.BasicSignatureSigningConfiguration"
+            p:signingCredentials-ref="shibboleth.DefaultSigningCredential"
+            p:keyInfoGeneratorManager-ref="NamedKeyInfoGeneratorManager">
+        <property name="signatureAlgorithms">
+            <list>
+                <util:constant
+                    static-field="org.opensaml.xmlsec.signature.support.SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256" />
+                <util:constant
+                    static-field="org.opensaml.xmlsec.signature.support.SignatureConstants.ALGO_ID_SIGNATURE_ECDSA_SHA256" />
+            </list>
+        </property>
+        <property name="signatureReferenceDigestMethods">
+            <list>
+                <util:constant
+                    static-field="org.opensaml.xmlsec.signature.support.SignatureConstants.ALGO_ID_DIGEST_SHA256" />
+            </list>
+        </property>
+    </bean>
+
+    <bean id="shibboleth.SigningConfiguration.SHA1" parent="shibboleth.BasicSignatureSigningConfiguration"
+            p:signingCredentials-ref="shibboleth.DefaultSigningCredential"
+            p:keyInfoGeneratorManager-ref="NamedKeyInfoGeneratorManager">
+        <property name="signatureAlgorithms">
+            <list>
+                <util:constant
+                    static-field="org.opensaml.xmlsec.signature.support.SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1" />
+                <util:constant
+                    static-field="org.opensaml.xmlsec.signature.support.SignatureConstants.ALGO_ID_SIGNATURE_ECDSA_SHA1" />
+            </list>
+        </property>
+        <property name="signatureReferenceDigestMethods">
+            <list>
+                <util:constant
+                    static-field="org.opensaml.xmlsec.signature.support.SignatureConstants.ALGO_ID_DIGEST_SHA1" />
+            </list>
+        </property>
+    </bean>
+
+    <!-- Convenience beans for directly injecting AES-CBC or AES-GCM usage.  -->
+    
+    <bean id="shibboleth.SecurityConfiguration.CBC" parent="shibboleth.DefaultSecurityConfiguration" lazy-init="true"
+        p:encryptionConfiguration-ref="shibboleth.EncryptionConfiguration.CBC" />
+
+    <bean id="shibboleth.SecurityConfiguration.GCM" parent="shibboleth.DefaultSecurityConfiguration" lazy-init="true"
+        p:encryptionConfiguration-ref="shibboleth.EncryptionConfiguration.GCM" />
+
+    <!-- Actual encryption configurations for AES-CBC and AES-GCM. -->
+
+    <bean id="shibboleth.EncryptionConfiguration.CBC" parent="shibboleth.BasicEncryptionConfiguration"
+            p:keyTransportKeyInfoGeneratorManager-ref="NamedKeyInfoGeneratorManager">
+        <property name="dataEncryptionAlgorithms">
+            <list>
+                <util:constant
+                    static-field="org.opensaml.xmlsec.encryption.support.EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES128" />
+            </list>
+        </property>
+        <property name="keyTransportEncryptionAlgorithms">
+            <list>
+                <util:constant
+                    static-field="org.opensaml.xmlsec.encryption.support.EncryptionConstants.ALGO_ID_KEYTRANSPORT_RSAOAEP" />
+            </list>
+        </property>
+    </bean>
+
+    <bean id="shibboleth.EncryptionConfiguration.GCM" parent="shibboleth.BasicEncryptionConfiguration"
+            p:keyTransportKeyInfoGeneratorManager-ref="NamedKeyInfoGeneratorManager">
+        <property name="dataEncryptionAlgorithms">
+            <list>
+                <util:constant
+                    static-field="org.opensaml.xmlsec.encryption.support.EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES128_GCM" />
+            </list>
+        </property>
+        <property name="keyTransportEncryptionAlgorithms">
+            <list>
+                <util:constant
+                    static-field="org.opensaml.xmlsec.encryption.support.EncryptionConstants.ALGO_ID_KEYTRANSPORT_RSAOAEP" />
+            </list>
+        </property>
+    </bean>
+    
+    <!-- Beans related to configuration of key agreement, mostly to provide abstract parent beans for impl classes -->
+    
+    <bean id="shibboleth.KeyAgreement.Configuration" abstract="true"
+        class="org.opensaml.saml.security.SAMLMetadataKeyAgreementEncryptionConfiguration" />
+
+    <bean id="shibboleth.KeyAgreement.ConcatKDF" abstract="true" class="org.opensaml.xmlsec.derivation.impl.ConcatKDF" />
+
+    <bean id="shibboleth.KeyAgreement.PBKDF2" abstract="true" class="org.opensaml.xmlsec.derivation.impl.PBKDF2" />
+
+    <bean id="shibboleth.KeyAgreement.DigestMethod" abstract="true" class="org.opensaml.xmlsec.agreement.impl.DigestMethod" />
+
+    <bean id="shibboleth.KeyAgreement.KANonce" abstract="true" class="org.opensaml.xmlsec.agreement.impl.KANonce" />
+
+    <bean id="shibboleth.MetadataPKIXValidationInformationResolver"
+        class="net.shibboleth.idp.saml.security.impl.MetadataPKIXValidationInformationResolver"
+        c:resolver-ref="shibboleth.RoleDescriptorResolver" />
+
+    <!-- Trust Engine options for message signature evaluation. -->
+        
+    <bean id="shibboleth.ExplicitKeySignatureTrustEngine"
+        class="org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine"
+        c:resolver-ref="shibboleth.MetadataCredentialResolver"
+        c:keyInfoResolver-ref="shibboleth.KeyInfoCredentialResolver" />
+
+    <bean id="shibboleth.PKIXSignatureTrustEngine"
+        class="org.opensaml.xmlsec.signature.support.impl.PKIXSignatureTrustEngine" 
+        c:resolver-ref="shibboleth.MetadataPKIXValidationInformationResolver"
+        c:keyInfoResolver-ref="shibboleth.KeyInfoCredentialResolver" />
+
+    <bean id="shibboleth.ChainingSignatureTrustEngine"
+            class="org.opensaml.xmlsec.signature.support.impl.ChainingSignatureTrustEngine">
+        <constructor-arg>
+            <list>
+                <ref bean="shibboleth.ExplicitKeySignatureTrustEngine" />
+                <ref bean="shibboleth.PKIXSignatureTrustEngine" />
+            </list>
+        </constructor-arg>
+    </bean>
+
+    <!-- Trust Engine options for X.509 certificate evaluation (e.g., TLS). -->
+
+    <bean id="shibboleth.ExplicitKeyX509TrustEngine"
+        class="org.opensaml.security.trust.impl.ExplicitKeyTrustEngine"
+        c:resolver-ref="shibboleth.MetadataCredentialResolver" />
+    <!-- Backward compatibility, remove in 5.0. -->
+    <alias name="shibboleth.ExplicitKeyX509TrustEngine" alias="shibboleth.ExplicitX509TrustEngine" />
+
+    <bean id="shibboleth.PKIXX509TrustEngine"
+        class="org.opensaml.security.x509.impl.PKIXX509CredentialTrustEngine"
+        c:resolver-ref="shibboleth.MetadataPKIXValidationInformationResolver" />
+
+    <bean id="shibboleth.ChainingX509TrustEngine" class="org.opensaml.security.trust.impl.ChainingTrustEngine">
+        <constructor-arg>
+            <list>
+                <ref bean="shibboleth.ExplicitKeyX509TrustEngine" />
+                <ref bean="shibboleth.PKIXX509TrustEngine" />
+            </list>
+        </constructor-arg>
+    </bean>
+
+    <!-- Defines decryption keys, along with support for optimizing lookup based on KeyInfo hints. -->
+    <bean id="defaultKeyEncryptionCredentialResolver" class="org.opensaml.xmlsec.keyinfo.impl.ChainingKeyInfoCredentialResolver">
+        <constructor-arg>
+            <list>
+                <bean class="org.opensaml.xmlsec.keyinfo.impl.LocalKeyInfoCredentialResolver">
+                    <constructor-arg name="localCredentialResolver">
+                        <bean class="org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver"
+                            c:credentials-ref="shibboleth.DefaultEncryptionCredentials" />
+                    </constructor-arg>
+                    <constructor-arg name="keyInfoProviders">
+                        <list>
+                            <bean class="org.opensaml.xmlsec.keyinfo.impl.provider.RSAKeyValueProvider" />
+                            <bean class="org.opensaml.xmlsec.keyinfo.impl.provider.DSAKeyValueProvider" />
+                            <bean class="org.opensaml.xmlsec.keyinfo.impl.provider.ECKeyValueProvider" />
+                            <bean class="org.opensaml.xmlsec.keyinfo.impl.provider.DEREncodedKeyValueProvider" />
+                            <bean class="org.opensaml.xmlsec.keyinfo.impl.provider.InlineX509DataProvider" />
+                            <bean class="org.opensaml.xmlsec.keyinfo.impl.provider.AgreementMethodKeyInfoProvider" />
+                        </list>
+                    </constructor-arg>
+                </bean>
+                <bean class="org.opensaml.xmlsec.keyinfo.impl.StaticKeyInfoCredentialResolver"
+                    c:credentials-ref="shibboleth.DefaultEncryptionCredentials" />
+            </list>
+        </constructor-arg>
+    </bean>
+    
+    <!-- Defines how the system locates EncryptedKey elements sent in messages containing encrypted objects. -->
+    <bean id="defaultEncryptedKeyResolver" class="org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver">
+        <constructor-arg name="encKeyResolvers">
+            <list>
+                <bean class="org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver" />
+                <bean class="org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver" />
+                <bean class="org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver" />
+                <bean class="org.opensaml.xmlsec.encryption.support.SimpleKeyInfoReferenceEncryptedKeyResolver" />
+            </list>
+        </constructor-arg>
+    </bean>
+
+</beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/services/net.shibboleth.idp.module.IdPModule b/sp-saml-conf-impl/src/main/resources/META-INF/services/net.shibboleth.idp.module.IdPModule
new file mode 100644
index 0000000..7d64308
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/services/net.shibboleth.idp.module.IdPModule
@@ -0,0 +1 @@
+net.shibboleth.sp.saml.conf.SAMLModule
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/services/net.shibboleth.idp.plugin.IdPPlugin b/sp-saml-conf-impl/src/main/resources/META-INF/services/net.shibboleth.idp.plugin.IdPPlugin
new file mode 100644
index 0000000..196fbf1
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/services/net.shibboleth.idp.plugin.IdPPlugin
@@ -0,0 +1 @@
+net.shibboleth.sp.saml.conf.SAMLPlugin
diff --git a/sp-saml-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/saml.properties b/sp-saml-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/saml.properties
new file mode 100644
index 0000000..004a930
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/saml.properties
@@ -0,0 +1,2 @@
+# SAML-specific SP settings
+
diff --git a/sp-saml-conf-impl/src/main/resources/net/shibboleth/sp/saml/conf/module.properties b/sp-saml-conf-impl/src/main/resources/net/shibboleth/sp/saml/conf/module.properties
new file mode 100644
index 0000000..3c3e729
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/net/shibboleth/sp/saml/conf/module.properties
@@ -0,0 +1,14 @@
+# Properties defining this module.
+
+# Class to Module ID mappings
+net.shibboleth.sp.saml.conf.SAMLModule = sp.SAML
+
+# Module Owner
+sp.SAML.plugin = net.shibboleth.plugin.shibd.saml
+
+sp.SAML.name = SP SAML Protocol Support
+sp.SAML.desc = SAML protocol plugin for SP processing hub plugin.
+sp.SAML.url = /SP/SAML
+
+sp.SAML.1.src = /net/shibboleth/idp/module/conf/sp/saml.properties
+sp.SAML.1.dest = conf/sp/saml.properties
diff --git a/sp-saml-conf-impl/src/main/resources/net/shibboleth/sp/saml/conf/plugin.properties b/sp-saml-conf-impl/src/main/resources/net/shibboleth/sp/saml/conf/plugin.properties
new file mode 100644
index 0000000..8af6f6f
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/net/shibboleth/sp/saml/conf/plugin.properties
@@ -0,0 +1,9 @@
+# Properties defining this plugin
+
+plugin.id = net.shibboleth.plugin.shibd.saml
+
+# Only used when package manifest is not available
+plugin.version = 1.0.0
+
+# No prereqs
+#plugin.modules.required =
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2SessionInitiatorFlowTest.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2SessionInitiatorFlowTest.java
new file mode 100644
index 0000000..06ae546
--- /dev/null
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2SessionInitiatorFlowTest.java
@@ -0,0 +1,429 @@
+/*
+ * 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.saml.flows.saml2;
+
+import java.io.IOException;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.saml2.core.AuthnContext;
+import org.opensaml.saml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
+import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.opensaml.saml.saml2.core.NameIDPolicy;
+import org.opensaml.saml.saml2.core.NameIDType;
+import org.opensaml.saml.saml2.core.RequestedAuthnContext;
+import org.opensaml.saml.saml2.metadata.NameIDFormat;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.flows.AbstractSPFlowTest;
+import net.shibboleth.sp.profile.InitiatorConstants;
+
+/**
+ * Unit test for the SP session-initiator flow.
+ */
+ at ContextConfiguration(
+        locations = {
+                "classpath:/net/shibboleth/sp/saml-test-beans.xml", },
+        initializers = {
+                TestSPSAMLEnvironmentApplicationContextInitializer.class,
+                }
+        )
+ at WebAppConfiguration
+public class SAML2SessionInitiatorFlowTest extends AbstractSPFlowTest {
+    
+    /** Flow ID. */
+    @Nonnull public static final String FLOW_ID = "sp/session-initiator";
+
+    /** Flow ID. */
+    @Nonnull public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/SAML2/POST";
+
+    protected SAML2SessionInitiatorFlowTest() {
+        super(FLOW_ID);
+    }
+
+    /**
+     * Test flow with unverified (no metadata) IdP specified.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void testUnverified() throws IOException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        setApplicationRequest("no-metadata", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
+    }
+
+    /**
+     * Test flow with profile disallowed.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void testProfileDisabled() throws IOException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        setApplicationRequest("no-profile", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
+    }
+
+    /**
+     * Test flow with no valid endpoint
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void testNoEndpoint() throws IOException {
+        setDefaultAuth();
+
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        setApplicationRequest("no-endpoint", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
+    }
+
+    /**
+     * Test flow without proper input.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void testNoResponseURL() throws IOException {
+        setDefaultAuth();
+
+        setApplicationRequest(APPLICATION_ID, null);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, AuthnEventIds.NO_POTENTIAL_FLOW);
+    }
+    
+    /**
+     * Test simple success case.
+     * 
+     * @throws IOException 
+     * @throws MessageDecodingException 
+     */
+    @Test
+    public void testSimple() throws IOException, MessageDecodingException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        setApplicationRequest(APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageEvent(result, null);
+        final AuthnRequest req = validateOutputMessage(result, null);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getRequestedAuthnContext());
+        Assert.assertNull(req.getScoping());
+        assertFalse(req.isForceAuthn());
+        assertFalse(req.isPassive());
+    }
+    
+    /**
+     * Test success supplying ForceAuthn from agent.
+     * 
+     * @throws IOException 
+     * @throws MessageDecodingException 
+     */
+    @Test
+    public void testForceAuthnFromAgent() throws IOException, MessageDecodingException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        input.addmember(AuthnRequest.FORCE_AUTHN_ATTRIB_NAME).integer(1);
+        setApplicationRequest(APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageEvent(result, null);
+        final AuthnRequest req = validateOutputMessage(result, null);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getRequestedAuthnContext());
+        Assert.assertNull(req.getScoping());
+        assertTrue(req.isForceAuthn());
+        assertFalse(req.isPassive());
+    }
+
+    /**
+     * Test failure supplying ForceAuthn from agent.
+     * 
+     * @throws IOException 
+     * @throws MessageDecodingException 
+     */
+    @Test
+    public void testForceAuthnDisallowed() throws IOException, MessageDecodingException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        input.addmember(AuthnRequest.FORCE_AUTHN_ATTRIB_NAME).integer(1);
+        setApplicationRequest("feature-blocking", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageEvent(result, null);
+        final AuthnRequest req = validateOutputMessage(result, null);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getRequestedAuthnContext());
+        Assert.assertNull(req.getScoping());
+        assertFalse(req.isForceAuthn());
+        assertFalse(req.isPassive());
+    }
+
+    /**
+     * Test success supplying IsPassive from agent.
+     * 
+     * @throws IOException 
+     * @throws MessageDecodingException 
+     */
+    @Test
+    public void testIsPassiveFromAgent() throws IOException, MessageDecodingException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        input.addmember(AuthnRequest.IS_PASSIVE_ATTRIB_NAME).integer(1);
+        setApplicationRequest(APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageEvent(result, null);
+        final AuthnRequest req = validateOutputMessage(result, null);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getRequestedAuthnContext());
+        Assert.assertNull(req.getScoping());
+        assertFalse(req.isForceAuthn());
+        assertTrue(req.isPassive());
+    }
+
+    /**
+     * Test success supplying NameIDPolicy format from agent.
+     * 
+     * @throws IOException 
+     * @throws MessageDecodingException 
+     */
+    @Test
+    public void testNameIDPolicyFromAgent() throws IOException, MessageDecodingException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        input.addmember(NameIDFormat.DEFAULT_ELEMENT_LOCAL_NAME).string(NameIDType.EMAIL);
+        setApplicationRequest(APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageEvent(result, null);
+        final AuthnRequest req = validateOutputMessage(result, NameIDType.EMAIL);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getRequestedAuthnContext());
+        Assert.assertNull(req.getScoping());
+        assertFalse(req.isForceAuthn());
+        assertFalse(req.isPassive());
+    }
+
+    /**
+     * Test failure supplying NameIDPolicy format from agent.
+     * 
+     * @throws IOException 
+     * @throws MessageDecodingException 
+     */
+    @Test
+    public void testNameIDPolicyDisallowed() throws IOException, MessageDecodingException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        input.addmember(NameIDFormat.DEFAULT_ELEMENT_LOCAL_NAME).string(NameIDType.EMAIL);
+        setApplicationRequest("feature-blocking", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageEvent(result, null);
+        final AuthnRequest req = validateOutputMessage(result, null);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getRequestedAuthnContext());
+        Assert.assertNull(req.getScoping());
+        assertFalse(req.isForceAuthn());
+        assertFalse(req.isPassive());
+    }
+
+    /**
+     * Test success supplying RequestedAuthnContext from agent.
+     * 
+     * @throws IOException 
+     * @throws MessageDecodingException 
+     */
+    @Test
+    public void testAuthnContextFromAgent() throws IOException, MessageDecodingException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        final DDF aclist = input.addmember(AuthnContextClassRef.DEFAULT_ELEMENT_LOCAL_NAME).list();
+        aclist.add(new DDF(null).string(AuthnContext.X509_AUTHN_CTX));
+        aclist.add(new DDF(null).string(AuthnContext.TIME_SYNC_TOKEN_AUTHN_CTX));
+        input.addmember("AuthnContextComparison").string(AuthnContextComparisonTypeEnumeration.MINIMUM.toString());
+        setApplicationRequest(APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageEvent(result, null);
+        final AuthnRequest req = validateOutputMessage(result, null);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getScoping());
+        assertFalse(req.isForceAuthn());
+        assertFalse(req.isPassive());
+        
+        final RequestedAuthnContext rac = req.getRequestedAuthnContext();
+        assert rac != null;
+        Assert.assertEquals(rac.getComparison(), AuthnContextComparisonTypeEnumeration.MINIMUM);
+        Assert.assertEquals(rac.getAuthnContextClassRefs().size(), 2);
+        Assert.assertEquals(rac.getAuthnContextClassRefs().get(0).getURI(), AuthnContext.X509_AUTHN_CTX);
+        Assert.assertEquals(rac.getAuthnContextClassRefs().get(1).getURI(), AuthnContext.TIME_SYNC_TOKEN_AUTHN_CTX);
+    }
+
+    /**
+     * Test success supplying RequestedAuthnContext from agent.
+     * 
+     * @throws IOException 
+     * @throws MessageDecodingException 
+     */
+    @Test
+    public void testAuthnContextDisallowed() throws IOException, MessageDecodingException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(InitiatorConstants.RESPONSE_URL).string(RESPONSE_URL);
+        final DDF aclist = input.addmember(AuthnContextClassRef.DEFAULT_ELEMENT_LOCAL_NAME).list();
+        aclist.add(new DDF(null).string(AuthnContext.X509_AUTHN_CTX));
+        aclist.add(new DDF(null).string(AuthnContext.TIME_SYNC_TOKEN_AUTHN_CTX));
+        setApplicationRequest("feature-blocking", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        assertOutputMessageEvent(result, null);
+        final AuthnRequest req = validateOutputMessage(result, null);
+        Assert.assertNull(req.getSubject());
+        Assert.assertNull(req.getRequestedAuthnContext());
+        Assert.assertNull(req.getScoping());
+        assertFalse(req.isForceAuthn());
+        assertFalse(req.isPassive());
+    }
+
+    /**
+     * Decode an encoded response and run sanity checks against it.
+     * 
+     * @param result flow execution result
+     * @param format NameID format to check for in policy element
+     * 
+     * @return the request object
+     * 
+     * @throws MessageDecodingException
+     */
+    @Nonnull private AuthnRequest validateOutputMessage(@Nonnull final FlowExecutionResult result, @Nullable final String format)
+            throws MessageDecodingException {
+        final ProfileRequestContext prc = retrieveProfileRequestContext(result);
+        assert prc != null;
+        final AgentRequestContext arc = prc.ensureSubcontext(AgentRequestContext.class);
+        final DDF output = arc.getOutput();
+
+        assert output != null;
+        Assert.assertTrue(output.isstruct());
+        final DDF http = output.getmember("http");
+        Assert.assertTrue(http.isstruct());
+        
+        final AuthnRequest authnRequest;
+        final String redirect = http.getmember("redirect").string();
+        if (redirect != null) {
+            final SAMLObject saml = decodeRedirect(redirect);
+            assert saml instanceof AuthnRequest;
+            authnRequest = (AuthnRequest) saml;
+            Assert.assertTrue(redirect.startsWith(authnRequest.getDestination()));
+        } else {
+            final byte[] body = http.getmember("response.data").unsafe_string();
+            Assert.assertNotNull(body);
+            // Not trivial to consider parsing the form, so just bypass that step.
+            final Object saml = prc.ensureOutboundMessageContext().ensureMessage();
+            assert saml instanceof AuthnRequest;
+            authnRequest = (AuthnRequest) saml;
+        }
+        
+        assert authnRequest != null;
+        Assert.assertNotNull(authnRequest.getID());
+        Assert.assertTrue(Instant.now().isAfter(authnRequest.getIssueInstant()));
+        
+        Assert.assertEquals(authnRequest.getAssertionConsumerServiceURL(), RESPONSE_URL);
+        
+        final NameIDPolicy pol = authnRequest.getNameIDPolicy();
+        assert pol != null;
+        assertTrue(pol.getAllowCreate());
+        Assert.assertEquals(pol.getFormat(), format);
+        
+        return authnRequest;
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java
new file mode 100644
index 0000000..1a7ffd3
--- /dev/null
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/TestSPSAMLEnvironmentApplicationContextInitializer.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.saml.flows.saml2;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.mock.env.MockPropertySource;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An {@link ApplicationContextInitializer} which prepends properties.
+ *
+ * <p>This needs to include the original IdP-test-layer properties and has to be
+ * set to {@link Ordered#LOWEST_PRECEDENCE} or things blow up.</p>
+ */
+ at Order(Ordered.LOWEST_PRECEDENCE)
+public class TestSPSAMLEnvironmentApplicationContextInitializer
+        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(TestSPSAMLEnvironmentApplicationContextInitializer.class);
+
+    /** {@inheritDoc} */
+    @Override public void initialize(@Nonnull final ConfigurableApplicationContext applicationContext) {
+        final MockPropertySource mock = new MockPropertySource();
+        mock.setProperty("idp.home", "classpath:/net/shibboleth/idp/module");
+        mock.setProperty("idp.webflows", "classpath*:/flows");
+        mock.setProperty("idp.service.metadata.resources", "test.sp.saml.MetadataResolverResources");
+        mock.setProperty("sp.service.agents.resources", "test.sp.saml.AgentResolverResources");
+        mock.setProperty("idp.additionalProperties",
+                "/conf/ldap.properties, /conf/saml-nameid.properties, /conf/services.properties, /conf/admin/admin.properties, /conf/authn/authn.properties, /conf/c14n/subject-c14n.properties, /credentials/secrets.properties, /conf/sp/sp.properties");
+        applicationContext.getEnvironment().getPropertySources().addFirst(mock);
+        log.info("Prepending properties '{}'", mock.getSource());
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/package-info.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/package-info.java
new file mode 100644
index 0000000..a7f80f4
--- /dev/null
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * Unit tests for SAML 2 flows.
+ */
+package net.shibboleth.sp.saml.flows.saml2;
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/test/resources/logback-test.xml b/sp-saml-conf-impl/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..acf56c0
--- /dev/null
+++ b/sp-saml-conf-impl/src/test/resources/logback-test.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%level [%logger:%line] - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <root>
+        <level value="WARN" />
+        <appender-ref ref="STDOUT" />
+    </root>
+    
+</configuration>
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/metadata-providers.xml b/sp-saml-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/metadata-providers.xml
new file mode 100644
index 0000000..ba3bf0f
--- /dev/null
+++ b/sp-saml-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/metadata-providers.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<MetadataProvider id="ShibbolethMetadata" xsi:type="ChainingMetadataProvider"
+    xmlns="urn:mace:shibboleth:2.0:metadata"
+    xmlns:security="urn:mace:shibboleth:2.0:security"
+    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
+    xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    xmlns:ds11="http://www.w3.org/2009/xmldsig11#"
+    xmlns:enc="http://www.w3.org/2001/04/xmlenc#"
+    xmlns:enc11="http://www.w3.org/2009/xmlenc11#"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd
+                        urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd
+                        urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd
+                        urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd
+                        urn:oasis:names:tc:SAML:metadata:algsupport http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-algsupport-v1.0.xsd
+                        http://www.w3.org/2000/09/xmldsig# http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd
+                        http://www.w3.org/2009/xmldsig11# http://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/xmldsig11-schema.xsd
+                        http://www.w3.org/2001/04/xmlenc# http://www.w3.org/TR/xmlenc-core/xenc-schema.xsd
+                        http://www.w3.org/2009/xmlenc11# http://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/xenc-schema-11.xsd">
+                        
+	<!-- ========================================== -->
+	<!-- Metadata Configuration -->
+	<!-- ========================================== -->
+
+	<!-- Example metadata provider. -->
+
+    <MetadataProvider id="InlineExample" xsi:type="InlineMetadataProvider" indexesRef="testbed.MetadataIndexes">
+        <md:EntitiesDescriptor Name="tests">
+            <md:EntityDescriptor entityID="https://idp.example.org">
+                <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+                    <md:KeyDescriptor>
+                        <ds:KeyInfo>
+                            <ds:X509Data>
+                                <ds:X509Certificate>
+MIIDtTCCAp2gAwIBAgIJAPmsD+VGldyPMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTQwNDExMTMzOTE4WhcNMjQwNDA4MTMzOTE4WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAxg0TyQAP/tIvOH89EtaXuRRn8SYzTj7W1TbNY4VvBmobjkRmSkki4hH9
+x4sQpi635wn6WtXTN/FNNmkTK3N/LspmBWxfZS+n+cc7I82E5yvCAPX67QsZgqgg
+lp2W5dvK/FsMMCS6X6SVqzBLMP88NenXKxY+HMxMs0sT0UKYh1cAEqadrHRBO65a
+DBcm5a0sBVYt9K6pgaOHrp/zSIbhnR5tFFLjBbtFktDpHL3AdGBH3OYidNGKBO3t
+J3Ms7LeKXsM0+0Y4P+9fHZINL2X3E2N6GVnKs5PZTg9sP0FtIpAbYm/+zCx7Yj1E
+T/Er8mDd6tNVGSQsn9s5xUBwGqn14wIDAQABo4GnMIGkMB0GA1UdDgQWBBSiQhSu
+p9BYjD2ZuMkEiQK7w/Zq0TB1BgNVHSMEbjBsgBSiQhSup9BYjD2ZuMkEiQK7w/Zq
+0aFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
+BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAPmsD+VGldyPMAwGA1UdEwQF
+MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHZmIo9GBTSsD5DJfKkCVUvBafwR089H
+BkgVPOuVuEe803BRlKd4BVIsuxAUAy3oqdJYqf9ptPEx8Ef+ALbcDhRbWINhMgO7
+0/S4x3pS9gOn7/Y9yZplOe4Jd2q3R8QBef+hKLcD/Uv0Sqy2nilM8BnMga5tqsL+
+8oFt0blzXtQ2vcOVyNyG326uZBZv2Cf6FXFsYQX1L/tLeTBJegefgGkg2dqCTKIU
+1Qy/Kd2P3/S01kQxjDeG7UfXc9qtelJ68kvzK2d3WOJ2qmsdMxjMNfTItP7FO54M
+i8V7gp9HK+EimdSbgu7xktKlrqA2Rsn+dBoPSgOUs/LOGtCS9/biF0w=
+                                </ds:X509Certificate>
+                            </ds:X509Data>
+                        </ds:KeyInfo>
+                    </md:KeyDescriptor>
+                    <md:SingleSignOnService
+                        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+                        Location="https://idp.example.org/idp/profile/SAML2/Redirect/SSO" />
+                    <md:SingleSignOnService
+                        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+                        Location="https://idp.example.org/idp/profile/SAML2/POST/SSO" />
+                </md:IDPSSODescriptor>
+            </md:EntityDescriptor>
+            
+            <md:EntityDescriptor entityID="https://noendpoint.example.org">
+                <md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+                    <md:KeyDescriptor>
+                        <ds:KeyInfo>
+                            <ds:X509Data>
+                                <ds:X509Certificate>
+MIIDtTCCAp2gAwIBAgIJAPmsD+VGldyPMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTQwNDExMTMzOTE4WhcNMjQwNDA4MTMzOTE4WjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAxg0TyQAP/tIvOH89EtaXuRRn8SYzTj7W1TbNY4VvBmobjkRmSkki4hH9
+x4sQpi635wn6WtXTN/FNNmkTK3N/LspmBWxfZS+n+cc7I82E5yvCAPX67QsZgqgg
+lp2W5dvK/FsMMCS6X6SVqzBLMP88NenXKxY+HMxMs0sT0UKYh1cAEqadrHRBO65a
+DBcm5a0sBVYt9K6pgaOHrp/zSIbhnR5tFFLjBbtFktDpHL3AdGBH3OYidNGKBO3t
+J3Ms7LeKXsM0+0Y4P+9fHZINL2X3E2N6GVnKs5PZTg9sP0FtIpAbYm/+zCx7Yj1E
+T/Er8mDd6tNVGSQsn9s5xUBwGqn14wIDAQABo4GnMIGkMB0GA1UdDgQWBBSiQhSu
+p9BYjD2ZuMkEiQK7w/Zq0TB1BgNVHSMEbjBsgBSiQhSup9BYjD2ZuMkEiQK7w/Zq
+0aFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
+BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAPmsD+VGldyPMAwGA1UdEwQF
+MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAHZmIo9GBTSsD5DJfKkCVUvBafwR089H
+BkgVPOuVuEe803BRlKd4BVIsuxAUAy3oqdJYqf9ptPEx8Ef+ALbcDhRbWINhMgO7
+0/S4x3pS9gOn7/Y9yZplOe4Jd2q3R8QBef+hKLcD/Uv0Sqy2nilM8BnMga5tqsL+
+8oFt0blzXtQ2vcOVyNyG326uZBZv2Cf6FXFsYQX1L/tLeTBJegefgGkg2dqCTKIU
+1Qy/Kd2P3/S01kQxjDeG7UfXc9qtelJ68kvzK2d3WOJ2qmsdMxjMNfTItP7FO54M
+i8V7gp9HK+EimdSbgu7xktKlrqA2Rsn+dBoPSgOUs/LOGtCS9/biF0w=
+                                </ds:X509Certificate>
+                            </ds:X509Data>
+                        </ds:KeyInfo>
+                    </md:KeyDescriptor>
+                    <md:SingleSignOnService
+                        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
+                        Location="https://idp.example.org/idp/profile/SAML2/Artifact/SSO" />
+                </md:IDPSSODescriptor>
+            </md:EntityDescriptor>
+        </md:EntitiesDescriptor>    
+    </MetadataProvider>
+
+</MetadataProvider>
diff --git a/sp-saml-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/saml-test-agents.xml b/sp-saml-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/saml-test-agents.xml
new file mode 100644
index 0000000..548932f
--- /dev/null
+++ b/sp-saml-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/saml-test-agents.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xmlns:c="http://www.springframework.org/schema/c"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+                           
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+
+    <bean id="testsp.example.org" parent="shibboleth.Agent" p:sharedSecrets="foo">
+        <property name="applications">
+            <set>
+                <bean p:id="default" parent="shibboleth.Application"
+                    p:issuer="https://testsp.example.org"
+                    p:authenticatingAuthority="https://idp.example.org"
+                    p:defaultConfiguration-ref="test.DefaultRelyingParty" />
+
+                <bean p:id="feature-blocking" parent="shibboleth.Application"
+                    p:issuer="https://testsp.example.org"
+                    p:authenticatingAuthority="https://idp.example.org"
+                    p:defaultConfiguration-ref="test.featureBlockingDefaultRelyingParty" />
+
+                <bean p:id="no-metadata" parent="shibboleth.Application"
+                    p:issuer="https://testsp.example.org"
+                    p:authenticatingAuthority="https://unknown.example.org"
+                    p:defaultConfiguration-ref="test.DefaultRelyingParty" />
+
+                <bean p:id="no-profile" parent="shibboleth.Application"
+                    p:issuer="https://testsp.example.org"
+                    p:authenticatingAuthority="https://idp.example.org"
+                    p:defaultConfiguration-ref="shibboleth.DefaultRelyingParty" />
+
+                <bean p:id="no-endpoint" parent="shibboleth.Application"
+                    p:issuer="https://testsp.example.org"
+                    p:authenticatingAuthority="https://noendpoint.example.org"
+                    p:defaultConfiguration-ref="test.DefaultRelyingParty" />
+                    
+                <bean p:id="no-initiators" parent="shibboleth.Application"
+                    p:issuer="https://testsp.example.org"
+                    p:authenticatingAuthority="https://idp.example.org"
+                    p:defaultConfiguration-ref="test.DefaultRelyingParty"
+                    p:sessionInitiators="#{{}}" />
+            </set>
+        </property>
+    </bean>
+
+    <bean id="test.DefaultRelyingParty" parent="RelyingParty">
+        <property name="profileConfigurations">
+            <list>
+                <ref bean="SAML2.SSO" />
+                <ref bean="SAML2.ECP" />
+                <ref bean="SAML2.Logout" />
+            </list>
+        </property>
+    </bean>
+
+    <bean id="test.featureBlockingDefaultRelyingParty" parent="RelyingParty">
+        <property name="profileConfigurations">
+            <list>
+                <bean parent="SAML2.SSO" p:disallowedFeatures="0x1F" />
+                <ref bean="SAML2.ECP" />
+                <ref bean="SAML2.Logout" />
+            </list>
+        </property>
+    </bean>
+
+</beans>
diff --git a/sp-saml-conf-impl/src/test/resources/net/shibboleth/sp/saml-test-beans.xml b/sp-saml-conf-impl/src/test/resources/net/shibboleth/sp/saml-test-beans.xml
new file mode 100644
index 0000000..b2b73b4
--- /dev/null
+++ b/sp-saml-conf-impl/src/test/resources/net/shibboleth/sp/saml-test-beans.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xmlns:c="http://www.springframework.org/schema/c"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+                           
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+
+      
+    <util:list id="test.sp.saml.AgentResolverResources">
+        <value>%{idp.home}/conf/sp/agents.xml</value>
+        <value>%{idp.home}/conf/sp/saml-test-agents.xml</value>
+    </util:list>
+
+    <util:list id="test.sp.saml.MetadataResolverResources">
+        <value>%{idp.home}/conf/sp/metadata-providers.xml</value>
+    </util:list>
+
+</beans>
diff --git a/sp-saml-dist/pom.xml b/sp-saml-dist/pom.xml
new file mode 100644
index 0000000..63ce31c
--- /dev/null
+++ b/sp-saml-dist/pom.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>net.shibboleth.sp</groupId>
+        <artifactId>sp-saml-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+
+    <name>Shibboleth SP :: SAML Protocol Plugin :: Distribution</name>
+    <description>SP SAML protocol plugin packaging.</description>
+    <artifactId>sp-saml-dist</artifactId>
+    <packaging>pom</packaging>
+
+    <properties>
+        <dist.finalName>shibboleth-idp-plugin-sp-saml-${project.version}</dist.finalName>
+        <dist.assemblyDirectory>${project.build.directory}/${dist.finalName}</dist.assemblyDirectory>
+        <checkstyle.configLocation>${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+    </properties>
+
+    <build>
+        <plugins>
+            <!-- Copy src/main/resources to assembly directory. -->
+            <plugin>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-resources</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <includeEmptyDirs>true</includeEmptyDirs>
+                            <outputDirectory>${dist.assemblyDirectory}</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>${basedir}/src/main/resources</directory>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <!-- Assemble -->
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <appendAssemblyId>false</appendAssemblyId>
+                    <descriptors>
+                        <descriptor>src/main/assembly/assembly.xml</descriptor>
+                    </descriptors>
+                    <finalName>${dist.finalName}</finalName>
+                    <overrideUid>1000</overrideUid>
+                    <overrideGid>1000</overrideGid>
+                    <tarLongFileMode>gnu</tarLongFileMode>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-enforcer-plugin</artifactId>
+                <dependencies>
+                    <dependency>
+                        <groupId>net.shibboleth.maven.enforcer.rules</groupId>
+                        <artifactId>maven-dist-enforcer</artifactId>
+                        <version>${maven-dist-enforcer.version}</version>
+                    </dependency>
+                </dependencies>
+                <executions>
+                    <execution>
+                        <id>totp-enforce</id>
+                        <phase>verify</phase>
+                        <goals>
+                            <goal>enforce</goal>
+                        </goals>
+                        <configuration>
+                            <rules>
+                                <jarEnforcer implementation="net.shibboleth.mvn.enforcer.impl.JarEnforcer">
+                                    <dataGroupId>net.shibboleth.maven.enforcer.rules</dataGroupId>
+                                    <dataArtifactId>maven-dist-enforcer-data</dataArtifactId>
+                                    <dataVersion>${maven-dist-enforcer-data.version}</dataVersion>
+                                    <dataKeyRing>${basedir}/src/main/enforcer/shibbolethKeys.gpg</dataKeyRing>
+                                    <parentPomDir>${basedir}/..</parentPomDir>
+                                    <tgzFiles>${project.build.directory}/${dist.finalName}.tar.gz</tgzFiles>
+                                    <checkSignatures>true</checkSignatures>
+                                    <checkDependencies>true</checkDependencies>
+                                    <listJarSources>false</listJarSources>
+                                    <checkM2>false</checkM2>
+                                </jarEnforcer>
+                            </rules>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+
+    </build>
+
+</project>
diff --git a/sp-saml-dist/src/main/assembly/assembly.xml b/sp-saml-dist/src/main/assembly/assembly.xml
new file mode 100644
index 0000000..e99ada9
--- /dev/null
+++ b/sp-saml-dist/src/main/assembly/assembly.xml
@@ -0,0 +1,43 @@
+<assembly>
+    <id>assembly-targz</id>
+    <formats>
+        <format>tar.gz</format>
+    </formats>
+    <fileSets>
+        <fileSet>
+            <directory>../sp-server-impl/target</directory>
+            <outputDirectory>webapp/WEB-INF/lib</outputDirectory>
+            <includes>
+                <include>sp-server-impl-*.jar</include>
+            </includes>
+            <excludes>
+                <exclude>*test*.jar</exclude>
+                <exclude>*javadoc.jar</exclude>
+                <exclude>*sources.jar</exclude>
+            </excludes>
+        </fileSet>
+        <fileSet>
+            <directory>../sp-server-impl/target/dependency</directory>
+            <outputDirectory>webapp/WEB-INF/lib</outputDirectory>
+            <includes>
+                <include>*.jar</include>
+            </includes>
+            <excludes>
+                <exclude>*test*.jar</exclude>
+                <exclude>*javadoc.jar</exclude>
+                <exclude>*sources.jar</exclude>
+            </excludes>
+        </fileSet>
+        <fileSet>
+            <directory>../sp-conf-impl/target/classes/net/shibboleth/idp/plugin/sp</directory>
+            <outputDirectory>bootstrap</outputDirectory>
+            <includes>
+                <include>plugin.properties</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>${dist.assemblyDirectory}</directory>
+            <outputDirectory></outputDirectory>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git a/sp-saml-dist/src/main/enforcer/shibbolethKeys.gpg b/sp-saml-dist/src/main/enforcer/shibbolethKeys.gpg
new file mode 100644
index 0000000..6d8fc48
Binary files /dev/null and b/sp-saml-dist/src/main/enforcer/shibbolethKeys.gpg differ
diff --git a/sp-saml-dist/src/main/resources/bootstrap/keys.txt b/sp-saml-dist/src/main/resources/bootstrap/keys.txt
new file mode 100644
index 0000000..a723b19
--- /dev/null
+++ b/sp-saml-dist/src/main/resources/bootstrap/keys.txt
@@ -0,0 +1,427 @@
+pub   4096R/02277962 2011-09-22
+uid                  Scott Cantor <cantor.2 at osu.edu>
+sig 3        02277962 2011-09-22  Scott Cantor <cantor.2 at osu.edu>
+sig 3        30A2362E 2011-09-23  Brian Swaney <swaney.29 at osu.edu>
+sig          47905D15 2011-09-26  Scott Cantor (Internet2) <cantor.2 at osu.edu>
+sig          EA2882BB 2011-09-28  Ian A. Young <ian at iay.org.uk>
+sig          A9D7AF55 2011-09-24  Michael Craft <mcraft at peak15.org>
+sig          D7079C77 2011-09-28  Ian A. Young <ian at iay.org.uk>
+sig          D2B37353 2011-10-11  Chad La Joie <lajoie at itumi.biz>
+sub   4096R/5B94AC0C 2011-09-22
+sig          02277962 2011-09-22  Scott Cantor <cantor.2 at osu.edu>
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.11 (Darwin)
+
+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
+=ShRP
+-----END PGP PUBLIC KEY BLOCK-----
+
+pub   rsa4096 2021-02-25 [SC]
+      B5B5DD332142AD657E8D87AC7D27E610B8A3DC52
+uid           [ultimate] Philip David Smart <philip.smart at jisc.ac.uk>
+sig 3        7D27E610B8A3DC52 2021-02-25  Philip David Smart <philip.smart at jisc.ac.uk>
+sig 3        B77C52EEC21771DD 2021-02-25  philsmart (gpg key) <philip.smart at jisc.ac.uk>
+sig 2        9A804E97D7079C77 2021-03-04  Ian A. Young <ian at iay.org.uk>
+uid           [ultimate] [jpeg image of size 9378]
+sig 3        7D27E610B8A3DC52 2021-02-25  Philip David Smart <philip.smart at jisc.ac.uk>
+sig 3        B77C52EEC21771DD 2021-02-25  philsmart (gpg key) <philip.smart at jisc.ac.uk>
+sig 2        9A804E97D7079C77 2021-03-04  Ian A. Young <ian at iay.org.uk>
+sub   rsa4096 2021-02-25 [E]
+sig          7D27E610B8A3DC52 2021-02-25  Philip David Smart <philip.smart at jisc.ac.uk>
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+AAEAEAAAkgoABQAAAAEAAAKqkhQAAwAAAAQAAAKyknwABwAAA+IAAAK6kpEAAgAA
+AAQ3MTAAkpIAAgAAAAQ3MTAAoAAABwAAAAQwMTAwoAIABAAAAAEAAACWoAMABAAA
+AAEAAADqohcAAwAAAAEAAgAAowEABwAAAAEBAAAApAIAAwAAAAEAAAAApAMAAwAA
+AAEAAAAApAUAAwAAAAEAHAAApAYAAwAAAAEAAAAApDIABQAAAAQAAAacpDMAAgAA
+AAYAAAa8pDQAAgAAACIAAAbCAAAAAAAAAAEAAAARAAAACQAAAAUyMDE4OjA3OjE2
+IDEwOjA3OjM4ADIwMTg6MDc6MTYgMTA6MDc6MzgAAAAKdAAAApMAAAhvAAAE+QAA
+KOIAAA/BAAAAAAAAAAEAAAGPAAAAZAXXBg4DvwPBQXBwbGUgaU9TAAABTU0AEwAB
+AAkAAAABAAAACQACAAcAAAIuAAAA+AADAAcAAABoAAADJgAEAAkAAAABAAAAAQAF
+AAkAAAABAAAA6AAGAAkAAAABAAAA8QAHAAkAAAABAAAAAQAIAAoAAAADAAADjgAM
+AAoAAAACAAADpgANAAkAAAABAAAAKAAOAAkAAAABAAAABAAPAAkAAAABAAAAAgAQ
+AAkAAAABAAAAAQARAAIAAAAlAAADtgAUAAkAAAABAAAABQAXAAkAAAABAAAAAAAZ
+AAkAAAABAAAAAAAaAAIAAAAGAAAD3AAfAAkAAAABAAAAAAAAAABicGxpc3QwME8R
+AgBHAkwCUQJVAlICSwJDAjsCLAIaAnMBzgCQAIgAiAB/AEsCUgJWAlYCTgJGAj4C
+MQIgAsoBjwB/AHkAgACBAIAATgJXAloCVgJOAkQCOAIlAg8CMwFwAHcAjACKAJYA
+jwBMAlYCVwJQAkYCOgIrAhIC4wGiAGUAkgCJAI4AjQCSAEoCUgJQAkcCOgIqAhYC
+9AFqAXgAdACRAIUAhQCMAIsASQJKAvYB+gC8AOIAjgGJAbEAcQBrAIMAeQB6AH4A
+gQBFAoEBTQDQALEA4QDiAL0ArgCZAIkAYwBoAHkAdQBuAEECBAF5ABgBtADHANUA
+uQCkAN0AyQCQAIMAfABqAGUAOwL2AHIAIgEEAb0ArACYAIYAuwDQAK8AXgBmAG8A
+eAAxAgkBUADvAIsAqwC/AJ0AbQB5AF8AZABvAHwAdAB1ACYC5gG3AHUAdgCuANwA
+DAHGAGIAYQBqAHEAdwBvAHAAHQIaAv8BtAGjAZ0BhwF4ATEBcwBtAHYAdQB0AGsA
+ZwAYAhkCDQL5AegB2gHFAagBcgGtAF8AdgBzAHIAagBoABUCGQIUAgkC/AHoAc0B
+sAGPARYBZgBmAHkAagBzAGYAEwITAg8CBAL4AeUBygGzAZsBfgHNAGEAcgBpAGsA
+YQAQAg0CCQL/AfQB4gHJAbMBoAGMAXwBUwEDAeAAcAByAAAIAAAAAAAAAgEAAAAA
+AAAAAQAAAAAAAAAAAAAAAAAAAgxicGxpc3QwMNQBAgMEBQYHCFVmbGFnc1V2YWx1
+ZVl0aW1lc2NhbGVVZXBvY2gQARMAAAU6zneKsRI7msoAEAAIERcdJy0vOD0AAAAA
+AAABAQAAAAAAAAAJAAAAAAAAAAAAAAAAAAAAP///xsEAChy8//+2DAAAShH///v9
+AArWDwAAADsAAAEAAAAAZQAAAQA0MkRDQUE3RC0wMTU0LTRBNTktQjY2NS03QTZB
+QjZBRkM4OEQAAHE4MjVzAAA/1d8AD/+1AD/V3wAP/7UAAAAJAAAABQAAAAkAAAAF
+QXBwbGUAaVBob25lIDcgYmFjayBjYW1lcmEgMy45OW1tIGYvMS44AAANAAEAAgAA
+AAJOAAAAAAIABQAAAAMAAAeGAAMAAgAAAAJXAAAAAAQABQAAAAMAAAeeAAUAAQAA
+AAEAAAAAAAYABQAAAAEAAAe2AAwAAgAAAAJLAAAAAA0ABQAAAAEAAAe+ABAAAgAA
+AAJUAAAAABEABQAAAAEAAAfGABcAAgAAAAJUAAAAABgABQAAAAEAAAfOAB8ABQAA
+AAEAAAfWAAAAAAAAADMAAAABAAAAIAAAAAEAAA5QAAAAZAAAAAMAAAABAAAACwAA
+AAEAABWDAAAAZAAAWb0AAADZAAAArQAABJ4AAN2FAAABWAAA3YUAAAFYAAAACAAA
+AAH/4Q1XaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVn
+aW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBt
+ZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2
+LjAuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5
+OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFi
+b3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIg
+eG1sbnM6bXdnLXJzPSJodHRwOi8vd3d3Lm1ldGFkYXRhd29ya2luZ2dyb3VwLmNv
+bS9zY2hlbWFzL3JlZ2lvbnMvIiB4bWxuczpzdEFyZWE9Imh0dHA6Ly9ucy5hZG9i
+ZS5jb20veG1wL3NUeXBlL0FyZWEjIiB4bWxuczphcHBsZS1maT0iaHR0cDovL25z
+LmFwcGxlLmNvbS9mYWNlaW5mby8xLjAvIiB4bWxuczpzdERpbT0iaHR0cDovL25z
+LmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL0RpbWVuc2lvbnMjIiB4bWxuczpwaG90
+b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtcDpD
+cmVhdGVEYXRlPSIyMDE4LTA3LTE2VDEwOjA3OjM4LjcxMCIgeG1wOkNyZWF0b3JU
+b29sPSIxMS40IiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOC0wNy0xNlQxMDowNzozOCIg
+cGhvdG9zaG9wOkRhdGVDcmVhdGVkPSIyMDE4LTA3LTE2VDEwOjA3OjM4LjcxMCI+
+IDxtd2ctcnM6UmVnaW9ucyByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+IDxtd2ct
+cnM6UmVnaW9uTGlzdD4gPHJkZjpTZXE+IDxyZGY6bGk+IDxyZGY6RGVzY3JpcHRp
+b24gbXdnLXJzOlR5cGU9IkZhY2UiPiA8bXdnLXJzOkFyZWEgc3RBcmVhOnk9IjAu
+NTEyOTk5OTk5OTk5OTk5OSIgc3RBcmVhOnc9IjAuMjM3OTk5OTk5OTk5OTk5OTki
+IHN0QXJlYTp4PSIwLjM3MSIgc3RBcmVhOmg9IjAuMzE3OTk5OTk5OTk5OTk5OTUi
+IHN0QXJlYTp1bml0PSJub3JtYWxpemVkIi8+IDxtd2ctcnM6RXh0ZW5zaW9ucyBh
+cHBsZS1maTpBbmdsZUluZm9ZYXc9IjAiIGFwcGxlLWZpOkFuZ2xlSW5mb1JvbGw9
+IjI3MCIgYXBwbGUtZmk6Q29uZmlkZW5jZUxldmVsPSIxMDAwIiBhcHBsZS1maTpU
+aW1lc3RhbXA9IjEzNzk5OTkyMTQ1MCIgYXBwbGUtZmk6RmFjZUlEPSI5Ii8+IDwv
+cmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpsaT4gPC9yZGY6U2VxPiA8L213Zy1yczpS
+ZWdpb25MaXN0PiA8bXdnLXJzOkFwcGxpZWRUb0RpbWVuc2lvbnMgc3REaW06aD0i
+MzAyNCIgc3REaW06dz0iNDAzMiIgc3REaW06dW5pdD0icGl4ZWwiLz4gPC9td2ct
+cnM6UmVnaW9ucz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94Onht
+cG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0
+IGVuZD0idyI/PgD/7QB4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAD8cAVoAAxsl
+RxwCAAACAAIcAj8ABjEwMDczOBwCPgAIMjAxODA3MTYcAjcACDIwMTgwNzE2HAI8
+AAYxMDA3MzgAOEJJTQQlAAAAAAAQOGix0QQ3mMOAExf+M5Hq4v/iAkBJQ0NfUFJP
+RklMRQABAQAAAjBBREJFAhAAAG1udHJSR0IgWFlaIAfQAAgACwATADMAO2Fjc3BB
+UFBMAAAAAG5vbmUAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtQURCRQAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmNwcnQA
+AAD8AAAAMmRlc2MAAAEwAAAAa3d0cHQAAAGcAAAAFGJrcHQAAAGwAAAAFHJUUkMA
+AAHEAAAADmdUUkMAAAHUAAAADmJUUkMAAAHkAAAADnJYWVoAAAH0AAAAFGdYWVoA
+AAIIAAAAFGJYWVoAAAIcAAAAFHRleHQAAAAAQ29weXJpZ2h0IDIwMDAgQWRvYmUg
+U3lzdGVtcyBJbmNvcnBvcmF0ZWQAAABkZXNjAAAAAAAAABFBZG9iZSBSR0IgKDE5
+OTgpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYWVogAAAAAAAA81EA
+AQAAAAEWzFhZWiAAAAAAAAAAAAAAAAAAAAAAY3VydgAAAAAAAAABAjMAAGN1cnYA
+AAAAAAAAAQIzAABjdXJ2AAAAAAAAAAECMwAAWFlaIAAAAAAAAJwYAABPpQAABPxY
+WVogAAAAAAAANI0AAKAsAAAPlVhZWiAAAAAAAAAmMQAAEC8AAL6c/8AAEQgA6gCW
+AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQ
+AAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHw
+JDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp
+anN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TF
+xsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEB
+AQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUh
+MQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4
+OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWW
+l5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp
+6vLz9PX29/j5+v/bAEMAGBgYGBgYKRgYKTopKSk6Tjo6OjpOY05OTk5OY3djY2Nj
+Y2N3d3d3d3d3d4+Pj4+Pj6enp6enu7u7u7u7u7u7u//bAEMBHR8fMCwwUiwsUsSF
+bYXExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTE
+xMTExP/dAAQACv/aAAwDAQACEQMRAD8A6KiiisiwpaSkJ4oAZK2xd3uB+fFP3CqL
+yJLbkSHG8HisFNVlKgHsNx9z7U7AdYWA6mkSRJOUIOK59NWaWHaqbpT949F/z7VU
+sbgR3u18qrZBBOMHt+FFgudLdzGC3eQdR/U4plndfaolcD6//WqvqhB0+TB5wD+R
+qvZ3QWAMdqgDCrnGcUAbmcVnS6gkc/2cAljtxj/aP9KgbVI8HjOOuCD+nWuc84rc
+iYHgMDn2z/hQkI7v2pFO5Q3rVR7qMRiUHgdacJ44lSNjzgY96Qy3RTA2VBNOBBGR
+3oAWiiigAooooA//0OiopKM1kaDZHCLk1z099I26ZT8oO1R+maXV7jcwiQ9B/Osm
+dgifZ+pRv8/zppCbFRv3ZllJI6KM9exqkW5zQWPTtTT61ZID5eRTyc1FnsacB2oE
+S+dKy7C7Y9MnH5U4TybPKzwOKreop49aBjt1OzmmH1pM8UATLM4XGSRxwfbpVn7U
+7yq7HJFZ465pwPNKwHYpdA5T0J/KpLScSbVB6Er+VctDK6knOR3rf0kFsue24/mf
+/rGpaKNyiiikAUUUUAf/0ehqmJ1G8ucYNW6x9RiIUzL34NYmhi6g+Z1kHBIz+dUC
+xJyetSzyGRuar7TWiIF69KNnekwRzTstTAYRSZqTBNBQ0AMJzQDinbDThEx7UBYY
+abVjyW9KTyW9KVwsQZpRTzGR1pmKYE0Z5ye9dZpGDbkjjnH5Vx4OK1dMuxDON5+U
+qQf5j8uaTGdlRTEdXUOpBB6EdKfUDCiiimI//9LeNU7xglu7E8AZq2TgZrldQuHe
+Z4g2VHGMYrJFsy2bJzihVJpwXJwOSa1YLcKMnrVN2ElcopbO9WksfWtFVAqUCocj
+RRKK2SCpfsielXRS0rlcqKi2kY6iphboOcVKM07mlcLFfyUHamGBSelWqaaLhYov
+aKRWbPZsvK1v1G6g1SkJxOTIIODQDg1q3duPvLWSwNaJ3MmrHW6PdRmEWxPzr0Hq
+K3M159BKY5klH8JBr0BTkAmkwQ6iiikM/9PTvLgW0Blxkjp9a493MjtK3Vjmt7WJ
+cRrF/eOfyrnh0rOJTLNsmW3GtpRxWdaKMVpgVMjSI4YpwpBTuaksdSjNIM04fWkM
+MGlwaMn1pOfWgQvNNNLk0mRQMbSGlyKQ0wK8yBl5rAnj2ua6RhkVh3Yy2auJlNFF
+CQcjgiu50+c3FqjnqBg59a4auo0OUmN4ic4OQParZmjfopKKgo//1K+sHM6DttNZ
+ArW1b/Xr/u1kjk1C2KZrWowuavCq0K7EAqcVmzVD8mnAtTBUlIoUE08ZpoFSUhiZ
+PtSZNLSgcUANyaTNPxSGgCPiilNJTAQ8isW8Ta2fWto1QvUDRFv7vNVEiS0MI8Gt
+fRiVvBjGMHP44rJYd60dLP8Apkf41ozE7SiiioKP/9WPWIsFZfwrHiGZFrodYXNs
+rf3WH68Vg2g3TAemTWa2L6mwtOLBetQySCMe9U/OYnJ61Ni7mh5oAyaVbhM4yKzy
+0j9ttHk9807BzGykit0qUc1kRqyHg1pRvxSaKTJsU7GBTAc04nApDIJZ1j6mqhvF
+PXipZVDnmqbxxCqSJbZbWZW6HNO8wd6zeF+4/wCdG6T6j2osHMaoORUTjcpHrVaO
+Q9KtZyuaQXOdbgkelWdPYLdxk9N39Krz8SsPeruloHvVHpk/lWhkdoKKQcDFFQM/
+/9a9frvtJB7VzliMyk+1dDfcwe2awrFSJGz2H9ayWxZYkXcxzUJB3bE61dI71Sfc
+WITigqxII4x985PuaUxREfKPxFUriLYFI5z3qOEFpEVMjOAfr3p2DmLoZozg8irU
+cmelQFSjbJOR605UKHjpSGa0RyKbI2KjibApshzUlFaRz0FQAKT83NWGUDk9aqyR
+M0bt3A4FUInDxDg4pTGpG5P0rHABUrjnOc+3pWgsLxojocNjkU7EqVyyq7utTKMD
+FNiORzU2Kkqxg3sZSYn15p+nytFdK6jJwR+dT6kn3X/Co7GMjMntirvoZ21Ostrg
+XAPGCvWrOKzNNHDmtSpG1qf/1714u+3YfSsm2Qo0hPritqbmJvpWXGeD9ayNUPIz
+ULJg5qegipLsVmAYYYZFIixocooB9asbAaXZTuFiH8KDyamI2iowKAJlHFBGacvS
+jvSKIskcUzOO1TY5p/lg8igViqI06hFz64qTGal2CnBR2p3FYaq4p9OApp4pDsU7
+5N0Deo5FNtVAiVfQCprk/uW9xinRrsRVPYVXQlLU0LBdqt9a0c1UtR+7OPWrVBMt
+z//Q05P9W30NZKHlhWs43KR6isjOGH5GsTVE1LTAafmkWhRS02jNBQjc02lagDNA
+iRTR3p6IO9DAUDEI70vOKapwcGpCOKAQ2lFNGaXNAx1MY0uaYxoJZBMN21PU5/Kp
+B8zZ/hFAXcTnpjFTKuflUfSmJGjajEP1JqxTUXYgT0FOpmbP/9HUNZlxHh9/Y1pG
+oyAeDWRpczgafUfQkU8Ui0OpV5NIKkXikURyEAiq3musmMZX1qw+D1qttweKaJZZ
+E1RyySkfusZ9TQExyakCfLntTC4kbMRhutWx0quuF6VMrA1LGgIppqWojQMKYadT
+DQIegOOAav28LKd78egotP8AUj6mrVUQ5dB1FJRQSf/S0mplPamVkWUJRiQimipb
+gYYN68VEDSZaJBSs2OlNB4zULS7OTRYdxSSacAOpqobjJwoNIJjn5lNOwI0D83Q0
+uVVdpNZ3m85UGnrI/XaaLFWLwIo6dKo7pT2FPDzZwoosJo0FbtQ1VkZs89asMcgU
+mhXGmm0tNwScDvQM1LcYhX86s1Eo2gKO3FPFMyHUtJRQB//T0mqOpDUZrIsilXeh
+HfqKog9q0aoyrsfI6GgaYqHsaYyAtzSr60ueaRRGVU9aQIOxqQgmmmLPTNBSY4Lj
+0p2D6ioPJb1PFOWF/wC8aY+ckCZ6mngBeBTVhYHrUoAX60hOQ0rzmnseKQ0hPGKC
+RCeKmtk3Sbz0X+dVuScVJDfQxO0Eg27OrdqaQmzXpwpikMAw5Bp4oJHUUlLQB//U
+0jURqQ1FWRYVFIocbTUlNNMCiPlO09RS5BNRswkd8fwnFR7iODSsO5aB4p2arBhU
+qsOlKw7k1ANN3cUAgcGgZLnim5pu4Gk3CiwXH0xjxURkycClCnrTsTcmQdzWLc/6
+9z6nH6VuqOM1h3IzJNzjkH9BVRCWxY0y6dbgK7Eq/BB9exrqhXKWMaYfJBJPTuPQ
+j6GunQkqC3Xv9aJEolpaZmlzUjP/1b5php1MNZFiVBOxWMlcZ7ZqY1UuwDGc+lNC
+ZnWZwHU9c1Zcd6o2f33rRbpTe41sVyvpSh2FLTT3oAeJDS+YfSoqUUAP3t0FKAzd
+aFqwoFACKoA4qULk4ph61OnSpGhTwMVh3OfMYrxmtlu9Y1x/rDQhsk09gsyhuQeC
+PWukiJBaNjll4P19axlAFohHtWhbH959VH9a0lsZrcv0tJRWRR//2YkCTgQTAQgA
+OBYhBLW13TMhQq1lfo2HrH0n5hC4o9xSBQJgN4HxAhsDBQsJCAcCBhUKCQgLAgQW
+AgMBAh4BAheAAAoJEH0n5hC4o9xSm/AQAM1VQ6ulVGptsosNJYB+HigxIMZiXtIn
+tddo1oJ2HzucgHGF2VJemu5m2NxKrckNQdlO5AvYiD3cGsx1GCcUCqrVJnpVSzn0
+x2vAUd5Iu5AO6VLuWfz6lLzLYFmR7le1eEVV9PWpiFo5JmhCxFlmzbJ/LXg2rm+b
+gGwYg9PaW4zeJR721BE0YYzAdiiyih0oYQqm1rIpp5+rzHL34UeajNU8krcViWDv
+Q6qSKnkk4RQzzWWo5ByjDxB9UBF5eX1Ls/CyFzCpsNaPooKAxC1MQy53GrQIN4Wq
+Y73BMsfEutMvdWxgL5/zWKIS5Fd+hD9wTtOotu00PzamCq+5CnsggILpJenrHWSR
+6xoSgBL6iYjAq1DaLvGUv8/+gnblLkdh3A9+lCqqb45yAbBlZS4w12Jmfov+WKLS
+e464WTG0VTySkShhrwHKWkekXzCusx8nIow1FKo257N97fMLJFWRlBhCHcgtoYmq
+3a49CUndqAmM9MT1/8RX21xUq/vI3aTSXqnjgo+y5EMKNM6Q8u0KgVvxzNRyoyh6
+cJWgW+lXM55X8M1SDBruQBfTF2lnJbxq02hXyuDhWbtuhLCs01z3AxO42PZVuwqI
+/U7chZGzC2e02DPY0TVwbGJU6SEKg6wrZEeAzH7igRWR2AHGUEX0AzEkZD72mvZZ
+TMtW3ci6/EGZiQIzBBMBCAAdFiEEh0AgjA6T/sgO6/kTt3xS7sIXcd0FAmA3iFkA
+CgkQt3xS7sIXcd26xQ//dvjcgYkVg0KdAUQWE0KWFwHgW5mJWyITZs5PcwkArIGh
+pJCns9CRq5EGwGQuLWvhWBAloH+qFI83ydptPoxztZoy7keK1tCdDTNhk2RLhp98
+4CP5oObh6xgpuMQ1hVtwwdhOErGpU/auHgIr4qtScvsfLlhoXHCttzvnBkgiGKi+
+bn7hH+mGS6XCDDTXggh4s3wRZ/snaqy8WcaprfK3dw5Tpgck0pexikVcyI6sbruu
+euGwbUZ5JwuFNnRQzmjPqJk6ZC3tYqPslZFBKi83HXq4T2liTSI2m8eK/gWU512G
+T4HeLlkiSMLYOJeX+gKI25StkzXBqQoVKQJVc6RG8wdv34hSdFT0h1T6xtGKmb6y
+W0c855yFrp2yefSS9+R3DFxBEqo7YtGF1J53v87GBeYnFMRw6vZnon+Kurttn8ah
+gZP0veyb1JtCgBWAS6HVb2q6k/Uw+nt++NAZbr69rtnyzyrMNVvMnuM/L1diYfD0
+bncHd1Oj8KTHmFj7J9He2ytelfB/MFUmnkPWtAugPtnQMTGPYyb4VsOF53QsR0+8
+bJGGI7M3tk/C03sXGfMpYMUyKK7uyGWbKg97iKXQLIoHaI3zbIYNUMh//JspxCqg
+14GbQx8yaLDdyeORn8wmgsVy9qNEsc/PHJO8644KNZJM9x1fDihztBsRbRzwjVaJ
+AjMEEgEIAB0WIQRebW6uFsPadUULIZyagE6X1wecdwUCYED2egAKCRCagE6X1wec
+d5gKD/42YYeUFgaQDABPG45afoqwyP8H1xDi6W5F47wVFwI1GdSBmIqKGhNONwYS
+toeFKcX1ed0gBbFVkwaWEFKCeo0VjWcLYE1YaxL8ybo+iVrFfd/oEHyf414uvnur
+4jNVwqUmtYQMaLlXq2a9tVAGNtsgqsZKEYBanWDXm24z5l2rDGxLlVi7xTGU3pZE
+LfTF3HvZlN3l/ggagXF+2ocT5O0vTX1OO8x0lknTqhbjTJfi+ObJxV4DyE0ifQyh
+3smBUyxZwCcF9MhRKYnEcf8gbsCPDLoiwCGz0o7cKz2blm7NsTgxZXoS0HRx0/wy
+QHS+6mIDuH1gMLeEvJUql7cdKqGAbG5QD9xGPoW6PiTpFTtVusjxD4dIvil5oth5
+r9btVNtoSysEuL13BjBkrO4JuqVZvEdHFGjNE00q6JrI/SpeBZrRR/YZvLCgUZ9J
+w9S6g/usazLEAnOs2EH37yMFA3h3Jr+8l31CkGWBlbLHWc/6ufuyYo4F98TJ7WRV
+vq2RbAMziHMkDH47iMt2PkycLhqPplzbTyVVS/2r1u1d8vfhWlznASuT0/d6cpi0
+H9L/nqe2Mkst32e/X7jr54lSfURmKC+plNn4QQY61DcaUDw5UsMvl/ieDg7OqBeI
+JVgitbDLfIC15B5oRHalAmBOE9OulvvnqxZCHdQ63XunRIbvi7kCDQRgN3jZARAA
+uiPmuwrDqHm5siU9XwHIE5XpzwQv03zgZkwXGKMDcLvPbu5bDpSKvSsRHraaIDcC
+YszGCJJI7usAxrFDeBOo1i4QlVnNcZRpd7Kt4VkZPtKovsfWnDC5qePANEMV6Qjl
+R/wrZ+gmmRwUcC6Nn7TC2IIRfZ7Z5DpFpiCxVI+Q6AdzX4L7mHA6ElWy1XCg1rg+
+SJNSA250DxnavL4jHr+ZjdHmjTIMZCKpAv+7JkEmwrvBCkNuOx0/v6zpBO7QUknU
+3t3lYoKzvBnJbrlU2JgU9OjmjmOfONtcz9e1CVbaBUH8ksmQ/XHOZwBtPcj3Ir54
+sAOUfITzD8gHU07SxAm1vKl1MOEtGPkEFLv63mbvprHRelZoBVaO38Z1s0mnukSE
+veg8vOTSc/JekE9L7AVuEyUcW0b+bpxqkdsjw8PJNETdCQRQZasXRMTXBc9eXnqJ
+KJZWgDduim6zVBoWBf4VZUBcMuhjZ61VAD3mHe/0qF03oEPohqc7iHx9y6dCwM/A
+m4Pa4vVqnBpcjtnjxeyxBiJr9MOMhBR6owWo1tij3v2slzpzYF+7qBLkOTpWDC/X
+ANzOLMhmWlR/cjmatUwVGrgfVGINp3U2nR87+2dcKQwryf2oSYqQ4CckTYeImCcW
+MpZk5/q1rmXDp9wrpRJxXm6C7VQ0uOjab0a/4h9WZAUAEQEAAYkCNgQYAQgAIBYh
+BLW13TMhQq1lfo2HrH0n5hC4o9xSBQJgN3jZAhsMAAoJEH0n5hC4o9xSR3MQAK41
+FXiGu9pBsGkDt6O1VsLXV7oRQtJdv5HzU2tgnSCMlGPt25dEWW1ZstSiJ7+w+oiZ
+nNISl+GQ2AzEbAxQw1wgVQiODNicvNwUOI9IIwaB1/ldpRLupH//OehO1zUvDIjb
+eDeIugc8jk3SanGan8oI5kIKG3bardVk3Vuts1tuaf2xRTrmR42kCaNIdWDYrYr6
+6VLXF+1nZPQr72XHal4gyrfA7EELgLcMCINehrxsPnwRPqru080tefwNIdtlLhl5
+keYuQ5qeBtXfFlYGpv80NGZg91o6LSiKTxBNHiBlACDwsOZ8yse5TaqRAwiheWQc
+WiZySqab+UmP1p6SvErDxO3zdCqv0GyA5CLrXW8LpZCVNzJPuTzV/Y68ozgNKmqx
+Fkw3ExWx/IIYf/dgRbbwhHnlPd2TDyL7btoFluEig6bKvBAkzKSRRMC9G+z+dcZl
+EqtDt2F52QyOm4iYZuXO9vCsVGuwy+2mWyB9EaS3HWctt6l8s3jMmvM+9Q9vrOjF
+J3O04ihMRvb/H47jCjDhID1rLR3umqgT2mLzR25GPrWQG83F9Aop3vIunTpC2hta
+8neA5s805aHc71bUsukzGItlzy2v9woWflZS4tayEPumasRrixQQ/QI4zjVAb7uF
+nWYitJKCbFCTTHqWL0ER7GbxvKXlzBZURQrVQaQ9
+=TFGe
+-----END PGP PUBLIC KEY BLOCK-----
\ No newline at end of file
diff --git a/sp-saml-impl/.checkstyle b/sp-saml-impl/.checkstyle
new file mode 100644
index 0000000..1feabed
--- /dev/null
+++ b/sp-saml-impl/.checkstyle
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false">
+  <local-check-config name="Shibboleth Checkstyle" location="/java-plugin-shibd/resources/checkstyle/checkstyle.xml" type="project" description="">
+    <additional-data name="protect-config-file" value="false"/>
+  </local-check-config>
+  <fileset name="all" enabled="true" check-config-name="Shibboleth Checkstyle" local="true">
+    <file-match-pattern match-pattern="." include-pattern="true"/>
+  </fileset>
+</fileset-config>
diff --git a/sp-saml-impl/.gitignore b/sp-saml-impl/.gitignore
new file mode 100644
index 0000000..1df25bf
--- /dev/null
+++ b/sp-saml-impl/.gitignore
@@ -0,0 +1,2 @@
+/test-output
+/target
diff --git a/sp-saml-impl/pom.xml b/sp-saml-impl/pom.xml
new file mode 100644
index 0000000..2e05e23
--- /dev/null
+++ b/sp-saml-impl/pom.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- See LICENSE.txt file in the root directory of this repository for the copyright/license information. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>net.shibboleth.sp</groupId>
+        <artifactId>sp-saml-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+    
+    <artifactId>sp-saml-impl</artifactId>
+    <description>SP SAML Implememntation.</description>
+    <name>Shibboleth SP :: SAML Protocol Plugin :: Implementation</name>
+    <packaging>jar</packaging>
+    
+    <properties>
+        <checkstyle.configLocation>${project.basedir}/../resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+        <automatic.module.name>net.shibboleth.sp.saml.impl</automatic.module.name>
+    </properties>
+        
+    <dependencies>
+        <!-- Compile dependencies -->
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>sp-saml-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <!-- Provided dependencies -->
+        <dependency>
+            <groupId>${shibd.groupId}</groupId>
+            <artifactId>sp-server-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-profile-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${shib-profile.groupId}</groupId>
+            <artifactId>shib-profile-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-profile.groupId}</groupId>
+            <artifactId>shib-saml-profile-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${shib-attribute.groupId}</groupId>
+            <artifactId>shib-attribute-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${shib-metadata.groupId}</groupId>
+            <artifactId>shib-metadata-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-saml-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        
+        <!-- Test Dependencies -->
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-testing</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2ArtifactConsumerProfileConfiguration.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2ArtifactConsumerProfileConfiguration.java
new file mode 100644
index 0000000..8693b36
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2ArtifactConsumerProfileConfiguration.java
@@ -0,0 +1,101 @@
+/*
+ * 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.saml.saml2.profile.config.impl;
+
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.logic.NoIntegrityMessageChannelPredicate;
+
+import net.shibboleth.saml.profile.config.SAMLArtifactConsumerProfileConfiguration;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+
+/**
+ * Configuration support for artifact-consuming profiles.
+ */
+public abstract class AbstractSAML2ArtifactConsumerProfileConfiguration extends AbstractSAML2ProfileConfiguration
+        implements SAMLArtifactConsumerProfileConfiguration {
+    
+    /** Predicate used to determine if artifact resolution requests should be signed. */
+    @Nonnull private Predicate<MessageContext> signArtifactRequestsPredicate;
+    
+    /** Predicate used to determine if artifact resolution requests should use client TLS. */
+    @Nonnull private Predicate<MessageContext> clientTLSArtifactRequestsPredicate;
+
+    /**
+     * Constructor.
+     * 
+     * @param profileId unique ID for this profile
+     */
+    protected AbstractSAML2ArtifactConsumerProfileConfiguration(@Nonnull @NotEmpty final String profileId) {
+        super(profileId);
+        signArtifactRequestsPredicate = new NoIntegrityMessageChannelPredicate();
+        clientTLSArtifactRequestsPredicate = new NoIntegrityMessageChannelPredicate().negate();
+    }
+
+    /** {@inheritDoc} */
+    public boolean isSignArtifactRequests(@Nullable final MessageContext messageContext) {
+        return signArtifactRequestsPredicate.test(messageContext);
+    }
+
+    /**
+     * Set whether artifact resolution requests should be signed.
+     * 
+     * @param flag flag to set
+     */
+    public void setSignArtifactRequests(final boolean flag) {
+        signArtifactRequestsPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set the predicate used to determine if artifact resolution requests should be signed.
+     * 
+     * @param predicate the predicate
+     */
+    public void setSignArtifactRequestsPredicate(@Nonnull final Predicate<MessageContext> predicate) {
+        signArtifactRequestsPredicate = Constraint.isNotNull(predicate, 
+                "Predicate used to determine artifact request signing may not be null");
+    }
+
+    /** {@inheritDoc} */
+    public boolean isClientTLSArtifactRequests(@Nullable final MessageContext messageContext) {
+        return clientTLSArtifactRequestsPredicate.test(messageContext);
+    }
+
+    /**
+     * Set whether artifact resolution requests should use client TLS.
+     * 
+     * @param flag flag to set
+     */
+    public void setClientTLSArtifactRequests(final boolean flag) {
+        clientTLSArtifactRequestsPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set the predicate used to determine if artifact resolution requests should use client TLS.
+     * 
+     * @param predicate the predicate
+     */
+    public void setClientTLSArtifactRequestsPredicate(@Nonnull final Predicate<MessageContext> predicate) {
+        clientTLSArtifactRequestsPredicate = Constraint.isNotNull(predicate, 
+                "Predicate used to determine artifact client TLS use may not be null");
+    }
+
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2AssertionConsumerProfileConfiguration.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2AssertionConsumerProfileConfiguration.java
new file mode 100644
index 0000000..343a5af
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2AssertionConsumerProfileConfiguration.java
@@ -0,0 +1,158 @@
+/*
+ * 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.saml.saml2.profile.config.impl;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.saml.profile.config.SAMLAssertionConsumingProfileConfiguration;
+import net.shibboleth.shared.annotation.constraint.NonNegative;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.StringSupport;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+/** Base class for SAML profile configurations. */
+public abstract class AbstractSAML2AssertionConsumerProfileConfiguration
+        extends AbstractSAML2ArtifactConsumerProfileConfiguration
+        implements SAMLAssertionConsumingProfileConfiguration {
+    
+    /** Whether to require signed assertions. */
+    @Nonnull private Predicate<ProfileRequestContext> requireSignedAssertionsPredicate;
+    
+    /** Lookup function to supply assertionLifetime property. */
+    @Nonnull private Function<ProfileRequestContext,Duration> assertionLifetimeLookupStrategy;
+
+    /** Lookup function to supply additionalAudiences property. */
+    @Nonnull private Function<ProfileRequestContext,Set<String>> assertionAudiencesLookupStrategy;
+    
+    /**
+     * Constructor.
+     * 
+     * @param profileId ID of the communication profile
+     */
+    public AbstractSAML2AssertionConsumerProfileConfiguration(@Nonnull @NotEmpty final String profileId) {
+        super(profileId);
+        
+        requireSignedAssertionsPredicate = PredicateSupport.alwaysFalse();
+        assertionLifetimeLookupStrategy = FunctionSupport.constant(null);
+        assertionAudiencesLookupStrategy = FunctionSupport.constant(null);
+    }
+
+    /** {@inheritDoc} */
+    public boolean isRequireSignedAssertions(@Nullable final ProfileRequestContext profileRequestContext) {
+        return requireSignedAssertionsPredicate.test(profileRequestContext);
+    }
+    
+    /**
+     * Set whether to require signed assertions.
+     * 
+     * @param flag flag to set
+     */
+    public void setRequireSignedAssertions(final boolean flag) {
+        requireSignedAssertionsPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set condition to determine whether to require signed assertions.
+     * 
+     * @param condition condition to set
+     */
+    public void setRequireSignedAssertionsPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        requireSignedAssertionsPredicate = Constraint.isNotNull(condition, "Condition cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nullable @NonNegative public Duration getAssertionLifetime(@Nullable final ProfileRequestContext profileRequestContext) {
+        final Duration lifetime = assertionLifetimeLookupStrategy.apply(profileRequestContext);
+        if (lifetime != null) {
+            Constraint.isFalse(lifetime.isNegative(), "Assertion lifetime must be non-negative");
+        }
+        return lifetime;
+    }
+
+    /**
+     * Set the maximum lifetime of an assertion.
+     * 
+     * @param lifetime lifetime of an assertion
+     */
+    public void setAssertionLifetime(@Nullable @NonNegative final Duration lifetime) {
+        if (lifetime != null) {
+            Constraint.isFalse(lifetime.isNegative(), "Assertion lifetime must be non-negative");
+        }
+        
+        assertionLifetimeLookupStrategy = FunctionSupport.constant(lifetime);
+    }
+
+    /**
+     * Set a lookup strategy for the maximum lifetime of an assertion.
+     *
+     * @param strategy  lookup strategy
+     */
+    public void setAssertionLifetimeLookupStrategy(@Nonnull final Function<ProfileRequestContext,Duration> strategy) {
+        assertionLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull @NonnullElements @NotLive public Set<String> getAssertionAudiences(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        
+        final Set<String> audiences = assertionAudiencesLookupStrategy.apply(profileRequestContext);
+        if (audiences != null) {
+            return CollectionSupport.copyToSet(audiences);
+        }
+        return CollectionSupport.emptySet();
+    }
+
+    /**
+     * Set the set of audiences, in addition to our entityID, to validate the assertion against.
+     * 
+     * @param audiences the additional audiences
+     */
+    public void setAssertionAudiences(@Nullable @NonnullElements final Collection<String> audiences) {
+
+        if (audiences == null || audiences.isEmpty()) {
+            assertionAudiencesLookupStrategy = FunctionSupport.constant(null);
+        } else {
+            assertionAudiencesLookupStrategy = FunctionSupport.constant(
+                    CollectionSupport.copyToSet(StringSupport.normalizeStringCollection(audiences)));
+        }
+    }
+
+    /**
+     * Set a lookup strategy for the set of audiences, in addition to our entityID, to validate the assertion against.
+     *
+     * @param strategy  lookup strategy
+     * 
+     * @since 4.0.0
+     */
+    public void setAssertionAudiencesLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Set<String>> strategy) {
+        assertionAudiencesLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2ProfileConfiguration.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2ProfileConfiguration.java
new file mode 100644
index 0000000..caa8026
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2ProfileConfiguration.java
@@ -0,0 +1,218 @@
+/*
+ * 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.saml.saml2.profile.config.impl;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.profile.config.AbstractConditionalProfileConfiguration;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.sp.saml.saml2.profile.config.SAML2ProfileConfiguration;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+/** Base class for SAML profile configurations. */
+public abstract class AbstractSAML2ProfileConfiguration extends AbstractConditionalProfileConfiguration implements
+        SAML2ProfileConfiguration {
+    
+    /** Function used to obtain our entityID. */
+    @Nonnull private Function<ProfileRequestContext,String> entityIDLookupStrategy;
+    
+    /** Predicate used to determine if the generated request should be signed. Default returns false. */
+    @Nonnull private Predicate<ProfileRequestContext> signRequestsPredicate;
+
+    /** Predicate used to determine if the generated response should be signed. Default returns false. */
+    @Nonnull private Predicate<ProfileRequestContext> signResponsesPredicate;
+
+    /** Whether encryption is optional in the face of no key, etc. */
+    @Nonnull private Predicate<ProfileRequestContext> encryptionOptionalPredicate;
+    
+    /** Predicate used to determine if name identifiers should be encrypted. */
+    @Nonnull private Predicate<ProfileRequestContext> encryptNameIDsPredicate;
+    
+    /** Lookup function to supply a message handler function. */
+    @Nonnull private Function<MessageContext,Function<MessageContext,Exception>> messageHandlerLookupStrategy;
+
+    
+    /**
+     * Constructor.
+     * 
+     * @param profileId ID of the communication profile
+     */
+    public AbstractSAML2ProfileConfiguration(@Nonnull @NotEmpty final String profileId) {
+        super(profileId);
+
+        entityIDLookupStrategy = FunctionSupport.constant(null);
+        signRequestsPredicate = PredicateSupport.alwaysFalse();
+        signResponsesPredicate = PredicateSupport.alwaysFalse();
+        encryptionOptionalPredicate = PredicateSupport.alwaysFalse();
+        encryptNameIDsPredicate = PredicateSupport.alwaysFalse();
+        
+        messageHandlerLookupStrategy = FunctionSupport.constant(null);
+    }
+    
+    /** {@inheritDoc} */
+    public boolean isIgnoreRequestSignatures(@Nullable ProfileRequestContext profileRequestContext) {
+        throw new UnsupportedOperationException("ignoreRequestSignatures is unsupported");
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable @NotEmpty public String getEntityID(@Nullable final ProfileRequestContext profileRequestContext) {
+        return entityIDLookupStrategy.apply(profileRequestContext);
+    }
+    
+    /**
+     * Set the entityID of this system.
+     * 
+     * @param id entityID
+     */
+    public void setEntityID(@Nullable @NotEmpty final String id) {
+        entityIDLookupStrategy = FunctionSupport.constant(id);
+    }
+    
+    /**
+     * Set the lookup strategy for the entityID of this system
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setEntityIDLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        entityIDLookupStrategy = Constraint.isNotNull(strategy, "EntityID lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    public boolean isSignRequests(@Nullable final ProfileRequestContext profileRequestContext) {
+        return signRequestsPredicate.test(profileRequestContext);
+    }
+
+    /**
+     * Set whether generated requests should be signed.
+     * 
+     * @param flag flag to set
+     */
+    public void setSignRequests(final boolean flag) {
+        signRequestsPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set the predicate used to determine if generated requests should be signed.
+     * 
+     * @param predicate predicate used to determine if generated requests should be signed
+     */
+    public void setSignRequestsPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        signRequestsPredicate = Constraint.isNotNull(predicate, "Condition cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    public boolean isSignResponses(@Nullable final ProfileRequestContext profileRequestContext) {
+        return signResponsesPredicate.test(profileRequestContext);
+    }
+
+    /**
+     * Set whether generated responses should be signed.
+     * 
+     * @param flag flag to set
+     */
+    public void setSignResponses(final boolean flag) {
+        signResponsesPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set the predicate used to determine if generated responses should be signed.
+     * 
+     * @param predicate predicate used to determine if generated responses should be signed
+     */
+    public void setSignResponsesPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        signResponsesPredicate = Constraint.isNotNull(predicate, "Condition cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    public boolean isEncryptionOptional(@Nullable final ProfileRequestContext profileRequestContext) {
+        return encryptionOptionalPredicate.test(profileRequestContext);
+    }
+    
+    /**
+     * Set whether encryption is optional in the face of a missing key, etc.
+     * 
+     * @param flag  flag to set
+     */
+    public void setEncryptionOptional(final boolean flag) {
+        encryptionOptionalPredicate = PredicateSupport.constant(flag);
+    }
+
+    /**
+     * Set a condition to determine whether encryption is optional in the face of a missing key, etc.
+     *
+     * @param condition condition to set
+     */
+    public void setEncryptionOptionalPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        encryptionOptionalPredicate = Constraint.isNotNull(condition, "Encryption optional predicate cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    public boolean isEncryptNameIDs(@Nullable final ProfileRequestContext profileRequestContext) {
+        return encryptNameIDsPredicate.test(profileRequestContext);
+    }
+
+    /**
+     * Set whether name identifiers should be encrypted.
+     * 
+     * @param flag  flag to set
+     */
+    public void setEncryptNameIDs(final boolean flag) {
+        encryptNameIDsPredicate = PredicateSupport.constant(flag);
+    }
+
+    /**
+     * Set the predicate used to determine if name identifiers should be encrypted.
+     * 
+     * @param predicate predicate used to determine if name identifiers should be encrypted
+     */
+    public void setEncryptNameIDsPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+        encryptNameIDsPredicate = Constraint.isNotNull(predicate, "Condition cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nullable
+    public Function<MessageContext,Exception> getMessageHandler(@Nullable final MessageContext messageContext) {
+        return messageHandlerLookupStrategy.apply(messageContext);
+    }
+    
+    /**
+     * Set a message handler for a SAML message produced or consumed by this profile.
+     * 
+     * @param handler message handler
+     */
+    public void setMessageHandler(@Nullable final Function<MessageContext,Exception> handler) {
+        messageHandlerLookupStrategy = FunctionSupport.constant(handler);
+    }
+    
+    /**
+     * Set a lookup strategy for a message handler for a SAML message produced or consumed by this profile.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setMessageHandlerLookupStrategy(
+            @Nonnull final Function<MessageContext,Function<MessageContext,Exception>> strategy) {
+        messageHandlerLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java
new file mode 100644
index 0000000..31fed31
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfiguration.java
@@ -0,0 +1,415 @@
+/*
+ * 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.saml.saml2.profile.config.impl;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.saml.profile.config.SAMLArtifactConsumerProfileConfiguration;
+import net.shibboleth.shared.annotation.constraint.NonNegative;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
+import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.opensaml.saml.saml2.core.SubjectLocality;
+import org.opensaml.saml.saml2.metadata.RequestedAttribute;
+
+/** Configuration support for SP SAML 2.0 Browser SSO. */
+public class BrowserSSOProfileConfiguration extends AbstractSAML2AssertionConsumerProfileConfiguration
+        implements SAMLArtifactConsumerProfileConfiguration, net.shibboleth.sp.saml.saml2.profile.config.BrowserSSOProfileConfiguration {
+
+    /** Whether to mandate forced authentication for the request. */
+    @Nonnull private Predicate<ProfileRequestContext> forceAuthnPredicate;
+
+    /** Whether to compare client and assertion addresses on inbound SSO. */
+    @Nonnull private Predicate<ProfileRequestContext> checkAddressPredicate;
+    
+    /** Lookup function to supply maximum time since inbound AuthnInstant. */
+    @Nonnull private Function<ProfileRequestContext,Duration> maximumTimeSinceAuthnLookupStrategy;
+
+    /** Lookup function to supply ProxyCount. */
+    @Nonnull private Function<ProfileRequestContext,Integer> proxyCountLookupStrategy;
+    
+    /** Lookup function for requested AC operator. */
+    @Nonnull private Function<ProfileRequestContext,String> authnContextComparisonLookupStrategy;
+    
+    /** Lookup function to supply default authentication methods. */
+    @Nonnull private Function<ProfileRequestContext,Collection<String>> authnContextClassRefLookupStrategy;
+    
+    /** Lookup function to supply NameID format. */
+    @Nonnull private Function<ProfileRequestContext,String> nameIDFormatLookupStrategy;
+
+    /** Lookup function to supply SPNameQualifier. */
+    @Nonnull private Function<ProfileRequestContext,String> nameQualifierLookupStrategy;
+
+    /** Lookup function to supply AttributeConsumingServiceIndex. */
+    @Nonnull private Function<ProfileRequestContext,Integer> attributeIndexLookupStrategy;
+
+    /** Lookup function to supply RequestedAttributes. */
+    @Nonnull private Function<ProfileRequestContext,Collection<RequestedAttribute>> requestedAttributesLookupStrategy;
+    
+    /** Lookup function for response binding. */
+    @Nonnull private Function<ProfileRequestContext,String> responseBindingLookupStrategy;
+    
+    /** Constructor. */
+    public BrowserSSOProfileConfiguration() {
+        this(PROFILE_ID);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param profileId unique ID for this profile
+     */
+    protected BrowserSSOProfileConfiguration(@Nonnull @NotEmpty final String profileId) {
+        super(profileId);
+        setEncryptNameIDs(true);
+        forceAuthnPredicate = PredicateSupport.alwaysFalse();
+        checkAddressPredicate = PredicateSupport.alwaysTrue();
+        maximumTimeSinceAuthnLookupStrategy = FunctionSupport.constant(null);
+        proxyCountLookupStrategy = FunctionSupport.constant(null);
+        authnContextComparisonLookupStrategy = FunctionSupport.constant(null);
+        authnContextClassRefLookupStrategy = FunctionSupport.constant(null);
+        nameIDFormatLookupStrategy = FunctionSupport.constant(null);
+        nameQualifierLookupStrategy = FunctionSupport.constant(null);
+        attributeIndexLookupStrategy = FunctionSupport.constant(null);
+        requestedAttributesLookupStrategy = FunctionSupport.constant(null);
+        responseBindingLookupStrategy = FunctionSupport.constant(SAMLConstants.SAML2_POST_BINDING_URI);
+    }
+
+    /** {@inheritDoc} */
+    public boolean isForceAuthn(@Nullable final ProfileRequestContext profileRequestContext) {
+        return forceAuthnPredicate.test(profileRequestContext);
+    }
+    
+    /**
+     * Set whether a fresh user presence proof should be required for this request.
+     * 
+     * @param flag flag to set
+     */
+    public void setForceAuthn(final boolean flag) {
+        forceAuthnPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set a condition to determine whether a fresh user presence proof should be required for this request.
+     * 
+     * @param condition condition to set
+     */
+    public void setForceAuthnPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        forceAuthnPredicate = Constraint.isNotNull(condition, "Forced authentication predicate cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    public boolean isCheckAddress(@Nullable final ProfileRequestContext profileRequestContext) {
+        return checkAddressPredicate.test(profileRequestContext);
+    }
+    
+    /**
+     * Set whether the client's address must match the address in an inbound {@link SubjectLocality}
+     * element during inbound SSO.
+     * 
+     * @param flag flag to set
+     */
+    public void setCheckAddress(final boolean flag) {
+        checkAddressPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set a condition to determine whether the client's address must match the address in an inbound
+     * {@link SubjectLocality} element during inbound SSO.
+     * 
+     * @param condition condition to set
+     */
+    public void setCheckAddressPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        checkAddressPredicate = Constraint.isNotNull(condition, "Address checking predicate cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable public Duration getMaximumTimeSinceAuthn(@Nullable final ProfileRequestContext profileRequestContext) {
+        final Duration amount = maximumTimeSinceAuthnLookupStrategy.apply(profileRequestContext);
+        Constraint.isFalse(amount != null && amount.isNegative(),
+                "Maximum time since authentication must be greater than or equal to 0");
+        return amount;
+    }
+
+    /**
+     * Set the maximum amount of time allowed to have elapsed since an incoming AuthnInstant.
+     * 
+     * <p>A null or 0 is interpreted as an unlimited amount.</p>
+     * 
+     * @param amount max time to allow
+     */
+    public void setMaximumTimeSinceAuthn(@Nullable final Duration amount) {
+        Constraint.isFalse(amount != null && amount.isNegative(),
+                "Maximum time since authentication must be greater than or equal to 0");
+        
+        maximumTimeSinceAuthnLookupStrategy = FunctionSupport.constant(amount);
+    }
+    
+    /**
+     * Set a lookup strategy for the maximum amount of time allowed to have elapsed since an incoming AuthnInstant.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setMaximumTimeSinceAuthnLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Duration> strategy) {
+        maximumTimeSinceAuthnLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @NonNegative @Nullable public Integer getProxyCount(@Nullable final ProfileRequestContext profileRequestContext) {
+        final Integer count = proxyCountLookupStrategy.apply(profileRequestContext);
+        if (count != null) {
+            Constraint.isGreaterThanOrEqual(0, count, "Proxy count must be greater than or equal to 0");
+        }
+        return count;
+    }
+    
+    /**
+     * Set the maximum number of times an assertion may be proxied to signal in the SAML request.
+     * 
+     * @param count maximum number of times an assertion may be proxied
+     */
+    public void setProxyCount(@Nullable @NonNegative final Integer count) {
+        if (count != null) {
+            Constraint.isGreaterThanOrEqual(0, count, "Proxy count must be greater than or equal to 0");
+        }
+        proxyCountLookupStrategy = FunctionSupport.constant(count);
+    }
+
+    /**
+     * Set a lookup strategy for the maximum number of times an assertion may be proxied to signal
+     * in the SAML request.
+     *
+     * @param strategy  lookup strategy
+     */
+    public void setProxyCountLookupStrategy(@Nonnull final Function<ProfileRequestContext,Integer> strategy) {
+        proxyCountLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable public AuthnContextComparisonTypeEnumeration getAuthnContextComparison(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        
+        final String comparison = authnContextComparisonLookupStrategy.apply(profileRequestContext);
+        if (comparison != null) {
+            return AuthnContextComparisonTypeEnumeration.valueOf(comparison.toUpperCase());
+        }
+        
+        return null;
+    }
+    
+    /**
+     * Set the comparison operator to use when issuing SAML requests containing requested context classes.
+     * 
+     * @param comparison comparison value or null
+     * 
+     * @since 4.0.0
+     */
+    public void setAuthnContextComparison(@Nullable final AuthnContextComparisonTypeEnumeration comparison) {
+        authnContextComparisonLookupStrategy =
+                FunctionSupport.constant(comparison != null ? comparison.toString() : null);
+    }
+
+    /**
+     * Set a lookup strategy for the comparison operator to use when issuing SAML requests containing
+     * requested context classes.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAuthnContextComparisonLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,String> strategy) {
+        authnContextComparisonLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull @NonnullElements @NotLive @Unmodifiable public List<String> getAuthnContextClassRefs(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        final Collection<String> refs = authnContextClassRefLookupStrategy.apply(profileRequestContext);
+        if (refs != null) {
+            return CollectionSupport.copyToList(refs);
+        }
+        return CollectionSupport.emptyList();
+    }
+        
+    /**
+     * Set the {@link AuthnContextClassRef} values to include in SAML request.
+     * 
+     * @param contexts default authentication context class references to use
+     */
+    public void setAuthnContextClassRefs(@Nullable @NonnullElements final Collection<String> contexts) {
+        if (contexts != null) {
+            authnContextClassRefLookupStrategy = FunctionSupport.constant(CollectionSupport.copyToList(contexts));
+        } else {
+            authnContextClassRefLookupStrategy = FunctionSupport.constant(null);
+        }
+    }
+
+    /**
+     * Set a lookup strategy for the {@link AuthnContextClassRef} values to include in SAML request.
+     *
+     * @param strategy  lookup strategy
+     */
+    public void setAuthnContextClassRefsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Collection<String>> strategy) {
+        authnContextClassRefLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public String getNameIDFormat(@Nullable final ProfileRequestContext profileRequestContext) {
+        return nameIDFormatLookupStrategy.apply(profileRequestContext);
+    }
+
+    /**
+     * Set the name identifier format to require via the SAML request.
+     * 
+     * @param format required format
+     */
+    public void setNameIDFormat(@Nullable final String format) {
+        nameIDFormatLookupStrategy = FunctionSupport.constant(format);
+    }
+
+    /**
+     * Set a lookup strategy for the name identifier format to require via the SAML request.
+     *
+     * @param strategy  lookup strategy
+     */
+    public void setNameIDFormatLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,String> strategy) {
+        nameIDFormatLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public String getSPNameQualifier(@Nullable final ProfileRequestContext profileRequestContext) {
+        return nameQualifierLookupStrategy.apply(profileRequestContext);
+    }
+
+    /**
+     * Set the SPNameQualifier to include in the SAML request.
+     * 
+     * @param qualifier requested SPNameQualifier
+     */
+    public void setSPNameQualifier(@Nullable final String qualifier) {
+        nameQualifierLookupStrategy = FunctionSupport.constant(qualifier);
+    }
+
+    /**
+     * Set a lookup strategy for the SPNameQualifier to include in the SAML request.
+     *
+     * @param strategy  lookup strategy
+     */
+    public void setSPNameQualifierLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,String> strategy) {
+        nameQualifierLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable public Integer getAttributeIndex(@Nullable final ProfileRequestContext profileRequestContext) {
+        return attributeIndexLookupStrategy.apply(profileRequestContext);
+    }
+
+    /**
+     * Set the AttributeConsumingServiceIndex to include in the SAML request.
+     * 
+     * @param index the AttributeConsumingServiceIndex
+     */
+    public void setAttributeIndex(@Nullable final Integer index) {
+        attributeIndexLookupStrategy = FunctionSupport.constant(index);
+    }
+
+    /**
+     * Set a lookup strategy for the AttributeConsumingServiceIndex to include in the SAML request.
+     *
+     * @param strategy  lookup strategy
+     */
+    public void setAttributeIndexLookupStrategy(@Nonnull final Function<ProfileRequestContext,Integer> strategy) {
+        attributeIndexLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull @NonnullElements @Unmodifiable @NotLive public Collection<RequestedAttribute> getRequestedAttributes(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        final Collection<RequestedAttribute> attrs = requestedAttributesLookupStrategy.apply(profileRequestContext);
+        if (attrs != null) {
+            return CollectionSupport.copyToList(attrs);
+        } else {
+            return CollectionSupport.emptyList();
+        }
+    }
+    
+    /**
+     * Set the list of {@link RequestedAttribute} objects to include in the SAML request (via extension).
+     * 
+     * @param attrs requested attributes
+     */
+    public void setRequestedAttributes(@Nullable @NonnullElements final Collection<RequestedAttribute> attrs) {
+        requestedAttributesLookupStrategy = FunctionSupport.constant(attrs);
+    }
+    
+    /**
+     * Set a lookup strategy for the list of {@link RequestedAttribute} objects to include in the SAML request
+     * (via extension).
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRequestedAttributesLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Collection<RequestedAttribute>> strategy) {
+        requestedAttributesLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public String getResponseBinding(@Nullable ProfileRequestContext profileRequestContext) {
+        return responseBindingLookupStrategy.apply(profileRequestContext);
+    }
+    
+    /**
+     * Set the response binding to place into the {@link AuthnRequest}.
+     * 
+     * <p>Defaults tp {@link SAMLConstants#SAML2_POST_BINDING_URI}.</p>
+     * 
+     * @param binding response binding
+     */
+    public void setResponseBinding(@Nullable final String binding) {
+        responseBindingLookupStrategy = FunctionSupport.constant(binding);
+    }
+    
+    /**
+     * Set a lookup strategy for 
+     * 
+     * @param strategy
+     */
+    public void setResponseBindingLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        responseBindingLookupStrategy =
+                Constraint.isNotNull(strategy, "Response binding lookup strategy cannot be null");
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/ECPProfileConfiguration.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/ECPProfileConfiguration.java
new file mode 100644
index 0000000..c9f3022
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/ECPProfileConfiguration.java
@@ -0,0 +1,26 @@
+/*
+ * 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.saml.saml2.profile.config.impl;
+
+/** Configuration support for SP SAML 2.0 ECP. */
+public class ECPProfileConfiguration extends BrowserSSOProfileConfiguration
+        implements net.shibboleth.sp.saml.saml2.profile.config.ECPProfileConfiguration {
+
+    /** Constructor. */
+    public ECPProfileConfiguration() {
+        super(net.shibboleth.saml.saml2.profile.config.ECPProfileConfiguration.PROFILE_ID);        
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/SingleLogoutProfileConfiguration.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/SingleLogoutProfileConfiguration.java
new file mode 100644
index 0000000..4df4d22
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/SingleLogoutProfileConfiguration.java
@@ -0,0 +1,194 @@
+/*
+ * 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.saml.saml2.profile.config.impl;
+
+import java.util.Collection;
+import java.util.List;
+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.profile.context.ProfileRequestContext;
+import org.opensaml.profile.logic.NoConfidentialityMessageChannelPredicate;
+import org.opensaml.profile.logic.NoIntegrityMessageChannelPredicate;
+import org.opensaml.saml.ext.saml2aslo.Asynchronous;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDType;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/** Configuration support for SP SAML 2.0 Single Logout. */
+public class SingleLogoutProfileConfiguration extends AbstractSAML2ArtifactConsumerProfileConfiguration
+        implements net.shibboleth.sp.saml.saml2.profile.config.SingleLogoutProfileConfiguration {
+
+    /** Predicate used to determine if SOAP-based requests should be signed. */
+    @Nonnull private Predicate<MessageContext> signSOAPRequestsPredicate;
+    
+    /** Predicate used to determine if SOAP-based requests should use client TLS. */
+    @Nonnull private Predicate<MessageContext> clientTLSSOAPRequestsPredicate;
+    
+    /** Predicate used to determine whether requests should carry the {@link Asynchronous} extension. */
+    @Nonnull private Predicate<ProfileRequestContext> asynchronousPredicate;
+    
+    /** Lookup function to supply qualifiedNameIDFormats property. */
+    @Nonnull private Function<ProfileRequestContext,Collection<String>> qualifiedNameIDFormatsLookupStrategy;
+    
+    /** Constructor. */
+    public SingleLogoutProfileConfiguration() {
+        this(PROFILE_ID);
+    }
+    
+    /**
+     * Constructor.
+     * 
+     * @param profileId unique ID for this profile
+     */
+    protected SingleLogoutProfileConfiguration(@Nonnull @NotEmpty final String profileId) {
+        super(profileId);
+        setSignRequestsPredicate(new NoIntegrityMessageChannelPredicate());
+        setSignResponsesPredicate(new NoIntegrityMessageChannelPredicate());
+        setEncryptNameIDsPredicate(new NoConfidentialityMessageChannelPredicate());
+
+        signSOAPRequestsPredicate = new org.opensaml.messaging.logic.NoIntegrityMessageChannelPredicate();
+        clientTLSSOAPRequestsPredicate = new org.opensaml.messaging.logic.NoIntegrityMessageChannelPredicate().negate();
+        
+        asynchronousPredicate = PredicateSupport.alwaysTrue();
+        
+        qualifiedNameIDFormatsLookupStrategy = FunctionSupport.constant(null);
+    }
+
+    /** {@inheritDoc} */
+    public boolean isSignSOAPRequests(@Nullable final MessageContext messageContext) {
+        return signSOAPRequestsPredicate.test(messageContext);
+    }
+
+    /**
+     * Set whether SOAP-based requests should be signed.
+     * 
+     * @param flag flag to set
+     */
+    public void setSignSOAPRequests(final boolean flag) {
+        signSOAPRequestsPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set the predicate used to determine if SOAP-based requests should be signed.
+     * 
+     * @param predicate the predicate
+     */
+    public void setSignSOAPRequestsPredicate(@Nonnull final Predicate<MessageContext> predicate) {
+        signSOAPRequestsPredicate = Constraint.isNotNull(predicate, 
+                "Predicate used to determine SOAP-based signing cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    public boolean isClientTLSSOAPRequests(@Nullable final MessageContext messageContext) {
+        return clientTLSSOAPRequestsPredicate.test(messageContext);
+    }
+    
+    /**
+     * Set whether SOAP-based requests should use client TLS.
+     * 
+     * @param flag flag to set
+     */
+    public void setClientTLSSOAPRequests(final boolean flag) {
+        clientTLSSOAPRequestsPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set the predicate used to determine if SOAP-based requests should use client TLS.
+     * 
+     * @param predicate the predicate
+     */
+    public void setClientTLSSOAPRequestsPredicate(@Nonnull final Predicate<MessageContext> predicate) {
+        clientTLSSOAPRequestsPredicate = Constraint.isNotNull(predicate, 
+                "Predicate used to determine SOAP-based client TLS use cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    public boolean isAsynchronous(@Nullable final ProfileRequestContext profileRequestContext) {
+        return asynchronousPredicate.test(profileRequestContext);
+    }
+    
+    /**
+     * Set whether to include the {@link Asynchronous} extension in requests.
+     * 
+     * @param flag flag to set
+     */
+    public void setAsynchronous(final boolean flag) {
+        asynchronousPredicate = PredicateSupport.constant(flag);
+    }
+    
+    /**
+     * Set a condition to determine whether to include the {@link Asynchronous} extension in requests.
+     * 
+     * @param condition condition set set
+     */
+    public void setAsynchronousPredicate(@Nonnull final Predicate<ProfileRequestContext> condition) {
+        asynchronousPredicate = Constraint.isNotNull(condition, "Condition cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull @NonnullElements @NotLive public Collection<String> getQualifiedNameIDFormats(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        final Collection<String> formats = qualifiedNameIDFormatsLookupStrategy.apply(profileRequestContext);
+        if (formats != null) {
+            return CollectionSupport.copyToList(formats);
+        }
+        return CollectionSupport.emptyList();
+    }
+
+    /**
+     * Set a collection of {@link NameID} Format values for which the use of the NameQualifier and
+     * SPNameQualifier attributes is defined to allow default/implicit values derived from the
+     * asserting and relying parties.
+     * 
+     * <p>In the core standard, only the {@link NameIDType#PERSISTENT} and {@link NameIDType#TRANSIENT}
+     * Formats are defined in this manner. This setting identifies <strong>additional</strong> Formats
+     * that should be handled in this way.</p>
+     * 
+     * @param formats additional Formats for which defaulting of qualifiers is permissable
+     */
+    public void setQualifiedNameIDFormats(@Nullable @NonnullElements final Collection<String> formats) {
+        if (formats == null || formats.isEmpty()) {
+            qualifiedNameIDFormatsLookupStrategy = FunctionSupport.constant(null);
+        } else {
+            qualifiedNameIDFormatsLookupStrategy =
+                    FunctionSupport.constant(List.copyOf(StringSupport.normalizeStringCollection(formats)));
+        }
+    }
+
+    /**
+     * Set a lookup strategy for the Format values for which the use of the NameQualifier and SPNameQualifier
+     * attributes is defined to allow default/implicit values derived from the asserting and relying parties.
+     *
+     * @param strategy  lookup strategy
+     */
+    public void setQualifiedNameIDFormatsLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,Collection<String>> strategy) {
+        qualifiedNameIDFormatsLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/package-info.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/package-info.java
new file mode 100644
index 0000000..5b08f92
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/impl/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * SAML 2.0 profile configuration classes.
+ */
+package net.shibboleth.sp.saml.saml2.profile.config.impl;
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
new file mode 100644
index 0000000..ef5ad74
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
@@ -0,0 +1,569 @@
+/*
+ * 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.saml.saml2.profile.impl;
+
+import java.time.Instant;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.config.navigate.IdentifierGenerationStrategyLookupFunction;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.InitiatorConstants;
+import net.shibboleth.sp.saml.saml2.profile.config.BrowserSSOProfileConfiguration;
+
+import org.opensaml.core.xml.XMLObjectBuilderFactory;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+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.saml.common.SAMLObjectBuilder;
+import org.opensaml.saml.common.SAMLVersion;
+import org.opensaml.saml.ext.reqattr.RequestedAttributes;
+import org.opensaml.saml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
+import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.opensaml.saml.saml2.core.Extensions;
+import org.opensaml.saml.saml2.core.IDPEntry;
+import org.opensaml.saml.saml2.core.IDPList;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDPolicy;
+import org.opensaml.saml.saml2.core.RequestedAuthnContext;
+import org.opensaml.saml.saml2.core.Scoping;
+import org.opensaml.saml.saml2.core.Subject;
+import org.opensaml.saml.saml2.metadata.NameIDFormat;
+import org.opensaml.saml.saml2.metadata.RequestedAttribute;
+import org.slf4j.Logger;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action that creates an {@link AuthnRequest} and sets it as the message returned by
+ * {@link ProfileRequestContext#getOutboundMessageContext()}.
+ * 
+ * <p>If an issuer value is returned via a lookup strategy, then it's set as the Issuer of the message.</p>
+ * 
+ * <p>Various other values are derived from the active configuration such as {@link RequestedAuthnContext},
+ * {@link NameIDPolicy}, and {@link Scoping}. This process is much more complex than the IdP, as it requires
+ * marrying together inputs from the agent against the profile configuration, including enforcing limits
+ * on what the agent can override/supply.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * 
+ * @post ProfileRequestContext.getOutboundMessageContext().getMessage() != null
+ */
+public class AddAuthnRequest extends AbstractApplicationAction {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(AddAuthnRequest.class);
+    
+    /** Overwrite an existing message? */
+    private boolean overwriteExisting;
+    
+    /** Controls whether to include {@link Scoping} element. */
+    private boolean includeScoping;
+
+    /** Strategy used to locate {@link RelyingPartyContext} if required. */
+    @Nullable private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+
+    /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
+    @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+    
+    /** Strategy used to obtain the request issuer value. */
+    @Nullable private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+    /** Optional strategy to populate request with a {@link NameID}. */
+    @Nullable private Function<ProfileRequestContext,NameID> nameIDLookupStrategy;
+    
+    /** The generator to use. */
+    @NonnullBeforeExec private IdentifierGenerationStrategy idGenerator;
+    
+    /** Applicable profile configuration. */
+    @NonnullBeforeExec private BrowserSSOProfileConfiguration profileConfiguration;
+
+    /** Input message from agent. */
+    @NonnullBeforeExec private DDF input;
+    
+    /** EntityID to populate into Issuer element. */
+    @Nullable private String issuerId;
+    
+    /** Constructor. */
+    public AddAuthnRequest() {
+        // Default strategy is a 16-byte secure random source.
+        idGeneratorLookupStrategy = new IdentifierGenerationStrategyLookupFunction();
+        
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+    }
+        
+    /**
+     * Set whether to overwrite an existing message.
+     * 
+     * @param flag flag to set
+     */
+    public void setOverwriteExisting(final boolean flag) {
+        checkSetterPreconditions();
+        overwriteExisting = flag;
+    }
+    
+    /**
+     * Set whether to include {@link Scoping} in request carrying a known entityID
+     * in an {@link IDPList}.
+     * 
+     * <p>Defaults to false.</p>
+     * 
+     * @param flag flag to set
+     */
+    public void setIncludeScoping(final boolean flag) {
+        checkSetterPreconditions();
+        includeScoping = true;
+    }
+    
+    /**
+     * Set lookup strategy to locate {@link RelyingPartyContext}, only relevant when
+     * also including {@link Scoping} element.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nullable final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+        checkSetterPreconditions();
+        relyingPartyContextLookupStrategy = strategy;
+    }
+
+    /**
+     * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIdentifierGeneratorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
+        checkSetterPreconditions();
+        idGeneratorLookupStrategy =
+                Constraint.isNotNull(strategy, "IdentifierGenerationStrategy lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the issuer value to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nullable final Function<ProfileRequestContext,String> strategy) {
+        checkSetterPreconditions();
+        issuerLookupStrategy = strategy;
+    }
+    
+    /**
+     * Set optional strategy to derive a {@link NameID} to populate into the {@link AuthnRequest}'s
+     * {@link Subject} element.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setNameIDLookupStrategy(@Nullable final Function<ProfileRequestContext,NameID> strategy) {
+        checkSetterPreconditions();
+        nameIDLookupStrategy = strategy;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        input = ensureAgentRequestContext().getInput();
+        if (input == null) {
+            log.error("{} No input message", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+        
+        final RelyingPartyContext rpCtx = profileRequestContext.getSubcontext(RelyingPartyContext.class);
+        if (rpCtx != null && rpCtx.getProfileConfig() instanceof BrowserSSOProfileConfiguration sso) {
+            profileConfiguration = sso;
+        }
+        if (profileConfiguration == null) {
+            log.error("{} BrowserSSOProfileConfiguration not found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        
+        final MessageContext outboundMessageCtx = profileRequestContext.getOutboundMessageContext();
+        if (outboundMessageCtx == null) {
+            log.debug("{} No outbound message context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        } else if (!overwriteExisting && outboundMessageCtx.getMessage() != null) {
+            log.debug("{} Outbound message context already contains a message", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+            return false;
+        }
+
+        idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+        if (idGenerator == null) {
+            log.debug("{} No identifier generation strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        if (issuerLookupStrategy != null) {
+            issuerId = issuerLookupStrategy.apply(profileRequestContext);
+        }
+
+        outboundMessageCtx.setMessage(null);
+        
+        return true;
+    }
+
+// Checkstyle: MethodLength OFF
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+        final SAMLObjectBuilder<AuthnRequest> requestBuilder =
+                (SAMLObjectBuilder<AuthnRequest>) bf.<AuthnRequest>ensureBuilder(
+                        AuthnRequest.DEFAULT_ELEMENT_NAME);
+        final SAMLObjectBuilder<NameIDPolicy> nipBuilder =
+                (SAMLObjectBuilder<NameIDPolicy>) bf.<NameIDPolicy>ensureBuilder(
+                        NameIDPolicy.DEFAULT_ELEMENT_NAME);
+
+        final AuthnRequest object = requestBuilder.buildObject();
+        object.setID(idGenerator.generateIdentifier());
+        object.setIssueInstant(Instant.now());
+        object.setVersion(SAMLVersion.VERSION_20);
+        
+        // Response URL always comes from agent.
+        object.setAssertionConsumerServiceURL(input.getmember(InitiatorConstants.RESPONSE_URL).string());
+        // Protocol binding always comes from config, may be null.
+        object.setProtocolBinding(profileConfiguration.getResponseBinding(profileRequestContext));
+        
+        if (issuerId != null) {
+            log.debug("{} Setting Issuer to {}", getLogPrefix(), issuerId);
+            final SAMLObjectBuilder<Issuer> issuerBuilder =
+                    (SAMLObjectBuilder<Issuer>) bf.<Issuer>ensureBuilder(Issuer.DEFAULT_ELEMENT_NAME);
+            final Issuer issuer = issuerBuilder.buildObject();
+            issuer.setValue(issuerId);
+            object.setIssuer(issuer);
+        } else {
+            log.debug("{} No issuer value available, leaving Issuer unset", getLogPrefix());
+        }
+
+        Integer index = input.getmember(AuthnRequest.ATTRIBUTE_CONSUMING_SERVICE_INDEX_ATTRIB_NAME).integer();
+        if (index == null) {
+            index = profileConfiguration.getAttributeIndex(profileRequestContext);
+        }
+        if (index != null) {
+            log.debug("{} Setting AttributeConsumingServiceIndex to '{}'", getLogPrefix(), index);
+            object.setAttributeConsumingServiceIndex(index);
+        }
+        
+        setForceAuthn(profileRequestContext, object);
+        
+        final Integer passive = input.getmember(AuthnRequest.IS_PASSIVE_ATTRIB_NAME).integer();
+        if (passive != null && passive == 1) {
+            log.debug("{} Setting IsPassive", getLogPrefix());
+            object.setIsPassive(true);
+        }
+
+        object.setNameIDPolicy(buildNameIDPolicy(profileRequestContext, nipBuilder));
+        object.setRequestedAuthnContext(buildRequestedAuthnContext(profileRequestContext));
+        object.setSubject(buildSubject(profileRequestContext));
+        object.setExtensions(buildExtensions(profileRequestContext));
+
+        if (includeScoping) {
+            object.setScoping(buildScoping(profileRequestContext));
+        }
+        
+        profileRequestContext.ensureOutboundMessageContext().setMessage(object);
+        
+        log.info("{} Generated AuthnRequest with ID {} from {}", getLogPrefix(), object.getID(), issuerId);
+    }
+    
+    /**
+     * Set ForceAuthn appropriately.
+     * 
+     * @param profileRequestContext profile request context
+     * @param authnRequest request object
+     */
+    private void setForceAuthn(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthnRequest authnRequest) {
+        
+        final Integer forceAuthn = input.getmember(AuthnRequest.FORCE_AUTHN_ATTRIB_NAME).integer();
+        if (forceAuthn != null) {
+            if (profileConfiguration.isFeatureDisallowed(profileRequestContext,
+                    BrowserSSOProfileConfiguration.FEATURE_FORCEAUTHN)) {
+                log.warn("{} Agent disallowed from overriding ForceAuthn", getLogPrefix());
+            } else if (forceAuthn == 1) {
+                log.debug("{} Setting ForceAuthn", getLogPrefix());
+                authnRequest.setForceAuthn(true);
+                return;
+            }
+        }
+        
+        if (profileConfiguration.isForceAuthn(profileRequestContext)) {
+            log.debug("{} Setting ForceAuthn", getLogPrefix());
+            authnRequest.setForceAuthn(true);
+        }
+    }
+    
+    /**
+     * Build {@link NameIDPolicy} object for request.
+     * 
+     * @param profileRequestContext profile request context
+     * @param builder object builder
+     * 
+     * @return policy object
+     */
+    @Nonnull NameIDPolicy buildNameIDPolicy(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final SAMLObjectBuilder<NameIDPolicy> builder) {
+        
+        final NameIDPolicy nip = builder.buildObject();
+        nip.setAllowCreate(true);
+
+        String format = input.getmember(NameIDFormat.DEFAULT_ELEMENT_LOCAL_NAME).string();
+        if (format != null && profileConfiguration.isFeatureDisallowed(profileRequestContext,
+                BrowserSSOProfileConfiguration.FEATURE_NAMEIDFORMAT)) {
+            log.warn("{} Agent disallowed from overriding NameIDPolicy Format", getLogPrefix());
+            format = null;
+        }
+
+        if (format == null) {
+            format = profileConfiguration.getNameIDFormat(profileRequestContext);
+        }
+        
+        if (format != null) {
+            log.debug("{} Setting NameIDPolicy Format to '{}'", getLogPrefix(), format);
+            nip.setFormat(format);
+        }
+        
+        String qualifier = input.getmember(NameIDPolicy.SP_NAME_QUALIFIER_ATTRIB_NAME).string();
+        if (qualifier != null) {
+            if (profileConfiguration.isFeatureDisallowed(profileRequestContext,
+                    BrowserSSOProfileConfiguration.FEATURE_SPNAMEQUALIFIER)) {
+                log.warn("{} Agent disallowed from overriding NameIDPolicy SPNameQualifier", getLogPrefix());
+            }
+            qualifier = null;
+        }
+        
+        if (qualifier == null) {
+            qualifier = profileConfiguration.getSPNameQualifier(profileRequestContext);
+        }
+        
+        if (qualifier != null) {
+            log.debug("{} Setting NameIDPolicy SPNameQualifier to '{}'", getLogPrefix(), qualifier);
+            nip.setSPNameQualifier(qualifier);
+        }
+        
+        return nip;
+    }
+    
+    /**
+     * Build a {@link RequestedAuthnContext} if warranted.
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return the object to include in the request, or null
+     */
+    @Nullable private RequestedAuthnContext buildRequestedAuthnContext(
+            @Nullable final ProfileRequestContext profileRequestContext) {
+        
+        List<String> classrefs = input.getmember(AuthnContextClassRef.DEFAULT_ELEMENT_LOCAL_NAME).asList()
+                .stream()
+                .map(DDF::string)
+                .collect(CollectionSupport.nonnullCollector(Collectors.toUnmodifiableList())).get();
+        if (!classrefs.isEmpty() && profileConfiguration.isFeatureDisallowed(profileRequestContext,
+                BrowserSSOProfileConfiguration.FEATURE_AUTHNCONTEXT)) {
+            log.warn("{} Agent disallowed from overriding RequestedAuthnContext", getLogPrefix());
+            classrefs = CollectionSupport.emptyList();
+        }
+        
+        if (classrefs.isEmpty()) {
+            profileConfiguration.getAuthnContextClassRefs(profileRequestContext);
+        }
+        
+        if (classrefs.isEmpty()) {
+            return null;
+        }
+
+        log.debug("{} Setting requested AuthnContextClassRef(s) {}", getLogPrefix(), classrefs);
+
+        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+
+        final SAMLObjectBuilder<RequestedAuthnContext> builder =
+                (SAMLObjectBuilder<RequestedAuthnContext>) bf.<RequestedAuthnContext>ensureBuilder(
+                        RequestedAuthnContext.DEFAULT_ELEMENT_NAME);
+
+        final SAMLObjectBuilder<AuthnContextClassRef> acBuilder =
+                (SAMLObjectBuilder<AuthnContextClassRef>) bf.<AuthnContextClassRef>ensureBuilder(
+                        AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
+
+        final RequestedAuthnContext rac = builder.buildObject();
+
+        classrefs.forEach(ref -> {
+            final AuthnContextClassRef obj = acBuilder.buildObject();
+            obj.setURI(ref);
+            rac.getAuthnContextClassRefs().add(obj);
+        });
+        
+        String opstring = input.getmember("AuthnContextComparison").string();
+        if (opstring != null && profileConfiguration.isFeatureDisallowed(profileRequestContext,
+                BrowserSSOProfileConfiguration.FEATURE_AUTHNCONTEXT)) {
+            log.warn("{} Agent disallowed from overriding RequestedAuthnContext operator", getLogPrefix());
+            opstring = null;
+        }
+        
+        final AuthnContextComparisonTypeEnumeration operator;
+        if (opstring != null) {
+            try {
+                operator = AuthnContextComparisonTypeEnumeration.valueOf(opstring.toUpperCase());
+            } catch (final IllegalArgumentException e) {
+                log.warn("{} Error translating RequestedAuthnContext operator string", getLogPrefix(), e);
+                return null;
+            }
+        } else {
+            operator = profileConfiguration.getAuthnContextComparison(profileRequestContext);
+        }
+        if (operator != null) {
+            log.debug("{} Setting RequestedAuthnContext operator to {}", getLogPrefix(), operator);
+            rac.setComparison(operator);
+        }
+        
+        return rac;
+    }
+// Checkstyle: MethodLength ON
+    
+    /**
+     * Build a {@link Subject} element if necessary.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return the {@link Subject} element to include
+     */
+    @Nullable private Subject buildSubject(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final NameID nameID = nameIDLookupStrategy != null
+                ? nameIDLookupStrategy.apply(profileRequestContext) : null;
+        if (nameID == null) {
+            return null;
+        }
+        
+        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+        final SAMLObjectBuilder<Subject> subjectBuilder =
+                (SAMLObjectBuilder<Subject>) bf.<Subject>ensureBuilder(Subject.DEFAULT_ELEMENT_NAME);
+        
+        final Subject subject = subjectBuilder.buildObject();
+        subject.setNameID(nameID);
+        
+        log.debug("{} Populating request with NameID '{}' and Format '{}'", getLogPrefix(),
+                nameID.getValue(), nameID.getFormat());
+        
+        return subject;
+    }
+    
+    /**
+     * Build a {@link Scoping} element, decrementing the proxy count if set.
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return populated {@link Scoping}
+     */
+    @Nullable private Scoping buildScoping(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final RelyingPartyContext rpContext = relyingPartyContextLookupStrategy != null ?
+                relyingPartyContextLookupStrategy.apply(profileRequestContext) : null;
+        final String entityID = rpContext != null ? rpContext.getRelyingPartyId() : null;
+        if (entityID == null) {
+            return null;
+        }
+        
+        final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+        
+        final SAMLObjectBuilder<Scoping> scopingBuilder =
+                (SAMLObjectBuilder<Scoping>) bf.<Scoping>ensureBuilder(Scoping.DEFAULT_ELEMENT_NAME);
+        final SAMLObjectBuilder<IDPList> idpListBuilder =
+                (SAMLObjectBuilder<IDPList>) bf.<IDPList>ensureBuilder(IDPList.DEFAULT_ELEMENT_NAME);
+        final SAMLObjectBuilder<IDPEntry> idpBuilder =
+                (SAMLObjectBuilder<IDPEntry>) bf.<IDPEntry>ensureBuilder(IDPEntry.DEFAULT_ELEMENT_NAME);
+
+        final IDPEntry entry = idpBuilder.buildObject();
+        entry.setProviderID(entityID);
+
+        final IDPList idps = idpListBuilder.buildObject();
+        idps.getIDPEntrys().add(entry);
+        
+        final Scoping scoping = scopingBuilder.buildObject();
+        scoping.setIDPList(idps);
+
+        return scoping;
+    }
+ 
+    /**
+     * Build {@link RequestedAttributes} extension if required.
+     * 
+     * @param profileRequestContext current profile request context
+     * 
+     * @return extension or null
+     */
+    @Nullable private Extensions buildExtensions(@Nonnull final ProfileRequestContext profileRequestContext) {
+                
+        assert profileConfiguration!=null;
+        final Collection<RequestedAttribute> attrs = profileConfiguration.getRequestedAttributes(profileRequestContext);
+        if (!attrs.isEmpty()) {
+            final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+            final SAMLObjectBuilder<Extensions> extBuilder =
+                    (SAMLObjectBuilder<Extensions>) bf.<Extensions>ensureBuilder(
+                            Extensions.DEFAULT_ELEMENT_NAME);
+            final SAMLObjectBuilder<RequestedAttributes> reqExtBuilder =
+                    (SAMLObjectBuilder<RequestedAttributes>) bf.<RequestedAttributes>ensureBuilder(
+                            RequestedAttributes.DEFAULT_ELEMENT_NAME);
+            final RequestedAttributes reqExt = reqExtBuilder.buildObject();
+            attrs.forEach(attr -> {
+                try {
+                    assert attr != null;
+                    reqExt.getRequestedAttributes().add(XMLObjectSupport.cloneXMLObject(attr));
+                } catch (final MarshallingException|UnmarshallingException e) {
+                    log.error("{} Error cloning RequestedAttribute from profile configuration", getLogPrefix(), e);
+                }
+            });
+            final Extensions ext = extBuilder.buildObject();
+            ext.getUnknownXMLObjects().add(reqExt);
+            return ext;
+        }
+        
+        return null;
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/package-info.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/package-info.java
new file mode 100644
index 0000000..f8d7d8e
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+/**
+ * Profile actions and classes for SAML 2.0 profile flows.
+ */
+package net.shibboleth.sp.saml.saml2.profile.impl;
\ No newline at end of file
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2ProfileConfigurationTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2ProfileConfigurationTest.java
new file mode 100644
index 0000000..8cc1b1b
--- /dev/null
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/config/impl/AbstractSAML2ProfileConfigurationTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.saml.saml2.profile.config.impl;
+
+import net.shibboleth.shared.logic.PredicateSupport;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/** Unit test for {@link AbstractSAML2ProfileConfiguration}. */
+ at SuppressWarnings("javadoc")
+public class AbstractSAML2ProfileConfigurationTest {
+
+    @Test
+    public void testEncryptionOptional(){
+        final MockSAML2ProfileConfiguration config = new MockSAML2ProfileConfiguration();
+        Assert.assertFalse(config.isEncryptionOptional(null));
+
+        config.setEncryptionOptional(true);
+        Assert.assertTrue(config.isEncryptionOptional(null));
+    }
+
+    @Test
+    public void testIndirectEncryptionOptional(){
+        final MockSAML2ProfileConfiguration config = new MockSAML2ProfileConfiguration();
+
+        config.setEncryptionOptionalPredicate(PredicateSupport.alwaysTrue());
+        Assert.assertTrue(config.isEncryptionOptional(null));
+    }
+
+    @Test public void testEncryptNameIDsPredicate() {
+        final MockSAML2ProfileConfiguration config = new MockSAML2ProfileConfiguration();
+
+        config.setEncryptNameIDs(true);
+        Assert.assertTrue(config.isEncryptNameIDs(null));
+    }
+
+    /** Mock class for testing {@link AbstractSAML2ProfileConfiguration}. */
+    private static class MockSAML2ProfileConfiguration extends AbstractSAML2ProfileConfiguration {
+
+        /** Constructor. */
+        public MockSAML2ProfileConfiguration() {
+            super("mock");
+        }
+    }
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfigurationTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfigurationTest.java
new file mode 100644
index 0000000..da854c3
--- /dev/null
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/config/impl/BrowserSSOProfileConfigurationTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.saml.saml2.profile.config.impl;
+
+import net.shibboleth.shared.logic.ConstraintViolationException;
+import net.shibboleth.shared.logic.FunctionSupport;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+
+/** Unit test for {@link BrowserSSOProfileConfiguration}. */
+ at SuppressWarnings("javadoc")
+public class BrowserSSOProfileConfigurationTest {
+
+    @Test
+    public void testProfileId() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        Assert.assertEquals(config.getId(), BrowserSSOProfileConfiguration.PROFILE_ID);
+    }
+
+    @Test public void testAssertionLifetime() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        Assert.assertNull(config.getAssertionLifetime(null));
+
+        config.setAssertionLifetime(Duration.ofMillis(100));
+        Assert.assertEquals(config.getAssertionLifetime(null), Duration.ofMillis(100));
+
+        try {
+            config.setAssertionLifetime(Duration.ofMillis(-100));
+            Assert.fail();
+        } catch (ConstraintViolationException e) {
+            // expected this
+        }
+    }
+
+    @Test public void testIndirectAssertionLifetime() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        
+        config.setAssertionLifetimeLookupStrategy(FunctionSupport.constant(Duration.ofMillis(500)));
+        Assert.assertEquals(config.getAssertionLifetime(null), Duration.ofMillis(500));
+    }
+
+    @Test public void testAssertionAudiences() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        Assert.assertNotNull(config.getAssertionAudiences(null));
+        Assert.assertTrue(config.getAssertionAudiences(null).isEmpty());
+
+        config.setAssertionAudiences(Arrays.asList("", null, " foo"));
+
+        final Set<String> audiences = config.getAssertionAudiences(null);
+        Assert.assertNotNull(audiences);
+        Assert.assertEquals(audiences.size(), 1);
+        Assert.assertTrue(audiences.contains("foo"));
+
+        try {
+            audiences.add("bar");
+            Assert.fail();
+        } catch (UnsupportedOperationException e) {
+            // expected this
+        }
+
+        config.setAssertionAudiences(null);
+        Assert.assertNotNull(config.getAssertionAudiences(null));
+        Assert.assertTrue(config.getAssertionAudiences(null).isEmpty());
+    }
+
+    @Test public void testIndirectAssertionAudiences() {
+final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        final Set<String> audiences = new HashSet<>();
+        audiences.add("foo");
+        audiences.add("bar");
+        config.setAssertionAudiencesLookupStrategy(FunctionSupport.constant(audiences));
+        Assert.assertEquals(config.getAssertionAudiences(null), audiences);
+    }
+
+    @Test
+    public void testNameIDFormat() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        Assert.assertNull(config.getNameIDFormat(null));
+
+        config.setNameIDFormat("foo");
+        Assert.assertEquals(config.getNameIDFormat(null), "foo");
+    }
+
+    @Test
+    public void testIndirectNameIDFormat() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+
+        config.setNameIDFormatLookupStrategy(FunctionSupport.constant("foo"));
+        Assert.assertEquals(config.getNameIDFormat(null), "foo");
+    }
+
+    @Test
+    public void testSPNameQualifier() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        Assert.assertNull(config.getSPNameQualifier(null));
+
+        config.setSPNameQualifier("foo");
+        Assert.assertEquals(config.getSPNameQualifier(null), "foo");
+    }
+
+    @Test
+    public void testIndirectSPNameQualifier() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+
+        config.setSPNameQualifierLookupStrategy(FunctionSupport.constant("foo"));
+        Assert.assertEquals(config.getSPNameQualifier(null), "foo");
+    }
+
+    @Test
+    public void testAttributeIndex() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        Assert.assertNull(config.getAttributeIndex(null));
+
+        config.setAttributeIndex(10);
+        Assert.assertEquals(config.getAttributeIndex(null), 10);
+    }
+
+    @Test
+    public void testIndirectAttributeIndex() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+
+        config.setAttributeIndexLookupStrategy(FunctionSupport.constant(10));
+        Assert.assertEquals(config.getAttributeIndex(null), 10);
+    }
+
+    @Test
+    public void testSignArtifactRequests() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        
+        config.setSignArtifactRequests(true);
+        Assert.assertTrue(config.isSignArtifactRequests(null));
+    }
+     
+    @Test
+    public void testClientTLSArtifactRequests() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        
+        config.setClientTLSArtifactRequests(true);
+        Assert.assertTrue(config.isClientTLSArtifactRequests(null));
+    }
+     
+    
+    @Test public void testProxyCount() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+        Assert.assertNull(config.getProxyCount(null));
+
+        config.setProxyCount(1);
+        Assert.assertEquals(config.getProxyCount(null), Integer.valueOf(1));
+    }
+
+    @Test public void testIndirectProxyCount() {
+        final BrowserSSOProfileConfiguration config = new BrowserSSOProfileConfiguration();
+
+        config.setProxyCountLookupStrategy(FunctionSupport.constant(1));
+        Assert.assertEquals(config.getProxyCount(null), Integer.valueOf(1));
+    }
+
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/config/impl/SingleLogoutProfileConfigurationTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/config/impl/SingleLogoutProfileConfigurationTest.java
new file mode 100644
index 0000000..a9d84b6
--- /dev/null
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/config/impl/SingleLogoutProfileConfigurationTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.saml.saml2.profile.config.impl;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import net.shibboleth.shared.logic.PredicateSupport;
+
+/** Unit test for {@link SingleLogoutProfileConfiguration}. */
+ at SuppressWarnings("javadoc")
+public class SingleLogoutProfileConfigurationTest {
+
+    @Test
+    public void testProfileId() {
+        final SingleLogoutProfileConfiguration config = new SingleLogoutProfileConfiguration();
+        Assert.assertEquals(config.getId(), SingleLogoutProfileConfiguration.PROFILE_ID);
+    }
+    
+    @Test
+    public void testSignArtifactRequests() {
+        final SingleLogoutProfileConfiguration config = new SingleLogoutProfileConfiguration();
+        
+        config.setSignArtifactRequests(true);
+        Assert.assertTrue(config.isSignArtifactRequests(null));
+    }
+     
+    @Test
+    public void testClientTLSArtifactRequests() {
+        final SingleLogoutProfileConfiguration config = new SingleLogoutProfileConfiguration();
+        
+        config.setClientTLSArtifactRequests(true);
+        Assert.assertTrue(config.isClientTLSArtifactRequests(null));
+    }
+
+    @Test
+    public void testAsynchronous() {
+        final SingleLogoutProfileConfiguration config = new SingleLogoutProfileConfiguration();
+        Assert.assertTrue(config.isAsynchronous(null));
+        
+        config.setAsynchronous(false);
+        Assert.assertFalse(config.isAsynchronous(null));
+    }
+
+    @Test
+    public void testIndirectAsynchronous() {
+        final SingleLogoutProfileConfiguration config = new SingleLogoutProfileConfiguration();
+        
+        config.setAsynchronousPredicate(PredicateSupport.alwaysFalse());
+        Assert.assertFalse(config.isAsynchronous(null));
+    }
+
+}
\ No newline at end of file

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


More information about the commits mailing list