Page MenuHomePhabricator

Duo MFA Support
Open, NormalPublic

Description

See PHI912. Broadly, I believe Duo is likely the best technical solution for "better-than-TOTP" MFA available today given that U2F lacks broad support, Yubikey OTP is a weird thing that normal users likely can't hope to figure out (see T8787 for both), and we're missing hardware capabilities on mobile to do this ourselves (push notifications, NFC; see T13230).

But, since Duo is a Cisco "Contact Sales" outfit that unironically has a "Success Planning" graphic, I'm looking at not bringing support upstream into the core. See T13229.


Duo Auth API:

The API generally seems fairly sane except that a couple calls are a little more synchronous than I'd ideally prefer. Support will look something like this:

  • MFA providers will become generalized and concrete. TOTP has no configuration today (although stuff like window size could be made configurable). Duo will get a key.
  • The "add a factor" flow will /enroll the user. I think we'll use their PHID as their username since we'll run into trouble if we use their actual username and users swap usernames later.
  • The "remove a factor" flow will do nothing? There's no way to un-enroll a user? I guess it will just say "delete this thing on your phone". We could ping /enroll_status until their enrollment becomes invalid.
  • The "mfa gate" flow will /preauth a user, then let them pick a device? We can also "auto" the device if we don't want to bother with this. We may need a way for a factor implementation to provide a "okay, keep going" response. This may return enroll if they delete Phabricator from their phone but don't delete the factor in Settings.
  • Once the user picks a device, we /auth them to push a request with async. The transaction ID becomes the challenge key.
  • We can verify with /auth_status plus an explicit timeout. This API is a little weird, and an endpoint which returns immediately would be a better fit on our end (this endpoint does not return until there is a status update). This flow could move to JS to become slightly less weird but we don't really want HTTP processes sitting there indefinitely waiting for users to push buttons on their phones.

At least for now, we will probably only support Duo Push since the other factors are kind of junk and I assume everyone uses Duo Push.

Event Timeline

epriestley created this task.

I expect Google to expose a similar solution to Duo soonish (if you use Android and try to login to Google, MFA is just like Duo now).

If you try to /enroll the same user PHID twice, the second /enroll fails:

[HTTP/400] 
{"code": 40002, "message": "Invalid request parameters", "message_detail": "username already exists", "stat": "FAIL"}

If you /preauth this user PHID, the response says to enroll them:

array(2) {
  ["response"]=>
  array(3) {
    ["enroll_portal_url"]=>
    string(91) "https://api-7ba6bd7b.duosecurity.com/portal?code=..."
    ["result"]=>
    string(6) "enroll"
    ["status_msg"]=>
    string(42) "Enroll an authentication device to proceed"
  }
  ["stat"]=>
  string(2) "OK"
}

This isn't great. There doesn't seem to be a way to say "they cancelled the workflow and I need to start enrolling them again".

Instead, we can omit the username and generate a random new user for each enrollment. This is the default behavior of the API, but it means that the admin panel will be full of made-up users if users cancel the flow. Maybe that's fine. The admin panel is very misleading about the state of these users:

This is actually "we started enrolling this user by calling /enroll". The user hasn't done anything and doesn't have a phone attached.

There's also a separate "Admin API" (vs "Auth API") but the access requirements for that are unclear ("Please contact Duo Support if you would like access to this API."). This generally seems like water we shouldn't be exploring as a pure auth client.

Without the Admin API, I think there are three ways forward here:

  • We can tell the user "your administrator has to go clean up the account stub in Duo". Yuck.
  • We can enroll random usernames (how the API works by default). Fine for us, maybe a mess administratively. There is nothing in Duo that ties the usernames / enrollment attempts back to Phabricator.
  • We can send the user along the alternate web auth flow instead, although there's no way for them to get back to Phabricator if they do this.

One bit of light at the end of the tunnel is that the enroll request has a valid_secs parameter, so it's possible that Duo cleans up the phantom username on its own after this TTL, even though the UI says "Active", not "Phantom Expiring in 56 minutes". I'll try some short TTLs and see if we get any leverage with that.

With a 15-second TTL, the user doesn't vanish or anything, and Duo does not let us do another enroll attempt even after the enroll time expires:

[HTTP/400] 
{"code": 40002, "message": "Invalid request parameters", "message_detail": "username already exists", "stat": "FAIL"}

This is somewhat perplexing (I think I'm on just about the most straightforward path possible) although Duo seems to be pushing client applications down the "mess of iframes" or "enroll portal" pathways. I think these are bad UX and bad technically so I'd strongly prefer not to end up there.

I'm going to try this instead:

  • We'll generate random usernames in the form phabricator-<username-at-the-time>-<entropy>.
  • That should give administrators a reasonable shot at figuring out what's going on.
  • The username will be static at time-of-creation (i.e., it won't change if a Phabricator username changes) but it's not important that it be stable, since it's a random identifier from Phabricator's viewpoint (more of a "factor identifier" than a "username") that just happens to be human-readable.

Duo wants us to <img src="..." /> a QR code.

This violates "Content-Security-Policy". Approaches:

  1. Add CSP exceptions.
  2. Proxy the image.
  3. Build our own QR code.

I'd like to extend CSP only as a method of last resort. Features like Quicksand (T13081) and the possible future introduction of a "sheets" UI mean that, in the general case, any page may load any other page via Javascript without actually performing a browser navigation event. We can work with this to some degree -- and the MFA enroll gate isn't a common workflow -- but I'd prefer to keep CSP as simple as possible. Adjusting CSP may also mean that we need to make assumptions that the image domain is the same as the API domain. This appears to be true but isn't explicit in the documentation.

Proxying (2) or rendering the QR code ourselves (3) are both fairly reasonable. Rendering the QR code ourselves means that we have to assume the QR code contains the same data as the activation URI, but this feels like a pretty safe assumption. Since we need QR rendering for TOTP anyway and rendering touches less stuff than proxying, I'm inclined to render.

When you delete a Duo factor from your phone app, you still get prompted to "auth", Duo just reports that you have no enrolled devices which support push.

I think we can maybe (?) let users recover from this state by sending them on Mr Bones' Wild Ride though Duo web enrollment (perhaps? But I'm not sure we can even get an enroll URI?), but I'm just going to treat this as a hard wall for now, similar to TOTP.

FWIW - most users of this feature (especially after T13229) will be organizations that already have DUO in their environment, and likely already have users defined in DUO. Creating random usernames, or PHID's is probably not going to work for most of those organizations. Phabricator does require email's to be unique (no reuse) so that might be a better choice for initial userid setting?

Also - again if this is in a corporate deployment - allowing arbitrary duo enrollment may not be desirable. It is also configurable in DUO if you allow users to enroll or not on a per application basis. Taking the view from Phabricator that enrollment must be done via a company owned service (and display a Admin defined help message) is likely the best path forward and removes most of this pain.

The Admin API allows for you to view existing users, access logs, etc - if you want to test using this as away for automatically validating users - I can get that set up, although it will complicate the enablement for end users (organizations that want DUO)

Do you envision that individuals would want to use DUO at a personal account level? AKA if they already have duo personally?

I'm imaging that one use case is that you just want everyone to use MFA, and Duo is a more attractive approach than TOTP (since it's easier to use) or SMS (since it's more secure). In this case, you might just be using Duo as "Better TOTP", and the enrollment/management side isn't as important. For example, if we deployed Duo on secure or in the Phacility cluster as a default option, we'd use it in this mode.

In this case, you aren't connecting to a personal account, Duo is just allowing anyone to self-enroll on an organization account. For secure or instances, this would be a Phacility-managed account. (This doesn't necessarily make much practical sense today given the Duo pricing model, but there's nothing technically wrong with it.)

The end user needs to download and install the app, but doesn't need to create a Duo account.

If we want to synchronize to existing accounts, I think it's not good enough for us to pick the user's email address unless that's also the username the organization already uses -- in your case, is it? It's not a great identifier on our side since it's freely mutable and not stable (humans often change their real name after marriage, and for other reasons).

We could have logic like this:

  • If an administrator enabled "Synchronize Duo accounts based on email addresses", when the user clicks "enroll", check if any of their verified email addresses are already enrolled as usernames. If they are, link the accounts.
  • If an administrator enabled "Synchronize Duo accounts based on Phabricator usernames", check if their username is enrolled. If it is, link the accounts. This potentially lets users create a huge mess if they can pick their own usernames.
  • If administrator checked "Allow self-enrollment in Duo", do the self-enroll flow with a "random" user name (like phabricator-epriestley-3gymzzhtiqyulwza).

This also creates a mild mess where user accounts can be in a state where we they are not enrolled and we can not enroll them inline. We can try sending them along the out-of-band enroll flow -- this is kind of garbage ("Click this link, do all the steps, then come back here."), but at least it converges to the right state.

If Duo usernames are based on some other identifier (like SSN or internal LDAP account ID), I'm not sure how we can link the accounts. It's possible that the web flow allows it, but that also means the user can link the wrong account by mistake. Duo doesn't seem to have (for example) an outward-facing OAuth flow where users can login to Duo.

If Duo usernames are arbitrary and not predictable from Phabricator credentials (LDAP account ID plus last 3 digits of your social security number), perhaps all we can do is let administrators configure Duo usernames on a per-account basis. An administrator would laboriously set that alice in Phabricator is alice_smith-392 in Duo whenever they created a new user, then we're good to go.

(My experience suggests that far more organizations are in the "every user has a totally different, inconsistent username in every system and all of them are completely mutable at any time with no mechanism for propagating or reconciling changes" bucket than the "every user has exactly one consistent, unique username everywhere" bucket, so I sort of assumed that Duo is not attempting to synchronize identities based on a single shared identifier, but maybe it is and this is common/expected and somehow IT has entered a new golden era in the last year or two?)

If we want to synchronize to existing accounts, I think it's not good enough for us to pick the user's email address unless that's also the username the organization already uses -- in your case, is it?

We only support oAuth with Google so email is a strong identifier in our use case. Since Phabricator let's users pick their own userid as part of the registration oAuth flow... we do not have a good relationship between email & userid (which drives me nuts)

This also creates a mild mess where user accounts can be in a state where we they are not enrolled and we can not enroll them inline. We can try sending them along the out-of-band enroll flow -- this is kind of garbage ("Click this link, do all the steps, then come back here."), but at least it converges to the right state.

I agree this is garbage, but probably the best way from an organizations point of view... if you are paying X$/user/Month - you want people to be consistent. FYI - DUO does have an auto disable setting, so if no auth events happen in N months, the user is removed....

If Duo usernames are based on some other identifier (like SSN or internal LDAP account ID), I'm not sure how we can link the accounts. It's possible that the web flow allows it, but that also means the user can link the wrong account by mistake. Duo doesn't seem to have (for example) an outward-facing OAuth flow where users can login to Duo.

DUO does some fuzzy lookups (automatically munge emails and AD style lookups) based on attributes, and you can now add multiple usernames to handle some of these cases https://help.duo.com/s/article/aliases-guide?language=en_US

You can configure DUO to trust an oAuth login as the first phase - and then have it do the MFA checks/User enrollments/etc... but I think that will likely be way to complex for what we are talking about here. (aka. DUO Access Gateway)

If Duo usernames are arbitrary and not predictable from Phabricator credentials (LDAP account ID plus last 3 digits of your social security number), perhaps all we can do is let administrators configure Duo usernames on a per-account basis. An administrator would laboriously set that alice in Phabricator is alice_smith-392 in Duo whenever they created a new user, then we're good to go.

If an org is using LDAP, the likelihood that the LDAP username and the DUO username was not synced would be very very low. Having the user specify it, or having an LDAP attribute for the DUO userid are both options.

(My experience suggests that far more organizations are in the "every user has a totally different, inconsistent username in every system and all of them are completely mutable at any time with no mechanism for propagating or reconciling changes" bucket than the "every user has exactly one consistent, unique username everywhere" bucket, so I sort of assumed that Duo is not attempting to synchronize identities based on a single shared identifier, but maybe it is and this is common/expected and somehow IT has entered a new golden era in the last year or two?)

IT has not entered a golden age, but a lot of IT teams are much more strict about supporting you if you decide that you want to be known as Foo, F00, F0o in different systems. :)
Name changes are still a pain - and frequently either involve you getting entirely new IDs or living with your original name :( .

Some tools like Okta/OneLogin try to handle this by triggering new provisioning steps if a name change occurs ... but usually you just end up with an email alias... and not change how you login to the dozens of tools... It seems the approach from DUO is just allowing an admin to add optional additional usernames.

Another issue here is that Duo doesn't seem to have a way to prevent new user creation. If you /preauth a user, they get an enroll link whether they already have an account in Duo or not, and there's no apparent way to distinguish between "this creates a new user" and "this encourages an existing user, who has already been created but has not enrolled a device yet, to enroll".

We can have a strict option like "no enrollment allowed", where you must already be enrolled to continue. This could dead-end users fairly hard, but I think it's the only way to prevent duplicate accounts if usernames/emails don't match between the two systems. It also saves us from the web enroll flow, which is really unfriendly (like 9 questions about what kind of device you're enrolling, compared to five seconds of waving your phone at the screen if you enroll inline).

Duo also doesn't seem to have any kind of fuzzy logic around merging accounts that enroll using the same device. I have like 35 different "users" that have all enrolled with the same copy of the application on the same device, but it treats them as completely different users.

Anyway, this stuff is navigable, I guess I'm mostly just surprised that a relatively recent entry into auth is putting all its eggs into the "everyone has one consistent username across all systems" basket.

Another issue is that we apparently (???) only get one shot at learning the user's (internal-to-Duo) User ID, and only if we're allowed to enroll them. The first call to /enroll gives us a user ID, subsequent calls do not and there's no other way to do the lookup. This isn't really a problem, it just means we're forced to use usernames everywhere (which are mutable/aliasable/etc/etc) when we could otherwise use nice stable user IDs.

Another issue here is that Duo doesn't seem to have a way to prevent new user creation. YIf you /preauth a user, they get an enroll link whether they already have an account in Duo or not, and there's no apparent way to distinguish between "this creates a new user" and "this encourages an existing user, who has already been created but has not enrolled a device yet, to enroll".

I am surprised by this- I’ll see what I can get form support - but my expectation is they expect you to fail due to no such user then try preauth.

Duo also doesn't seem to have any kind of fuzzy logic around merging accounts that enroll using the same device. I have like 35 different "users" that have all enrolled with the same copy of the application on the same device, but it treats them as completely different users.

This is by design - having the same phone attached to multiple devices can be helpful, and allows it staff to reassign devices incase people lose their phones. Their should only be one copy of the device across each user though.

Anyway, this stuff is navigable, I guess I'm mostly just surprised that a relatively recent entry into auth is putting all its eggs into the "everyone has one consistent username across all systems" basket.

Or that this way they make more money? ;)

I think I have this working fairly reasonably, now. There are four major cases:

Case A: Stars Align

Setup: An administrator set up your account in Duo already. You received the Duo email and enrolled your phone. Your Phabricator account correctly produces an identifier which matches your Duo account.

Flow: You click continue on this dialog and you're all done.


If any of the steps above are not in the right state, we go to the other cases. In particular:

  • Your account is not set up in Duo yet.
  • You weren't sent an enroll email.
  • You haven't gotten the email yet, or haven't enrolled.
  • Phabricator's identifier doesn't match Duo (e.g., alice.smith@gmail.com vs alicesmith@gmail.com).

As far as I can tell, we can't distinguish between these cases, so the flow we end up on depends on how willing we are to bet that we're not in the "mismatched identifier" case:

Case B: No Enrollment

Setup: To avoid creating new accounts through self-enrollment, Phabricator is configured to prevent all enrollment.

Flow: Users have to go figure out what's wrong. We try to help them, but there's not much we can do.

Case C: Inline Enrollment

Setup: We're trusting that the identifiers are a 100% match, so it's okay to enroll. The user is doing enrollment for the first time, so we got a QR code back from /enroll.

Flow: Users scan the code in the Duo application, or click the button if they're browsing Phabricator from their phone.

Case D: External Enrollment

Setup: Same as above, except the user accidentally clicked "Cancel" the first time. Since the second call to /enroll fails (???) instead of giving us a new QR code, we fall back to the portal flow and go on a quest.

Flow: Click so so so many times.

This final screen is animated and there's nothing you can click to do anything or continue: