The Shibboleth IdP V3 software has reached its End of Life and is no longer supported. This documentation is available for historical purposes only. See the IDP4 wiki space for current documentation on the supported version.

Replicating Multi-Context Broker Functionality (Duo + Username/Password with user-opt-in forcing Duo)

This document is only applicable to versions prior to 3.3. Do not try to follow this document if you are using IdPv3.3 or greater.


This document describes how the University of Chicago configured their Identity Provider under IdPv3 to behave like it did under IdPv2 with the MCB installed.  In our installation all users are forced to use Password for initial authentication.  Then users can opt-in to having Duo Security layered on top of their initial authentication.

Note: There are two Duo plugins for Shibboleth v3

University of Chicago uses a Duo plugin developed by Unicon. Later, Duo Security Inc released their own Shibboleth v3 plugin; see Contributions and Extensions. Duo's plugin cannot (easily) support the full MCB functionality described on this page, but it might have some other advantages. If you're shopping around, look at both.

Step-by-step guide

  1. Download and install/configure the Unicon Duo Security login flow from GitHub.
  2. Create a new webflow /opt/shibboleth-idp/flows/authn/Duo/duo-authn-flow.xml.  The contents are below.  The changes we made ensure that on a forceAuthn request, the user is prompted to also enter their username-password again (so long as it's not their first pass through the IdP – determined by a 2-minute timer from when the IdPSession is created).

    <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, authn/conditions">
        <var name="duoCredential" class="net.unicon.iam.shibboleth.idp.authn.duo.authn.DuoCredential"/>
        <var name="currentTime" class="java.util.Date" />
        <!-- if the IdP session is < 2 minutes then don't check for forceAuthn.  If it's more than that
        then check for forceAuthn and if forceAuthn is in play make the user do a 2nd Password login.
        This eliminates the problem we had with a user hitting a forceAuthn site as their first SP & then
        having to double-enter their username/password.
        -->
        <decision-state id="checkFirstLogin">
    	<if test="(currentTime.getTime() - opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.session.context.SessionContext)).getIdPSession().getCreationInstant()) > 120000"
                then="checkForceAuth"
                else="duo" />	
        </decision-state>
        <decision-state id="checkForceAuth">
            <if test="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).isForceAuthn()"
                then="CallPasswordFlow"
                else="duo" />
        </decision-state>
        <subflow-state id="CallPasswordFlow" subflow="authn/Password">
            <input name="calledAsSubflow" value="true"/>
            <transition on="proceed" to="duo"/>
        </subflow-state>
        <view-state id="duo" view="duo" model="duoCredential">
            <binder>
                <binding property="signedDuoResponse"/>
            </binder>
            <on-render>
                <evaluate expression="environment" result="viewScope.environment"/>
                <evaluate
                        expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))"
                        result="viewScope.authenticationContext"/>
                <set name="viewScope.request" value="flowRequestContext.getExternalContext().getNativeRequest()"/>
                <set name="viewScope.sigRequest"
                     value="duoAuthenticationService.generateSignedRequestToken(authenticationContext)"/>
                <set name="viewScope.apiHost" value="duoAuthenticationService.getApiHost()"/>
            </on-render>
            <transition on="proceed" bind="true" validate="false" to="validateDuo"/>
        </view-state>
    
        <action-state id="validateDuo">
            <evaluate expression="ValidateDuoResponse"/>
            <evaluate expression="'proceed'"/>
            <transition on="proceed" to="proceed"/>
        </action-state>
    </flow>
  3. Setup global.xml to use our local Duo AuthnContextClass instead of the default from the plugin

    global.xml config
        <bean id="authn/Duo" parent="shibboleth.AuthenticationFlow"
              p:nonBrowserSupported="false" p:forcedAuthenticationSupported="%{duo.forcedAuthenticationSupported:true}">
            <property name="supportedPrincipals">
                <util:list>
                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
                          c:classRef="http://uchicago.edu/duo" />
                                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
                          c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" />
                    <bean parent="shibboleth.SAML1AuthenticationMethod"
                          c:method="http://uchicago.edu/duo" />
                </util:list>
            </property>
        </bean>
  4. Download and install the University of Chicago AuthnClassPredicate IdPv3 extension from GitHub.
  5. Setup the attribute-resolver to configure an attribute to release what AuthnClassContexts a user is allowed to use.  We used eduPersonAssurance.  In our example, we push Grouper groups to our LDAP server & then use a MappedAttribute to translate the Grouper values to AuthnContextClass values

    Example eduPersonAssurance attribute
     <resolver:AttributeDefinition xsi:type="Mapped" xmlns="urn:mace:shibboleth:2.0:resolver:ad"
                                      id="eduPersonAssurance" sourceAttributeID="ucisMemberOf">
            <resolver:Dependency ref="directory"/>
            <resolver:AttributeEncoder xsi:type="SAML2String" xmlns="urn:mace:shibboleth:2.0:attribute:encoder"
                                       name="urn:oid:1.3.6.1.4.1.1466.115.121.1.15" friendlyName="eduPersonAssurance"/>
            <ValueMap>
                <ReturnValue>http://id.incommon.org/assurance/silver</ReturnValue>
                <SourceValue>uc:reference:account:assurance:silver:authorized</SourceValue>
            </ValueMap>
            <ValueMap>
                <ReturnValue>http://uchicago.edu/duo</ReturnValue>
                <SourceValue>uc:applications:shibboleth:mcb:duo-eligible</SourceValue>
            </ValueMap>
            <ValueMap>
                <ReturnValue>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</ReturnValue>
                <SourceValue>uc:applications:shibboleth:mcb:password</SourceValue>
            </ValueMap>
        </resolver:AttributeDefinition>
  6. Configure in idp.properties the following for AuthN flows:

    # Regular expression matching login flows to enable, e.g. IPAddress|Password
    idp.authn.flows=Password|Duo
    # Regular expression of forced "initial" methods when no session exists,
    # usually in conjunction with the idp.authn.resolveAttribute property below.
    idp.authn.flows.initial = Password
    # Set to an attribute ID to resolve prior to selecting authentication flows;
    # its values are used to filter the flows to allow.
    idp.authn.resolveAttribute = eduPersonAssurance
  7. In order to prevent bypassing of MFA during forceAuthn caused by a principal switch (another user logs in during the force prompt who has MFA where the first user didn't have MFA), you need to further adjust idp.properties and set idp.authn.identitySwitchIsError to true.

  8. Setup your authn-comparison.xml to replicate the MCB functionality of defining for the IdP which contexts will satisfy which other contexts.  In this example Duo and Silver are ok for Password and only duo is equivalent for PasswordProtectedTransport:

     <bean id="shibboleth.BetterClassRefMatchFactory" parent="shibboleth.InexactMatchFactory">
            <property name="matchingRules">
                <map>
                    <entry key="urn:oasis:names:tc:SAML:2.0:ac:classes:Password">
                        <list>
                            <value>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</value>
                            <value>http://uchicago.edu/duo</value>
    			<value>http://id.incommon.org/assurance/silver</value> 
                        </list>
                    </entry>
    		<entry key="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport">
    			<list>
    				<value>http://uchicago.edu/duo</value>
    				<!-- remove silver from PPT equivalence as it causes silvered users who have opted
    				into 2FA forcing to not have 2FA because silver is just as good as Duo for PPT -->
    				<!-- <value>http://id.incommon.org/assurance/silver</value> -->
    			</list>
    		</entry>	
                </map>
            </property>
        </bean>
    
    
  9. In conf/intercept/context-check-intercept-config.xml define further the relationship between PasswordProtectedTransport & Duo/Silver (which are both initially handled by Password flow).  What you're doing here is causing the IdP to ensure that if the SP requests the Silver context that the user was able to meet the context via the eduPersonAssurance lookup.  If that condition cannot be met, then fail.

    <?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">
        <!--
                 Condition to evaluate to interrupt SSO flows to check the state of the transaction before allowing.
        
        Typically the flow itself will be activated based on configuration in relying-party.xml, and this controls
        whether to proceed if the flow is activated. The most common use for this flow is to check the set of
        resolved/filtered attributes and values to see if the user is authorized or provisioned into a service.
        -->
        <bean id="shibboleth.context-check.Condition" parent="shibboleth.Conditions.AND">
            <constructor-arg>
                <list>
                    <bean class="net.shibboleth.idp.profile.logic.AuthnClassPredicate"
                        c:authnClassesToMatch-ref="authnClassesToMatch"
    		    c:authnClassesToForgive-ref="authnClassesToForgive" 	
                        c:predicateToDelegate-ref="attributePredicate" />
                </list>
            </constructor-arg>
        </bean>
        <util:set id="authnClassesToMatch">
          	<value>http://id.incommon.org/assurance/silver</value>
    	<value>http://uchicago.edu/duo</value>
        </util:set>
        <util:set id="authnClassesToForgive">
            <value>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</value>
        </util:set>
    
        <bean id="attributePredicate" class="net.shibboleth.idp.profile.logic.SimpleAttributePredicate">
            <property name="attributeValueMap">
                <map>
                    <entry key="eduPersonAssurance">
                        <list>
                            <value>http://id.incommon.org/assurance/silver</value>
    						<value>http://uchicago.edu/duo</value>
                        </list>
                    </entry>
                </map>
            </property>
        </bean>
    </beans>
    
    


    You should now have an IdP that acts like it did under the v2 MCB assuming you used Password initial authN and user-opt-in to Duo.  

    One note about this config.  If an SP explicitly requests PasswordProtectedTransport, and the user is trying to force all authN to require Duo (by not allowing PPT in the eduPersonAssurance attribute) the user will not be prompted for Duo.  The Duo forcing only happens if the AuthnContextClass was either explicitly unspecified or not sent in the AuthnRequest. 

Related articles

Alternative Implementation

University of Chicago uses a Duo plugin developed by Unicon. Later, Duo Security Inc released their own Shibboleth v3 plugin; see Contributions and Extensions. Duo's plugin cannot (easily) support the full MCB functionality described on this page, but it might have some other advantages. If you're shopping around, look at both.