DuoOIDCAuthnConfiguration
Former user (Deleted)
Scott Cantor
Rod Widdowson
This plugin does not depend on the use of the original Duo login flow and enabling that feature/module is not required in order to install and use this plugin. They can even theoretically co-exist in older versions of the IdP, but this hasn't been extensively tested.
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 AuthAPI). 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.
Starting with IdP 4.2 you can install the latest plugin version supported on your IdP version with.\plugin.sh -I
<PluginId>
Plugin | Plugin ID | Module(s) | Authentication Flow ID | Bug Reporting |
---|
Plugin | Plugin ID | Module(s) | Authentication Flow ID | Bug Reporting |
---|---|---|---|---|
Duo Universal Prompt via the Shibboleth Nimbus Client | net.shibboleth.idp.plugin.authn.duo.nimbus | idp.authn.DuoOIDC | authn/DuoOIDC | |
Duo Universal Prompt via the Duo WebSDK v4 Client | net.shibboleth.idp.plugin.authn.duo.sdk | idp.authn.DuoOIDC | authn/DuoOIDC |
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.
Feature | Duo WebSDK v4 | Shibboleth Nimbus |
---|
Feature | Duo WebSDK v4 | Shibboleth Nimbus |
---|---|---|
Based on the official SDK | X |
|
Duo Endpoint and Configuration Health Check | X | X |
Duo 2FA result token signature (HMAC) checking | X | X |
Duo 2FA result token encryption handling (not provided by Duo) |
|
|
Duo 2FA result token claims verification | X | X |
Duo 2FA result token nonce verification |
| X |
Customizable HttpClient implementation |
| X |
Customizable TrustEngine implementation |
| X |
HTTP Public Key Pinning | X | X |
Supports TLS Certification Revocation Checking | X | X |
Customisable JSON response mapper |
| X |
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.
IdP installations upgraded from versions prior to V4.1 are also likely to require adding the idp.searchForProperties=true property to their idp.properties file, or else an explicit reference would have to be added to the new property file added by the module. It's easiest to clean up the property situation prior to using plugins that add their own.
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"), and typically will need to configure the MFA flow to make use of this one as a second factor.
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 Topics
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.xml. The 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 Certificate | Public Key Hash |
---|
Root Certificate | Public Key Hash |
---|---|
C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Assured ID Root CA | sha256/I/Lt/z7ekCWanjD0Cvj5EqXls2lOaThEA0H2Bg4BT/o= |
C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root CA | sha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E= |
C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert High Assurance EV Root CA | sha256/WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18= |
C=US, O=SecureTrust Corporation, CN=SecureTrust CA | sha256/dykHF2FLJfEpZOvbOLX4PKrcD2w2sHd/iA/G3uHTOcw= |
C=US, O=SecureTrust Corporation, CN=Secure Global CA | sha256/JZaQTcTWma4gws703OR/KFk313RkrDcHRvUt6na6DCg= |
C=US, O=Amazon, CN=Amazon Root CA 1 | sha256/++MBgDH5WGvL9Bcn5Be30cRcL0f5O+NyoXuWtQdX1aI= |
C=US, O=Amazon, CN=Amazon Root CA 2 | sha256/f0KW/FtqTjs108NpYj42SrGvOB2PpxIVM8nWxjPqJGE= |
C=US, O=Amazon, CN=Amazon Root CA 3 | sha256/NqvDJlas/GRcYbcWE8S/IceH9cq77kg0jVhZeAPXq8k= |
C=US, O=Amazon, CN=Amazon Root CA 4 | sha256/9+ze1cZgR9KO1kZrVDxA4HQ6voHRCSVNz4RdTCx4U8U= |
C=BM, O=QuoVadis Limited, CN=QuoVadis Root CA 2 | sha256/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. In the examples, the bean properties set are purely examples, they could be set inline or rely on property names of your own choice.
Per Integration Supported Principals
<bean id="Duo1" parent="shibboleth.authn.DuoOIDC.DuoIntegration"
p:APIHost="%{idp.duo.oidc.apiHost:none}"
p:clientId="%{idp.duo.oidc.clientId:none}"
p:secretKey="%{idp.duo.oidc.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="Duo2" parent="shibboleth.authn.DuoOIDC.DuoIntegration"
p:APIHost="%{idp.second.duo.oidc.apiHost:none}"
p:clientId="%{idp.second.duo.oidc.clientId:none}"
p:secretKey="%{idp.second.duo.oidc.secretKey:none}">
<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"
c: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 irrespective of the value of the property idp.authn.DuoOIDC.addDefaultPrincipals. 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. In the examples, the bean properties set are purely examples, they could be set inline or rely on property names of your own choice.
Multiple Duo Integrations
<bean id="DefaultDuo" parent="shibboleth.authn.DuoOIDC.DuoIntegration"
p:APIHost="%{idp.duo.oidc.apiHost:none}"
p:clientId="%{idp.duo.oidc.clientId:none}"
p:secretKey="%{idp.duo.oidc.secretKey:none}" />
<bean id="SpecialDuo" parent="shibboleth.authn.DuoOIDC.DuoIntegration"
p:APIHost="%{idp.specialduo.oidc.apiHost:none}"
p:clientId="%{idp.specialduo.oidc.clientId:none}"
p:secretKey="%{idp.duo.oidc.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.DuoOIDC.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:
Revocation checking from a Certificate Revocation List defined in a local store.
Revocation checking from a Certificate Revocation List fetched from a distribution point defined in the certificate.
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:
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.
Enable CRL checking from an online distribution point via the system property com.sun.security.enableCRLDP (as described in the CertPathDocs)
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.
The HTTPClientBuilder object that is the base for the HTTPClient beans has properties for HTTP proxy settings. To configure a proxy for use by the Nimbus plugin, add this bean to conf/authn/duo-oidc-authn-config.xml:
<bean id="shibboleth.authn.DuoOIDC.nimbus.HttpClient"
parent="shibboleth.authn.DuoOIDC.nimbus.InternalHttpClient"
p:connectionProxyHost="proxy.example.org"
p:connectionProxyPort="3128" />
Before each Duo 2FA request, a back-channel lookup is made to Duo’s health check endpoint to determine if the Duo servers are accessible and accepting requests. If for some reason they aren’t, the 2FA attempt fails but the IdP’s authentication flows resume. This is a standard part of Duo’s 2FA workflow. The benefit of this approach is, it occurs before the URL redirect in the browser, and if the Duo 2FA endpoint were not available the IdP remains in control of the authentication process. Otherwise, the user’s browser might timeout during the 2FA request, or the user might get stuck on an error page of some kind.
However, this involves an extra, frequent, back-channel network lookup and as such is subject to the same reliability/availability issues as the actual 2FA request. From investigation, it appears possible to bypass this check and still have the 2FA proceed as normal. Consequently, from v1.3.0 onward we have included a property (idp.duo.oidc.healthcheck.enabled) that allows the deployer to turn off this check.
Since V1.3.0 it is possible to enable audit logging for Duo2FA interactions by setting the property idp.duo.oidc.audit.enabled to true in /conf/authn/duo-oidc.properties. From V1.4.0 onward you also need to enable the general authentication audit logging on the IdP using the property idp.authn.audit.enabled in /conf/authn.properties.
Once enabled, audit log statements are routed through a logger named Shibboleth-Audit.DuoOIDC. From V1.4.0 onward, you can change the name of this logger using the property idp.duo.oidc.audit.category in /conf/authn/duo-oidc.properties.
Without further configuration, the audit statements will appear in the normal idp-audit.log log file. This may be sufficient for your needs, if not and you would rather those exclusively appear in a new audit log file, you will need to add a new Logback appender and logger to the logback.xml file, for example:
<appender name="IDP_DUO2FA_AUDIT" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>${idp.logfiles}/idp-duo2fa-audit.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${idp.logfiles}/idp-duo2fa-audit-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>${idp.loghistory}</maxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<charset>UTF-8</charset>
<Pattern>%msg%n</Pattern>
</encoder>
</appender>
<!-- Set additivity false if you do not want DuoOIDC events in the standard audit log -->
<logger name="Shibboleth-Audit.DuoOIDC" level="ALL" additivity="false">
<appender-ref ref="${idp.audit.appender:-IDP_DUO2FA_AUDIT}"/>
</logger>
Audit Format
The default audit format is shown below (the fields are described in the table underneath).
%AAF|%a|%T|%DuoU|%DuoRedirect|%DuoCID|%DuoReqS|%DuoRespS|%DuoTXID|%DuoDID|%DuoDN|%DuoR|%DuoF
Custom Audit Format V1.3.0
For V1.3.0, the audit format can be adjusted by specifying your own bean, shibboleth.authn.DuoOIDC.AuditFormattingMap, in /conf/authn/duo-oidc-authn-config.xml.
Custom Audit Format V1.4.0+
For V1.4.0+, the audit format can be adjusted using the idp.duo.oidc.audit.format property in /conf/authn/duo-oidc.properties.
Audit Logging Fields
Field | Description |
---|
Field | Description |
---|---|
AAF | The ID of the currently running authentication flow i.e. authn/DuoOIDC in this case |
DuoRedirect | The location of the IdP’s callback endpoint Duo will redirect the user too after successful authentication |
DuoCID | The Duo client identifier used for this request |
DuoTXID | The transaction ID contained in the Duo 2FA response |
DuoDID | The ID of the device used to authenticate the user |
DuoDN | The friendly name of the device used to authenticate the user |
DuoR | The authentication reason |
DuoF | The factor used for 2FA e.g. a Security Key or SMS. |
DuoU | The username of the authenticating user sent in the Duo 2FA request |
DuoRespS | The state OAuth 2.0 parameter returned in the Duo response |
DuoReqS | The state OAuth 2.0 parameter sent to Duo in the 2FA request (client authorization request) |
V2.1 and later versions of the plugin can be configured to support passwordless authentication using FIDO2/WebAuthn technology, such as platform authenticators like TouchID and Windows Hello, Passkeys, or hardware tokens that support PINs. Refere to the DuoOIDCPasswordless topic for complete documentation.
Reference
Bean ID / Type | Default | Description |
---|
Bean ID / Type | Default | Description |
---|---|---|
shibboleth.authn.DuoOIDC.DuoIntegration | Derived from properties in conf/authn/duo-oidc.properties | Defines a single/static Duo OIDC Integration with Duo, you can override this bean to supply a non-property-configured alternative |
shibboleth.authn.DuoOIDC.DuoIntegrationStrategy | Optional bean to supply the Duo OIDC integration settings dynamically | |
shibboleth.authn.DuoOIDC.UsernameLookupStrategy Function<ProfileRequestContext,String> | Optional bean to supply username | |
shibboleth.authn.DuoOIDC.resultCachingPredicate | shibboleth.Conditions.TRUE | Bean ID controlling whether to preserve the authentication result in an IdP session |
shibboleth.authn.DuoOIDC.CleanUpHook | Bean that removes the DuoOIDAuthenticationContext from the tree | A cleanup hook that is executed on successful authentication. |
shibboleth.authn.DuoOIDC.jwt.claims.CleanUpHook | 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> | Combines 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> | 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> | The authenticating principal's username from the context pertaining to the request. | Lookup the authenticating principal's username to match Duo's |
shibboleth.authn.DuoOIDC.jwt.AuthTimeActivationCondition | 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> | 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 | 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 | 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.PreDuoPopulateAuditExtractors 1.3.0 Map<String,Function<ProfileRequestContext,Object>> |
| Map of Pre-Duo 2FA redirect audit extractors that take the ProfileRequestContext and return an object (usually a String) |
shibboleth.authn.DuoOIDC.PostDuoPopulateAuditExtractors 1.3.0 Map<String,Function<ProfileRequestContext,Object>> |
| Map of Post-Duo 2FA audit extractors that take the ProfileRequestContext and return an object (usually a String) |
shibboleth.authn.DuoOIDC.AuditFormattingMap 1.3.0(only) Map<String, String> |
| Map of logging categories to audit formatting strings for Duo 2FA audit logging |
These beans are specific to the Duo WebSDK-based plugin only:
Bean ID / Type | Default | Description |
---|
Bean ID / Type | Default | Description |
---|---|---|
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 / Type | Default | Description |
---|
Bean ID / Type | Default | Description |
---|---|---|
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 |
| 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 |
These beans refer to the Duo Auth API (non-OIDC) which supports non-browser flows.
Bean ID / Type | Default | Description |
---|
Bean ID / Type | Default | Description |
---|---|---|
shibboleth.authn.DuoOIDC.NonBrowser.DuoIntegration | Derived from properties in conf/authn/duo-oidc.properties | Defines a single/static Duo AuthAPI integration for non-browser support |
shibboleth.authn.DuoOIDC.NonBrowser.DuoIntegrationStrategy | Optional bean to supply the Duo AuthAPI integration settings dynamically | |
shibboleth.authn.DuoOIDC.NonBrowser.HttpClient HttpClient | Internal/default HttpClient instance | Overrides the HttpClient implementation and settings to use for the AuthAPI (see HttpClientConfiguration) |
shibboleth.authn.DuoOIDC.NonBrowser.HttpClientSecurityParameters | Custom security settings for the AuthAPI calls (see HttpClientConfiguration) |
The DuoOIDC-specific properties defined in conf/authn/duo-oidc.properties follow:
Name | Default | Description |
---|
Name | Default | Description |
---|---|---|
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.factor | X-Shibboleth-Duo-Factor | Name of HTTP request header for Duo AuthAPI factor |
idp.duo.oidc.nonbrowser.header.device | X-Shibboleth-Duo-Device | Name of HTTP request header for Duo AuthAPI device ID or name |
idp.duo.oidc.nonbrowser.header.passcode | X-Shibboleth-Duo-Passcode | Name of HTTP request header for Duo AuthAPI passcode |
idp.duo.oidc.nonbrowser.auto | true | Allow the factor to be defaulted in as "auto" if no headers are received |
idp.duo.oidc.nonbrowser.clientAddressTrusted | true | Pass client address to Duo in API calls to support logging, push display, and network-based Duo policies |
idp.authn.DuoOIDC.addDefaultPrincipals 1.3.0 | true | If set to false this will prevent the addition of the default principals even if a ContextToPrincipalMappingStrategy is not set. Previous to 1.3.0 if the ContextToPrincipalMappingStrategy was not set, the default principals would always have been added, you can now control that by setting this to false. |
idp.duo.oidc.healthcheck.enabled 1.3.0 | true | Perform the Duo health check for every 2FA request? Defaults to true because this is the standard Duo workflow. |
idp.duo.oidc.audit.enabled 1.3.0 | false | Enable Duo audit logging. |
idp.duo.oidc.audit.format 1.4.0 | %AAF|%a|%T|%DuoU|%DuoRedirect|%DuoCID|%DuoReqS|%DuoRespS|%DuoTXID|%DuoDID|%DuoDN|%DuoR|%DuoF | The audit format to use for audit log statements. |
idp.duo.oidc.audit.category 1.4.0 | Shibboleth-Audit.DuoOIDC | The audit logging category to use |
The below table are properties that only apply to the Shibboleth Nimbus plugin:
Name | Default | Description |
---|
Name | Default | Description |
---|---|---|
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 | false | To enable certificate revocation checking. See Certificate Revocation Checking |
The general properties configuring this flow via authn/authn.properties are:
Name | Default | Description |
---|
Name | 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 1.3.0 | 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:
<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="#{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.