...
- Download and install/configure the Unicon Duo Security login flow from GitHub.
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).Code Block language xml <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>
Setup
global.xml
to use our local Duo AuthnContextClass instead of the default from the pluginCode Block language xml title 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>
- Download and install the University of Chicago AuthnClassPredicate IdPv3 extension from GitHub.
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
Code Block language xml title 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>
Configure in
idp.properties
the following for AuthN flows:Code Block language java # 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
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 setidp.authn.identitySwitchIsError
to true.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:Code Block language xml <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>
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.Code Block language xml <?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.Warning 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.
...