Credit for the idea to create this approach goes to Steven Premeau, University of Maine System.

This feature requires V2.1.0+ of the DuoOIDC plugin.


The Duo plugin includes optional support for using the Duo service as the sole authentication mechanism in the IdP rather than as a second factor solution. Typically this is useful in order to use Duo to support passwordless authentication using modern approaches like WebAuthn, as in the newly branded “passkeys” technology integrated into most newer devices.

There are limitations; in particular, the requirement to know (or ask for) the username first. The use of on-device “discoverable” credentials without identifying a user ahead of time thus cannot be supported, though they are supported once the user is identified to Duo. In addition, Duo does not presently allow any integration to disable the use of bypass codes (as might be issued by help desk staff), but the plugin can be configured to detect and block their use, or any other unexpected factor, in order to ensure that if the flow completes successfully that only one of a set of expected methods was used.

However, there are substantial advantages to using the existing service for organizations already committed to the Duo service, and the intetgration is relatively simple to enable and configure, and supports fallback to traditional authentication methods. It does some work to prevent “trapping” a user into attempting a login they cannot complete, though it is impossible to prevent that completely due to Duo lacking a “cancel” feature at present.

The new feature also has been designed with some knowledge of shared machine considerations, and also allows for various kinds of “gradual adoption” scenarios such as pilot groups and so forth, to manage the introduction of the feature to a large community.


Ultimately the MFA flow is used to control the use of this new feature and orchestrate the use of the flow in its new form or its traditional mode. Examples of how to configure this are included near the end of this topic, and of course the MFA feature in the IdP can be configured to do many more exotic things in support of this workflow depending on local requirements.

The general behavior modeled (if the feature is enabled and triggered):

  1. After a standard Duo authentication is completed, a condition is evaulated against the request and if true, the subject is given a choice to “opt-in” so that an encrypted cookie containing the username is set for future requests. The plugin will also identify scenarios where an existing cookie should be removed. If the subject opts-out, then this decision is also remembered with a fixed cookie value to avoid constant nagging.

  2. Subsequent requests with the feature triggered will detect the presence of the cookie, and it will (generally) present a new view prior to the handoff to Duo that identifies who it believes the subject to be so the person may indicate how they want to proceed.

  3. If traditional login is preferred, or the subject self-identifies as a different identity, then control returns to the MFA flow with specific events, after which the MFA flow is expected to do what it had been doing originally, whatever that was.

  4. If passwordless login is chosen, the plugin will invoke the Duo service using the desired passwordless integration.

  5. The factor used is checked against a set that is deemed acceptable (configurable of course) and if so, the resulting Java Subject will automatically include a UsernamePrincipal object containing the username passed to Duo, allowing the MFA flow to potentially complete with no additional configuration needed.

In most cases, once “primed”, the subject is expected to be able to simply press the Return key to trigger authentication on a previously opted-in device.

The flow also supports offering the same “shared machine” and “revoking consent” options that exist on the default login form, the former impacting the behavior by optionally clearing the opt-in cookie. It is assumed that the passwordless model is not in general a good fit for shared environments, but this is somewhat configurable.

Deployers with more advanced requirements can also “assume responsibility” for establishing the user’s identity and manage the cookie to some degree themselves (via an API), and drive the passwordless behavior more directly, even bypassing the built-in view to go directly into Duo.

Demonstration Videos

A number of videos demonstrating the features in different situations are attached to this topic:







Standard Username and Password Authentication

The user has not opted into passwordless authentication, and the service provider only requires password authentication. This demonstrates a standard, basic, username and password flow.


Opt-In To Passwordless Authentication

The service provider requires multi-factor authentication, the user performs multi-factor with Duo and uses a second factor that is acceptable for passwordless. The user opts-in to passwordless and uses that as the sole factor on the next authentication.


Unable To Opt-In to Passwordless Authentication

The service provider requires multi-factor authentication, the user performs multi-factor with Duo but uses a second factor that is unacceptable for passwordless. The user can not use passwordless for subsequent authentications.


Opt-In To Passwordless, But Then Use an Unacceptable Factor For Passwordless

The service provider requires multi-factor authentication, the user performs multi-factor with Duo and uses a second factor that is acceptable for passwordless. Opts-in to passwordless. However, for the subsequent passwordless authentication they change to an unacceptable sole factor. The login fails.


Already Opted-In To Passwordless, But Chooses The Password Flow

The user has previously opted-in to passwordless authentication but decides to use username and password authentication instead.


A Different User Using the Same Browser As A User Who Opted-In

A different user, using the same browser, opted-in to passwordless authentication. The current user recognises it is not them (and their credentials would not work) and uses the ‘Not You’ link.


Administrative Flow for User Control Of Opt-In Status

The user signs into the administrative endpoint to manage their passwordless opt-in status (cookie)


Administrative Flow for Admin User Control Of Opt-In Status and Username

A user, with administrative rights, signs into the administrative endpoint to manage both their passwordless opt-in status (cookie) and the username stored inside the cookie.


Duo Integration Considerations

This is paramount: you almost certainly CANNOT use your existing Duo integration(s) to support this use case, and there are options that one might normally set without much thought that really need a different perspective here. The model here is to leverage Duo to do less than it normally does functionally, and that’s a bit of an odd way of thinking that isn’t natural to everybody.

Until there’s more general guidance about this from Duo, we’ll note the following:

You need to very carefully think about the factors you enable in the integration, and that also extends to the Remember Me feature. By default our plugin’s behavior is to require the use of a specific type of factor, “Platform authenticator (2fa)”, and only that. As a result, even if you enable Remember Me, it won’t be permitted and so it should generally be turned off, along with all the other factors they support. Eventually they may allow turning off Bypass Codes as well, but that isn’t possible yet.

The question of whether to permit so-called “Roaming authenticator (2fa)” devices may be “no”, but Duo supports an option to require user verification for roaming devices. Unfortunately, that flag is something the client asserts, and we know for certain that there are roaming authenticator implementations that signal UV, but do not actually perform it. It’s not clear at this point if that wlll be common or rare, or who would push back on such implementations. They are disallowed by default at present.

In addition, you will need to ensure that the option to allow unrecognized users to enroll themselves has to be disabled as well. Normally Duo assumes a first factor was used to authenticate the username before taking control and here that doesn’t happen. Blocking unknown users outright is what you want here.

Plugin Configuration

The additional files added by this feature are the new view templates, views/passwordless-optin.vm and views/passwordless.vm. The former is presented after second factor usage (conditionally) and gives the subject the option to have a cookie issued to their browser to to allow passwordless authentication as that identity in the future. To be clear, the cookie is not used to authenticate the subject but rather triggers the presentation of the passwordless option and then drives the eventual use of Duo using that username.

Note that it is impractical, though not impossible, for somebody to manipulate the username that ends up being used, but that doesn’t buy an attacker much unless they actually possess and can use WebAuthn credentials for that identity.

A set of new properties is supported to enable this feature and configure a dedicated Duo integration for this use case. Many of the advanced settings will default back to their usual or overridden values for existing Duo integrations. The idp.duo.oidc.passwordless.enabled property is the main toggle to turn on to use the new featurs, though there are other steps to actually make it run described later. Usually only these few need to be provided:

idp.duo.oidc.passwordless.enabled = true idp.duo.oidc.passwordless.apiHost = yourvalue idp.duo.oidc.passwordless.clientId = yourvalue idp.duo.oidc.passwordless.secretKey = yourvalue

By default, the passwordless integration will enforce the use of the “Platform authenticator (2fa)” factor. To override this default, the property idp.duo.oidc.passwordless.allowedFactors can be set to a comma-delimited list to accept. These values are based on what Duo returns in the “factor” claim in its responses and their consistency/stability is at present unclear, and may depend on the exact Duo service version/level one is using as well as when and how a particular credential was enrolled. Over time, Duo adds support for new technologies in ways that alters how newly enrolled credentials may appear but will not “retroactively” update older ones.

Multiple integrations are supported (in the manner described in the main plugin documentation) by means of a dedicated bean named shibboleth.authn.DuoOIDC.Passwordless.DuoIntegrationStrategy, though we anticipate this being less of a need for this use case.

In the event of a requirement to directly define new integrations, the parent bean shibboleth.authn.DuoOIDC.Passwordless.DuoIntegration can be used, and it automatically configures the integration to be recognized by this feature as intended for this use case. A standard integration defined using the original parent bean will not be accepted for this purpose, to limit accidents.

Eligibility / Opt-In Control

The creation of the (normally required) cookie to allow use of this feature is managed through an opt-in mechanism that runs after “traditional” use of the plugin as a second factor solution. Based on various deductions, the code will clear an existing cookie if warranted and/or evaluate a defined condition to determine whether to offer the subject the choice to opt-in to create a new cookie to allow the feature, essentially an agreement to see the passwordless option presented in the future so that it isn’t a surprise.

A bean of type Predicate<ProfileRequestContext> is invoked after a “traditional” Duo login completes. If the condition evaluates to false, than any existing opt-in cookie is cleared, and the flow finishes in the ordinary way. If true, then the subject is “eligible” to opt-in for passwordless in the future.

The default implementation of this condition is primitive and merely checks whether the factor used during the request is one of the “allowed” factors governed by the idp.duo.oidc.passwordless.allowedFactors property mentioned above. This acts as a signal that the subject “should” be able to successfully perform passwordless authentication in the future, but that decision is left to an opt-in form that obviously can be tailored to local needs with documentation, etc. Notably, it would be possible to add a meta-refresh to that page to automate the acceptance process should that be desired, but it was felt that simply “switching” the login experience on people without a choice to do so would be suboptimal.

The most obvious use of this extension point is to introduce identity-based considerations into the decision, such as an attribute-driven check for membership in a pilot group to allow gradual/limited adoption of the feature.

This logic can be replaced entirely by setting the property idp.duo.oidc.passwordless.guardCondition to the name of the bean to use; you may also leverage the bean named shibboleth.authn.DuoOIDC.Passwordless.DefaultCondition to compose your own logic with the default implementation.

If the subject elects not to opt-in, this decision is also recorded in the cookie to avoid asking over and over.

Event Handling

This flow uses more event signals than is common in the IdP because it deals with a number of different scenarios triggered by the subject’s input and is designed to allow for a significant amount of control by the MFA flow to respond to those signals.

The following events may be signalled by the flow in various cases to distinguish the outcome:

  • RequestUnsupported

    • This is a general event that indicates the flow could not attempt passwordless authentication for technical or data-driven reasons, generally because either:

      • The flow could not determine the username to present in the view, typically because the guard cookie did not exist or couldn’t be read and decrypted. This is the typical “not primed” case if the flow is run ahead of the subject opting into the capability.

      • Or because of an atempt to perform passwordless authentication for a request using a non-browser profile or with the “passive” option applied, which is not possible. This allows the MFA logic to avoid extra checking for these conditions and allow the flow to detect them if desired.

  • ReselectFlow

    • Indicates the subject pressed the “Password Login” button (or whatever you may have customized it to display). This is the signal for “I don’t want to do Passwordless even if you think I can”.

  • IdentitySwitch

    • Indicates the subject clicked the “Not You?” link (or whatever you may have customized it to display). This is the signal for “You thought I was Alice, but I’m Bob, so I can’t do this.”

  • InvalidCredentials

    • Usually indicates that the subject completed the Duo challenge with a factor that is disallowed, such as a bypass code.

Bypassing the Initial View

If desired, it is possible to leverage the MFA logic used to “prepare” the request ahead of invoking the plugin flow to inject the username to use instead of relying on the cookie and to bypass the normal view presented if desired. The DuoPasswordlessContext (primary usage of which is described below in the examples) can be pre-populated with a username (bypassing reading that from a cookie) and the bypassView flag can be set to true to skip display of the view and directly pass into the Duo service using the pre-set username.

You may set the username and keep the view, but if you bypass the view, you MUST set the username as well or the flow will fail.

The API bean shibboleth.authn.DuoOIDC.Passwordless.CookieManager of type PasswordlessCookieManager is available to support deployer manipulation of the opt-in cookie, including creating it if desired via a different mechanism. Note that the opt-out value is handled in a special way and you will need to test that method explicitly to distinguish between the cookie being absent and the opt-out being present.

Administrative Management Flow

In addition to providing the API bean, a new administrative flow is also provided to allow for direct user or administrator control over the state of the opt-in/out cookie on a device. This flow lives at /idp/profile/admin/DuoOIDC and defaults to requiring user authentication to access it (most previous admin flows tend to be system-related and are accessed via command line tools via address-based access control). There are two different properties defined that control access to this flow:

  • idp.duo.oidc.admin.accessPolicy

    • This controls overall user access and defaults to a policy that doesn’t actually exist in the IdP’s access control configuration at present, so it has to be defined to allow access (or changed to a policy that does exist). More on this below.

  • idp.duo.oidc.admin.adminAccessPolicy

    • This controls an additional feature of the flow that allow an administrator to not only control the cookie, but actually allows control over the username that appears inside the cookie (normal user access only allows a cookie to be created with the authenticated principal name in it, i.e. the user themself).

The defauly policy for user access is named “AccessByAnyone” and can be added to the map in conf/access-control.xml like so:

... <entry key="AccessByAnyone"> <bean parent="shibboleth.PredicateAccessControl" c:_0-ref="shibboleth.Conditions.TRUE" /> </entry> ...

The flow itself relies on a view template in views/admin/passwordless-admin.vm. The view provides a form that includes some simple logic at the top allowing it to determine the current state of the cookie and whether the user was granted admin access or merely control over one’s own cookie, and then provides the buttons to set an opt-in or opt-out cookie or clear it entirely. Admin access allows the username to be set explicitly rather than derived.

In the event a user actually clicks the “Finish” button (which frees resources devoted to the flow conversation), it will redirect them to a location set by the property idp.duo.oidc.admin.landingPage

This is to some degree an example/demo of how one might provide this sort of functionality but because the cookie is generally scoped to the IdP, it is more or less assumed that a mechanism living within the IdP or at least running on the same server would need to be used to do so.

Note that allowing access to this flow would by default allow anybody to opt-in to the feature without passing the usual eligibility check that applies to the mainline behavior, but one could of course include checks for particular attributes (e.g., a group membership) in an access policy if desired.


MFA Flow Configuration Example

The use of this feature is controlled by means of an external signal, the creation of a new context object of the type net.shibboleth.idp.authn.plugin.duo.context.DuoPasswordlessContext. The contents don’t necessarily matter but the existence of the object in the tree acts as a signal that the passwordless “mode” of the plugin could or should be engaged when possible. Absent the attached context object, everything runs as usual, though the plugin will perform the opt-in check and presentation in the event that the fundamental property to enable the passwordless feature is set, which is likely not desirable.

In a typical scenario, we assume the use of passwordless authentication to the extent possible, and fall back to a traditional login process (in this example that’s just Password followed by Duo conditionally based on whether the application requires it). That is, while not every application may actually require stronger authentication, we assume that the whole idea is that using this approach should be “better” for the user than using a password anyway, so there’s no need to “prevent” its use simply because it’s not required. This differs from the traditional use of Duo, which is viewed as an “extra” step and a source of user annoyance.

A complete example follows. It is largely Spring boilerplate around the scripts and rules used to drive the behavior. The initial step is to run a script bean (tryPasswordless), and detect the “non-successful” expected results.

In a successful case, nothing else is done, the flow simply completes using the result produced. In one of the two other cases, the flow instead does what it normally tends to do (run the Password flow followed by conditionally running the Duo flow), but with a pre-step script (“firstFactor”) that clears the special context object from the tree to revert the behavior of the Duo flow if it in fact is run again.

As a general matter, most use cases will likely involve those basic steps and scripts, with specific logic extending the behavior probably added to them in special cases, but the fundamental steps are probably the right ones for most sites.

... <util:map id="shibboleth.authn.MFA.TransitionMap"> <entry key=""> <bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="tryPasswordless" /> </entry> <entry key="authn/DuoOIDC"> <bean parent="shibboleth.authn.MFA.Transition"> <property name="nextFlowStrategyMap"> <map> <entry key="RequestUnsupported" value-ref="firstFactor" /> <entry key="ReselectFlow" value-ref="firstFactor" /> <entry key="IdentitySwitch" value-ref="firstFactor" /> <entry key="InvalidCredentials" value-ref="firstFactor" /> </map> </property> </bean> </entry> <entry key="authn/Password"> <bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="checkSecondFactor" /> </entry> </util:map> <bean id="tryPasswordless" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript"> <constructor-arg> <value> <![CDATA[ // Add passwordless context. authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext"); authCtx.ensureSubcontext("net.shibboleth.idp.plugin.authn.duo.context.DuoPasswordlessContext") "authn/DuoOIDC"; ]]> </value> </constructor-arg> </bean> <bean id="firstFactor" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript"> <constructor-arg> <value> <![CDATA[ // Clear passwordless context. authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext"); var duoCtx = authCtx.getSubcontext("net.shibboleth.idp.plugin.authn.duo.context.DuoPasswordlessContext"); if (duoCtx != null) { duoCtx.removeFromParent(); } "authn/Password"; ]]> </value> </constructor-arg> </bean> <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> ...

Optimizing the Initial Decision

A optional approach that isn’t much more complex allows the initial MFA rule to check for the opt-in cookie and pre-populate it into the context before running the Duo flow, or if it’s absent just run the Password flow instead. This skips the extra overhead of starting up a subflow that will immediately terminate itself after doing a bit of work. The only difference is a change to the tryPasswordless script bean and injecting a special cookie manager object into the script:

Group-Based Eligibility

A common use case is to manage opt-in testing/piloting of this feature by including a user-based check into the eligibility condition that runs to determine whether the opt-in form is displayed. A group management system like Grouper could be used for example to manage the permissions for this.

Note that this does not inherently prevent somebody from using the administrative flow feature to create the opt-in cookie directly. If this is a concern, you would want to apply this group check to the general access control rule for end-user access to that feature and toggle the property on to resolve attributes after logging in to access that feature.

An example of performing an attribute lookup for a group is demonstrated below. Note that it also shows how to combine a more “expensive” check with the built-in condition based on Duo factor in an AND condition so that if the user doesn’t demonstrate the ability to use a passwordless factor first, the group check is skipped.

User Verifying Roaming Authenticators

The default PasswordlessCondition works very well with authenticator types that always include user verification, as is the case with Platform Authenticators. Many Security Key devices support both simple presence verification in addition to user verification. Duo’s current implementation allows only requiring or discouraging user verification – if it asks the client for it, it must be provided – there is no option to allow (fall through to) presence verification if the user verification process can not be completed. This means you would not want to add 'Roaming authenticator (2fa)' to the list of allowedFactors, since the “traditional” second factor Duo process will typically not be configured to require user verification and could allow the single-factor security key through or bring the user to a an MFA test that can not be passed, depending on the how much the configuration of the Duo Application profile used during Passwordless authentication differs from the profile used during the traditional second factor authentication.

An example that uses the AdminAPI access feature of the plug-in to determine if the roaming authenticator used by the accountholder is known to be capable of user verification is below. As written, this example would replace the built-in check, but could easily be combined with that check via the method shown in the example above.

Note: Duo determines the capabilities of the authentication device during enrollment, and does not appear to update them during the authentication process. As noted above, the information used during this check may be inaccurate for that and other reasons. At a minimum, some authentication devices may need to be removed and re-enrolled to return expected results.


Name / Type



Name / Type






Master switch enabling passwordless features, including opt-in step




If true, non-cacheable authentication events will clear the guard cookie and will not offer the opt-in to create one




If true, clears guard cookie if the contents don’t match the username in the request being handled




API host for default passwordless Duo integration




Client ID for default passwordless Duo integration




Secret key for default passwordless Duo integration




Redirection URI to which the 2FA response will be sent


Comma-delimited URL list


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


URL path


Duo's OAuth 2.0 health check endpoint


URL path


Duo's OAuth 2.0 token endpoint


URL path


Duo's OAuth 2.0 authorization endpoint


Comma-delimited String list

Platform authenticator (2fa)

The factor labels to accept after a passwordless login




Name of form field used to signal that the authentication result should not be cached fir SSO




Name of cookie used to authorize use of passwordless for a device/user




Overrides the max-age of the opt-in/out cookie


Bean ID of Predicate<ProfileRequestContext>

Internal default

Name of condition bean used to determine whether to offer opt-in to subject to create the guard cookie




API host for Admin API access




Integration key for Admin API access




Secret key for Admin API access




Initial delay in ms if Duo Admin API indicates request exhaustion




Maximum delay in ms if Duo Admin API indicates request exhaustion




Multiplier applied each time a backoff/retry loop occurs




Location to redirect browser after clicking “Finish” on the cookie mgmt page




Policy name (absent by default) for access control policy for overall access to cookie mgmt flow




Policy name (absent by default) for access control policy for special admin access to cookie mgmt flow




Whether the cookie mgmt flow allows for non-browser-based access




Whether the cookie mgmt flow requires user authentication




Whether the cookie mgmt flow should resolve attributes following user authentication (for access control purposes generally)


Comma-delimited list of flow names


Comma separated list of post authentication interceptor flows that should be run after user authentication to cookie mgmt flow




Comma-delimited list of protocol-specific Principal strings to require from authentication to cookie mgmt flow