Supporting the REFEDS MFA Profile

Background

The REFEDS MFA Profile is a convention for defining basic criteria needed to plausibly claim that one has applied multi-factor authentication to a subject and a standard SAML AuthnContext class reference value for communicating that between IdPs and SPs so that it's possible to leverage MFA on a wider scale than simple internally to an IdP organization.

Typically, MFA is something people apply today based on IdP-centric policy rules, whereas in the original SAML standard it was imagined that SPs would not have to rely on an IdP rule but simply ask for MFA when they needed it at runtime. This became a problem not only because of lack of software support outside of Shibboleth but also because there were no sensible values to use to communicate this. REFEDS as a community can't fix the former, but sought to address the latter by defining a value to use with a reasonably but not overly onerous bar for IdPs to meet in satisfying it.

Please note that this is not the same as, or related really at all to, the REFEDS Assurance Profile. Identity Assurance is distinct from, and expressed differently, than authentication quality.

It is difficult to document exactly how to "support" this because it is dependent on how one operates their IdP and what mechanisms are used for authentication but as a basic guide, this recipe assumes a very vanilla configuration and uses Password and Duo authentication as the two factors involved. The examples should be readily generalizeable to other methods because the IdP treats all login methods more or less abstractly and with the same machinery.

There are separate examples for V3.3 - V4.0, and for V4.1 and above, because of changes made in V4.1 to simplify some of the configuration that is directly applicable.

Do NOT attempt to just cut and paste these examples; in most cases they will NOT work as is because they're excerpts of files and even beans that will contain other settings. They simply demonstrate the specific settings involved in this use case.

Also, this example is NOT suitable for use by anyone using third-party SAML IdP products to actually handle user authentication. That's a much different, and much more complex, problem that involves fairly advanced usage of the SAML proxying support in the IdP. That would require very faithful support of the SAML standard that is unlikely to be found in the commercial IdP space. In particularly, the IdP product would have to support requesting and signaling MFA via the SAML <AuthnContextClassRef> mechanism, or it may be impossible (or at least much more difficult) to pull off what this example is demonstrating for a Shibboleth-only deployment.

Assumptions

The example assumes that the IdP is using the Password and Duo flows together in a simple/standard way by means of the MFA flow that ties them together. The approach shown is usable to support both IdP-based and SP-based rules for applying the second (Duo) factor to a request.

The example further assumes that you want to define both an internal means of identifying and communicating MFA but also support the REFEDS signal value of "https://refeds.org/profile/mfa". The benefit of this approach is that it isolates internal use and separates the behavior of a REFEDS Profile deployment from any local policies or exceptions needed because of internal business decisions. For example, locally a decision to exempt certain users from a requirement to perform MFA through some kind of bypass mechanism would violate the REFEDS Profile requirements, so using a different string internally allows for that flexibility.

The example uses a made-up internal value which should be adjusted into a locally-defined/owned URI. The REFEDS value is of course the actual value needed.

Initial Setup

The example proceeds by establishing the configuration to support the internal value, which for example purposes is just "http://example.org/ac/classes/mfa". The REFEDS support will be added later below. For completeness, the example also includes support for expressing MFA as a SAML 1.1 AuthenticationMethod but this is unlikely to matter much at this point.

To begin with, the Password and Duo configurations must be established, and this is largely outside the scope of this example. Having done so, the MFA flow is then configured to tie the two factors together in a simple way that is designed to always apply the password factor and conditionally apply the Duo factor when and if it is necessary to do so. (For V4.1+, the MFA "module" has to be enabled first, which is noted in the documentation, so we assume this is already done.)

This is virtually verbatim done via the distributed example that is installed with the software, but is repeated here for context:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <util:map id="shibboleth.authn.MFA.TransitionMap"> <!-- First rule runs the Password login flow. --> <entry key=""> <bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="authn/Password" /> </entry> <!-- Second rule runs a function if Password succeeds, to determine whether an additional factor is required. --> <entry key="authn/Password"> <bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="checkSecondFactor" /> </entry> <!-- An implicit final rule will return whatever the final flow returns. --> </util:map> <!-- Example script to see if second factor is required. --> <bean id="checkSecondFactor" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript"> <constructor-arg> <value> <![CDATA[ nextFlow = "authn/Duo"; // Check if second factor is necessary for request to be satisfied. authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext"); mfaCtx = authCtx.getSubcontext("net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext"); if (mfaCtx.isAcceptable()) { nextFlow = null; } nextFlow; // pass control to second factor or end with the first ]]> </value> </constructor-arg> </bean>

The additional setup will vary by version and is the process by which you associate the "internal" SAML AuthnContext class reference value with your second-factor method (Duo in this case) so that it can act as a signal.

New (or modernized) installs of V4.1+ can be taught what context classes to associate with login flows using conf/authn/authn.properties:

conf/authn/authn.properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ... idp.authn.Duo.supportedPrincipals = \ saml2/http://example.org/ac/classes/mfa, \ saml1/http://example.org/ac/classes/mfa ... idp.authn.MFA.supportedPrincipals = \ saml2/http://example.org/ac/classes/mfa, \ saml2/urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport, \ saml2/urn:oasis:names:tc:SAML:2.0:ac:classes:Password, \ saml1/http://example.org/ac/classes/mfa, \ saml1/urn:oasis:names:tc:SAML:1.0:am:password

Earlier versions rely on the conf/authn/general-authn.xml file to associate the class reference, using the supportedPrincipals property on the login flows. The class reference object must be connected to both the Duo and MFA flows because the latter is a superset of the former. The IdP is being told that these flows "support" this context class so it's necessary that both flows are told to do so. This is an excerpt (note the "..."'s) of the complete file and just the relevant aspects are shown; other settings will vary by local need.

conf/authn/general-authn.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <util:list id="shibboleth.AvailableAuthenticationFlows"> ... <!-- Associates use of Duo with the local MFA context class. --> <bean p:id="authn/Duo" parent="shibboleth.AuthenticationFlow"> <property name="supportedPrincipals"> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="http://example.org/ac/classes/mfa" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:classRef="http://example.org/ac/classes/mfa" /> </property> </bean> ... <!-- Associates MFA flow with both password- and Duo-based authentication. --> <bean p:id="authn/MFA" parent="shibboleth.AuthenticationFlow"> <property name="supportedPrincipals"> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="http://example.org/ac/classes/mfa" /> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" /> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:Password" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:classRef="http://example.org/ac/classes/mfa" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:method="urn:oasis:names:tc:SAML:1.0:am:password" /> </property> </bean> ... </util:list>

Testing

Once this is in place, you can test the behavior (e.g., in development) by temporarily adjusting the default behavior of the IdP to require MFA for all requests by attaching the same local value to the DefaultRelyingParty configuration of the SAML 2.0 SSO profile:

conf/relying-party.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <bean id="shibboleth.DefaultRelyingParty" parent="RelyingParty"> <property name="profileConfigurations"> <list> ... <bean parent="SAML2.SSO"> <property name="defaultAuthenticationMethods"> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="http://example.org/ac/classes/mfa" /> </property> </bean> ... </list> </property> </bean>

Aside from seeing MFA applied, you should also find that the resulting assertions carry an <AuthnContextClassRef> element matching the expected value.

Adding REFEDS

The final step is to simply extend what's already been done to include the REFEDS context class so that SPs that need it can request it. This is a straight addition to the principal sets in the earlier examples, and the original values are included for completeness. You could also add the value as a SAML 1.1 option in an obvious way but that isn't likely to matter or come into play given that SAML 1.1 SPs, if they exist, can't actually request anything.

conf/authn/authn.properties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ... idp.authn.Duo.supportedPrincipals = \ saml2/https://refeds.org/profile/mfa, \ saml2/http://example.org/ac/classes/mfa, \ saml1/http://example.org/ac/classes/mfa ... idp.authn.MFA.supportedPrincipals = \ saml2/https://refeds.org/profile/mfa, \ saml2/http://example.org/ac/classes/mfa, \ saml2/urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport, \ saml2/urn:oasis:names:tc:SAML:2.0:ac:classes:Password, \ saml1/http://example.org/ac/classes/mfa, \ saml1/urn:oasis:names:tc:SAML:1.0:am:password
conf/authn/general-authn.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <util:list id="shibboleth.AvailableAuthenticationFlows"> ... <!-- Associates use of Duo with the local MFA context class. --> <bean p:id="authn/Duo" parent="shibboleth.AuthenticationFlow"> <property name="supportedPrincipals"> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="https://refeds.org/profile/mfa" /> <!-- REFEDS added here --> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="http://example.org/ac/classes/mfa" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:classRef="http://example.org/ac/classes/mfa" /> </property> </bean> ... <!-- Associates MFA flow with both password- and Duo-based authentication. --> <bean p:id="authn/MFA" parent="shibboleth.AuthenticationFlow"> <property name="supportedPrincipals"> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="https://refeds.org/profile/mfa" /> <!-- REFEDS added here --> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="http://example.org/ac/classes/mfa" /> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" /> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:Password" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:classRef="http://example.org/ac/classes/mfa" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:method="urn:oasis:names:tc:SAML:1.0:am:password" /> </property> </bean> ... </util:list>

Testing

Once the REFEDS value is in place, you can test that in the same way as earlier, just changing the trigger value:

conf/relying-party.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <bean id="shibboleth.DefaultRelyingParty" parent="RelyingParty"> <property name="profileConfigurations"> <list> ... <bean parent="SAML2.SSO"> <property name="defaultAuthenticationMethods"> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="https://refeds.org/profile/mfa" /> </property> </bean> ... </list> </property> </bean>

Supporting Multiple Duo Policies

As a final extention of this example, suppose that, having deployed separate context class values for REFEDS and local use, a decision is made to implement a Duo policy of some kind that would violate the REFEDS Profile. The only way to prevent that change from contaminating requests for the REFEDS Profile is to establish a second Duo integration for local use where that policy change is implemented. The original integration would be left alone.

The Duo flow documentation describes in general how to support multiple integrations. The same general approach applies to the DuoOIDC plugin (with one exception, noted at the end), so this example can be adapted to that version as well, but for simplicity, the example will use the original one. The point of this example is to demonstrate concretely how to use <AuthnContextClassRef> values in the form of custom Principals to make the determination as to which integration to use at runtime.

The core of this approach is that you want the Duo and MFA flows to support both the local and REFEDS context class values, which was done earlier, but you then need to further subdivide things by explicitly associating each Duo integration object with the specific context class Principals supported by that integration. In other words, the Duo flow as a whole supports both, but the actual integrations don’t. Then you need a scripted function that will properly select among the two based on whether they support the current request or not. This approach actually generalizes to more than just 2 possible context class values, so multiple local values could also be defined to represent different policies, but the example will stick to just two.

The example also makes use of additional custom properties to allow storage of the various keys outside the XML.

A key assumption of this example is that the “local” integration as less strict in some sense than the REFEDS integration. That is, it assumes that a request for local MFA would be satisfied if the user had already performed REFEDS MFA. Because of that assumption, the appropriate configuration is for the “local” integration to support only the local Principal values, but the REFEDS integration to support both local and REFEDS Principal vales. If the local integration were actually more strict, such that a request for REFEDS MFA is satsified by local MFA but not the converse, then this would be reversed.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 <!-- This integration is for local use. --> <bean id="DuoLocal" parent="shibboleth.authn.Duo.DuoIntegration" p:APIHost="%{idp.duo.apiHost}" p:applicationKey="%{idp.duo.applicationKey}" p:integrationKey="%{idp.duo.integrationKey}" p:secretKey="%{idp.duo.secretKey}"> <property name="supportedPrincipals"> <list> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="http://example.org/ac/classes/mfa" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:method="http://example.org/ac/classes/mfa" /> </list> </property> </bean> <!-- This integration supports either local or REFEDS use. --> <bean id="DuoREFEDS" parent="shibboleth.authn.Duo.DuoIntegration" p:APIHost="%{idp.duo.apiHost.refeds}" p:applicationKey="%{idp.duo.applicationKey.refeds}" p:integrationKey="%{idp.duo.integrationKey.refeds}" p:secretKey="%{idp.duo.secretKey.refeds}"> <property name="supportedPrincipals"> <list> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="http://example.org/ac/classes/mfa" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:method="http://example.org/ac/classes/mfa" /> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="https://refeds.org/profile/mfa" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:method="https://refeds.org/profile/mfa" /> </list> </property> </bean> <!-- Order matters here; we want the most lenient integration first. --> <util:list id="DuoIntegrationList"> <ref bean="DuoDefault" /> <ref bean="DuoREFEDS" /> </util:list> <!-- This simple function just iterates over each integration until it finds an acceptable one. --> <bean id="shibboleth.authn.Duo.DuoIntegrationStrategy" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript" p:customObject-ref="DuoIntegrationList"> <constructor-arg name="scriptSource"> <value> <![CDATA[ duo = null; authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext"); iter = custom.iterator(); while (duo == null && iter.hasNext()) { duo = iter.next(); if (!authCtx.isAcceptable(duo)) { duo = null; } } duo; ]]> </value> </constructor-arg> </bean>

Finally, to make this all work, you need to prevent the Duo flow from automatically attaching all of the Principals the flow supports to the resulting Subject. This allows only the Principals supported by the integration that was actually used to be included, preventing an earlier local MFA result from being reused for REFEDS requests.

In V4.1+, you would simply set the property idp.authn.Duo.addDefaultPrincipals to false in conf/authn/authn/properties.

In earlier versions, you would need to edit the Duo flow descriptor bean and set the addDefaultPrincipals property to false in conf/authn/general-authn.xml.

DuoOIDC Use

The example works essentially the same for the Universal Prompt plugin, but due to a bug, the plugin doesn’t actually support a property for turning off the addDefaultPrincipals property yet (a bug has been filed and will be fixed in a subsequent patch). There is a bit of a complex rule used by the plugin now to decide how to set that property and a workaround for now (that avoids the need to declare the whole flow descriptor somewhere) is to add this additional function to conf/authn/duo-oidc-authn-config.xml:

1 2 3 4 5 6 <!-- Need this to override addDefaultPrincipals behavior. --> <bean id="shibboleth.authn.DuoOIDC.ContextToPrincipalMappingStrategy" parent="shibboleth.Functions.Constant"> <constructor-arg> <null/> </constructor-arg> </bean>

That function hook provides a more advanced way to derive the right set of Principals to attach at runtime, but using a null function tricks the plugin into not automatically adding any, allowing the per-integration Principals to be added instead. Even after the bug is fixed, that won’t break.