DuoAuthnConfiguration

Current File(s): conf/authn/duo-authn-config.xml, conf/authn/duo.properties, conf/authn/authn.properties (V4.1+), views/duo.vm
Format: Native Spring, Properties

Overview

This IFRAME-based integration with Duo Security is now deprecated at the request of the company, which is requiring that customers begin to migrate to their new integration strategy based on OpenID Connect and a full-frame redirect (effectively a proxied authentication via OIDC to Duo Security's servers).

The replacement feature is available as a plugin to V4.1+ of the IdP software and will not be shipped with it. This original integration will be removed at a future time not yet determined, as this will depend on Duo Security's timeline for retiring their support. We have no plans to remove the feature prior to Duo's retirement of the option. (The warning in the log does say "next major version" but that's an artifact of the standard method we invoke to log deprecations.)

The authn/Duo login flow is a native implementation of the Duo Security product's DuoWeb authentication interface that leverages an embedded IFRAME. Duo is a popular commercial solution for adding additional authentication factors to existing credentials. It is designed to be used in conjunction with an existing factor, usually a password. It is therefore implemented in a manner that assumes an earlier authentication step has already been completed, and is designed to be used in conjunction with the MFA login flow as part of a combined workflow.

The result of this flow is a Java Subject containing a DuoPrincipal. Note that no actual "username" is produced; it is assumed that one or more other login flows will contribute content to the Subject suitable for an existing subject canonicalization flow to operate on the aggregate result. All this means is that it assumes the Password login flow (or some other traditional mechanism) is used as part of an MFA workflow, and the result will normally be correct.

There is also support for the Duo AuthAPI in support of non-browser profiles like ECP. Similarly to the Password flow, it supports a set of HTTP request headers to communicate factor, device, and/or passcode values from a client to the IdP and the flow will consume them and attempt a native Duo authentication without use of the IFRAME. For normal profiles, the flow will "fall into" the IFRAME UI, much as the Password flow falls into an HTML form, but for non-browser profiles like ECP, the authentication simply succeeds or fails and the result is passed back.

Enabling Module (V4.1+)

For V4.1+, configuring and using this feature requires that you first enable the "idp.authn.Duo" module if it isn't already enabled. Systems upgraded from older releases generally come pre-enabled due to the prior state of the configuration tree.

1 2 3 4 5 (Windows) C:\opt\shibboleth-idp> bin\module.bat -t idp.authn.Duo || bin\module.bat -e idp.authn.Duo (Other) $ bin/module.sh -t idp.authn.Duo || bin/module.sh -e idp.authn.Duo

General Configuration

For a typical integration, configuration of the Duo flow itself is very simple: just fill in your Duo integration settings (they're in the Duo console if you have access to it, see below) in conf/authn/duo.properties. This assumes you have a single Duo integration, which is true for most sites. There are more advanced scenarios that can be accommodated, and details are included below.

The other important step is to define something to represent this mechanism to the outside "SAML" world as an authentication context class by adjusting the supportedPrincipals property on the Duo (and probably MFA) flow descriptors in conf/authn/general-authn.xml (V4.0) or the idp.authn.Duo.supportedPrincipals property (V4.1+).

There is no "standard" context class (or SAML 1 authentication method) to represent most forms of MFA, and moreover, experience has shown that it's a bad idea to create a strong coupling between applications and the exact technologies that you use for authentication.

As a result, the default configuration contains only a placeholder value to use that you will need to change, but there is currently no standard value to use. One possible choice to consider is the REFEDS profile, but your particular deployment may or may not satisfy its requirements. Regardless of specifics, the approach is a good one in general: a generic URI representing the use of MFA.

Whatever value is used, in most cases you can simply add it where needed, and the Duo flow will automatically contribute that value to the results it builds.

AuthAPI and Non-Browser/ECP Use

To enable support for the AuthAPI, you typically will need to define an additional integration with Duo. A second set of properties is defined to allow this.

Also, the default settings for this login flow mark it as not supporting non-browser use. This needs to be adjusted by removing the nonBrowserSupported property setter on the "auth/Duo" flow descriptor bean in authn/general-auth.xml (V4.0), or setting the idp.authn.Duo.nonBrowserSupported property (V4.1+).

By default, a built-in HttpClient bean is used to communicate with the Duo AuthAPI with fairly vanilla TLS behavior that relies on the system defaults. It's possible to customize this heavily using a pair of beans. More advanced documentation is in the HttpClientConfiguration topic, but an example is included below under Advanced Configuration.

User Interface

The Duo flow normally relies on an embedded IFRAME that points directly to your Duo API host, and a Velocity template (views/duo.vm) is provided that contains the necessary HTML, around which you're free to customize. All of the necessary JavaScript is included with the IdP in a fashion that allows us to update it underneath the template when Duo provides updated versions.

The non-browser variant has no UI and relies on a set of HTTP request headers from the client. Authentication relies on knowing the type of Duo factor to use, the device to use, and occasionally a passcode. Often none are needed and the whole process is automatic (the factor and device are defaulted to "auto"). Specifying a device is generally done using a name the user must associate with the device themselves. Some factors rely on a passcode being supplied.

The headers can be changed but default to:

  • X-Shibboleth-Duo-Factor

  • X-Shibboleth-Duo-Device

  • X-Shibboleth-Duo-Passcode

Factor is one of "auto", "push", "phone", or "passcode". The "sms" factor does not work, but in V4.1+ it will fail while resulting in the issuance of codes via SMS for subsequent use.

Advanced Configuration

Username Determination

By default, the Duo flow is designed to operate with a username derived from one of:

  • a pre-existing session

  • a previously executed login flow

Configuring it to run after a "first factor" flow will automatically satisfy this requirement, and allows you to supply a canonical username from a previous method into the Duo API, which is typically the best approach.

If you need a more flexible approach, you can configure a Function<ProfileRequestContext,String> in a bean named shibboleth.authn.Duo.UsernameLookupStrategy, which can be defined in conf/authn/duo-authn-config.xml

Multiple Duo Integrations

In the event that you need to support multiple sets of Duo integration parameters, you can implement a Function<ProfileRequestContext,DuoIntegration> in Java or a script in a bean named shibboleth.authn.Duo.DuoIntegrationStrategy, which can be defined in conf/authn/duo-authn-config.xml

As an example, let's say you want to create a table that maps certain services to a particular integration, and uses a separate default for everything else. You can implement this with a simple map and a script that operates on it.

Multiple Duo Integrations
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 <bean id="DefaultDuo" class="net.shibboleth.idp.authn.duo.BasicDuoIntegration" p:APIHost="%{idp.duo.apiHost:none}" p:applicationKey="%{idp.duo.applicationKey:none}" p:integrationKey="%{idp.duo.integrationKey:none}" p:secretKey="%{idp.duo.secretKey:none}" /> <bean id="SpecialDuo" class="net.shibboleth.idp.authn.duo.BasicDuoIntegration" p:APIHost="%{idp.specialduo.apiHost:none}" p:applicationKey="%{idp.specialduo.applicationKey:none}" p:integrationKey="%{idp.specialduo.integrationKey:none}" p:secretKey="%{idp.specialduo.secretKey:none}" /> <util:map id="DuoIntegrationMap"> <entry key="default" value-ref="DefaultDuo" /> <entry key="https://special1.example.org/shibboleth" value-ref="SpecialDuo" /> <entry key="https://special2.example.org/shibboleth" value-ref="SpecialDuo" /> </util:map> <bean id="shibboleth.authn.Duo.DuoIntegrationStrategy" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript" p:customObject-ref="DuoIntegrationMap"> <constructor-arg> <value> <![CDATA[ duo = null; rpCtx = input.getSubcontext("net.shibboleth.idp.profile.context.RelyingPartyContext"); if (rpCtx) { duo = custom.get(rpCtx.getRelyingPartyId()); } if (duo == null) { duo = custom.get("default"); } duo; ]]> </value> </constructor-arg> </bean>

Integration-Specific Principal Sets

In conjunction with the above feature, you can also cause the resulting Java Subject to carry Principals specific to a given Duo integration, which is useful if you want to segregate the integrations during SSO. That is, given two integrations A and B, you may want the use of A to satisfy a request that would require B, but you may not want that.

By default, since the Principals added into the result come from the underlying login flow descriptor for "authn/Duo", a request that uses either integration will produce a result that will be usable on any later request for Duo authentication. To prevent this, you need to do a couple of things:

  • Disable the addDefaultPrincipals feature for the Duo flow:

    • V4.0 – Create a bean in authn/duo-authn-config.xml named shibboleth.authn.Duo.addDefaultPrincipals set to Boolean.FALSE

    • V4.1+ – Set idp.authn.Duo.addDefaultPrincipals property in authn/authn.properties to false

  • Define separate Principal collections unique to each integration and associate them with the Duo integrations you define

Building on the previous example, the following shows how this might look. The approach is different in that the choice of Duo integration to use is implemented by evaluating each one to pick the first one that satisfies the request. This works in conjunction with triggering the integration to use based on SPs requesting the AuthnContextClassRef defined below (or setting it in a relying party override).

Multiple Integrations with Distinct Principal Sets
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 <!-- Turn off default behavior in favor of integration-specific principals below. --> <util:constant id="shibboleth.authn.Duo.addDefaultPrincipals" static-field="java.lang.Boolean.FALSE" /> <bean id="DefaultDuo" class="net.shibboleth.idp.authn.duo.BasicDuoIntegration" p:APIHost="%{idp.duo.apiHost:none}" p:applicationKey="%{idp.duo.applicationKey:none}" p:integrationKey="%{idp.duo.integrationKey:none}" p:secretKey="%{idp.duo.secretKey:none}"> <property name="supportedPrincipals"> <list> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="http://example.org/ac/classes/mfa/default" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:method="http://example.org/ac/classes/mfa/default" /> </list> </property> </bean> <bean id="SpecialDuo" class="net.shibboleth.idp.authn.duo.BasicDuoIntegration" p:APIHost="%{idp.specialduo.apiHost:none}" p:applicationKey="%{idp.specialduo.applicationKey:none}" p:integrationKey="%{idp.specialduo.integrationKey:none}" p:secretKey="%{idp.specialduo.secretKey:none}"> <property name="supportedPrincipals"> <list> <bean parent="shibboleth.SAML2AuthnContextClassRef" c:classRef="http://example.org/ac/classes/mfa/special" /> <bean parent="shibboleth.SAML1AuthenticationMethod" c:method="http://example.org/ac/classes/mfa/special" /> </list> </property> </bean> <util:list id="DuoIntegrationList"> <ref bean="SpecialDuo" /> <ref bean="DefaultDuo" /> </util:list> <bean id="shibboleth.authn.Duo.DuoIntegrationStrategy" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript" p:customObject-ref="DuoIntegrationList"> <constructor-arg> <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>

Advanced HttpClient Scenarios

As a REST API, the AuthAPI requires an HTTP client to access the API and to secure it. Obviously this is a particularly sensitive API to rely on, and while the defaults should be relatively safe, that isn't a great thing to assume. It would be a sensible thing to explicitly control the TLS verification of the Duo API certificate so that there are no nasty surprises. Unfortunately this creates other maintenance responsibilities; companies like Duo don't think that blindly trusting 100 CAs or more is a problem and they change their CA any time they feel like it. Of course, it is a problem. All we can do is provide the tools to manage this more carefully, but we can't fix the web.

Assuming you want to do this, the following example to place in conf/authn/duo-authn-config.xml will ensure that the root CA used by Duo is a specific CA. It assumes you've pulled down and stored the CA certificate in a file, and it also assumes the subject name Duo uses continues to be the same wildcard, which seems likely. But if they change their CA, as they probably will someday, you won't know and it will break, so if that bothers you, you should speak to Duo about it.

duo-authn-config.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 37 38 39 <?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"> <!-- Require an explicit CA root on Duo AuthAPI calls. --> <bean id="shibboleth.authn.Duo.NonBrowser.HttpClientSecurityParameters" class="org.opensaml.security.httpclient.HttpClientSecurityParameters"> <property name="tLSTrustEngine"> <bean parent="shibboleth.StaticPKIXTrustEngine" p:trustedNames="*.duosecurity.com" p:checkNames="true"> <property name="certificates"> <list> <bean class="org.springframework.core.io.FileSystemResource" c:_0="%{idp.home}/credentials/duo-ca.pem"/> </list> </property> </bean> </property> </bean> <bean id="shibboleth.authn.Duo.NonBrowser.HttpClient" parent="shibboleth.HttpClientFactory" p:connectionTimeout="%{idp.httpclient.connectionTimeout:PT1M}" p:connectionRequestTimeout="%{idp.httpclient.connectionRequestTimeout:PT1M}" p:socketTimeout="%{idp.httpclient.socketTimeout:PT1M}" p:maxConnectionsTotal="%{idp.httpclient.maxConnectionsTotal:100}" p:maxConnectionsPerRoute="%{idp.httpclient.maxConnectionsPerRoute:100}" p:tLSSocketFactory-ref="shibboleth.SecurityEnhancedTLSSocketFactory" /> </beans>

Reference

The beans defined, or expected to be defined, in authn/duo-authn-config.xml follow:

Bean ID / Type

Default

Description

Bean ID / Type

Default

Description

shibboleth.authn.Duo.DuoIntegration

DuoIntegration

Derived from settings in duo.properties

Defines a single/static DuoWeb ntegration with Duo, you can override this bean to supply a non-property-configured alternative

shibboleth.authn.Duo.NonBrowser.DuoIntegration

DuoIntegration

Derived from settings in duo.properties

Defines a single/static AuthAPI integration with Duo, you can override this bean to supply a non-property-configured alternative

shibboleth.authn.Duo.DuoIntegrationStrategy

Function<ProfileRequestContext,DuoIntegration>

 

Optional bean to supply the DuoWeb integration settings dynamically

shibboleth.authn.Duo.NonBrowser.DuoIntegrationStrategy

Function<ProfileRequestContext,DuoIntegration>

 

Optional bean to supply the Duo AuthAPI integration settings dynamically

shibboleth.authn.Duo.UsernameLookupStrategy

Function<ProfileRequestContext,String>

CanonicalUsernameLookupStrategy

Optional bean to supply username

shibboleth.authn.Duo.resultCachingPredicate

Predicate<ProfileRequestContext>

 

An optional bean that can be defined to control whether to preserve the authentication result in an IdP session

shibboleth.authn.Duo.addDefaultPrincipals

Boolean

true

Whether to add the content of the supportedPrincipals property of the underlying flow descriptor to the resulting Subject

shibboleth.authn.Duo.NonBrowser.HttpClient

HttpClient

Internal/default HttpClient instance

Overrides the HttpClient implementation and settings to use for the AuthAPI (see HttpClientConfiguration)

shibboleth.authn.Duo.NonBrowser.HttpClientSecurityParameters

HttpClientSecurityParameters

 

Custom security settings for the AuthAPI calls (see HttpClientConfiguration)

The properties defined in conf/authn/duo.properties follow:

Name

Default

Function

Name

Default

Function

idp.duo.apiHost    

 

DuoWeb API hostname assigned to the integration

idp.duo.applicationKey

 

A secret supplied by you and not shared with Duo; see https://duo.com/docs/duoweb-v2, "Generate an akey".

idp.duo.integrationKey

 

DuoWeb integration key (supplied by Duo as Client ID)

idp.duo.secretKey

 

DuoWeb secret key (supplied by Duo as Client secret)

idp.duo.nonbrowser.apiHost

${idp.duo.apiHost}                  

Duo AuthAPI hostname assigned to the integration

idp.duo.nonbrowser.applicationKey



a secret supplied by you and not shared with Duo; see https://duo.com/docs/duoweb-v2, "Generate an akey".

idp.duo.nonbrowser.integrationKey

 

Duo AuthAPI integration key (supplied by Duo as Client ID)

idp.duo.nonbrowser.secretKey

 

Duo AuthAPI secret key (supplied by Duo as Client secret)

idp.duo.nonbrowser.header.factor

X-Shibboleth-Duo-Factor

Name of HTTP request header for Duo AuthAPI factor

idp.duo.nonbrowser.header.device

X-Shibboleth-Duo-Device

Name of HTTP request header for Duo AuthAPI device ID or name

idp.duo.nonbrowser.header.passcode

X-Shibboleth-Duo-Passcode

Name of HTTP request header for Duo AuthAPI passcode

idp.duo.nonbrowser.auto

true

Allow the factor to be defaulted in as "auto" if no headers are received

idp.duo.nonbrowser.clientAddressTrusted

true

Pass client address to Duo in API calls to support logging, push display, and network-based Duo policies

The Duo-specific properties defined in conf/authn/duo.properties follow:

Name

Default

Description

Name

Default

Description

idp.duo.apiHost    

 

DuoWeb API hostname assigned to the integration

idp.duo.applicationKey

 

A secret supplied by you and not shared with Duo; see https://duo.com/docs/duoweb-v2, "Generate an akey".

idp.duo.integrationKey

 

DuoWeb integration key (supplied by Duo as Client ID)

idp.duo.secretKey

 

DuoWeb secret key (supplied by Duo as Client secret)

idp.duo.nonbrowser.apiHost

${idp.duo.apiHost}                  

Duo AuthAPI hostname assigned to the integration

idp.duo.nonbrowser.integrationKey

 

Duo AuthAPI integration key (supplied by Duo as Client ID)

idp.duo.nonbrowser.secretKey

 

Duo AuthAPI secret key (supplied by Duo as Client secret)

idp.duo.nonbrowser.header.factor

X-Shibboleth-Duo-Factor

Name of HTTP request header for Duo AuthAPI factor

idp.duo.nonbrowser.header.device

X-Shibboleth-Duo-Device

Name of HTTP request header for Duo AuthAPI device ID or name

idp.duo.nonbrowser.header.passcode

X-Shibboleth-Duo-Passcode

Name of HTTP request header for Duo AuthAPI passcode

idp.duo.nonbrowser.auto

true

Allow the factor to be defaulted in as "auto" if no headers are received

idp.duo.nonbrowser.clientAddressTrusted

true

Pass client address to Duo in API calls to support logging, push display, and network-based Duo policies

The general properties configuring this flow via authn/authn.properties are:

Name

Default

Description

Name

Default

Description

idp.authn.Duo.order

1000

Flow priority relative to other enabled login flows (lower is "higher" in priority)

idp.authn.Duo.nonBrowserSupported

false

Whether the flow should handle non-browser request profiles (e.g., ECP)

idp.authn.Duo.passiveAuthenticationSupported

false

Whether the flow allows for passive authentication

idp.authn.Duo.forcedAuthenticationSupported

true

Whether the flow supports forced authentication

idp.authn.Duo.proxyRestrictionsEnforced

%{idp.authn.enforceProxyRestrictions:true}

Whether the flow enforces upstream IdP-imposed restrictions on proxying

idp.authn.Duo.proxyScopingEnforced

false

Whether the flow considers itself to be proxying, and therefore enforces SP-signaled restrictions on proxying

idp.authn.Duo.discoveryRequired

false

Whether to invoke IdP-discovery prior to running flow

idp.authn.Duo.lifetime

%{idp.authn.defaultLifetime:PT1H}

Lifetime of results produced by this flow

idp.authn.Duo.inactivityTimeout

%{idp.authn.defaultTimeout:PT30M}

Inactivity timeout of results produced by this flow

idp.authn.Duo.reuseCondition

shibboleth.Conditions.TRUE

Bean ID of Predicate<ProfileRequestContext> controlling result reuse for SSO

idp.authn.Duo.activationCondition

shibboleth.Conditions.TRUE

Bean ID of Predicate<ProfileRequestContext> determining whether flow is usable for request

idp.authn.Duo.subjectDecorator

 

Bean ID of BiConsumer<ProfileRequestContext,Subject> for subject customization

idp.authn.Duo.supportedPrincipals

(see below)

Comma-delimited list of protocol-specific Principal strings associated with flow

idp.authn.Duo.addDefaultPrincipals

true

Whether to auto-attach the preceding set of Principal objects to each Subject produced by this flow

As a non-password based flow, the supportedPrincipals property defaults to the following XML:

1 2 3 4 5 6 <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>

In property form, this is expressed as (note the trailing commas):

1 2 3 idp.authn.Duo.supportedPrincipals = \ saml2/http://example.org/ac/classes/mfa, \ saml1/http://example.org/ac/classes/mfa

However, this default is (obviously) intended purely as an illustrative example of how to define your own values, as there are no standard ones to use. Some suggestions are mentioned earlier under General Configuration.

To replace the internally defined flow descriptor bean, the following XML is required:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <util:list id="shibboleth.AvailableAuthenticationFlows"> <bean p:id="authn/Duo" parent="shibboleth.AuthenticationFlow" p:order="%{idp.authn.Duo.order:1000}" p:nonBrowserSupported="%{idp.authn.Duo.nonBrowserSupported:false}" p:passiveAuthenticationSupported="%{idp.authn.Duo.passiveAuthenticationSupported:false}" p:forcedAuthenticationSupported="%{idp.authn.Duo.forcedAuthenticationSupported:true}" p:proxyRestrictionsEnforced="%{idp.authn.Duo.proxyRestrictionsEnforced:%{idp.authn.enforceProxyRestrictions:true}}" p:proxyScopingEnforced="%{idp.authn.Duo.proxyScopingEnforced:false}" p:discoveryRequired="%{idp.authn.Duo.discoveryRequired:false}" p:lifetime="%{idp.authn.Duo.lifetime:%{idp.authn.defaultLifetime:PT1H}}" p:inactivityTimeout="%{idp.authn.Duo.inactivityTimeout:%{idp.authn.defaultTimeout:PT30M}}" p:reuseCondition-ref="#{'%{idp.authn.Duo.reuseCondition:shibboleth.Conditions.TRUE}'.trim()}" p:activationCondition-ref="#{'%{idp.authn.Duo.activationCondition:shibboleth.Conditions.TRUE}'.trim()}" p:subjectDecorator-ref="#{getObject('%{idp.authn.Duo.subjectDecorator:}'.trim())}"> <property name="supportedPrincipalsByString"> <bean parent="shibboleth.CommaDelimStringArray" c:_0="#{'%{idp.authn.Duo.supportedPrincipals:}'.trim()}" /> </property> </bean> </util:list>

In older versions and upgraded systems, this list is defined in conf/authn/general-authn.xml. In V4.1+, no default version of the list is provided and it may simply be placed in conf/global.xml if needed.