[java-identity-provider] branch master updated: IDP-1494 - Login flow for proxied authentication

Scott Cantor cantor.2 at osu.edu
Fri Oct 18 11:34:31 EDT 2019


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

scantor pushed a commit to branch master
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=d27b3786c02501d37ff2db4fcccd0032390eaf9c

The following commit(s) were added to refs/heads/master by this push:
       new  d27b378   IDP-1494 - Login flow for proxied authentication
d27b378 is described below

commit d27b3786c02501d37ff2db4fcccd0032390eaf9c
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Fri Oct 18 11:34:27 2019 -0400

    IDP-1494 - Login flow for proxied authentication
    
    https://issues.shibboleth.net/jira/browse/IDP-1494
    
    Fix MVC URL mapping behavior.
    Working proxy flow with discovery hook.
    WIP on SAML subflow with request and decoding working.
---
 .../idp/authn/AbstractAuthenticationAction.java    |   3 +-
 .../idp/authn/context/AuthenticationContext.java   |  48 ++++-
 .../impl/DiscoveryProfileRequestFunction.java      | 148 +++++++++++++
 .../authn/proxy/impl/ExtractDiscoveryResponse.java |  70 ++++++
 .../impl/InitializeProxyProfileRequestContext.java | 140 ++++++++++++
 .../idp/authn/proxy/impl/package-info.java         |  22 ++
 .../authn/spnego/impl/SPNEGOAuthnController.java   |   3 +
 .../impl/DiscoveryProfileRequestFunctionTest.java  |  71 ++++++
 .../proxy/impl/ExtractDiscoveryResponseTest.java   |  69 ++++++
 .../InitializeProxyProfileRequestContextTest.java  |  66 ++++++
 .../main/resources/conf/authn/general-authn.xml    |   5 +
 .../resources/conf/authn/proxy-authn-config.xml    |  33 +++
 .../main/resources/system/conf/audit-system.xml    | 101 +++++++++
 .../src/main/resources/system/conf/mvc-beans.xml   |  14 +-
 .../system/conf/relying-party-mddriven.xml         |   3 +
 .../resources/system/conf/saml-binding-config.xml  |   7 +
 .../src/main/resources/system/conf/utilities.xml   |  15 ++
 .../main/resources/system/conf/webflow-config.xml  |   4 +
 .../system/flows/authn/authn-abstract-flow.xml     |   6 +
 .../system/flows/authn/proxy-authn-beans.xml       |  43 ++++
 .../system/flows/authn/proxy-authn-flow.xml        |  58 +++++
 .../system/flows/authn/proxy/saml-proxy-beans.xml  | 221 +++++++++++++++++++
 .../system/flows/authn/proxy/saml-proxy-flow.xml   |  60 ++++++
 .../system/flows/logout/propagation/saml2-flow.xml |   2 +-
 .../system/flows/saml/saml-abstract-beans.xml      |   2 +-
 .../system/flows/saml/saml-abstract-flow.xml       |   2 +-
 .../relyingparty/RelyingPartyConfiguration.java    |   2 +-
 .../profile/impl/WebFlowProfileActionAdaptor.java  |   4 +-
 .../config/AbstractSAMLProfileConfiguration.java   |   2 +-
 .../config/BrowserSSOProfileConfiguration.java     |  50 +++++
 .../impl/SpringAwareMessageDecoderFactory.java     | 114 ++++++++++
 .../impl/SpringAwareMessageEncoderFactory.java     |   9 +-
 .../profile/impl/AddAuthnRequest.java}             | 178 ++++++++-------
 .../saml/saml2/profile/impl/SAMLAuthnContext.java  | 101 +++++++++
 .../saml2/profile/impl/SAMLAuthnController.java    | 239 +++++++++++++++++++++
 .../profile/impl/ValidateSAMLAuthentication.java   |  94 ++++++++
 .../idp/saml/session/impl/AddLogoutRequest.java    |   2 +-
 .../session/impl/PrepareInboundMessageContext.java |  47 ++--
 38 files changed, 1921 insertions(+), 137 deletions(-)

diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AbstractAuthenticationAction.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AbstractAuthenticationAction.java
index 23cecfc..7536181 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AbstractAuthenticationAction.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/AbstractAuthenticationAction.java
@@ -64,7 +64,8 @@ public abstract class AbstractAuthenticationAction
      * 
      * @param strategy  lookup strategy function for {@link AuthenticationContext}.
      */
-    public void setLookupStrategy(@Nonnull final Function<ProfileRequestContext, AuthenticationContext> strategy) {
+    public void setAuthenticationContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,AuthenticationContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
         authnCtxLookupStrategy = Constraint.isNotNull(strategy, "Strategy cannot be null");
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationContext.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationContext.java
index 9ebabc2..3903eab 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationContext.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationContext.java
@@ -79,6 +79,9 @@ public final class AuthenticationContext extends BaseContext {
     
     /** Allowed time since an {@link AuthenticationResult} was created that it can be reused for this request. */
     @Nullable private Duration maxAge;
+    
+    /** Name of a proxied authentication source to use. */
+    @Nullable private String authenticatingAuthority;
 
     /** Lookup strategy for a fixed event to return from validators for testing. */
     @Nullable private Function<ProfileRequestContext,String> fixedEventLookupStrategy;
@@ -363,6 +366,36 @@ public final class AuthenticationContext extends BaseContext {
     }
     
     /**
+     * Gets the identifier or location, depending on protocol, of an authentication system to use
+     * as a proxied source of authentication.
+     * 
+     * @return identifier/location of proxy authentication source
+     * 
+     * @since 4.0.0
+     */
+    @Nullable public String getAuthenticatingAuthority() {
+        return authenticatingAuthority;
+    }
+
+    /**
+     * Sets the identifier or location, depending on protocol, of an authentication system to use
+     * as a proxied source of authentication.
+     * 
+     * <p>Used to track the result of a hardcoded or discovery-driven determination of a
+     * third party authentication source.</p>
+     * 
+     * @param authority identifier/location of proxy authentication source
+     * 
+     * @return this context
+     * 
+     * @since 4.0.0
+     */
+    @Nonnull public AuthenticationContext setAuthenticatingAuthority(@Nullable final String authority) {
+        authenticatingAuthority = StringSupport.trimOrNull(authority);
+        return this;
+    }
+    
+    /**
      * Get optional lookup strategy to return a fixed event to return from credential validation
      * to exercise error and warning logic.
      * 
@@ -535,10 +568,9 @@ public final class AuthenticationContext extends BaseContext {
         final RequestedPrincipalContext rpCtx = getSubcontext(RequestedPrincipalContext.class);
         if (rpCtx != null) {
             return rpCtx.isAcceptable(component);
-        } else {
-            // No requirements so anything is acceptable.
-            return true;
         }
+        // No requirements so anything is acceptable.
+        return true;
     }
         
     /**
@@ -554,10 +586,9 @@ public final class AuthenticationContext extends BaseContext {
         final RequestedPrincipalContext rpCtx = getSubcontext(RequestedPrincipalContext.class);
         if (rpCtx != null) {
             return rpCtx.isAcceptable(principals);
-        } else {
-            // No requirements so anything is acceptable.
-            return true;
         }
+        // No requirements so anything is acceptable.
+        return true;
     }
 
     /**
@@ -574,10 +605,9 @@ public final class AuthenticationContext extends BaseContext {
         final RequestedPrincipalContext rpCtx = getSubcontext(RequestedPrincipalContext.class);
         if (rpCtx != null) {
             return rpCtx.isAcceptable(principal);
-        } else {
-            // No requirements so anything is acceptable.
-            return true;
         }
+        // No requirements so anything is acceptable.
+        return true;
     }
 
     /**
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/DiscoveryProfileRequestFunction.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/DiscoveryProfileRequestFunction.java
new file mode 100644
index 0000000..045bfe2
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/DiscoveryProfileRequestFunction.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.authn.proxy.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+import javax.servlet.http.HttpServletRequest;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.webflow.execution.RequestContext;
+
+import com.google.common.escape.Escaper;
+import com.google.common.net.UrlEscapers;
+
+/**
+ * A {@link Function} that produces a discovery request URL using the protocol defined in
+ * https://wiki.oasis-open.org/security/IdpDiscoSvcProtonProfile
+ * 
+ * <p>Since there is no upstream "relying party" yet, the identity of the system is derived
+ * from the currently in-effect entityID that will be used to respond to the downstream
+ * relying party.</p>
+ */
+ at ThreadSafe
+public class DiscoveryProfileRequestFunction extends AbstractInitializableComponent
+        implements Function<Pair<RequestContext,ProfileRequestContext>,String> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DiscoveryProfileRequestFunction.class);
+
+    /** URL query parameter escaper. */
+    @Nonnull private Escaper escaper;
+    
+    /** Lookup strategy for locating {@link RelyingPartyContext}. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextLookupStrategy;
+    
+    /** Lookup strategy for determining the "base" discovery URL. */
+    @NonnullAfterInit private Function<ProfileRequestContext,String> discoveryURLLookupStrategy;
+    
+    /** Constructor. */
+    public DiscoveryProfileRequestFunction() {
+        escaper = UrlEscapers.urlFormParameterEscaper();
+        relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
+    }
+    
+    /**
+     * Set the lookup strategy for the {@link RelyingPartyContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setRelyingPartyContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        relyingPartyContextLookupStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the lookup strategy for the "base" discovery service URL to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setDiscoveryURLLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        discoveryURLLookupStrategy = Constraint.isNotNull(strategy, "Discovery URL lookup strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (discoveryURLLookupStrategy == null) {
+            throw new ComponentInitializationException("Discovery URL lookup strategy cannot be null");
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable public String apply(@Nonnull final Pair<RequestContext,ProfileRequestContext> input) {
+        
+        final RelyingPartyContext rpCtx = relyingPartyContextLookupStrategy.apply(input.getSecond());
+        Constraint.isNotNull(rpCtx, "RelyingPartyContext cannot be null");
+        Constraint.isNotNull(rpCtx.getConfiguration(), "RelyingPartyConfiguration cannot be null");
+        
+        final String baseURL = discoveryURLLookupStrategy.apply(input.getSecond());
+        Constraint.isNotEmpty(baseURL, "Discovery URL cannot be null or empty");
+
+        final String entityID = rpCtx.getConfiguration().getResponderId(input.getSecond());
+
+        final StringBuilder builder = new StringBuilder(baseURL);
+        
+        builder.append(baseURL.contains("?") ? '&' : '?').append("entityID=").append(escaper.escape(entityID));
+        
+        final AuthenticationContext authenticationContext =
+                input.getSecond().getSubcontext(AuthenticationContext.class);
+        if (authenticationContext != null && authenticationContext.isPassive()) {
+            builder.append("&isPassive=true");
+        }
+        
+        final HttpServletRequest httpServletRequest =
+                (HttpServletRequest) input.getFirst().getExternalContext().getNativeRequest();
+        
+        final StringBuilder selfBuilder = new StringBuilder(httpServletRequest.getScheme());
+        selfBuilder.append("://").append(httpServletRequest.getServerName());
+        
+        final int port = httpServletRequest.getServerPort();
+        if (port != (httpServletRequest.isSecure() ? 443 : 80)) {
+            selfBuilder.append(':').append(port);
+        }
+        
+        selfBuilder.append(input.getFirst().getFlowExecutionUrl()).append("&_eventId_proceed=1");
+        
+        builder.append("&return=").append(escaper.escape(selfBuilder.toString()));
+        
+        return builder.toString();
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/ExtractDiscoveryResponse.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/ExtractDiscoveryResponse.java
new file mode 100644
index 0000000..a05d9c5
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/ExtractDiscoveryResponse.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.authn.proxy.impl;
+
+import javax.annotation.Nonnull;
+import javax.servlet.http.HttpServletRequest;
+
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An action that extracts a discovery service result and copies it to the {@link AuthenticationContext}.
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link AuthnEventIds#NO_CREDENTIALS}
+ * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
+ * @post If getHttpServletRequest() != null, the content of the "entityID" parameter will be
+ * added via {@link AuthenticationContext#setAuthenticatingAuthority(String)}.
+ */
+public class ExtractDiscoveryResponse extends AbstractAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractDiscoveryResponse.class);
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        final HttpServletRequest request = getHttpServletRequest();
+        if (request == null) {
+            log.debug("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+
+        final String entityID = StringSupport.trimOrNull(request.getParameter("entityID"));
+        
+        if (entityID == null) {
+            log.debug("{} No entityID parameter found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+
+        authenticationContext.setAuthenticatingAuthority(entityID);
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/InitializeProxyProfileRequestContext.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/InitializeProxyProfileRequestContext.java
new file mode 100644
index 0000000..1473d2a
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/InitializeProxyProfileRequestContext.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.authn.proxy.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+
+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 net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * Action that creates a new {@link ProfileRequestContext} via a creation strategy,
+ * and sets the profile and logging IDs, if provided.
+ * 
+ * <p>This is designed by default for use in creating a nested context tree
+ * beneath an active {@link AuthenticationContext} for use in managing a nested
+ * profile interaction with an external IdP.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @post context is created per the supplied strategy
+ */
+ at ThreadSafe
+public final class InitializeProxyProfileRequestContext extends AbstractProfileAction {
+
+    /** Context creation strategy. */
+    @Nonnull private Function<ProfileRequestContext,ProfileRequestContext> profileRequestContextCreationStrategy;
+    
+    /** The profile ID to initialize the context to. */
+    @Nullable private String profileId;
+
+    /** The logging ID to initialize the context to. */
+    @Nullable private String loggingId;
+    
+    /** Whether this is a browser-based profile request. */
+    private boolean browserProfile;
+    
+    /** Constructor. */
+    public InitializeProxyProfileRequestContext() {
+        
+        // Defaults to PRC -> AuthenticationContext -> PRC
+        profileRequestContextCreationStrategy =
+                new ChildContextLookup<>(ProfileRequestContext.class, true).compose(
+                        new ChildContextLookup<>(AuthenticationContext.class));
+    }
+    
+    /**
+     * Set the strategy to use to locate/create the {@link ProfileRequestContext to operate on.
+     * 
+     * @param strategy lookup/creation strategy
+     */
+    public void setProfileRequestContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,ProfileRequestContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        profileRequestContextCreationStrategy = Constraint.isNotNull(strategy, "Creation strategy cannot be null");
+    }
+    
+    /**
+     * Set the profile ID to populate into the context.
+     * 
+     * @param id    profile ID to populate into the context
+     */
+    public void setProfileId(@Nullable final String id) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        profileId = StringSupport.trimOrNull(id);
+    }
+
+    /**
+     * Set the logging ID to populate into the context.
+     * 
+     * @param id    logging ID to populate into the context
+     */
+    public void setLoggingId(@Nullable final String id) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        loggingId = StringSupport.trimOrNull(id);
+    }
+    
+    /**
+     * Set whether the request is browser-based, defaults to false.
+     * 
+     * @param browser   true iff the request is browser based
+     */
+    public void setBrowserProfile(final boolean browser) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        browserProfile = browser;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull public void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final ProfileRequestContext prc = profileRequestContextCreationStrategy.apply(profileRequestContext);
+        if (prc == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        
+        if (profileId != null) {
+            prc.setProfileId(profileId);
+        }
+        
+        if (loggingId != null) {
+            prc.setLoggingId(loggingId);
+        }
+        
+        prc.setBrowserProfile(browserProfile);
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/package-info.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/package-info.java
new file mode 100644
index 0000000..01021bc
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/proxy/impl/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.
+ */
+
+/**
+ * Classes implementing proxied authentication flows.
+ */
+
+package net.shibboleth.idp.authn.proxy.impl;
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/spnego/impl/SPNEGOAuthnController.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/spnego/impl/SPNEGOAuthnController.java
index fb8f179..f4f97f2 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/spnego/impl/SPNEGOAuthnController.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/spnego/impl/SPNEGOAuthnController.java
@@ -42,6 +42,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpHeaders;
+import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -56,6 +57,8 @@ import org.springframework.web.servlet.ModelAndView;
  * {@link ExternalAuthentication#finishExternalAuthentication(String, HttpServletRequest, HttpServletResponse)} and
  * returning null. On unrecoverable errors, an exception is thrown.
  */
+ at Controller
+ at RequestMapping(value="%{idp.authn.spnego.externalAuthnPath:/Authn/SPNEGO}")
 public class SPNEGOAuthnController {
     
     /** Event ID indicating that SPNEGO is not supported by the client or is not available for other reasons. */
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/proxy/impl/DiscoveryProfileRequestFunctionTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/proxy/impl/DiscoveryProfileRequestFunctionTest.java
new file mode 100644
index 0000000..f532a00
--- /dev/null
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/proxy/impl/DiscoveryProfileRequestFunctionTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.authn.proxy.impl;
+
+import net.shibboleth.idp.authn.impl.BaseAuthenticationContextTest;
+import net.shibboleth.idp.profile.ActionTestingSupport;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+import net.shibboleth.utilities.java.support.net.URISupport;
+
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.webflow.test.MockFlowExecutionContext;
+import org.springframework.webflow.test.MockFlowExecutionKey;
+import org.springframework.webflow.test.MockRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/** {@link DiscoveryProfileRequestFunction} unit test. */
+public class DiscoveryProfileRequestFunctionTest extends BaseAuthenticationContextTest {
+    
+    private DiscoveryProfileRequestFunction function; 
+    
+    @BeforeMethod public void setUp() throws Exception {
+        super.setUp();
+        
+        ((MockFlowExecutionContext) ((MockRequestContext) src).getFlowExecutionContext()).setKey(new MockFlowExecutionKey("flowkey"));
+        
+        function = new DiscoveryProfileRequestFunction();
+        function.setDiscoveryURLLookupStrategy(FunctionSupport.constant("https://ds.example.org/DS"));
+        function.initialize();
+    }
+    
+    @Test public void test() throws Exception {
+        final URL url = new URL(function.apply(new Pair<>(src, prc)));
+        
+        Assert.assertEquals(url.getProtocol(), "https");
+        Assert.assertEquals(url.getHost(), "ds.example.org");
+        Assert.assertEquals(url.getPort(), -1);
+        Assert.assertEquals(url.getPath(), "/DS");
+        
+        final List<Pair<String,String>> params = URISupport.parseQueryString(url.getQuery());
+        Assert.assertEquals(params.size(), 2);
+        
+        final Map<String,String> map = new HashMap<>();
+        params.forEach((p) -> map.put(p.getFirst(), p.getSecond()));
+        
+        Assert.assertEquals(map.get("entityID"), ActionTestingSupport.OUTBOUND_MSG_ISSUER);
+        Assert.assertEquals(map.get("return"), "http://localhost?execution=flowkey&_eventId_proceed=1");
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/proxy/impl/ExtractDiscoveryResponseTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/proxy/impl/ExtractDiscoveryResponseTest.java
new file mode 100644
index 0000000..f003ad4
--- /dev/null
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/proxy/impl/ExtractDiscoveryResponseTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.authn.proxy.impl;
+
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.impl.BaseAuthenticationContextTest;
+import net.shibboleth.idp.profile.ActionTestingSupport;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/** {@link ExtractDiscoveryResponse} unit test. */
+public class ExtractDiscoveryResponseTest extends BaseAuthenticationContextTest {
+    
+    private ExtractDiscoveryResponse action; 
+    
+    @BeforeMethod public void setUp() throws Exception {
+        super.setUp();
+        
+        action = new ExtractDiscoveryResponse();
+        action.setHttpServletRequest(new MockHttpServletRequest());
+        action.initialize();
+    }
+    
+    @Test public void testNoServlet() throws Exception {
+        action = new ExtractDiscoveryResponse();
+        action.initialize();
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+        Assert.assertNull(prc.getSubcontext(AuthenticationContext.class).getAuthenticatingAuthority());
+    }
+
+    @Test public void testFailure() throws Exception {
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+        Assert.assertNull(prc.getSubcontext(AuthenticationContext.class).getAuthenticatingAuthority());
+    }
+
+    @Test public void testSuccess() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addParameter("entityID", "foo");
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        Assert.assertEquals(prc.getSubcontext(AuthenticationContext.class).getAuthenticatingAuthority(), "foo");
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/proxy/impl/InitializeProxyProfileRequestContextTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/proxy/impl/InitializeProxyProfileRequestContextTest.java
new file mode 100644
index 0000000..134de8d
--- /dev/null
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/proxy/impl/InitializeProxyProfileRequestContextTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.authn.proxy.impl;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.profile.RequestContextBuilder;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+import org.opensaml.profile.action.ActionTestingSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/** Unit test for {@link InitializeProxyProfileRequestContext}. */
+public class InitializeProxyProfileRequestContextTest {
+
+    private ProfileRequestContext prc;
+    
+    private InitializeProxyProfileRequestContext action;
+    
+    @BeforeMethod
+    public void setUp() throws ComponentInitializationException {
+        prc = new RequestContextBuilder().buildProfileRequestContext();
+        
+        action = new InitializeProxyProfileRequestContext();
+        action.setProfileId("nested");
+        action.initialize();
+    }
+    
+    @Test public void testFailure() throws Exception {
+
+        action.execute(prc);
+        ActionTestingSupport.assertEvent(prc, EventIds.INVALID_PROFILE_CTX);
+    }
+    
+    @Test public void testSuccess() throws Exception {
+
+        prc.addSubcontext(new AuthenticationContext());
+        
+        action.execute(prc);
+        ActionTestingSupport.assertProceedEvent(prc);
+
+        final ProfileRequestContext prc2 =
+                prc.getSubcontext(AuthenticationContext.class).getSubcontext(ProfileRequestContext.class);
+        Assert.assertNotNull(prc2);
+        Assert.assertEquals(prc2.getProfileId(), "nested");
+    }
+
+}
\ No newline at end of file
diff --git a/idp-conf/src/main/resources/conf/authn/general-authn.xml b/idp-conf/src/main/resources/conf/authn/general-authn.xml
index 5699022..906db6c 100644
--- a/idp-conf/src/main/resources/conf/authn/general-authn.xml
+++ b/idp-conf/src/main/resources/conf/authn/general-authn.xml
@@ -134,6 +134,11 @@
             </property>
         </bean>
 
+        <bean id="authn/Proxy" parent="shibboleth.AuthenticationFlow"
+                p:nonBrowserSupported="false"
+                p:passiveAuthenticationSupported="true"
+                p:forcedAuthenticationSupported="true" />
+
     </util:list>
 
     <!--
diff --git a/idp-conf/src/main/resources/conf/authn/proxy-authn-config.xml b/idp-conf/src/main/resources/conf/authn/proxy-authn-config.xml
new file mode 100644
index 0000000..531234c
--- /dev/null
+++ b/idp-conf/src/main/resources/conf/authn/proxy-authn-config.xml
@@ -0,0 +1,33 @@
+<?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">
+
+    <!--  General Configuration -->
+
+    <!-- Optional Function<ProfileRequest,String> to supply name of proxied IdP. -->
+    
+    <bean id="shibboleth.authn.Proxy.discoveryFunction" parent="shibboleth.Functions.Constant"
+        c:target="urn:mace:incommon:osu.edu" />
+
+    <!-- Alternatively use a standardized discovery service. -->
+    
+    <!--
+    <bean id="shibboleth.authn.Proxy.discoveryURL" class="java.lang.String"
+        c:_0="https://ds.example.org/shibboleth-ds/index.html" />
+     -->
+        
+    <util:list id="shibboleth.authn.Proxy.precedence">
+        <value>SAML</value>
+    </util:list>
+ 
+</beans>
diff --git a/idp-conf/src/main/resources/system/conf/audit-system.xml b/idp-conf/src/main/resources/system/conf/audit-system.xml
index 599c359..3002f17 100644
--- a/idp-conf/src/main/resources/system/conf/audit-system.xml
+++ b/idp-conf/src/main/resources/system/conf/audit-system.xml
@@ -223,6 +223,107 @@
             </map>
         </property>
     </bean>
+
+    <bean id="shibboleth.DefaultPostRequestAuditExtractors" lazy-init="true"
+            class="org.springframework.beans.factory.config.MapFactoryBean">
+        <property name="sourceMap">
+            <map>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.saml.profile.SAMLAuditFields.PROTOCOL"/>
+                    </key>
+                    <bean parent="shibboleth.Functions.Constant"
+                        c:target-ref="shibboleth.MetadataLookup.Protocol" />
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.saml.profile.SAMLAuditFields.REQUEST_BINDING"/>
+                    </key>
+                    <bean parent="shibboleth.Functions.Compose">
+                        <constructor-arg name="g">
+                            <bean class="org.opensaml.saml.common.messaging.context.navigate.SAMLBindingContextBindingFunction"
+                                p:useShortName="%{idp.audit.shortenBindings:false}" />
+                        </constructor-arg>
+                        <constructor-arg name="f">
+                            <bean parent="shibboleth.Functions.Compose"
+                                c:g-ref="shibboleth.ChildLookup.SAMLBindingContext"
+                                c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+                        </constructor-arg>
+                    </bean>
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.saml.profile.SAMLAuditFields.SIGNING"/>
+                    </key>
+                    <bean parent="shibboleth.Functions.Compose">
+                        <constructor-arg name="g">
+                            <bean class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="forPredicate">
+                                <constructor-arg>
+                                    <bean class="net.shibboleth.ext.spring.util.SpringExpressionPredicate"
+                                        c:_0="#input != null ? T(org.opensaml.saml.common.binding.SAMLBindingSupport).isMessageSigned(#input) : false" />
+                                </constructor-arg>
+                            </bean>
+                        </constructor-arg>
+                        <constructor-arg name="f" ref="shibboleth.MessageContextLookup.Outbound"/>
+                    </bean>
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.saml.profile.SAMLAuditFields.REQUEST_ID"/>
+                    </key>
+                    <bean parent="shibboleth.Functions.Compose">
+                        <constructor-arg name="g">
+                            <bean class="org.opensaml.saml.common.messaging.context.navigate.SAMLMessageInfoContextIDFunction" />
+                        </constructor-arg>
+                        <constructor-arg name="f">
+                            <bean parent="shibboleth.Functions.Compose"
+                                c:g-ref="shibboleth.ChildLookupOrCreate.SAMLMessageInfoContext"
+                                c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+                        </constructor-arg>
+                    </bean>
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.saml.profile.SAMLAuditFields.REQUEST_ISSUE_INSTANT"/>
+                    </key>
+                    <bean parent="shibboleth.Functions.Compose">
+                        <constructor-arg name="g">
+                            <bean class="org.opensaml.saml.common.messaging.context.navigate.SAMLMessageInfoContextIssueInstantFunction" />
+                        </constructor-arg>
+                        <constructor-arg name="f">
+                            <bean parent="shibboleth.Functions.Compose"
+                                c:g-ref="shibboleth.ChildLookupOrCreate.SAMLMessageInfoContext"
+                                c:f-ref="shibboleth.MessageContextLookup.Inbound" />
+                        </constructor-arg>
+                    </bean>
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.saml.profile.SAMLAuditFields.IS_PASSIVE"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.saml.audit.impl.IsPassiveAuditExtractor">
+                        <constructor-arg>
+                            <bean parent="shibboleth.Functions.Compose"
+                                c:g-ref="shibboleth.MessageLookup.AuthnRequest"
+                                c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+                        </constructor-arg>
+                    </bean>
+                </entry>
+                <entry>
+                    <key>
+                        <util:constant static-field="net.shibboleth.idp.saml.profile.SAMLAuditFields.FORCE_AUTHN"/>
+                    </key>
+                    <bean class="net.shibboleth.idp.saml.audit.impl.ForceAuthnAuditExtractor">
+                        <constructor-arg>
+                            <bean parent="shibboleth.Functions.Compose"
+                                c:g-ref="shibboleth.MessageLookup.AuthnRequest"
+                                c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+                        </constructor-arg>
+                    </bean>
+                </entry>
+            </map>
+        </property>
+    </bean>
         
     <bean id="shibboleth.DefaultPostLookupAuditExtractors" lazy-init="true"
             class="org.springframework.beans.factory.config.MapFactoryBean">
diff --git a/idp-conf/src/main/resources/system/conf/mvc-beans.xml b/idp-conf/src/main/resources/system/conf/mvc-beans.xml
index 036de12..8b59e39 100644
--- a/idp-conf/src/main/resources/system/conf/mvc-beans.xml
+++ b/idp-conf/src/main/resources/system/conf/mvc-beans.xml
@@ -36,17 +36,6 @@
 
     <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" p:order="2" />
 
-    <!-- Add this one to mount any additional MVC controllers we create. -->
-
-    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" p:order="3">
-        <property name="mappings">
-            <props>
-                <prop key="%{idp.authn.spnego.externalAuthnPath:/Authn/SPNEGO}/**">shibboleth.SPNEGOAuthnController</prop>
-                <prop key="/RaiseError">shibboleth.RaiseErrorController</prop>
-            </props>
-        </property>
-    </bean>
-
     <!-- HandlerAdapters: These aren't ordered. Add our WebFlow one, then the standard Spring MVC default ones. 
          Have to enumerate them all explicitly b/c we're overriding the built-in defaults. -->
     
@@ -63,9 +52,10 @@
     
     <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" />
     
-    <!-- Additional MVC controllers -->
+    <!-- Additional MVC controllers scanned for annotations. -->
     
     <bean id="shibboleth.SPNEGOAuthnController" class="net.shibboleth.idp.authn.spnego.impl.SPNEGOAuthnController" />
+    <bean id="shibboleth.SAMLAuthnPOSTController" class="net.shibboleth.idp.saml.saml2.profile.impl.SAMLAuthnController" />
     <bean id="shibboleth.RaiseErrorController" class="net.shibboleth.ext.spring.error.ErrorRaisingController" />
 
     <!-- Exception handling -->
diff --git a/idp-conf/src/main/resources/system/conf/relying-party-mddriven.xml b/idp-conf/src/main/resources/system/conf/relying-party-mddriven.xml
index e30f884..fa60b06 100644
--- a/idp-conf/src/main/resources/system/conf/relying-party-mddriven.xml
+++ b/idp-conf/src/main/resources/system/conf/relying-party-mddriven.xml
@@ -392,6 +392,9 @@
                 <constructor-arg value="false" />
             </bean>
         </property>
+        <property name="authnContextComparisonLookupStrategy">
+            <bean parent="shibboleth.MDDrivenStringProperty" p:propertyName="authnContextComparison" />
+        </property>
         <property name="defaultAuthenticationMethodsLookupStrategy">
             <bean parent="shibboleth.MDDrivenListProperty" p:propertyName="defaultAuthenticationMethods"
                 p:propertyType="#{T(net.shibboleth.idp.saml.authn.principal.AuthnContextClassRefPrincipal)}" />
diff --git a/idp-conf/src/main/resources/system/conf/saml-binding-config.xml b/idp-conf/src/main/resources/system/conf/saml-binding-config.xml
index 6ee5272..4ebcfcd 100644
--- a/idp-conf/src/main/resources/system/conf/saml-binding-config.xml
+++ b/idp-conf/src/main/resources/system/conf/saml-binding-config.xml
@@ -95,6 +95,13 @@
         <ref bean="shibboleth.Binding.SAML2Artifact" />
     </util:list>
 
+    <util:list id="shibboleth.OutgoingSAML2SSORequestBindings">
+        <ref bean="shibboleth.Binding.SAML2Redirect" />
+        <ref bean="shibboleth.Binding.SAML2POST" />
+        <ref bean="shibboleth.Binding.SAML2POSTSimpleSign" />
+        <ref bean="shibboleth.Binding.SAML2Artifact" />
+    </util:list>
+
     <util:list id="shibboleth.OutgoingSAML2SLOFrontBindings">
         <ref bean="shibboleth.Binding.SAML2Redirect" />
         <ref bean="shibboleth.Binding.SAML2POST" />
diff --git a/idp-conf/src/main/resources/system/conf/utilities.xml b/idp-conf/src/main/resources/system/conf/utilities.xml
index f11b383..eb4fa6e 100644
--- a/idp-conf/src/main/resources/system/conf/utilities.xml
+++ b/idp-conf/src/main/resources/system/conf/utilities.xml
@@ -146,6 +146,9 @@
         c:type="#{ T(net.shibboleth.idp.ui.context.RelyingPartyUIContext) }"
         c:createContext="true" />
 
+    <bean id="shibboleth.ChildLookup.ProfileRequestContext"
+        class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+        c:type="#{ T(org.opensaml.profile.context.ProfileRequestContext) }" />
     <bean id="shibboleth.ChildLookup.AttributeContext"
         class="org.opensaml.messaging.context.navigate.ChildContextLookup"
         c:type="#{ T(net.shibboleth.idp.attribute.context.AttributeContext) }" />
@@ -191,6 +194,18 @@
         c:type="#{ T(org.opensaml.saml.common.messaging.context.SAMLMessageInfoContext) }"
         c:createContext="true" />
 
+    <!-- Wiring for proxy login flows to locate nested PRC. -->
+    <bean id="shibboleth.ChildLookup.ProxyProfileRequestContext" parent="shibboleth.Functions.Compose">
+        <constructor-arg>
+            <bean parent="shibboleth.Functions.Compose" c:g-ref="shibboleth.ChildLookup.ProfileRequestContext"
+                c:f-ref="shibboleth.ChildLookup.AuthenticationContext" />
+        </constructor-arg>
+        <constructor-arg>
+            <bean class="net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup" />
+        </constructor-arg>
+    </bean>
+
+
     <bean id="shibboleth.ResponderIdLookup.Simple"
         class="net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction" />
 
diff --git a/idp-conf/src/main/resources/system/conf/webflow-config.xml b/idp-conf/src/main/resources/system/conf/webflow-config.xml
index 7f75dd4..d3f5c66 100644
--- a/idp-conf/src/main/resources/system/conf/webflow-config.xml
+++ b/idp-conf/src/main/resources/system/conf/webflow-config.xml
@@ -86,6 +86,10 @@
                 <entry key="authn/Duo" value="../system/flows/authn/duo-authn-flow.xml" />
                 <entry key="authn/MFA" value="../system/flows/authn/mfa-authn-flow.xml" />
                 <entry key="authn/Function" value="../system/flows/authn/function-authn-flow.xml" />
+                <entry key="authn/Proxy" value="../system/flows/authn/proxy-authn-flow.xml" />
+          
+                <!-- Proxied login methods. -->
+                <entry key="proxy/SAML" value="../system/flows/authn/proxy/saml-proxy-flow.xml" />
           
                 <!-- Master flow for subject c14n. -->
                 <entry key="c14n.events" value="../conf/c14n/subject-c14n-events-flow.xml" />
diff --git a/idp-conf/src/main/resources/system/flows/authn/authn-abstract-flow.xml b/idp-conf/src/main/resources/system/flows/authn/authn-abstract-flow.xml
index afe3f0e..3db09fb 100644
--- a/idp-conf/src/main/resources/system/flows/authn/authn-abstract-flow.xml
+++ b/idp-conf/src/main/resources/system/flows/authn/authn-abstract-flow.xml
@@ -23,6 +23,9 @@
     <end-state id="InvalidCredentials" />
     <end-state id="InvalidEvent" />
     <end-state id="InvalidProfileContext" />
+    <end-state id="InvalidProfileConfiguration" />
+    <end-state id="InvalidRelyingPartyConfiguration" />
+    <end-state id="InvalidRelyingPartyContext" />
     <end-state id="InvalidSubjectContext" />
     <end-state id="InvalidSubjectCanonicalizationContext" />
     <end-state id="NoCredentials" />
@@ -51,6 +54,9 @@
         <transition on="InputOutputError" to="InputOutputError" />
         <transition on="InvalidCredentials" to="InvalidCredentials" />
         <transition on="InvalidProfileContext" to="InvalidProfileContext" />
+        <transition on="InvalidProfileConfiguration" to="InvalidProfileConfiguration" />
+        <transition on="InvalidRelyingPartyConfiguration" to="InvalidRelyingPartyConfiguration" />
+        <transition on="InvalidRelyingPartyContext" to="InvalidRelyingPartyContext" />
         <transition on="InvalidSubjectContext" to="InvalidSubjectContext" />
         <transition on="InvalidSubjectCanonicalizationContext" to="InvalidSubjectCanonicalizationContext" />
         <transition on="NoCredentials" to="NoCredentials" />
diff --git a/idp-conf/src/main/resources/system/flows/authn/proxy-authn-beans.xml b/idp-conf/src/main/resources/system/flows/authn/proxy-authn-beans.xml
new file mode 100644
index 0000000..57d35e2
--- /dev/null
+++ b/idp-conf/src/main/resources/system/flows/authn/proxy-authn-beans.xml
@@ -0,0 +1,43 @@
+<?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 class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"
+        p:placeholderPrefix="%{" p:placeholderSuffix="}" />
+
+    <bean class="net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor" />
+    <bean class="net.shibboleth.idp.profile.impl.ProfileActionBeanPostProcessor" />
+
+    <!-- Default strategy function to obtain the external path. -->
+    <bean id="shibboleth.authn.Proxy.discoveryURLStrategy" parent="shibboleth.Functions.Constant" lazy-init="true"
+        c:target-ref="shibboleth.authn.Proxy.discoveryURL" />
+
+    
+
+    <import resource="../../../conf/authn/proxy-authn-config.xml" />
+
+    <bean id="DiscoveryProfileRequestFunction" lazy-init="true"
+        class="net.shibboleth.idp.authn.proxy.impl.DiscoveryProfileRequestFunction"
+        p:discoveryURLLookupStrategy-ref="shibboleth.authn.Proxy.discoveryURLStrategy" />
+
+    <!-- Action beans. -->
+
+    <bean id="ExtractDiscoveryResponse"
+        class="net.shibboleth.idp.authn.proxy.impl.ExtractDiscoveryResponse" scope="prototype"
+        p:httpServletRequest-ref="shibboleth.HttpServletRequest" />
+
+    <bean id="PopulateSubjectCanonicalizationContext"
+        class="net.shibboleth.idp.authn.impl.PopulateSubjectCanonicalizationContext" scope="prototype"
+        p:availableFlows-ref="shibboleth.PostLoginSubjectCanonicalizationFlows" />
+
+</beans>
diff --git a/idp-conf/src/main/resources/system/flows/authn/proxy-authn-flow.xml b/idp-conf/src/main/resources/system/flows/authn/proxy-authn-flow.xml
new file mode 100644
index 0000000..ab83bfe
--- /dev/null
+++ b/idp-conf/src/main/resources/system/flows/authn/proxy-authn-flow.xml
@@ -0,0 +1,58 @@
+<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="authn.abstract">
+
+    <!-- This is a login flow for oroxied authentication implemented via protocol-specific subflows. -->
+
+    <on-start>
+        <set name="flowScope.protocolIterator" value="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.authn.Proxy.precedence').iterator()" />
+        <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.authn.Proxy.discoveryFunction') ? opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).setAuthenticatingAuthority(flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.authn.Proxy.discoveryFunction').apply(opensamlProfileRequestContext)) : null" />
+    </on-start>
+
+    <decision-state id="CheckDiscoveryDone">
+        <if test="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getAuthenticatingAuthority() == null"
+            then="DoDiscovery"
+            else="SelectProxyFlow" />
+    </decision-state>
+
+    <view-state id="DoDiscovery" view="externalRedirect:#{DiscoveryProfileRequestFunction.apply(new net.shibboleth.utilities.java.support.collection.Pair(flowRequestContext, opensamlProfileRequestContext))}">
+        <transition on="proceed" to="ExtractDiscoveryResponse" />
+    </view-state>
+    
+    <action-state id="ExtractDiscoveryResponse">
+        <evaluate expression="ExtractDiscoveryResponse" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="SelectProxyFlow" />
+        <transition to="ReselectFlow" />
+    </action-state>
+    
+    <decision-state id="SelectProxyFlow">
+        <if test="protocolIterator.hasNext()" then="CallProxyFlow" else="ReselectFlow" />
+    </decision-state>
+    
+    <subflow-state id="CallProxyFlow" subflow="proxy/#{protocolIterator.next()}">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="ContinueSuccessfulAuthentication" />
+        <transition on="ReselectFlow" to="SelectProxyFlow" />
+    </subflow-state>
+
+    <action-state id="ContinueSuccessfulAuthentication">
+        <evaluate expression="PopulateSubjectCanonicalizationContext" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="CallSubjectCanonicalization" />
+    </action-state>
+
+    <!-- This runs a c14n step on the result of the authentication. -->
+    <subflow-state id="CallSubjectCanonicalization" subflow="c14n">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="proceed" to="proceed" />
+        
+        <!-- This shouldn't generally happen, but if c14n fails, it's allowable to fall through. -->
+        <transition on="SubjectCanonicalizationError" to="ReselectFlow" />
+    </subflow-state>
+
+    <bean-import resource="proxy-authn-beans.xml" />
+</flow>
diff --git a/idp-conf/src/main/resources/system/flows/authn/proxy/saml-proxy-beans.xml b/idp-conf/src/main/resources/system/flows/authn/proxy/saml-proxy-beans.xml
new file mode 100644
index 0000000..547120c
--- /dev/null
+++ b/idp-conf/src/main/resources/system/flows/authn/proxy/saml-proxy-beans.xml
@@ -0,0 +1,221 @@
+<?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 class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"
+        p:placeholderPrefix="%{" p:placeholderSuffix="}" />
+
+    <bean class="net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor" />
+
+    <bean id="shibboleth.authn.SAML.externalAuthnPath" class="java.lang.String"
+        c:_0="servletRelative:%{idp.authn.saml.externalAuthnPath:/Authn/SAML2/POST/SSO}" />
+
+    <!-- Default strategy function to obtain the external path. -->
+    <bean id="shibboleth.authn.SAML.externalAuthnPathStrategy" parent="shibboleth.Functions.Constant"
+        c:target-ref="shibboleth.authn.SAML.externalAuthnPath" />
+
+<!--     <import resource="../../../../conf/authn/proxy/saml-proxy-config.xml" /> -->
+    
+    <!-- Parent beans for indirecting into nested PRC. -->
+    
+    <bean id="NestedWebFlowMessageHandlerAdaptor" abstract="true"
+        class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext" />
+
+    <bean id="NestedWebFlowProfileActionAdaptor" abstract="true"
+        class="net.shibboleth.idp.profile.impl.WebFlowProfileActionAdaptor" scope="prototype"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext" />
+        
+    <bean id="ParentAuthenticiationContextLookup" class="org.opensaml.messaging.context.navigate.ParentContextLookup"
+        c:type="net.shibboleth.idp.authn.context.AuthenticationContext" />
+    
+
+    <!-- Action beans. -->
+
+    <!-- Explicitly wrapped by a non-nested action adaptor. -->
+    <bean id="InitializeProxyProfileRequestContext"
+        class="net.shibboleth.idp.authn.proxy.impl.InitializeProxyProfileRequestContext"
+        p:profileId="#{T(net.shibboleth.idp.saml.saml2.profile.config.BrowserSSOProfileConfiguration).PROFILE_ID}"
+        p:loggingId="%{idp.service.logging.saml2sso:SSO}"
+        p:browserProfile="true" />
+
+    <bean id="FlowStartPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:fieldExtractors="#{getObject('shibboleth.FlowStartAuditExtractors') ?: getObject('shibboleth.DefaultFlowStartAuditExtractors')}" />
+
+    <!-- Declares audit field extractor functions injected into beans that populate the audit context. -->
+    <import resource="../../../conf/audit-system.xml" />
+
+    <bean id="PrepareInboundMessageContext"
+            class="net.shibboleth.idp.saml.session.impl.PrepareInboundMessageContext" scope="prototype"
+            p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext">
+        <property name="relyingPartyLookupStrategy">
+           <bean parent="shibboleth.Functions.Compose" c:f-ref="ParentAuthenticiationContextLookup">
+               <constructor-arg name="g">
+                   <bean parent="shibboleth.Functions.Expression" c:expression="#input.getAuthenticatingAuthority()" />
+               </constructor-arg>
+           </bean>
+        </property>
+    </bean>
+
+    <bean id="SAMLProtocolAndRole" parent="NestedWebFlowMessageHandlerAdaptor" c:executionDirection="INBOUND">
+        <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="NestedWebFlowMessageHandlerAdaptor" c:executionDirection="INBOUND">
+        <constructor-arg name="messageHandler">
+            <bean class="org.opensaml.saml.common.binding.impl.SAMLMetadataLookupHandler" scope="prototype">
+                <property name="roleDescriptorResolver">
+                    <bean class="org.opensaml.saml.metadata.resolver.impl.PredicateRoleDescriptorResolver"
+                        c:mdResolver-ref="shibboleth.MetadataResolver" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+
+    <bean id="InitializeRelyingPartyContextFromSAMLPeer"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext" />
+
+    <bean id="SelectRelyingPartyConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+        p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyConfigurationResolver" />
+
+    <bean id="PostLookupPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+        p:fieldExtractors="#{getObject('shibboleth.PostLookupAuditExtractors') ?: getObject('shibboleth.DefaultPostLookupAuditExtractors')}" />
+
+    <bean id="SelectProfileConfiguration"
+        class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext" />
+
+    <bean id="InitializeOutboundMessageContext"
+        class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext" />
+
+    <bean id="InitializeMessageChannelSecurityContext" parent="NestedWebFlowProfileActionAdaptor" scope="prototype">
+        <constructor-arg>
+            <bean class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity"
+                p:confidentialityActive="false" p:integrityActive="false" />
+        </constructor-arg>
+    </bean>
+
+    <util:constant id="shibboleth.EndpointType"
+        static-field="org.opensaml.saml.saml2.metadata.SingleSignOnService.DEFAULT_ELEMENT_NAME" />
+
+    <bean id="shibboleth.OutgoingBindingsLookupStrategy"
+        class="net.shibboleth.utilities.java.support.logic.FunctionSupport" factory-method="constant"
+        c:_0-ref="shibboleth.OutgoingSAML2SSORequestBindings" />
+
+    <bean id="PopulateBindingAndEndpointContexts"
+        class="net.shibboleth.idp.saml.profile.impl.PopulateBindingAndEndpointContexts" scope="prototype"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+        p:endpointResolver-ref="shibboleth.EndpointResolver"
+        p:endpointType-ref="shibboleth.EndpointType"
+        p:bindingDescriptorsLookupStrategy-ref="shibboleth.OutgoingBindingsLookupStrategy"
+        p:artifactImpliesSecureChannel="%{idp.artifact.secureChannel:true}" />
+
+    <bean id="PopulateRequestSignatureSigningParameters" parent="NestedWebFlowProfileActionAdaptor" scope="prototype">
+        <constructor-arg>
+            <bean class="org.opensaml.saml.common.profile.impl.PopulateSignatureSigningParameters"
+                    p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+                    p:signatureSigningParametersResolver-ref="shibboleth.SignatureSigningParametersResolver"
+                    p:noResultIsError="false">
+                <property name="activationCondition">
+                    <bean class="net.shibboleth.idp.saml.profile.config.logic.SignRequestsPredicate" />
+                </property>
+            </bean>
+        </constructor-arg>
+    </bean>
+    
+    <bean id="AddAuthnRequest"
+            class="net.shibboleth.idp.saml.saml2.profile.impl.AddAuthnRequest" scope="prototype"
+            p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+            p:overwriteExisting="true"
+            p:issuerLookupStrategy-ref="shibboleth.ResponderIdLookup.Simple">
+        <property name="identifierGeneratorLookupStrategy">
+            <bean class="net.shibboleth.idp.profile.config.navigate.IdentifierGenerationStrategyLookupFunction"
+                p:defaultIdentifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy" />
+        </property>
+    </bean>
+
+    <bean id="PostRequestPopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+        p:fieldExtractors="#{getObject('shibboleth.PostRequestAuditExtractors') ?: getObject('shibboleth.DefaultPostRequestAuditExtractors')}" />
+
+    <bean id="WriteAuditLog" class="net.shibboleth.idp.profile.audit.impl.WriteAuditLog" scope="prototype"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+        p:formattingMap-ref="shibboleth.AuditFormattingMap"
+        p:dateTimeFormat="#{getObject('shibboleth.AuditDateTimeFormat')}"
+        p:useDefaultTimeZone="#{getObject('shibboleth.AuditDefaultTimeZone') ?: false}"
+        p:httpServletRequest-ref="shibboleth.HttpServletRequest" />
+
+    <!-- These beans are stashed into the context tree to be picked up and run outside SWF by the SAML controller. -->
+    
+    <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.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler" scope="prototype"
+                        p:signErrorResponses="%{idp.errors.signed:true}">
+                    <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="org.opensaml.profile.action.impl.EncodeMessage" scope="prototype"
+        p:messageEncoderFactory-ref="messageEncoderFactory"
+        p:messageHandler="#{getObject('shibboleth.BindingSpecificMessageHandler')}" />
+
+    <!-- This is mostly a placeholder in case we get forced to support other bindings at some point. -->
+
+    <bean id="messageDecoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageDecoderFactory">
+        <property name="beanMappings">
+            <map>
+                <entry key="POST" value="SAMLAuthnPostDecoder" />
+            </map>
+        </property>
+    </bean>
+
+    <bean id="SAMLAuthnPostDecoder"
+        class="org.opensaml.saml.saml2.binding.decoding.impl.HTTPPostDecoder" scope="prototype" init-method=""
+        p:parserPool-ref="shibboleth.ParserPool"
+        p:httpServletRequest-ref="shibboleth.HttpServletRequest"
+        p:bindingDescriptor-ref="shibboleth.Binding.SAML2POST" />
+
+    <bean id="ValidateSAMLAuthentication"
+        class="net.shibboleth.idp.saml.saml2.profile.impl.ValidateSAMLAuthentication" scope="prototype"
+        p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup"
+        p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext" />
+
+</beans>
diff --git a/idp-conf/src/main/resources/system/flows/authn/proxy/saml-proxy-flow.xml b/idp-conf/src/main/resources/system/flows/authn/proxy/saml-proxy-flow.xml
new file mode 100644
index 0000000..9991204
--- /dev/null
+++ b/idp-conf/src/main/resources/system/flows/authn/proxy/saml-proxy-flow.xml
@@ -0,0 +1,60 @@
+<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="authn.abstract">
+
+    <!-- This is a login flow for oroxied authentication implemented via SAML 2.0. -->
+
+    <action-state id="IssueRequest">
+        <evaluate expression="InitializeProxyProfileRequestContext" />
+        <evaluate expression="FlowStartPopulateAuditContext" />
+        
+        <evaluate expression="PrepareInboundMessageContext" />
+        <evaluate expression="SAMLProtocolAndRole" />
+        <evaluate expression="SAMLMetadataLookup" />
+
+        <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+
+        <evaluate expression="PostLookupPopulateAuditContext" />
+        
+        <evaluate expression="SelectProfileConfiguration" />
+        
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="InitializeMessageChannelSecurityContext" />
+        <evaluate expression="PopulateBindingAndEndpointContexts" />
+
+        <evaluate expression="PopulateRequestSignatureSigningParameters" />
+        
+        <evaluate expression="AddAuthnRequest" />
+        <evaluate expression="PostRequestPopulateAuditContext" />
+        <evaluate expression="WriteAuditLog" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="EncodeMessage" />
+    </action-state>
+    
+    <view-state id="EncodeMessage" view="externalRedirect:#{T(net.shibboleth.idp.authn.ExternalAuthentication).getExternalRedirect(flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.authn.SAML.externalAuthnPathStrategy').apply(opensamlProfileRequestContext) + '/start', flowExecutionContext.getKey().toString())}">
+        <on-render>
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).addSubcontext(new net.shibboleth.idp.authn.context.ExternalAuthenticationContext(new net.shibboleth.idp.authn.impl.ExternalAuthenticationImpl(false)), true).setFlowExecutionUrl(flowExecutionUrl + '&_eventId_proceed=1')" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).addSubcontext(new net.shibboleth.idp.saml.saml2.profile.impl.SAMLAuthnContext(EncodeMessage, messageDecoderFactory))" result="flowScope.samlContext" />
+            <evaluate expression="flowScope.samlContext.setOutboundMessageHandler(PreEncodeMessageHandler)" />
+        </on-render>
+
+        <transition on="proceed" to="ProcessResponse" />
+    </view-state>
+    
+    <action-state id="ProcessResponse">
+        <evaluate expression="ValidateSAMLAuthentication" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="proceed" />
+    </action-state>
+
+    <global-transitions>
+        <!-- Route everything out as a graceful failure to allow subsequent options to run. -->
+        <transition on="#{!'proceed'.equals(currentEvent.id)}" to="ReselectFlow" />
+    </global-transitions>
+
+    <bean-import resource="saml-proxy-beans.xml" />
+</flow>
diff --git a/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml b/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml
index 56cf2af..9d54e5e 100644
--- a/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml
+++ b/idp-conf/src/main/resources/system/flows/logout/propagation/saml2-flow.xml
@@ -12,7 +12,7 @@
         <evaluate expression="SAMLProtocolAndRole" />
         <evaluate expression="SAMLMetadataLookup" />
 
-        <evaluate expression="InitializeRelyingPartyContexFromSAMLPeer" />
+        <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
         <evaluate expression="SelectRelyingPartyConfiguration" />
 
         <evaluate expression="PostLookupPopulateAuditContext" />
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml-abstract-beans.xml b/idp-conf/src/main/resources/system/flows/saml/saml-abstract-beans.xml
index 4cf6c8b..2ce289e 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml-abstract-beans.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml-abstract-beans.xml
@@ -89,7 +89,7 @@
             class="net.shibboleth.idp.saml.saml2.profile.impl.MapRequestedAttributesInAttributeConsumingService"
             p:transcoderRegistry-ref="shibboleth.AttributeRegistryService" />
 
-    <bean id="InitializeRelyingPartyContexFromSAMLPeer"
+    <bean id="InitializeRelyingPartyContextFromSAMLPeer"
         class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype" />
 
     <bean id="SelectRelyingPartyConfiguration"
diff --git a/idp-conf/src/main/resources/system/flows/saml/saml-abstract-flow.xml b/idp-conf/src/main/resources/system/flows/saml/saml-abstract-flow.xml
index b1e3b22..0ded5b7 100644
--- a/idp-conf/src/main/resources/system/flows/saml/saml-abstract-flow.xml
+++ b/idp-conf/src/main/resources/system/flows/saml/saml-abstract-flow.xml
@@ -20,7 +20,7 @@
         <evaluate expression="SAMLPopulateAttributeConsumingService" />
         <evaluate expression="MapRequestedAttributesInAttributeConsumingService" />
 
-        <evaluate expression="InitializeRelyingPartyContexFromSAMLPeer" />
+        <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
         <evaluate expression="SelectRelyingPartyConfiguration" />
 
         <evaluate expression="PostLookupPopulateAuditContext" />
diff --git a/idp-profile-api/src/main/java/net/shibboleth/idp/relyingparty/RelyingPartyConfiguration.java b/idp-profile-api/src/main/java/net/shibboleth/idp/relyingparty/RelyingPartyConfiguration.java
index f1c7ff5..2559014 100644
--- a/idp-profile-api/src/main/java/net/shibboleth/idp/relyingparty/RelyingPartyConfiguration.java
+++ b/idp-profile-api/src/main/java/net/shibboleth/idp/relyingparty/RelyingPartyConfiguration.java
@@ -82,7 +82,7 @@ public class RelyingPartyConfiguration extends AbstractIdentifiableInitializable
      * @return ID to use when responding
      */
     @Nonnull @NotEmpty public String getResponderId(@Nullable final ProfileRequestContext profileRequestContext) {
-        return Constraint.isNotNull(responderIdLookupStrategy.apply(profileRequestContext),
+        return Constraint.isNotEmpty(responderIdLookupStrategy.apply(profileRequestContext),
                 "ResponderId cannot be null");
     }
 
diff --git a/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/impl/WebFlowProfileActionAdaptor.java b/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/impl/WebFlowProfileActionAdaptor.java
index 563a2e1..37818f7 100644
--- a/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/impl/WebFlowProfileActionAdaptor.java
+++ b/idp-profile-impl/src/main/java/net/shibboleth/idp/profile/impl/WebFlowProfileActionAdaptor.java
@@ -33,7 +33,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
  */
 @ThreadSafe
 public class WebFlowProfileActionAdaptor extends AbstractProfileAction {
-
+    
     /** A POJO bean being adapted.  */
     @Nonnull private final ProfileAction action;
     
@@ -48,7 +48,7 @@ public class WebFlowProfileActionAdaptor extends AbstractProfileAction {
 
     /** {@inheritDoc} */
     @Override
-    @Nonnull public void execute(@Nonnull final ProfileRequestContext profileRequestContext) {
+    public void execute(@Nonnull final ProfileRequestContext profileRequestContext) {
         action.execute(profileRequestContext);
     }
 
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/AbstractSAMLProfileConfiguration.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/AbstractSAMLProfileConfiguration.java
index d5e1da6..9ea38aa 100644
--- a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/AbstractSAMLProfileConfiguration.java
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/profile/config/AbstractSAMLProfileConfiguration.java
@@ -57,7 +57,7 @@ public abstract class AbstractSAMLProfileConfiguration extends AbstractCondition
     /** 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 true. */
+    /** Predicate used to determine if the generated response should be signed. Default returns false. */
     @Nonnull private Predicate<ProfileRequestContext> signResponsesPredicate;
 
     /** Predicate used to determine if the generated assertion should be signed. Default returns false. */
diff --git a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
index 1e37e34..8e79e42 100644
--- a/idp-saml-api/src/main/java/net/shibboleth/idp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
+++ b/idp-saml-api/src/main/java/net/shibboleth/idp/saml/saml2/profile/config/BrowserSSOProfileConfiguration.java
@@ -43,6 +43,7 @@ import net.shibboleth.utilities.java.support.logic.FunctionSupport;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
 import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
 
 import com.google.common.base.Predicates;
 import com.google.common.collect.Collections2;
@@ -83,6 +84,9 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2ArtifactAwarePr
     /** Lookup function to supply maximum delegation chain length. */
     @Nonnull private Function<ProfileRequestContext,Long> maximumTokenDelegationChainLengthLookupStrategy;
 
+    /** Lookup function for requested AC operator. */
+    @Nonnull private Function<ProfileRequestContext,String> authnContextComparisonLookupStrategy;
+    
     /** Lookup function to supply default authentication methods. */
     @Nonnull private Function<ProfileRequestContext,Collection<AuthnContextClassRefPrincipal>>
             defaultAuthenticationContextsLookupStrategy;
@@ -119,6 +123,7 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2ArtifactAwarePr
         allowDelegationPredicate = Predicates.alwaysFalse();
         authenticationFlowsLookupStrategy = FunctionSupport.constant(null);
         postAuthenticationFlowsLookupStrategy = FunctionSupport.constant(null);
+        authnContextComparisonLookupStrategy = FunctionSupport.constant(null);
         defaultAuthenticationContextsLookupStrategy = FunctionSupport.constant(null);
         nameIDFormatPrecedenceLookupStrategy = FunctionSupport.constant(null);
     }
@@ -357,6 +362,51 @@ public class BrowserSSOProfileConfiguration extends AbstractSAML2ArtifactAwarePr
         maximumTokenDelegationChainLengthLookupStrategy =
                 Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
     }
+    
+    /**
+     * Get the comparison operator to use when issuing SAML requests containing requested context classes.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return comparison value or null
+     * 
+     * @since 4.0.0
+     */
+    @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
+     * 
+     * @since 4.0.0
+     */
+    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<Principal> getDefaultAuthenticationMethods(
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageDecoderFactory.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageDecoderFactory.java
new file mode 100644
index 0000000..0b184b9
--- /dev/null
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageDecoderFactory.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.saml.profile.impl;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+import org.opensaml.messaging.decoder.MessageDecoder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+
+/**
+ * A function that returns the correct {@link MessageDecoder} to use based on a simple map of
+ * strings to bean IDs.
+ */
+ at ThreadSafeAfterInit
+public class SpringAwareMessageDecoderFactory extends AbstractInitializableComponent
+        implements Function<String,MessageDecoder>, ApplicationContextAware {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SpringAwareMessageDecoderFactory.class);
+    
+    /** Application context injected by surroundings. */
+    @Nullable private ApplicationContext applicationContext;
+    
+    /** Map of strings to bean IDs. */
+    @Nonnull private Map<String,String> beanMappings;
+    
+    /** Constructor. */
+    public SpringAwareMessageDecoderFactory() {
+        beanMappings = Collections.emptyMap();
+    }
+    
+    /**
+     * Set mappings of strings to names of {@link MessageDecoder} beans.
+     * 
+     * @param mappings string to bean ID mappings
+     */
+    public void setBeanMappings(@Nonnull final Map<String,String> mappings) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        Constraint.isNotNull(mappings, "Mappings cannot be null");
+        
+        beanMappings = new HashMap<>(mappings.size());
+        
+        for (final Map.Entry<String,String> entry : mappings.entrySet()) {
+            final String key = StringSupport.trimOrNull(entry.getKey());
+            final String value = StringSupport.trimOrNull(entry.getValue());
+            if (key != null && value != null) {
+                beanMappings.put(key, value);
+            }
+        }
+    }
+    
+    /** {@inheritDoc} */
+    public void setApplicationContext(@Nullable final ApplicationContext context) {
+        applicationContext = context;
+    }
+    
+    /** {@inheritDoc} */
+    @Nullable public MessageDecoder apply(@Nullable final String input) {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        
+        final String beanID = beanMappings.get(StringSupport.trimOrNull(input));
+        
+        if (applicationContext == null) {
+            log.warn("No Spring ApplicationContext set");
+            return null;
+        } else if (beanID == null) {
+            log.warn("No bean ID associated with input value {}", input);
+            return null;
+        }
+        
+        log.debug("Looking up message decoder with bean ID: {}", beanID);
+        
+        try {
+            return applicationContext.getBean(beanID, MessageDecoder.class);
+        } catch (final BeansException e) {
+            log.warn("Error instantiating message decoder from bean ID {}", beanID, e);
+        }
+        
+        return null;
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageEncoderFactory.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageEncoderFactory.java
index 77bb0ff..b34e8f7 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageEncoderFactory.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/profile/impl/SpringAwareMessageEncoderFactory.java
@@ -23,6 +23,9 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.shibboleth.idp.saml.binding.BindingDescriptor;
+import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
 
 import org.opensaml.messaging.encoder.MessageEncoder;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -37,7 +40,8 @@ import org.springframework.context.ApplicationContextAware;
 /**
  * A function that returns the correct {@link MessageEncoder} to use based on an underlying {@link BindingDescriptor}.
  */
-public class SpringAwareMessageEncoderFactory
+ at ThreadSafeAfterInit
+public class SpringAwareMessageEncoderFactory extends AbstractInitializableComponent
         implements Function<ProfileRequestContext,MessageEncoder>, ApplicationContextAware {
 
     /** Class logger. */
@@ -53,6 +57,7 @@ public class SpringAwareMessageEncoderFactory
     
     /** {@inheritDoc} */
     @Nullable public MessageEncoder apply(@Nullable final ProfileRequestContext profileRequestContext) {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
         
         if (applicationContext == null) {
             log.warn("No Spring ApplicationContext set");
@@ -86,4 +91,4 @@ public class SpringAwareMessageEncoderFactory
         return null;
     }
     
-}
\ No newline at end of file
+}
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnRequest.java
similarity index 61%
copy from idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java
copy to idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnRequest.java
index bcd6666..b50a733 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/AddAuthnRequest.java
@@ -15,16 +15,20 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.saml.session.impl;
+package net.shibboleth.idp.saml.saml2.profile.impl;
 
 import java.time.Instant;
+import java.util.List;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import net.shibboleth.idp.saml.session.SAML2SPSession;
-import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+import net.shibboleth.idp.saml.saml2.profile.config.BrowserSSOProfileConfiguration;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
@@ -32,69 +36,64 @@ import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifie
 
 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.AbstractProfileAction;
+import org.opensaml.messaging.context.navigate.ParentContextLookup;
 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.saml2.core.AuthnRequest;
 import org.opensaml.saml.saml2.core.Issuer;
-import org.opensaml.saml.saml2.core.LogoutRequest;
-import org.opensaml.saml.saml2.core.NameID;
-import org.opensaml.saml.saml2.core.SessionIndex;
+import org.opensaml.saml.saml2.core.NameIDPolicy;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Action that creates a {@link LogoutRequest} based on an {@link SAML2SPSession} in a
- * {@link LogoutPropagationContext} and sets it as the message returned by
+ * 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. The outbound relay state is also
+ * set to the flow execution key.</p>
+ * 
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_MSG_CTX}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
  * @event {@link EventIds#MESSAGE_PROC_ERROR}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
  * 
  * @post ProfileRequestContext.getOutboundMessageContext().getMessage() != null
  */
-public class AddLogoutRequest extends AbstractProfileAction {
+public class AddAuthnRequest extends AbstractAuthenticationAction {
 
     /** Class logger. */
-    @Nonnull private Logger log = LoggerFactory.getLogger(AddLogoutRequest.class);
+    @Nonnull private Logger log = LoggerFactory.getLogger(AddAuthnRequest.class);
     
     /** Overwrite an existing message? */
     private boolean overwriteExisting;
 
-    /** Include SessionIndex in the request? */
-    private boolean includeSessionIndex;
-
     /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
     @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
 
+    /** Strategy used to obtain the relay state token to provide. */
+    @Nonnull private Function<ProfileRequestContext,String> relayStateLookupStrategy;
+    
     /** Strategy used to obtain the response issuer value. */
     @Nullable private Function<ProfileRequestContext,String> issuerLookupStrategy;
-
-    /** Logout propagation context lookup strategy. */
-    @Nonnull private Function<ProfileRequestContext,LogoutPropagationContext> logoutPropContextLookupStrategy;
     
     /** The generator to use. */
     @Nullable private IdentifierGenerationStrategy idGenerator;
-
-    /** The {@link SAML2SPSession} to base the inbound context on. */
-    @Nullable private SAML2SPSession saml2Session;
+    
+    /** Applicable profile configuration. */
+    @Nullable private BrowserSSOProfileConfiguration profileConfiguration;
 
     /** EntityID to populate into Issuer element. */
     @Nullable private String issuerId;
     
     /** Constructor. */
-    public AddLogoutRequest() {
+    public AddAuthnRequest() {
         // Default strategy is a 16-byte secure random source.
         idGeneratorLookupStrategy = new Function<>() {
             public IdentifierGenerationStrategy apply(final ProfileRequestContext input) {
@@ -102,8 +101,8 @@ public class AddLogoutRequest extends AbstractProfileAction {
             }
         };
         
-        logoutPropContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
-        includeSessionIndex = true;
+        // Fool the parent class into looking above instead of below the PRC for the context.
+        setAuthenticationContextLookupStrategy(new ParentContextLookup<>(AuthenticationContext.class));
     }
     
     /**
@@ -118,17 +117,6 @@ public class AddLogoutRequest extends AbstractProfileAction {
     }
 
     /**
-     * Set whether to include a SessionIndex in the request.
-     * 
-     * @param flag flag to set
-     */
-    public void setIncludeSessionIndex(final boolean flag) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        
-        includeSessionIndex = flag;
-    }
-
-    /**
      * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
      * 
      * @param strategy lookup strategy
@@ -140,37 +128,47 @@ public class AddLogoutRequest extends AbstractProfileAction {
         idGeneratorLookupStrategy =
                 Constraint.isNotNull(strategy, "IdentifierGenerationStrategy lookup strategy cannot be null");
     }
-
+    
     /**
-     * Set the strategy used to locate the issuer value to use.
+     * Set the strategy used to obtain the RelayState value to supply for flow restoration.
      * 
      * @param strategy lookup strategy
      */
-    public void setIssuerLookupStrategy(@Nullable final Function<ProfileRequestContext,String> strategy) {
+    public void setRelayStateLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
-        issuerLookupStrategy = strategy;
+        
+        relayStateLookupStrategy = Constraint.isNotNull(strategy, "RelayState lookup srategy cannot be null");
     }
 
     /**
-     * Set the logout propagation context lookup strategy.
+     * Set the strategy used to locate the issuer value to use.
      * 
      * @param strategy lookup strategy
      */
-    public void setLogoutPropagationContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,LogoutPropagationContext> strategy) {
+    public void setIssuerLookupStrategy(@Nullable final Function<ProfileRequestContext,String> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
-        logoutPropContextLookupStrategy =
-                Constraint.isNotNull(strategy, "LogoutPropagationContext lookup strategy cannot be null");
+        issuerLookupStrategy = strategy;
     }
 
-// Checkstyle: CyclomaticComplexity OFF 
+// Checkstyle: CyclomaticComplexity OFF
     /** {@inheritDoc} */
     @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
 
-        if (!super.doPreExecute(profileRequestContext)) {
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            return false;
+        }
+        
+        final RelyingPartyContext rpCtx = profileRequestContext.getSubcontext(RelyingPartyContext.class);
+        if (rpCtx != null && rpCtx.getConfiguration() != null &&
+                rpCtx.getProfileConfig() instanceof BrowserSSOProfileConfiguration) {
+            profileConfiguration = (BrowserSSOProfileConfiguration) rpCtx.getProfileConfig();
+        }
+        if (profileConfiguration == null) {
+            log.error("{} BrowserSSOProfileConfiguration not found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
             return false;
         }
         
@@ -180,7 +178,7 @@ public class AddLogoutRequest extends AbstractProfileAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return false;
         } else if (!overwriteExisting && outboundMessageCtx.getMessage() != null) {
-            log.debug("{} Outbound message context already contains a response", getLogPrefix());
+            log.debug("{} Outbound message context already contains a message", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return false;
         }
@@ -196,54 +194,34 @@ public class AddLogoutRequest extends AbstractProfileAction {
             issuerId = issuerLookupStrategy.apply(profileRequestContext);
         }
 
-        final LogoutPropagationContext logoutPropCtx = logoutPropContextLookupStrategy.apply(profileRequestContext);
-        if (logoutPropCtx == null) {
-            log.debug("{} No logout propagation context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        } else if (logoutPropCtx.getSession() == null || !(logoutPropCtx.getSession() instanceof SAML2SPSession)) {
-            log.debug("{} Logout propgation context did not contain a SAML2SPSession", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
-        
-        saml2Session = (SAML2SPSession) logoutPropCtx.getSession();
-        if (saml2Session.getId() == null) {
-            log.debug("{} SAML2SPSession in logout propagation context did not contain a service ID", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
-        
         outboundMessageCtx.setMessage(null);
         
         return true;
     }
+// Checkstyle: CyclomaticComplexity ON
 
     /** {@inheritDoc} */
     @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
 
+        log.debug("{} Building AuthnRequest for upstream IdP ({})", getLogPrefix(),
+                authenticationContext.getAuthenticatingAuthority());
+        
         final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
-        final SAMLObjectBuilder<LogoutRequest> requestBuilder =
-                (SAMLObjectBuilder<LogoutRequest>) bf.<LogoutRequest>getBuilderOrThrow(
-                        LogoutRequest.DEFAULT_ELEMENT_NAME);
+        final SAMLObjectBuilder<AuthnRequest> requestBuilder =
+                (SAMLObjectBuilder<AuthnRequest>) bf.<AuthnRequest>getBuilderOrThrow(
+                        AuthnRequest.DEFAULT_ELEMENT_NAME);
+        final SAMLObjectBuilder<NameIDPolicy> nipBuilder =
+                (SAMLObjectBuilder<NameIDPolicy>) bf.<NameIDPolicy>getBuilderOrThrow(
+                        NameIDPolicy.DEFAULT_ELEMENT_NAME);
 
-        final LogoutRequest object = requestBuilder.buildObject();
+        final AuthnRequest object = requestBuilder.buildObject();
         
         object.setID(idGenerator.generateIdentifier());
         object.setIssueInstant(Instant.now());
         object.setVersion(SAMLVersion.VERSION_20);
 
-        try {
-            final NameID nameId = XMLObjectSupport.cloneXMLObject(saml2Session.getNameID());
-            object.setNameID(nameId);
-        } catch (final MarshallingException|UnmarshallingException e) {
-            log.error("{} Error cloning NameID for use in LogoutRequest for {}", getLogPrefix(),
-                    saml2Session.getId(), e);
-            ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
-            return;
-        }
-
         if (issuerId != null) {
             log.debug("{} Setting Issuer to {}", getLogPrefix(), issuerId);
             final SAMLObjectBuilder<Issuer> issuerBuilder =
@@ -255,17 +233,31 @@ public class AddLogoutRequest extends AbstractProfileAction {
             log.debug("{} No issuer value available, leaving Issuer unset", getLogPrefix());
         }
         
-        if (includeSessionIndex) {
-            final SAMLObjectBuilder<SessionIndex> indexBuilder =
-                    (SAMLObjectBuilder<SessionIndex>) bf.<SessionIndex>getBuilderOrThrow(
-                            SessionIndex.DEFAULT_ELEMENT_NAME);
-            final SessionIndex index = indexBuilder.buildObject();
-            index.setSessionIndex(saml2Session.getSessionIndex());
-            object.getSessionIndexes().add(index);
+        // ForceAuthn may come from request or config.
+        if (authenticationContext.isForceAuthn() || profileConfiguration.isForceAuthn(profileRequestContext)) {
+            log.debug("{} Setting ForceAuthn for SAML AuthnRequest", getLogPrefix());
+            object.setForceAuthn(true);
+        }
+        
+        // Only set passive based on request.
+        if (authenticationContext.isPassive()) {
+            log.debug("{} Setting IsPassive for SAML AuthnRequest", getLogPrefix());
+            object.setIsPassive(true);
+        }
+        
+        final NameIDPolicy nip = nipBuilder.buildObject();
+        nip.setAllowCreate(true);
+        
+        // TODO: use metadata for NameID Formats too?
+        final List<String> formats = profileConfiguration.getNameIDFormatPrecedence(profileRequestContext);
+        if (!formats.isEmpty()) {
+            log.debug("{} Setting NameIDPolicy Format to '{}' for SAML AuthnRequest", getLogPrefix(), formats.get(0));
+            nip.setFormat(formats.get(0));
         }
         
+        object.setNameIDPolicy(nip);
+        
         profileRequestContext.getOutboundMessageContext().setMessage(object);
     }
-// Checkstyle: CyclomaticComplexity ON
     
 }
\ No newline at end of file
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SAMLAuthnContext.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SAMLAuthnContext.java
new file mode 100644
index 0000000..9ee5dc1
--- /dev/null
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SAMLAuthnContext.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.saml.saml2.profile.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+import org.opensaml.messaging.decoder.MessageDecoder;
+import org.opensaml.messaging.handler.MessageHandler;
+import org.opensaml.profile.action.ProfileAction;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Manages state for the webflow -> external -> webflow transitions
+ * during proxied SAML authentication.
+ * 
+ * @since 4.0.0
+ */
+public class SAMLAuthnContext extends BaseContext {
+
+    /** Outbound message handler to run prior to encoding. */
+    @Nullable private MessageHandler outboundMessageHandler;
+
+    /** Profile action to execute to produce outbound message response. */
+    @Nonnull private ProfileAction encodeMessageAction;
+
+    /** The function to use to obtain a decoder. */
+    @Nonnull private Function<String,MessageDecoder> decoderFactory;
+    
+    /**
+     * Constructor.
+     *
+     * @param action message-encoding profile action
+     * @param factory factory function to obtain decoders
+     */
+    public SAMLAuthnContext(@Nonnull final ProfileAction action,
+            @Nonnull final Function<String,MessageDecoder> factory) {
+        encodeMessageAction = Constraint.isNotNull(action, "Profile action cannot be null");
+        decoderFactory = Constraint.isNotNull(factory, "MessageDecoder factory cannot be null");
+    }
+    
+    /**
+     * Get the message-encoding profile action.
+     * 
+     * @return profile action
+     */
+    @Nonnull public ProfileAction getEncodeMessageAction() {
+        return encodeMessageAction;
+    }
+    
+    /**
+     * Get the factory function to obtain message decoders.
+     * 
+     * @return factory function
+     */
+    @Nonnull public Function<String,MessageDecoder> getMessageDecoderFactory() {
+        return decoderFactory;
+    }
+    
+    /**
+     * Get the outbound {@link MessageHandler} to run prior to encoding.
+     * 
+     * @return the outbound {@link MessageHandler}
+     */
+    @Nullable public MessageHandler getOutboundMessageHandler() {
+        return outboundMessageHandler;
+    }
+
+    /**
+     * Set the outbound {@link MessageHandler} to run prior to encoding.
+     * 
+     * @param handler outbound {@link MessageHandler} to set
+     * 
+     * @return this context
+     */
+    @Nonnull public SAMLAuthnContext setOutboundMessageHandler(@Nullable final MessageHandler handler) {
+        outboundMessageHandler = handler;
+        
+        return this;
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SAMLAuthnController.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SAMLAuthnController.java
new file mode 100644
index 0000000..99326bd
--- /dev/null
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/SAMLAuthnController.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.saml.saml2.profile.impl;
+
+import java.io.IOException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.shibboleth.idp.authn.ExternalAuthentication;
+import net.shibboleth.idp.authn.ExternalAuthenticationException;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.decoder.MessageDecoder;
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.EventContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.binding.SAMLBindingSupport;
+import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+/**
+ * MVC controller that handles outbound and inbound message I/O for
+ * proxied SAML authentication.
+ * 
+ * <p>Outbound messaging is necessary to ensure webflow hygiene with respect to
+ * flow state, and inbound messaging is necessary to ensure a fixed URL for
+ * SAML endpoint management.</p>
+ * 
+ * @since 4.0.0
+ */
+ at Controller
+ at RequestMapping("%{idp.authn.saml.externalAuthnPath:/Authn/SAML2}")
+public class SAMLAuthnController extends AbstractInitializableComponent {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(SAMLAuthnController.class);
+
+    /** Lookup strategy to locate the nested ProfileRequestContext. */
+    @Nonnull private Function<ProfileRequestContext,ProfileRequestContext> profileRequestContextLookupStrategy;
+
+    /** Lookup strategy to locate the SAML context. */
+    @Nonnull private Function<ProfileRequestContext,SAMLAuthnContext> samlContextLookupStrategy;
+    
+    /** Constructor. */
+    public SAMLAuthnController() {
+        // PRC -> AC -> nested PRC
+        profileRequestContextLookupStrategy = new ChildContextLookup<>(ProfileRequestContext.class).compose(
+                new ChildContextLookup<>(AuthenticationContext.class));
+        
+        // PRC -> AC -> SAMLAuthnContext
+        samlContextLookupStrategy = new ChildContextLookup<>(SAMLAuthnContext.class).compose(
+                new ChildContextLookup<>(AuthenticationContext.class));
+    }
+    
+    /**
+     * Set the lookup strategy used to locate the nested {@link ProfileRequestContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setProfileRequestContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,ProfileRequestContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        profileRequestContextLookupStrategy = Constraint.isNotNull(strategy,
+                "ProfileRequestContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the lookup strategy used to locate the {@link SAMLAuthnContext}.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSAMLAuthnContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,SAMLAuthnContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        samlContextLookupStrategy = Constraint.isNotNull(strategy, "SAMLAuthnContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Outbound initiation of the process, triggered with a fixed addition to the path.
+     * 
+     * @param httpRequest servlet request
+     * @param httpResponse servlet response
+     * @param binding a key for the eventual inbound binding
+     * 
+     * @throws ExternalAuthenticationException if an error occurs
+     * @throws IOException if an I/O error occurs
+     */
+    @GetMapping("/{binding}/SSO/start")
+    @Nullable public void startSAML(@Nonnull final HttpServletRequest httpRequest,
+            @Nonnull final HttpServletResponse httpResponse, @PathVariable @Nonnull @NotEmpty final String binding)
+                    throws ExternalAuthenticationException, IOException {
+        
+        final String key = ExternalAuthentication.startExternalAuthentication(httpRequest);
+        final ProfileRequestContext prc = ExternalAuthentication.getProfileRequestContext(key, httpRequest);
+        
+        final SAMLAuthnContext samlContext = samlContextLookupStrategy.apply(prc);
+        if (samlContext == null) {
+            log.error("SAMLAuthnContext not found");
+            httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, EventIds.INVALID_PROFILE_CTX);
+            ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+            return;
+        }
+        
+        final ProfileRequestContext nestedPRC = profileRequestContextLookupStrategy.apply(prc);
+        if (nestedPRC == null) {
+            log.error("Nested ProfileRequestContext not found");
+            httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, EventIds.INVALID_PROFILE_CTX);
+            ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+            return;
+        }
+        
+        // Fill in the AuthnRequest's ACS URL and set RelayState to the EA key.
+        if (nestedPRC.getOutboundMessageContext() != null &&
+                nestedPRC.getOutboundMessageContext().getMessage() instanceof AuthnRequest) {
+            SAMLBindingSupport.setRelayState(nestedPRC.getOutboundMessageContext(), key);
+            final StringBuffer url = httpRequest.getRequestURL();
+            ((AuthnRequest) nestedPRC.getOutboundMessageContext().getMessage()).setAssertionConsumerServiceURL(
+                    url.substring(0, url.lastIndexOf("/start")));
+        } else {
+            log.error("Outbound AuthnContext message not found");
+            httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, EventIds.INVALID_MESSAGE);
+            ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+            return;
+        }
+        
+        try {
+            if (samlContext.getOutboundMessageHandler() != null) {
+                samlContext.getOutboundMessageHandler().invoke(nestedPRC.getOutboundMessageContext());
+            }
+            
+            samlContext.getEncodeMessageAction().execute(nestedPRC);
+            final EventContext eventCtx = prc.getSubcontext(EventContext.class);
+            if (eventCtx != null && eventCtx.getEvent() != null
+                    && !EventIds.PROCEED_EVENT_ID.equals(eventCtx.getEvent())) {
+                log.error("Message encoding action signaled non-proceed event {}", eventCtx.getEvent());
+                httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY,
+                        eventCtx.getEvent().toString());
+                ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+                return;
+            }
+        } catch (final MessageHandlerException e) {
+            log.error("Caught message handling exception", e);
+            httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, EventIds.MESSAGE_PROC_ERROR);
+            ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+        }
+    }
+
+    /**
+     * Inbound completion of the process, triggered by default for any methods.
+     * 
+     * @param httpRequest servlet request
+     * @param httpResponse servlet response
+     * @param binding a key for the inbound binding
+     * 
+     * @throws ExternalAuthenticationException if an error occurs
+     * @throws IOException if an I/O error occurs
+     */
+    @RequestMapping("/{binding}/SSO")
+    @Nullable public void finishSAML(@Nonnull final HttpServletRequest httpRequest,
+            @Nonnull final HttpServletResponse httpResponse, @PathVariable @Nonnull @NotEmpty final String binding)
+                    throws ExternalAuthenticationException, IOException {
+        
+        final String key = httpRequest.getParameter("RelayState");
+        if (key == null) {
+            throw new ExternalAuthenticationException("No RelayState parameter, unable to resume flow execution");
+        }
+        
+        final ProfileRequestContext prc = ExternalAuthentication.getProfileRequestContext(key, httpRequest);
+        final SAMLAuthnContext samlContext = samlContextLookupStrategy.apply(prc);
+        if (samlContext == null) {
+            log.error("SAMLAuthnContext not found");
+            httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, EventIds.INVALID_PROFILE_CTX);
+            ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+            return;
+        }
+                
+        final ProfileRequestContext nestedPRC = profileRequestContextLookupStrategy.apply(prc);
+        if (nestedPRC == null) {
+            log.error("Nested ProfileRequestContext not found");
+            httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, EventIds.INVALID_PROFILE_CTX);
+            ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+            return;
+        }
+        
+        try {
+            final MessageDecoder decoder = samlContext.getMessageDecoderFactory().apply(binding);
+            if (decoder == null) {
+                throw new MessageDecodingException("Unable to obtain MessageDecoder for binding key: " + binding);
+            }
+            try {
+                decoder.initialize();
+                decoder.decode();
+                nestedPRC.setInboundMessageContext(decoder.getMessageContext());
+            } finally {
+                decoder.destroy();
+            }
+        } catch (final MessageDecodingException | ComponentInitializationException e) {
+            log.error("Unable to decode SAML response", e);
+            httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, EventIds.UNABLE_TO_DECODE);
+        }
+        
+        ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ValidateSAMLAuthentication.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ValidateSAMLAuthentication.java
new file mode 100644
index 0000000..a89ca72
--- /dev/null
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/saml2/profile/impl/ValidateSAMLAuthentication.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.saml.saml2.profile.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.ExternalAuthenticationContext;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.saml2.core.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An action that checks for an {@link ExternalAuthenticationContext} for a signaled event via the
+ * {@link ExternalAuthenticationContext#getAuthnError()} method, and otherwise enforces the presence
+ * of an inbound SAML Response to process.
+ * 
+ * <p>This is a bridge from the external portion of the SAML proxy implementation to transition
+ * back into the flow and pick up any signaled errors if necessary.</p>
+ *  
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@Link EventIds#MESSAGE_PROC_ERROR}
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
+ * @event {@link AuthnEventIds#NO_CREDENTIALS}
+ * @event various
+ */
+public class ValidateSAMLAuthentication extends AbstractAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateSAMLAuthentication.class);
+
+    /** Context containing the result to examine. */
+    @Nullable private ExternalAuthenticationContext extContext;
+
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            return false;
+        }
+        
+        extContext = authenticationContext.getSubcontext(ExternalAuthenticationContext.class);
+        if (extContext == null) {
+            log.debug("{} No ExternalAuthenticationContext available within authentication context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        if (extContext.getAuthnError() != null) {
+            log.info("{} SAML authentication attempt signaled an error: {}", getLogPrefix(),
+                    extContext.getAuthnError());
+            ActionSupport.buildEvent(profileRequestContext, extContext.getAuthnError());
+        } else if (profileRequestContext.getInboundMessageContext() == null) {
+            log.info("{} No inbound SAML Response found", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+        } else if (!(profileRequestContext.getInboundMessageContext().getMessage() instanceof Response)) {
+            log.info("{} Inbound message was not a SAML Response", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java
index bcd6666..a263af5 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/AddLogoutRequest.java
@@ -180,7 +180,7 @@ public class AddLogoutRequest extends AbstractProfileAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return false;
         } else if (!overwriteExisting && outboundMessageCtx.getMessage() != null) {
-            log.debug("{} Outbound message context already contains a response", getLogPrefix());
+            log.debug("{} Outbound message context already contains a message", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return false;
         }
diff --git a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContext.java b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContext.java
index 9887f36..17febf8 100644
--- a/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContext.java
+++ b/idp-saml-impl/src/main/java/net/shibboleth/idp/saml/session/impl/PrepareInboundMessageContext.java
@@ -39,15 +39,15 @@ import org.slf4j.LoggerFactory;
 
 /**
  * Action that adds an inbound {@link MessageContext} and a {@link SAMLPeerEntityContext} to the
- * {@link ProfileRequestContext} based on the identity of the relying party bound to a {@link SAML2SPSession}
- * found in a {@link LogoutPropagationContext}.  
+ * {@link ProfileRequestContext} based on the identity of a relying party, by default from a
+ * {@link SAML2SPSession} found in a {@link LogoutPropagationContext}.  
  * 
- * <p>This action mocks up a minimal amount of machinery on the inbound message side to drive a
+ * <p>This action primarily mocks up a minimal amount of machinery on the inbound message side to drive a
  * SAML 2 Logout Propagation flow, which needs to issue a logout request message for the {@link SAML2SPSession}
  * it's given.</p>
  * 
- * <p>It's named generically so that if we need to expand it to support something beyond SAML 2 (some kind of
- * hack for SAML 1?) we can do that.
+ * <p>It has some generic capability to allow it to be used for some other outbound messaging cases, such as
+ * SAML 2 SSO proxying.</p>
  * 
  * @event {@link EventIds#PROCEED_EVENT_ID}
  * @event {@link EventIds#INVALID_PROFILE_CTX}
@@ -56,12 +56,15 @@ public class PrepareInboundMessageContext extends AbstractProfileAction {
 
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(PrepareInboundMessageContext.class);
-
+    
     /** Logout propagation context lookup strategy. */
     @Nonnull private Function<ProfileRequestContext,LogoutPropagationContext> logoutPropContextLookupStrategy;
 
-    /** The {@link SAML2SPSession} to base the inbound context on. */
-    @Nullable private SAML2SPSession saml2Session;
+    /** Optional circumvention of usual method to identify the relying party name. */
+    @Nullable private Function<ProfileRequestContext,String> relyingPartyLookupStrategy;
+    
+    /** The relying party name to base the inbound context on. */
+    @Nullable private String relyingPartyId;
 
     /** Constructor. */
     public PrepareInboundMessageContext() {
@@ -80,6 +83,20 @@ public class PrepareInboundMessageContext extends AbstractProfileAction {
         logoutPropContextLookupStrategy =
                 Constraint.isNotNull(strategy, "LogoutPropagationContext lookup strategy cannot be null");
     }
+    
+    /**
+     * Set an optional lookup strategy to identify the relying party name, as a substitute for the session/logout
+     * assumptions made by the action otherwise.
+     * 
+     * @param strategy lookup strategy
+     * 
+     * @since 4.0.0
+     */
+    public void setRelyingPartyLookupStrategy(@Nullable final Function<ProfileRequestContext,String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        relyingPartyLookupStrategy = strategy;
+    }
 
     /** {@inheritDoc} */
     @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -88,6 +105,13 @@ public class PrepareInboundMessageContext extends AbstractProfileAction {
             return false;
         }
         
+        if (relyingPartyLookupStrategy != null) {
+            relyingPartyId = relyingPartyLookupStrategy.apply(profileRequestContext);
+            if (relyingPartyId != null) {
+                return true;
+            }
+        }
+        
         final LogoutPropagationContext logoutPropCtx = logoutPropContextLookupStrategy.apply(profileRequestContext);
         if (logoutPropCtx == null) {
             log.debug("{} No logout propagation context", getLogPrefix());
@@ -99,8 +123,7 @@ public class PrepareInboundMessageContext extends AbstractProfileAction {
             return false;
         }
         
-        saml2Session = (SAML2SPSession) logoutPropCtx.getSession();
-
+        relyingPartyId = ((SAML2SPSession) logoutPropCtx.getSession()).getId();
         return true;
     }
 
@@ -111,9 +134,9 @@ public class PrepareInboundMessageContext extends AbstractProfileAction {
         profileRequestContext.setInboundMessageContext(msgCtx);
 
         final SAMLPeerEntityContext peerContext = msgCtx.getSubcontext(SAMLPeerEntityContext.class, true);
-        peerContext.setEntityId(saml2Session.getId());
+        peerContext.setEntityId(relyingPartyId);
 
-        log.debug("{} Initialized inbound message context for logout of {}", getLogPrefix(), saml2Session.getId());
+        log.debug("{} Initialized inbound context for message to {}", getLogPrefix(), relyingPartyId);
     }
     
 }
\ 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