Current File(s): conf/authn/mfa-authn-config.xml

Format: Native Spring

Overview

The authn/MFA login flow is a special "composite" mechanism that provides a scriptable (or programmable) way of combining other login flows to produce simple or complex sequences of login challenges that combine to provide stronger or more flexible authentication options than individual methods can provide on their own.

It simplifies the development of new login methods by avoiding the need to include complicated business logic to control how methods are combined for specific users, services, networks, times of day, etc.

By itself, the MFA flow is just an orchestration tool and doesn't itself perform any specific kinds of authentication, instead relying on existing login methods to do its work, together with rules you must provide to control behavior. It requires that you plan out what you need to do first, and then implement that plan using Java code or Java Scripting to supply the business logic.

General Configuration

Use authn/mfa-authn-config.xml to configure this flow.

Note that when you use the MFA flow, it's common that it will be the only flow enabled via the idp.authn.flows property. In particular, any flows you direct the MFA flow to run via rules and scripts should not generally be enabled themselves because to do so would cause the IdP to run them itself in ways that are likely to subvert your intent.

Another note which is repeated below, regarding the overall method selection process: at the top level, the IdP won't run the MFA login flow at all if its supported principal collection (in authn/general-authn.xml) does not satisfy the request. That is, you generally will need to advertise support for all possible custom principals represented by the various factors the MFA flow may produce results for in order to convince the IdP to run it. As an example, if your MFA processing rules allow for certificate authentication, then you will need to ensure the MFA flow descriptor includes appropriate supported principals reflecting use of certificates, or the IdP won't know it can run the MFA flow for requests for that type of authentication.

A bean called shibboleth.authn.MFA.TransitionMap is commonly defined with the map of rules to run to control the transition between flows and determine when to complete work. This is a map whose keys are the flows to "exit" from, and whose values are a bean of the class type net.shibboleth.idp.authn.MultiFactorAuthenticationTransition (a parent bean called shibboleth.authn.MFA.Transition is provided). The first rule is marked with an empty or null key value in the map.

Each transition rule is itself a mapping from the exit state (event) of a flow and the logic to run to decide the next flow to run. There are a couple of simple properties to define transitions to use after a "proceed" event (the usual success indicator), and a more advanced property to supply a full range of event/logic mappings.

If you need to support multiple workflows at the same time (e.g., if you wish to deploy special workflows to handle different profiles and functions provided by the IdP), you can supply a function in a bean called shibboleth.authn.MFA.TransitionMapStrategy to return the actual map of rules to use. Normally a single set is enough, since you can embed conditional logic in the rules, but this provides a separate axis of conditional logic you can use.

Defining Transitions

There are three approaches to defining transitions, outlined below, with examples.

Directly Selecting Flows

The simplest type of transition rule just provides a specific subflow to run if the previous step (if any) was successful. This is specified with the nextFlow property of a transition bean. Note that this can invoke any subflow, not just login flows, though that is the most common case. You could build your own subflows to display views to collect user input, etc. and invoke them using the same mechanism.

Consider a simple example that implements this sequence:

  1. Run the "authn/Flow1" flow.
  2. If Step 1 succeeds, run the "authn/Flow2" flow.
  3. If Step 2 succeeds, combine the results of the two flows into one.
  4. If either Step 1 or 2 fails, return that failure as the MFA flow result.

This simple example doesn't require any logic or scripting:

<util:map id="shibboleth.authn.MFA.TransitionMap">
	<!-- Run authn/Flow1 first. -->
	<entry key="">
		<bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="authn/Flow1" />
	</entry>

	<!-- If that returns "proceed", run authn/Flow2 next. -->
	<entry key="authn/Flow1">
		<bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="authn/Flow2" />
	</entry>
        
	<!-- An implicit final rule will return whatever the second flow returns. -->
</util:map>

The "combine the results" behavior in step 3 above actually comes from a built-in bean that you can override for more customized behavior. A bean named shibboleth.authn.MFA.resultMergingStrategy will be used to supply a function to run to merge together all of the authentication results produced by an entire sequence of steps in a customized way (this is discussed in more detail below).

Programmatically Selecting Flows

A more complex transition rule can run a function (which can be in Java, a script, or a Spring Expression) to return a flow to run if the previous step (if any) was successful, or it can return null to signal that processing should stop. This is specified with the nextFlowStrategy property of a transition bean.

This relatively complex example relies on a single script that does a number of interesting things to achieve the following sequence:

  1. Run the "authn/Flow1" flow.
  2. If Step 1 succeeds and the result is sufficient to satisfy the request, resolve an attribute about the user identified by Step 1.
  3. If the result from Step 1 is sufficient AND the attribute resolved indicates that a user may use that method alone, then finish with the result from Step 1.
  4. If the result from Step 1 is not sufficient OR the attribute resolved indicates that an additional factor is required, run the "authn/Flow2" flow.
  5. If successful, combine the results from the two flows into one.
  6. If either method fails, return that failure as the MFA flow result.
<util:map id="shibboleth.authn.MFA.TransitionMap">
	<!-- Run authn/Flow1 first. -->
	<entry key="">
		<bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="authn/Flow1" />
	</entry>

	<!--
	Second rule runs a function if authn/Flow1 succeeds, to determine whether an additional
	factor is required.
	-->
	<entry key="authn/Flow1">
		<bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="checkSecondFactor" />
	</entry>

	<!-- An implicit final rule will return whatever the second flow returns. -->
</util:map>

<!-- Example script to see if second factor is required. -->
<bean id="checkSecondFactor" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript"
		p:customObject-ref="shibboleth.AttributeResolverService">
	<constructor-arg>
		<value>
        <![CDATA[
            nextFlow = "authn/Flow2";

            // Go straight to second factor if we have to, or set up for an attribute lookup first.
            authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext");
            mfaCtx = authCtx.getSubcontext("net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext");
            if (mfaCtx.isAcceptable()) {
                // Attribute check is required to decide if first factor alone is enough.
                resCtx = input.getSubcontext(
                    "net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext", true);
                
				// Look up the username
                usernameLookupStrategyClass = Java.type("net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy");
                usernameLookupStrategy = new usernameLookupStrategyClass();
                resCtx.setPrincipal(usernameLookupStrategy.apply(input));


				// resolve the attribute to determine if a first factor is sufficient
                resCtx.getRequestedIdPAttributeNames().add("allowedLoginMethods");
                resCtx.resolveAttributes(custom);
                
                // Check for an attribute value that authorizes use of first factor.
                attribute = resCtx.getResolvedIdPAttributes().get("allowedLoginMethods");
                valueType =  Java.type("net.shibboleth.idp.attribute.StringAttributeValue");
                if (attribute != null && attribute.getValues().contains(new valueType("Flow1"))) {
                    nextFlow = null;
                }
                
                input.removeSubcontext(resCtx);   // cleanup
            }
            
            nextFlow;   // pass control to second factor or end with the first
        ]]>
	</value>
	</constructor-arg>
</bean>

There's a fair amount going in on the script, but it's mostly just navigating and populating contexts into the tree, and the whole middle section is doing the attribute lookup and check.

If you study that sequence, you will find that it represents the general outline of a typical "opt-in" strategy that is based on the user, and is also optimized to avoid extra work if specific requests require the second factor and addresses the failure scenario in which it's not possible to obtain the opt-in attribute (it fails in the direction of requiring the second factor).

Hopefully you can see how to adapt it for a similar scenario involving your particular strategy. Obviously the attribute name, values, and the specific flows involved will be specific to you.

When appliable, an ActivationConditions may also be used from the script. For instance, to run the second flow only when the client adress is located outside internal network:

<util:map id="shibboleth.authn.MFA.TransitionMap">
	<entry key="">
		<bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="authn/Flow1" />
	</entry>
	<entry key="authn/Flow1">
		<bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="checkSecondFactor" />
	</entry>
</util:map>

<bean id="InternalNetwork" class="org.opensaml.profile.logic.IPRangePredicate"
    p:httpServletRequest-ref="shibboleth.HttpServletRequest"
    p:ranges="#{ '192.168.1.0/24' }" />

<bean id="checkSecondFactor" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript"
    p:customObject-ref="InternalNetwork">
    <constructor-arg>
        <value>
            <![CDATA[
if (custom.apply(input)) {
    nextFlow = null;
} else {
    nextFlow = "authn/Duo";
}
nextFlow;
]]>
        </value>
    </constructor-arg>
</bean>


This is still a relative simple strategy in that it is driven only by successful results at each step, with errors left to fall through back into the IdP.

Full Control Over Transitions

The final tier of complexity is one that provides total control over transitions based on specific Spring WebFlow events that occur. That is, you can run scripts or signal flows to run in response to specific error events from previous steps, or build a subflow that deliberately signals a number of different events as a simple way to pass input from a user (via a view) back into the system to decide what to do. This should make building selection screens for choosing login methods fairly simple.

For full control, you must build a map bean and set it as the value of the nextFlowStrategyMap property of a transition bean. The keys to the map are the events to "catch", and the values of the map can either be a simple String (a subflow to run) or a Function<ProfileRequestContext,String> to run to return the flow to run.

Another feature is the ability to handle all events not otherwise specified (a wildcard). This is signified by a map key of "*" (an asterisk).

As an example, if you built a view-centric flow that signaled events indicating the type of authentication to perform, it might look something like the following:

<util:map id="shibboleth.authn.MFA.TransitionMap">
	<!-- Run authn/Flow1 first. -->
	<entry key="">
		<bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="authn/Flow1" />
	</entry>

	<!-- Second rule calls a flow to display a view to select a method to run. -->
	<entry key="authn/Flow1">
		<bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="custom/methodChooser" />
	</entry>

	<!-- Third rule decides what to call based on event signaled by the view. -->
	<entry key="custom/methodChooser">
		<bean parent="shibboleth.authn.MFA.Transition">
			<property name="nextFlowStrategyMap">
				<map>
					<!-- Maps event to a flow -->
					<entry key="ChooseMethodA" value="authn/FlowA" />
					<entry key="ChooseMethodB" value="authn/FlowB" />

					<!-- Maps event to a scripted function bean reference-->
					<entry key="EventToHandle" value-ref="eventHandlerScript" />
				</map>
			</property>
		</bean>
	</entry>	

	<!-- An implicit final rule will return whatever the chosen flow returns. -->
</util:map>

<!-- Event Handler Script -->
<bean id="eventHandlerScript" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript">
    <constructor-arg>
	    <value>
		    <![CDATA[
			...
		    ]]>
		</value>
    </constructor-arg>
</bean>

The example above is geared around using a dedicated custom webflow to offer a selection UI, so such a flow controls its own rules for what events it can signal. If you wanted to, for example, stick a button or link on the login form used by the Password login flow and signal that back to the MFA logic as a custom event, you will need to modify conf/authn/authn-events-flow.xml (see the general discussion under Custom Events).

Signaling Completion

As shown in one of the examples above, a transition rule that either doesn't exist, doesn't handle a particular event, or returns an explicit null, is a signal to the MFA flow "proper" to finish work. At that point, the event is examined and if anything but "proceed" is signaled, that event becomes the final result of the MFA login flow and control passes back to the IdP. A basic example of this would be a call to a login flow that returned "InvalidPassword" after a number of failed attempts.

On the other hand, if "proceed" is the final event, then the MFA flow assumes success and performs a "merge" as described in the following section.

Merging Results

The IdP expects/requires a login flow to produce a single AuthenticationResult when it finishes work successfully. To avoid a total redesign, the MFA flow is built to maintain this constraint by combining the individual results it may accumulate in the course of executing into one final result. It does this using a default function that can be overridden by defining a bean named shibboleth.authn.MFA.resultMergingStrategy of type Function<ProfileRequestContext,AuthenticationResult> (i.e., a function that returns the result to use).

It is hoped that the default behavior will be sufficient for most people, which is to perform a true merge of all of the Principals, and public and private Credentials contained in the individual Subjects across all of the active results contained in the MultiFactorAuthenticationContext that tracks the overall state of the request. In addition, to enable the SSO behavior described below, the default function actually wraps the individual results into a special custom Principal type that allows the MFA login flow to actually recover the individual results that it obtained earlier and use them to avoid re-prompting a user with factors that have already been performed, just as the IdP itself does.

Note that if you wish to replace this function, you will likely have to look at, and possibly replicate some of, the default behavior the system supplies to do so, but you do have the ability to do anything you like in producing a final result.

Running Login Flows and Reusing Results

If you have followed along this far, you probably should be figuring out that in a lot of respects, this login flow actually replaces the selection behavior that was baked into the IdP itself in prior versions. That behavior is still there, but once the IdP runs this flow, there's sort of an "algorithm within the algorithm" running to decide what flows to run.

To avoid creating a lot of disconnect with existing behavior, there are certain things that this flow does to maintain existing expectations.

Basic Flow Validation

By default, the MFA flow checks some of the basic parameters of a request and the ability of a login flow to handle them, before running it. That is, it checks things like forced authentication, passive authentication, and non-browser support, before it actually attempts a login flow, even if you signal that it should run one. This is mostly to keep certain kinds of scenarios working automatically without having to be handled in everybody's scripts and rules. If you want to turn off that behavior, you can supply a bean named shibboleth.authn.MFA.validateLoginTransitions set to a Boolean false constant.

One thing it doesn't do is apply any activation condition you've attached to the flow descriptor before running the flow. If you want to conditionally run a flow, that's the point of the MFA transition rule logic so you're meant to apply those conditions yourself if you want them. This provides more flexibility since you get to control when the condition applies, which allows, for example, different conditions to attach to the same login method depending on the situation. It's all the same in the end, it's just where the logic runs.

Request Sufficiency

One thing that is normally done by the IdP that is not done in the MFA flow is to check whether individual login flows are compatible with the authentication requirements of a request. Often MFA scenarios require more customized decision making (e.g., forcing use of a method because of the user's identity, or preventing a method from running for some reason), and so you have more latitude in this area. But you need to bear in mind that at the end of the process, the result you produce had still better satisfy the request or the IdP will reject it. For example, if a service requests strong authentication in some way, and you short-circuit that by returning the result of password authentication, that is likely to be rejected by the IdP and fail the request. You can lie of course, with sufficient cleverness, but the IdP isn't going to lie for you.

Repeating the note at the top of this page: at the top level, the IdP won't run the MFA login flow at all if its supported principal collection does not satisfy the request. That is, you generally will need to advertise support for all possible custom principals represented by the various factors the MFA flow may produce results for in order to convince the IdP to run it. As an example, if your MFA processing rules allow for certificate authentication, then you will need to ensure the MFA flow descriptor includes appropriate supported principals reflecting use of certificates, or the IdP won't know it can run the MFA flow for requests for that type of authentication.

Single Sign-On / Reuse By the MFA Flow

Like the IdP "proper", the MFA flow has a feature to reuse the results of the login flows it runs by pulling them out of a previous session. This is a little subtle: this does not compose with uses of login flows outside of the MFA flow. That is, if you have the IdP run the Password flow directly, that result is not recognized by the MFA flow, but if the MFA flow runs the Password flow, then the MFA flow will recognize it.

Unless a request includes forced re-authentication, any active/previous results produced and tracked by the MFA flow will be reused under these conditions:

You will note this does not include a check for whether the result is specifically applicable to the request's requirements, because it assumes you are in control of that decision if you choose to run the flow.

Note that one feature the MFA flow does not have is individual timeouts. For various reasons, it was not practical to maintain an activity timeout on the individual results within the MFA flow, and so that check is not done. Using a shorter lifetime generally will compensate for that.

On the other hand, the overall MFA result that contains all of the individual results does have the normal lifetime/timeout policy the IdP supports. You can still time out any memory of any of the results, just not at a fine-grained level.

Also be aware that any given login flow can supply only a single result at a time to the overall merged result, and any result from a flow will overwrite a previous result.

Reuse of the Entire authn/MFA Flow Result (When Is a MFA Next Flow Strategy Executed?)

As with any other login flow, if the IdP determines that an active MFA flow result with a particular principal satisfies a request it will reuse the entire MFA result with that principal. As such the IdP will not rerun the authn/MFA flow and any logic in a next flow strategy in the transition map will not have a chance to execute a second time for that user.

The IdP may, however, be configured so that the IdP runs the MFA flow and executes the next flow strategy logic even if the result would normally satisfy the request.

There are two ways to do this, the old way that was a workaround for a missing feature and the new way supported in V3.4.

V3.4+

There is now an explicit property you can set on the login flow descriptor bean in general-authn.xml that attaches a second kind of condition logic to the login flows called a "reuse condition". Think of it as a "SSO or not?" flag on each login method that allows you to customize when the system will reuse a previously built result or re-run the flow. This is possible with any login flow, but it's of particular value with the MFA flow since it generally contains logic that may need to run to determine whether SSO should happen.

You can split these concerns any way you prefer, but if you can include at least some of your logic in the reuse condition rather than the MFA logic itself, that can improve efficiency. But in the simplest case, if you want the MFA rules to run on every request no matter what, just do this:

...
		<bean id="authn/MFA" parent="shibboleth.AuthenticationFlow"
                p:passiveAuthenticationSupported="true"
                p:forcedAuthenticationSupported="true"
				p:reuseCondition="false">

			...etc...

        </bean>
...

That simply says "never reuse results". Bear in mind this is referring to the MFA flow itself, and not the individual "sub factors" that it uses internally to build its results, so individual factors may have results reused when the MFA logic actually runs them, which is generally what is desired.

For more advanced cases or to improve efficiency, a bean can be defined for a script or Java logic that defines the condition to evaluate to decide on reuse, and you can attach that via p:reuseCondition-ref in the usual Spring manner.

V3.3.x and Earlier

A workaround exists in older versions before the problem was fully understood that can usually manage to force the MFA logic to run. To configure this behavior in a scenario involving two login factors:

  1. Add an ordered list of principals with the principal corresponding to your elevated factor first, as the value for the defaultAuthenticationMethods property for the relevant SPs. This is best done via some kind of metadata-based rule, in which you are tagging any SP for which your MFA logic is required to run. As in any case in which you need to rely on this property, you should also ensure that the applicable SPs are required to sign their requests, or if you cannot do so, be sure to disallow the ability for them to request their own context classes (the latter is shown below).

    <bean id="MfaPrincipal" parent="shibboleth.SAML2AuthnContextClassRef" 
      c:classRef="https://refeds.org/profile/mfa" />
    
    <bean id="PasswordPrincipal" parent="shibboleth.SAML2AuthnContextClassRef" 
      c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" />
    
    <bean parent="RelyingPartyByTag">
        <constructor-arg name="candidates">
            <list>
                <bean parent="TagCandidate" c:name="http://macedir.org/entity-category"
                    p:values="http://example.org/mfa-candidate"/>
            </list>
        </constructor-arg>
        <property name="profileConfigurations">
         <list>
           <bean parent="SAML2.SSO" p:disallowedFeatures-ref="SAML2.SSO.FEATURE_AUTHNCONTEXT">
             <property name="defaultAuthenticationMethods">
               <list>
                 <ref bean="MfaPrincipal" />
                 <ref bean="PasswordPrincipal" />
               </list>
              </property>
            </bean>
          </list>
        </property>
    </bean>
  2. You must also ensure idp.authn.favorSSO is unset or set to false in idp.properties.

The combination of the two changes will cause the IdP to always try and produce a result satisfying the first principal even if the user has an active result satisfying the second principal. The result is that the MFA transition strategy logic has a chance to run again.

Note that if the user does have an active result satisfying the first principal, then the IdP will immediately reuse it as long as it satisfies the incoming request (it most likely will of course). So once the MFA transition strategy has run and has produced a result constituted from both factors, it will not run again (modulo forced re-authentication and the lifetime for the result).

What's happening here is that when the "favorSSO" property is false, the IdP more strictly evaluates a request and tries to satisfy the first criteria in the list if it can, even if SSO would allow it to satisfy the second.

Reference

Beans

The beans defined in authn/mfa-authn-config.xml follow:

Bean IDTypeDefaultFunction
shibboleth.authn.MFA.TransitionMapMap<String,MultiFactorAuthenticationTransition>
Static ruleset containing the starting point for MFA execution and the rules to use to decide how to do work
shibboleth.authn.MFA.TransitionMapStrategyFunction<ProfileRequestContext,Map<String,MultiFactorAuthenticationTransition>Returns shibboleth.authn.MFA.TransitionMapFunction bean to return the ruleset to use instead of using a static ruleset
shibboleth.authn.MFA.TransitionMultiFactorAuthenticationTransition
Parent bean for defining transition rules in the values of the previous bean's map entries
shibboleth.authn.MFA.validateLoginTransitionsBooleantrueWhether login flows should only be run with regard for forceAuthn/isPassive/nonBrowser conditions
shibboleth.authn.MFA.resultMergingStrategyFunction<ProfileRequestContext,AuthenticationResult>described aboveFunction to run to produce final merged result of MFA login flow during successful completion
shibboleth.authn.MFA.resultCachingPredicatePredicate<ProfileRequestContext>
An optional bean that can be defined to control whether to preserve the authentication result in an IdP session

V2 Compatibility

There is no comparable V2 feature.

Notes

It's been observed by early deployers, accurately, that the data required to track the use of this feature in the session cache is on the order of 2-3 times as large as just a "simple" authentication result. While it is believed that this remains acceptable with the use of cookies, some storage service implementations such as that for Memcached rely on a less reliable persistence model that may prematurely evict data, so such options may not be a good fit with this feature.