Page tree
Skip to end of metadata
Go to start of metadata

Quick Setup Guide

If you're looking for a quick reference guide that assumes a basic, default, configuration, see here. Note, you really should read this page first.

Overview

This authentication plugin (DuoOIDC) supports Duo’s strong two-factor authentication using their OIDC-based integration model introduced in 2021 (Duo OIDC Auth API). This includes both the traditional prompt and the new Universal Prompt. The Universal Prompt is a major UX redesign of the older in-page iFrame prompt. In both cases, the user is redirected, via a full-frame redirect, to a Duo-hosted site using the OIDC protocol to perform second-factor authentication, and the results are made available to the IdP as a form of an OIDC ID Token. Duo's support is compliant with OIDC with a few caveats.

Like the original integration based on their WebSDK V2, this plug-in is designed to be used as a second factor of authentication, so is therefore used in conjunction with an existing ‘first-factor', usually orchestrated by the MFA login flow (see MultiFactorAuthnConfiguration).

By default, the first-factor must produce an “official” username as part of post-login canonicalization which the DuoOIDC flow can use as the Duo username in the second-factor authentication request. In unusual cases it is possible to customize the source of the username.

The result of this flow is a Java Subject containing a DuoPrincipal as well as a custom set of additional Principals, typically representing SAML AuthenticationContextClassRefs.

Plugin Installation

There are two different DuoOIDC Auth API plugin implementations. Both share the majority of their codebase, the difference being how they interact with Duo’s OIDC Provider. One is based on the official Duo WebSDK v4, and one is based on a Shibboleth implementation using Nimbus’s JOSE-JWT handling - although it is worth noting that part of the common codebase uses the Nimbus library for certain tasks irrespective of which plugin you use.

In most cases, we would suggest trying the Nimbus-based plugin first, particuarly if you plan to make use of the OIDC OP plugin as well, as this avoids a number of duplicated code libraries in the IdP. Duo built their SDK on top of a different OIDC/JOSE library stack, whereas we used Nimbus, allowing more code to be shared across the different components.

Dependencies

This module depends on the Shibboleth OIDCCommon plugin which you must install first. The installer should prevent installation if this is not in place.

PluginPlugin IDModule IDAuthentication Flow IDVersion
Duo Universal Prompt via the Shibboleth Nimbus Clientnet.shibboleth.idp.plugin.authn.duo.nimbusidp.authn.DuoOIDCauthn/DuoOIDC1.0.0
Duo Universal Prompt via Duo WebSDK v4 Clientnet.shibboleth.idp.plugin.authn.duo.sdkidp.authn.DuoOIDCauthn/DuoOIDC1.0.0


The following table highlights the differences in their technical specification to help you decide which to install. Note, their functional specification (how it works for the end-user) is the same for either.

FeatureDuo WebSDK v4Shibboleth Nimbus
Based on the official SDKX
Duo Endpoint and Configuration Health CheckXX
Duo 2FA result token signature (HMAC) checkingXX
Duo 2FA result token encryption handling (not provided by Duo)

Duo 2FA result token claims verificationXX
Duo 2FA result token nonce verification
X
Customizable HttpClient implementation
X
Customizable TrustEngine implementation
X
HTTP Public Key PinningXX
Supports TLS Certification Revocation CheckingXX
Customisable JSON response mapper
X


For a detailed guide on how to install plugins, see here.

In summary, use the plugin command that ships with the IdP to install the plugin from either a local file pre-downloaded, or from a URL.

Installation
$ /opt/shibboleth-idp/bin/plugin.sh -i <plugin.tar.gz>

or

C:>\opt\shibboleth-idp\bin\plugin.bat -i <plugin.zip>

If installing from a local file, you need to ensure the GPG detached signature (e.g. the .asc file) is placed alongside the main plugin archive on disk.

Listing Installed Plugins
$ /opt/shibboleth-idp/bin/plugin.sh -l

or

C:>\opt\shibboleth-idp\bin\plugin.bat -l

Enabling the Module

For a detailed guide on configuring modules, see the ModuleConfiguration topic. Once the plugin has been installed, its module should be enabled automatically for you:

Check Module Is Enabled
/%{idp.home}/bin$ ./module.sh -l

...
Module: idp.authn.DuoOIDC [ENABLED]

However, if you need to enable it you can using the module command:

Enable the module
/%{idp.home}/bin$ ./module.sh -e idp.authn.DuoOIDC

Either manual or automatic module enablement will copy across the following configuration files from the jar:

Configuration files
jar:duo-oidc-authn-config.xml ->  conf/authn/duo-oidc-authn-config.xml
jar:duo-oidc.properties -> conf/authn/duo-oidc.properties

Automatic Flow Registration

The flow definition, default beans, and authentication flow descriptor are loaded automatically from well-known location(s) from the plugin’s classpath. The default behavior configured in those files can be overridden via the two configuration files shown above.

Once installed and enabled, you will then need to start configuring the flow.

General Configuration

Once you have configured a Shibboleth ‘Protected Application’ and enabled support for the Universal Prompt in the Duo Admin Panel (see also Duo Universal Prompt), you'll need to copy across your client ID, API hostname and client secret into the conf/authn/duo-oidc.properties file to form your Duo Integration. The client ID and secret will likely appear as integration key and secret key until you make your first request using the new AuthAPI i.e. actually use this plugin for authentication. Note, you may want to keep the client secret in credentials/secrets.properties for consistency with other IdP secrets

Next, you need to specify a redirection URI as per the OAuth2.0 specification (RFC6749). This is the endpoint the Duo Universal Prompt will redirect the end-users user-agent (browser) to after successful second-factor authentication. By default, Duo does not require you to pre-register redirect URIs (you can request this if desired),  instead taking one supplied by the IdP inside a signed JWT request object. This opens a few different possibilities depending on which of the two clients you choose. The simplest, supported by both clients, is to define a static idp.duo.oidc.redirectURL property in the conf/authn/duo-oidc.properties file:

Redirect URI
idp.duo.oidc.redirectURL = https://<hostname>:<port>/idp/profile/Authn/Duo/2FA/duo-callback

Where <hostname> and <port> match that of your running IdP - the port can be omitted if your IdP uses the standard HTTP/HTTPS port e.g. 80 and 443 respectively. You are free to change part of that path i.e. Authn/Duo/2FA by setting the idp.duo.oidc.externalAuthnPath property. Although in most cases there is little reason for doing so. The endpoint itself needs only be accessible by the end-user’s user-agent (browser).

Alternatively, you can let the plugin determine the redirect URI from the Host header sent from the client that issues the 'first' Duo 2FA request. To do so, comment out the idp.duo.oidc.redirectURL property and then, in order to prevent HTTP Host header injection attacks (and possibly leaking your authorization code to a malicious actor), declare one or more comma-seperated allowed 'origins' [RFC6454] in the property idp.duo.oidc.redirecturl.allowedOrigins. Each origin is a combination of scheme, host, and port. For schemes where the default port is used e.g. HTTPS on port 443, the port can be omitted. For example, assuming a production IdP has a hostname of prod.example.com using the HTTPS scheme over the default port, and a matching development IdP has a hostname of dev.example.com running on a custom port 8443, the following origins would be sufficient:

Allowed Origins
idp.duo.oidc.redirecturl.allowedOrigins = https://prod.example.com, https://dev.example.com:8443

This can be useful if you want to keep a single generic configuration between development, staging, and production servers, etc.

Further to this, if you are using the Shibboleth Nimbus client, the redirect URI will be created dynamically from the Host header per-request - there is no extra configuration for this, it works per-request by default. This could be useful, for example, if a single IdP instance is serving requests from more than one virtual-host, and each Duo 2FA request will need to be redirected back to the originating Hostname in order to successfully complete the request.

As an advanced configuration option, you can specify more than one Duo integration and use a runtime function to determine which is used per authentication request. 

Given that Duo's 'second-factor' authentication runs after a 'first-factor' authentication method, you will also need to enable the "idp.authn.MFA" module in addition to an appropriate first-factor e.g. the Password flow module "idp.authn.Password". See the MFA configuration tab for a sample configuration.

The flow supports a secondary integration allowing use of the Duo AuthAPI for non-browser use cases such as SAML ECP. To enable support for the AuthAPI, you 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 setting the idp.authn.DuoOIDC.nonBrowserSupported property.

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.

Here we describe an example MFA flow using both the MFA and Password flows (in addition to the new DuoOIDC flow). Of course, this example assumes you have also enabled those modules in addition to this plugin/module, as per their documentation pages.

For testing, the credential store of the Password flow can be configured from a simple flat file of usernames/passwords, for example, using the HTPasswdCredentialValidator in conf/authn/password-authn-config.xml as shown below. If this file is missing, you have not yet enabled the "idp.authn.Password" module.

Example password validator
<util:list id="shibboleth.authn.Password.Validators">
    <bean parent="shibboleth.HTPasswdValidator" p:resource="%{idp.home}/conf/authn/htpasswd.txt" />
</util:list>

Or, you may already have configured a 'production' ready password validator e.g. using LDAP. Either way, your next step is to compose both these flows into a suitable multi-factor authentication flow. An example flow defined in conf/authn/mfa-authn-config.xml file is shown below. If this file is missing, you have not yet enabled the "idp.authn.MFA" module.

Example MFA configuration
<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. Currently just returns the DuoOIDC flow -->
    <bean id="checkSecondFactor" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript">
        <constructor-arg>
            <value>
            <![CDATA[
                nextFlow = "authn/DuoOIDC";

                // 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>

In summary, the IdP will run the "authn/Password" flow followed by the "authn/DuoOIDC" flow. More complex business/orchestration logic can be added to the checkSecondFactor script if required, see the MFA page for a full discussion of the possibilities.

After configuring the MFA orchestration logic appropriately, you should consider how to represent the multi-factor authentication mechanism to the outside world.

As the DuoOIDC plugin typically runs after a first-factor authentication method orchestrated by the MFA flow, the MFA flow must present to the system a supportedPrincipals collection compatible with this type of authentication mechanism alongside any other factor(s) used.  In the SAML 2.0 world (and nowadays more generally, e.g., OpenID Connect) these are specified as an Authentication Context Class.

Those already set by default on the MFA flow are described by the idp.authn.MFA.supportedPrincipals property in the conf/authn/authn.properties file. (If your system has been upgraded from V4.0 or earlier, then you may not have migrated to the property-centric settings V4.1 allows, and may have the supported Principals enumerated in conf/authn/general-authn.xml.) Either way,these need to be adjusted to also include those that are also exposed by the "authn/DuoOIDC" flow.

This flow defaults to an example set defined by the idp.authn.DuoOIDC.supportedPrincipals property in conf/duo-oidc.properties. If not set, it will fall back to the definition of the idp.authn.Duo.supportedPrincipals property.

As for what these values should be:

Supported Principals

There is no "standard" context class (or SAML 1 authentication method) to represent most forms of MFA, and 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.

For more advanced supportedPrincipal configurations, see below

The plugin itself does not provide a native IdP view to customize because all second-factor authentication interactions occur on a Duo-hosted site to which the IdP redirects the user-agent (browser). See the Duo Admin Panel to see what customizations are possible on the Duo site. It is important to note that unsuccessful second-factor authentication terminates on the Duo site. The IdP will not receive notification of the failure, and hence authentication failure cannot be propagated further to the SP.

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 will fail while resulting in the issuance of codes via SMS for subsequent use.

Advanced Configuration

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 integration, which is typically the best approach.

If you need a more flexible approach, you can configure a bean named shibboleth.authn.DuoOIDC.UsernameLookupStrategy of type Function<ProfileRequestContext,String>, which can be defined in conf/authn/duo-oidc-authn-config.xmlThe function returns the canonical username to use and can introspect the ProfileRequestContext if required.

Both plugin variants are, by default, configured with the same static set of ‘pinned’ root certificate authority certificates (trust anchors). As a result, the chain of trust associated with the end-entity certificate presented by the Duo API must be anchored by one of these root authorities and no other e.g. not the default set provided by the JDK. The current set of pinned root certificates are listed in the table below:

Root CertificatePublic Key Hash
C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Assured ID Root CAsha256/I/Lt/z7ekCWanjD0Cvj5EqXls2lOaThEA0H2Bg4BT/o=
C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root CAsha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=
C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert High Assurance EV Root CAsha256/WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=
C=US, O=SecureTrust Corporation, CN=SecureTrust CAsha256/dykHF2FLJfEpZOvbOLX4PKrcD2w2sHd/iA/G3uHTOcw=
C=US, O=SecureTrust Corporation, CN=Secure Global CAsha256/JZaQTcTWma4gws703OR/KFk313RkrDcHRvUt6na6DCg=
C=US, O=Amazon, CN=Amazon Root CA 1sha256/++MBgDH5WGvL9Bcn5Be30cRcL0f5O+NyoXuWtQdX1aI=
C=US, O=Amazon, CN=Amazon Root CA 2sha256/f0KW/FtqTjs108NpYj42SrGvOB2PpxIVM8nWxjPqJGE=
C=US, O=Amazon, CN=Amazon Root CA 3sha256/NqvDJlas/GRcYbcWE8S/IceH9cq77kg0jVhZeAPXq8k=
C=US, O=Amazon, CN=Amazon Root CA 4sha256/9+ze1cZgR9KO1kZrVDxA4HQ6voHRCSVNz4RdTCx4U8U=
C=BM, O=QuoVadis Limited, CN=QuoVadis Root CA 2sha256/j9ESw8g3DxR9XM06fYZeuN1UB4O6xp/GAIjjdD/zM3g=

The Duo WebSDK v4 plugin uses public-key hash pinning, whereas the Shibboleth Nimbus plugin pins the entire CA certificate. The set of pinned hashes/certificates can be overridden for each plugin by including one of the following beans in the conf/authn/duo-oidc-authn-config.xml file:

For the Shibboleth Nimbus plugin - the values in the list are any valid Spring Resource:

Override Shibboleth Nimbus Pinned Trusted Certificates
<util:list id="shibboleth.authn.DuoOIDC.nimbus.TrustedCertificates">
    <value>file://cert-filename.crt</value>
</util:list>

For the Duo WebSDK v4 plugin - the values in the list are the public key hashes of pinned certificates:

Override Duo WebSDK v4 Pinned Trusted Certificates
<util:list id="shibboleth.authn.DuoOIDC.sdk.TrustedCertificates">
    <value>sha256/public-key-hashvalue</value>
</util:list>

Empty Certificate List

If you set an empty list for the Duo WebSDK v4 plugin's TrustedCertificates, the default certificate pins hardcoded inside the Duo WebSDK v4 client will be used. For the Shibboleth Nimbus plugin, an empty list represents an empty set of trusted root certificates, and requests will always fail.

For a basic configuration, you set the supportedPrincipals property on both the "authn/MFA" and "authn/DuoOIDC" underlying AuthenticationFlowDescriptor objects. These are copied across as Principal objects to the resulting Java Subject after successful authentication. If you need greater granularity, you can configure custom Principal sets in the following ways:

  • Directly on the DuoOIDCIntegration bean. This is useful if you have more than one Duo integration and want one to satisfy a request the other does not, usually due to different policies or factors supported on the Duo end. This works the same as the original Duo integration.
  • Via a strategy function. This allows runtime decisions about Principal to be made from the Duo 2FA result token.

Both are described below.

Directly on the DuoOIDCIntegration

You can define Principal sets per Duo Integration. This is particularly useful if you have more than one integration, and want each to satisfy a different Authentication Context Class Reference requested by a Service Provider. For example:

Per Integration Supported Principals
<bean id="shibboleth.authn.DuoOIDC.DuoIntegration"
        class="net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration" 
		p:APIHost="%{idp.duo.oidc.apiHost:none}"
        p:clientId="%{idp.duo.oidc.clientId:none}" p:secretKey="%{idp.duo.oidc.secretKey:none}"
        p:registeredRedirectURI="%{idp.duo.oidc.redirectURL:}" 
        p:healthCheckEndpoint="%{idp.duo.oidc.endpoint.health:/oauth/v1/health_check}"
        p:tokenEndpoint="%{idp.duo.oidc.endpoint.token:/oauth/v1/token}"
        p:authorizeEndpoint="%{idp.duo.oidc.endpoint.authorize:/oauth/v1/authorize}"
		p:allowedOrigins="%{idp.duo.oidc.redirecturl.allowedOrigins:}">
		<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="shibboleth.authn.DuoOIDC.SecondDuoIntegration"
        class="net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration" 
		p:APIHost="%{idp.second.duo.oidc.apiHost:none}"
        p:clientId="%{idp.second.duo.oidc.clientId:none}" p:secretKey="%{idp.second.duo.oidc.secretKey:none}"
        p:registeredRedirectURI="%{idp.second.duo.oidc.redirectURL:none}" 
        p:healthCheckEndpoint="%{idp.second.duo.oidc.endpoint.health:/oauth/v1/health_check}"
        p:tokenEndpoint="%{idp.second.duo.oidc.endpoint.token:/oauth/v1/token}"
        p:authorizeEndpoint="%{idp.second.duo.oidc.endpoint.authorize:/oauth/v1/authorize}"
		p:allowedOrigins="%{idp.duo.oidc.redirecturl.allowedOrigins:}">
		<property name="supportedPrincipals">
       	 	<list>
          		  <bean parent="shibboleth.SAML2AuthnContextClassRef"
             		   c:classRef="http://example.org/ac/classes/mfa/second" />
        	</list>
    	</property>
</bean>


Context to Principal Mapping Strategy

If you want to add Principals to the Java Subject based on runtime information received inside a Duo result (authentication) token, you can add a Java bean named shibboleth.authn.DuoOIDC.ContextToPrincipalMappingStrategy that could, for example, inspect the JWT ClaimsSet of the Duo token. The type of this bean is Function<ProfileRequestContext,Collection<Principal>> (input is the profile request context tree, and the output is the collection to inject into the Subject).

The following example script adds the ACR "http://example.org/ac/classes/mfa/strong" to the Java Subject if the 'duo_push' two-factor authentication method was used (this is not a commentary on the "strength" of push-based MFA, strictly an example).

Example Context To Principal Mapping Strategy
<bean id="shibboleth.authn.DuoOIDC.ContextToPrincipalMappingStrategy" parent="shibboleth.ContextFunctions.Scripted" 
        factory-method="inlineScript"
        p:outputType="java.util.Collection"
        p:hideExceptions="false">
        <constructor-arg>
            <value>
        <![CDATA[
            //setup logger and types
            logger = Java.type("org.slf4j.LoggerFactory").getLogger("org.example.duo.script");
            var ArrayList = Java.type("java.util.ArrayList");
            var ACR = Java.type("net.shibboleth.idp.saml.authn.principal.AuthnContextClassRefPrincipal");
            
            //create new output collection
            var principals = new ArrayList(1);
            
            var ac = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext");
            if (ac != null){
                var dc = ac.getSubcontext("net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext");
                if (dc != null){
                     if (dc.getAuthToken() != null && "duo_push".equals(dc.getAuthToken().getJWTClaimsSet().getJSONObjectClaim("auth_context").get("factor"))){
                         principals.add(new ACR("http://example.org/ac/classes/mfa/strong"));
                     }
                }
            }
            //return obj is the value of the last expression.
            principals;
         ]]>
            </value>
        </constructor-arg>
</bean>

It is important to note that if the mapping strategy bean is defined, the default supportedPrincipals of the underlying flow descriptor are not added to the Java Subject. Instead, alongside those added by the Function, only a DuoPrincipal, and any Principals defined on the DuoOIDCIntegration itself are added.

The following JSON listing shows an example of the current Duo JWT ClaimsSet for reference when constructing a mapping strategy. Refer to Duo's documentation as the final word on the subject.

Duo JWT ClaimsSet
{
    "iss": "https://api.duosecurity.com/oauth/v1/token",
    "sub": "subject",
    "aud": "DDFGGSGERGERR",
    "exp": 1590070939,
    "iat": 1590067340,
    "auth_time": 1590067339,
    "auth_result": {
        "status_msg": "Login Successful",
        "status": "allow",
        "result": "allow"
    },
    "auth_context": {
        "result": "success",
        "timestamp": 1590067339,
        "auth_device": {
            "ip": "100.100.100.12",
            "name": "telephone",
            "location": {
                "state": "State",
                "city": "City",
                "country": "United Kingdom"
            }
        },
        "txid": "b1287968-1dd1-4488-bb3c-0c72fc398b8b",
        "event_type": "authentication",
        "reason": "user_approved",
        "access_device": {
            "ip": "100.100.100.13",
            "location": {
                "state": "State",
                "city": "City",
                "country": "United Kingdom"
            }
        },
        "application": {
            "key": "DDFGGSGERGERR",
            "name": "Duo Integration"
        },
        "factor": "duo_push",
        "user": {
            "key": "DDFGGSGERGERR",
            "name": "username"
        }
    }
}

As is common for OIDC Providers, Duo presents an HTTP API for the IdP to communicate with directly as a callback. With the Shibboleth Nimbus plugin, you have the option to override the default HttpClient object used during that communication by specifying your own HttpClientFactoryBean bean named shibboleth.authn.DuoOIDC.nimbus.HttpClient.

It is then up to you what 'security' features you add to this client e.g. certificate trust, alongside standard connection properties such as connection timeouts.

The default client already contains enough sensible, overridable, defaults that it would only be in very unusual cases that you would want to override it but is a standard approach to allow for it.

Unchanged functionality

If you're already family with this functionality from the original Duo integration, very little of the approach has changed.

If you need to support multiple sets of Duo integration parameters, you can implement a Function<ProfileRequestContext,DuoOIDCIntegration> in Java or a script in a bean named shibboleth.authn.DuoOIDC.DuoIntegrationStrategy, which can be defined in conf/authn/duo-oidc-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
<bean id="DefaultDuo" class="net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration"
   		p:APIHost="%{idp.duo.oidc.apiHost:none}"
        p:clientId="%{idp.duo.oidc.clientId:none}" 
		p:secretKey="%{idp.duo.oidc.secretKey:none}"
        p:registeredRedirectURI="%{idp.duo.oidc.redirectURL:}" 
        p:healthCheckEndpoint="%{idp.duo.oidc.endpoint.health:/oauth/v1/health_check}"
        p:tokenEndpoint="%{idp.duo.oidc.endpoint.token:/oauth/v1/token}"
        p:authorizeEndpoint="%{idp.duo.oidc.endpoint.authorize:/oauth/v1/authorize}" 
		p:allowedOrigins="%{idp.duo.oidc.redirecturl.allowedOrigins:}"/>

<bean id="SpecialDuo" class="net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration"
   		p:APIHost="%{idp.specialduo.oidc.apiHost:none}"
        p:clientId="%{idp.specialduo.oidc.clientId:none}" 
		p:secretKey="%{idp.duo.oidc.secretKey:none}"
        p:registeredRedirectURI="%{idp.specialduo.oidc.redirectURL:}" 
        p:healthCheckEndpoint="%{idp.specialduo.oidc.endpoint.health:/oauth/v1/health_check}"
        p:tokenEndpoint="%{idp.specialduo.oidc.endpoint.token:/oauth/v1/token}"
        p:authorizeEndpoint="%{idp.specialduo.oidc.endpoint.authorize:/oauth/v1/authorize}" 
		p:allowedOrigins="%{idp.duo.oidc.redirecturl.allowedOrigins:}"/>

<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>

Neither plugin/client is configured by default to check the revocation status of the certificates presented during the TLS handshake. Generally, there are three options for this:

  1. Revocation checking from a Certificate Revocation List defined in a local store.
  2. Revocation checking from a Certificate Revocation List fetched from a distribution point defined in the certificate.
  3. Revocation checking from querying an Online Certificate Status Protocol (OCSP) endpoint defined in the certificate.

To configure revocation checking with the Duo WebSDK v4 plugin, use the normal Java Trust Manager system properties (see the CertPathDocs).

To enable revocation checking with the Shibboleth Nimbus plugin, you will need to set the property idp.duo.oidc.nimbus.checkRevocation to true in the conf/authn/duo-oidc.properties file *and* do one or more of the following:

  1. Add one or more 'approved' (issuer and signature verified) static CRLs as Resources to a list bean named shibboleth.authn.DuoOIDC.tls.CRLs to the conf/authn/duo-oidc-authn-config.xml file.
  2. Enable CRL checking from an online distribution point via the system property com.sun.security.enableCRLDP (as described in the CertPathDocs)
  3. Enable OCSP via the security system property oscp.enabled (as described in the CertPathDocs)

Note that Duo does not support OSCP stapling, so this is not an option at this stage.

Reference

Bean ID / TypeDefaultDescription

shibboleth.authn.DuoOIDC.DuoIntegration

DuoOIDCIntegration

Derived from properties in conf/authn/duo-oidc.propertiesDefines a single/static Duo OIDC Integration with Duo, you can override this bean to supply a non-property-configured alternative

shibboleth.authn.DuoOIDC.DuoIntegrationStrategy

Function<ProfileRequestContext,DuoOIDCIntegration>


Optional bean to supply the Duo OIDC integration settings dynamically

shibboleth.authn.DuoOIDC.NonBrowser.DuoIntegration

DuoIntegration

Derived from properties in conf/authn/duo-oidc.propertiesDefines a single/static Duo AuthAPI integration for non-browser support

shibboleth.authn.DuoOIDC.NonBrowser.DuoIntegrationStrategy

Function<ProfileRequestContext,DuoIntegration>


Optional bean to supply the Duo AuthAPI integration settings dynamically

shibboleth.authn.DuoOIDC.UsernameLookupStrategy

Function<ProfileRequestContext,String>

CanonicalUsernameLookupStrategyOptional bean to supply username

shibboleth.authn.DuoOIDC.resultCachingPredicate

Predicate<ProfileRequestContext>

shibboleth.Conditions.TRUEBean ID controlling whether to preserve the authentication result in an IdP session

shibboleth.authn.DuoOIDC.CleanUpHook

Consumer<ProfileRequestContext>

Bean that removes the DuoOIDAuthenticationContext from the tree

A cleanup hook that is executed on successful authentication.

shibboleth.authn.DuoOIDC.jwt.claims.CleanUpHook

Consumer<ProfileRequestContext>

Bean that removes the nonce value from the DuoOIDAuthenticationContext

A cleanup hook to execute after either successful or unsuccessful claims validation

shibboleth.authn.DuoOIDC.DuoTokenClaimsVerifier

JWTClaimsValidation

DefaultDuoTokenClaimsVerifier Claims verification in accordance with the Duo specification. Also OIDC compliant for the special Duo id_token case.

Duo result token (OIDC id_token) claims verifier using a 'chain' of ClaimsValidators e.g. audience, issuer, expiration checks etc. You can either replace the claims validator completely, change some of the behavior of existing validators individually, or add to a new validation check using a custom BiFunction, see shibboleth.authn.DuoOIDC.ExtendedClaimsValidator.

shibboleth.authn.DuoOIDC.jwt.IssuerLookupStrategy

BiFunction<ProfileRequestContext, JWTClaimsSet, String>

Returns the issuer by combining the HTTPS scheme, with the Duo API Hostname, and the Duo token IssuerPath.Lookup strategy that returns the OIDC issuer. An issuer contains the scheme, host, and optionally, port and path components that identify the id_token issuer. 

shibboleth.authn.DuoOIDC.jwt.AudienceLookupStrategy

BiFunction<ProfileRequestContext,JWTClaimsSet, String>

Returns the clientID of the Duo Integration pertaining to the request.Lookup the client_id for the Relying Party.

shibboleth.authn.DuoOIDC.jwt.UsernameLookupStrategy

BiFunction<ProfileRequestContext,JWTClaimsSet, String>

Returns the authenticating principal's username from the context pertaining to the request.Lookup the authenticating principal's username to match Duo's preferred_username field in the id_token.

shibboleth.authn.DuoOIDC.jwt.AuthTimeActivationCondition

BiPredicate<ProfileRequestContext,JWTClaimsSet>

Returns true if forced authentication has been requested by the Relying Party.Should the auth_time field be validated for the given request?

shibboleth.authn.DuoOIDC.jwt.NonceLookupStrategy

BiFunction<ProfileRequestContext,JWTClaimsSet, String>

Returns the nonce that was used in the authorization request and stored in the Duo authentication context.Lookup the nonce that was used in the authorization request and should be present in the id_token.

shibboleth.authn.DuoOIDC.jwt.NonceActivationCondition

BiPredicate<ProfileRequestContext,JWTClaimsSet>

Returns true iff the id_token contains a nonce.Should we validate the nonce value in the id_token?

shibboleth.authn.DuoOIDC.RequiredOIDCClaims

Set<String>

Used by the DefaultDuoTokenClaimsVerifier above. Defaults to those claims required by the OIDC specification (https://openid.net/specs/openid-connect-core-1_0.html#IDToken)

The names of the claims required to be present in the Duo result token (OIDC id_token). 

shibboleth.authn.DuoOIDC.ContextToPrincipalMappingStrategy

Function<ProfileRequestContext,Collection<Principal>>


Map information in the ProfileRequestContext, most likely in the Duo result id_token, to a collection of Principals the execution of the flow supports. See this advanced topic.

shibboleth.authn.DuoOIDC.ExtendedClaimsValidator

BiFunction<JWTClaimsSet,ProfileRequestContext,JWTValidationException>


Optional BiFunction extension point for custom claims validation of the Duo token

shibboleth.authn.DuoOIDC.NonBrowser.HttpClient

HttpClient

Internal/default HttpClient instanceOverrides the HttpClient implementation and settings to use for the AuthAPI (see HttpClientConfiguration)

shibboleth.authn.DuoOIDC.NonBrowser.HttpClientSecurityParameters

HttpClientSecurityParameters


Custom security settings for the AuthAPI calls (see HttpClientConfiguration)
These beans are specific to the Duo WebSDK-based plugin only:
Bean ID / TypeDefaultDescription

shibboleth.authn.DuoOIDC.sdk.TrustedCertificates

List<String>

DefaultTrustedCertificates

A default list of trust root CA public key hashes. See HTTP Public Key Pinning

These beans are specific to the Nimbus-based plugin only:

Bean ID / TypeDefaultDescription

shibboleth.authn.DuoOIDC.nimbus.TrustedCertificates

List<Resource>

Default trust list

A default list of trust root CA certificates. See HTTP Public Key Pinning

shibboleth.authn.DuoOIDC.nimbus.HttpClientSecurityParameters

HttpClientSecurityParameters


Custom security settings for the Duo OIDC calls

shibboleth.authn.DuoOIDC.nimbus.HttpClient

HttpClient

Internal/default HttpClient instance

The HttpClient used to connect to Duo's OIDC endpoints. Properties can be overridden in the duo-oidc.properties file.

shibboleth.authn.DuoOIDC.nimbus.TrustEvaluator

PKIXTrustEvaluator

Default TrustEvaluator

Tied to a TLS Trust Engine and hence the HttpClientSecurityParameters

Evaluates the X509 Credential

shibboleth.authn.DuoOIDC.tls.CRLs

List<Resource>


Supplied list of static CRL resources. See Certificate Revocation Checking

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

NameDefaultFunction

idp.duo.oidc.apiHost


DuoOIDC API hostname assigned to the integration

idp.duo.oidc.clientId


The OAuth 2.0 Client Identifier valid at the Authorization Server

idp.duo.oidc.redirectURL


Redirection URI to which the 2FA response will be sent

idp.duo.oidc.redirecturl.allowedOrigins


If the idp.duo.oidc.redirectURL is not set, one will be computed dynamically and checked against this list of allowed origins - to prevent Http Host Header injection. 

idp.duo.oidc.secretKey


The client secret used to verify the client in exchanging the authorization code for a Duo 2FA result token (id_token).

idp.duo.oidc.endpoint.health

/oauth/v1/health_check

Duo's OAuth 2.0 health check endpoint

idp.duo.oidc.endpoint.token

/oauth/v1/token

Duo's OAuth 2.0 token endpoint

idp.duo.oidc.endpoint.authorize

/oauth/v1/authorize

Duo's OAuth 2.0 authorization endpoint

idp.duo.oidc.jwt.verifier.clockSkew

PT60S

Leeway allowed in token expiry calculations

idp.duo.oidc.jwt.verifier.iatWindow

PT60S

Maximum amount (in either direction from now) of duration for which a token is valid after it is issued

idp.duo.oidc.jwt.verifier.issuerPath

/oauth/v1/token

The path component of the Duo token issuer. The full issuer string takes the format: HTTPS://<idp.duo.oidc.apiHost>+<idp.duo.oidc.jwt.verifier.issuerPath>

idp.duo.oidc.jwt.verifier.preferredUsername

preferred_username

The result token JWT claim name that represents the username sent in the duo_uname field in the authorization request.

idp.duo.oidc.jwt.verifier.authLifetime

PT60S

How long the authentication is valid. Only applies to forced authentication requests.
The properties below are used when enabling non-browser / AuthAPI support:
idp.duo.oidc.nonbrowser.apiHost${idp.duo.oidc.apiHost}                  Duo AuthAPI hostname assigned to the integration
idp.duo.oidc.nonbrowser.integrationKey
Duo AuthAPI integration key (supplied by Duo)
idp.duo.oidc.nonbrowser.secretKey
Duo AuthAPI secret key (supplied by Duo)
idp.duo.oidc.nonbrowser.header.factorX-Shibboleth-Duo-FactorName of HTTP request header for Duo AuthAPI factor
idp.duo.oidc.nonbrowser.header.deviceX-Shibboleth-Duo-DeviceName of HTTP request header for Duo AuthAPI device ID or name
idp.duo.oidc.nonbrowser.header.passcodeX-Shibboleth-Duo-PasscodeName of HTTP request header for Duo AuthAPI passcode
idp.duo.oidc.nonbrowser.autotrueAllow the factor to be defaulted in as "auto" if no headers are received
idp.duo.oidc.nonbrowser.clientAddressTrustedtruePass client address to Duo in API calls to support logging, push display, and network-based Duo policies

The below table are properties that only apply to the default configuration of the Shibboleth Nimbus plugin

NameDefaultFunction

idp.duo.oidc.connectionTimeout

defaults to the global HttpClient options in services.properties (PT1M)

Maximum length of time to wait for the connection to be established

idp.duo.oidc.connectionRequestTimeout

defaults to the global HttpClient options in services.properties (PT1M)

Maximum length of time to wait for a connection to be returned from the connection manager

idp.duo.oidc.socketTimeout

defaults to the global HttpClient options in services.properties (PT1M)

Maximum period inactivity between two consecutive data packets

idp.duo.oidc.maxConnectionsTotal

defaults to the global HttpClient options in services.properties (100)

Max total simultaneous connections allowed by the pooling connection manager

idp.duo.oidc.maxConnectionsPerRoute

defaults to the global HttpClient options in services.properties (100)

Max simultaneous connections per route allowed by the pooling connection manager

idp.duo.oidc.nimbus.checkRevocation

falseTo enable certificate revocation checking. See Certificate Revocation Checking


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

Property Default Description
idp.authn.DuoOIDC.order 1000 Flow priority relative to other enabled login flows (lower is "higher" in priority)
idp.authn.DuoOIDC.nonBrowserSupported false Whether the flow should handle non-browser request profiles (e.g., ECP)
idp.authn.DuoOIDC.passiveAuthenticationSupported false Whether the flow allows for passive authentication
idp.authn.DuoOIDC.forcedAuthenticationSupported true Whether the flow supports forced authentication
idp.authn.DuoOIDC.proxyRestrictionsEnforced %{idp.authn.enforceProxyRestrictions:true} Whether the flow enforces upstream IdP-imposed restrictions on proxying
idp.authn.DuoOIDC.proxyScopingEnforced false Whether the flow considers itself to be proxying, and therefore enforces SP-signaled restrictions on proxying
idp.authn.DuoOIDC.discoveryRequired false Whether to invoke IdP-discovery prior to running flow
idp.authn.DuoOIDC.lifetime %{idp.authn.defaultLifetime:PT1H} Lifetime of results produced by this flow
idp.authn.DuoOIDC.inactivityTimeout %{idp.authn.defaultTimeout:PT30M} Inactivity timeout of results produced by this flow
idp.authn.DuoOIDC.reuseCondition shibboleth.Conditions.TRUE Bean ID of Predicate<ProfileRequestContext> controlling result reuse for SSO
idp.authn.DuoOIDC.activationCondition shibboleth.Conditions.TRUE Bean ID of Predicate<ProfileRequestContext> determining whether flow is usable for request
idp.authn.DuoOIDC.subjectDecorator Bean ID of BiConsumer<ProfileRequestContext,Subject> for subject customization
idp.authn.DuoOIDC.supportedPrincipals (see below) Comma-delimited list of protocol-specific Principal strings associated with flow
idp.authn.DuoOIDC.addDefaultPrincipals false 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:

<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:

idp.authn.DuoOIDC.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.

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

<util:list id="shibboleth.AvailableAuthenticationFlows">

    <bean p:id="authn/DuoOIDC" parent="shibboleth.AuthenticationFlow"
            p:order="%{idp.authn.DuoOIDC.order:1000}"
            p:nonBrowserSupported="%{idp.authn.DuoOIDC.nonBrowserSupported:false}"
            p:passiveAuthenticationSupported="%{idp.authn.DuoOIDC.passiveAuthenticationSupported:false}"
            p:forcedAuthenticationSupported="%{idp.authn.DuoOIDC.forcedAuthenticationSupported:true}"
            p:proxyRestrictionsEnforced="%{idp.authn.DuoOIDC.proxyRestrictionsEnforced:%{idp.authn.enforceProxyRestrictions:true}}"
            p:proxyScopingEnforced="%{idp.authn.DuoOIDC.proxyScopingEnforced:false}"
            p:discoveryRequired="%{idp.authn.DuoOIDC.discoveryRequired:false}"
            p:lifetime="%{idp.authn.DuoOIDC.lifetime:%{idp.authn.defaultLifetime:PT1H}}"
            p:inactivityTimeout="%{idp.authn.DuoOIDC.inactivityTimeout:%{idp.authn.defaultTimeout:PT30M}}"
            p:reuseCondition-ref="#{'%{idp.authn.DuoOIDC.reuseCondition:shibboleth.Conditions.TRUE}'.trim()}"
            p:activationCondition-ref="#{'%{idp.authn.DuoOIDC.activationCondition:shibboleth.Conditions.TRUE}'.trim()}"
            p:subjectDecorator-ref="#{getObject('%{idp.authn.DuoOIDC.subjectDecorator}'.trim())}">
        <property name="supportedPrincipalsByString">
            <bean parent="shibboleth.CommaDelimStringArray"
                c:_0="#{'%{idp.authn.DuoOIDC.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.



  • No labels