Closed rfk closed 6 years ago
Depends on https://github.com/mozilla/fxa-auth-server/issues/2315 for getting this information back from the auth-server.
Useful context: list of "authentication method reference" values for OAuth: https://tools.ietf.org/html/draft-ietf-oauth-amr-values-04
(In case it wasn't obvious, I'm just going to brain-dump notes to myself into this issue until we come up with a concrete proposal)
From the literature of OAuth-adjacent standards, the relevant terms of art here are:
The OpenID Connect spec allows reliers to provide an acr_values
parameter when they initiate the OAuth dance, to tell the IdP what acr values are preferred. It also defines "acr"
and "amr"
claims to be included in the id_token
to reflect the properties of the completed login flow, and since the id_token is signed, this allows the relier to trust that they accurately reflect what happened during the login flow.
So my high-level proposal is basically:
acr_values
parameter when initiating a login flow.acr
and amr
claims in the id_token
at the end of the login flow.id_token
.For bonus points, we can allow the relier to specify an "essential acr claim" which will cause us to error out during the auth flow if it can't be satisfied. But that feels unnecessary for MVP.
RFC 6711 governs the registration namespace for well-known LoA strings that can be used as acr values, but doesn't actually define any of its own.
This spec adds one called "mod-mf" for multi-factor authentication, although it may be intentionally mobile-carrier-as-IdP-specific:
http://openid.net/specs/openid-connect-modrna-authentication-1_0.html
RFC 6711 governs the registration namespace for well-known LoA strings that can be used as acr values, but doesn't actually define any of its own.
This spec adds one called "phr" for "phishing resistant", which I don't think we can meet with an OTP approach, but might be useful when we add support for webauthn:
https://openid.net/specs/openid-connect-eap-acr-values-1_0.html
NIST has a bunch of literature on this that I haven't finished digesting, and defines levels of assurance from "0" (worst) to "4" (best)
These levels are probably still useful, but I believe they're superseded by the shiny new "Digital Identity Guidelines" documents here:
Interestingly, 800-63-3 section 2.5.3 notes that they have explicitly removed "email" as a valid channel for out-of-band authenticators, which might affect what we can standards-compliantly-claim as the assurance level for our email-loop verification flow.
Section 5.1.2 of https://pages.nist.gov/800-63-3/sp800-63b.html#sec5 uses the term "lookup secret" to encompass what we refer to as "recovery codes". It defines them as "something you have" which makes them suitable as a second factor for AAL2.
Section 5.1.2.2 has some interesting requirements around the use of lookup secrets, including the requirement that they SHALL be hashed for storage. /cc @vbudhram.
Section 5.1.4.1 has some interesting requiremnts for single-factor OTP devices, of which TOTP is one. In particular it requires that we SHALL only accept a TOTP code once. We don't have to comply with these requirements of course, but we should be aware of them and consider them.
In summary, https://pages.nist.gov/800-63-3/sp800-63b.html is a long but excellent read, has lots of advice that's relevant to what we're doing with 2FA, and its "Authenticator Assurance Levels" are a nice fit for the acr values we want to use here.
So, here's a concrete proposal that tries to hew closely to existing standards and specs without getting too lost in the weeds.
Let's use the list of amr
values from RFC 8176 where they meet our needs, and invent similar ones where they don't. Some examples:
amr: ["pwd", "otp"]
.amr: ["pwd", "email"]
.amr: ["pwd"]
.Let's use acr values that map to the Authenticator Assurance Levels of SP 880-63B as follows:
acr: "0"
will, per the OIDC spec, indicate that the user was authenticated through a long-lived browser session rather than by providing an authentication factor of any kind.acr: "AAL1"
will indicate that a single authentication factor was provided.
amr: ["pwd"]
will produce this acr
.amr: ["pwd", "email"]
will produce this acr
, since according to SP880-63B, these are both "something you know" auth factors.acr: "AAL2"
will indicate that two different authentication factors were provided.
amr: ["pwd", "otp"]
will produce this acr
, because one is "something you know" and the other is "something you have".acr: "AAL3"
is not supported by our service at this time, but might become so in the future.The process for a relier to require MFA during a login flow would then look like this:
access_token
and id_token
id_token
and extract the acr
and amr
claimsacr
claim contains "AAL2"
as requestedamr
claim for further info about the authentication process@vbudhram what do you think? The amr
values here could map directly onto the verificationMethod
values from the sessionTokens table in the db, while the oauth-server could maintain an internal table to map combinations of amr
values onto an acr
value.
/cc @gdestuynder @linuxwolf it would be great to get your input on this if you have a few moments.
Validate the id_token and extract the acr and amr claims
I'll note that this is a signed token, so the relier should ideally validate the signature before accepting the claims therein. But given that they just fetched the token directly from https://accounts.firefox.com
over a secure channel, then in the interests of simplicity, they could probably get away with just parsing the claims out of it without checking the signature...
Authing with amr: ["pwd", "email"] will produce this acr, since according to SP880-63B, these are both "something you know" auth factors.
If we wanted, we could invent an intermediate acr
that would allow reliers to request this combination, but we shouldn't call it AAL2
because SP880-63B says very explicitly that it isn't.
@rfk Nice summary! Here are my thoughts
requirements around the use of lookup secrets, including the requirement that they SHALL be hashed for storage.
It shouldn't be an issue to hash the lookup secrets (recovery codes). However, the messaging to the user will definitely need to change to let them know that they can't see these codes again.
Authing with amr: ["pwd", "email"] will produce this acr, since according to SP880-63B, these are both "something you know" auth factors.
It seems a little strange to me that verifying with a password and verifying with a password and email, gives the same assurance level (AAL1). I agree that we should have some intermediate acr
to signify these differences, maybe AAL1+
or AAL1b
.
It shouldn't be an issue to hash the lookup secrets (recovery codes). However, the messaging to the user will definitely need to change to let them know that they can't see these codes again.
*nod*. FWIW we're not bound to follow any particular detail in that document, it's just a very nice collection of best practices.
It seems a little strange to me that verifying with a password and verifying with a password and email, gives the same assurance level (AAL1).
Yeah. On the other hand, we know that if you can auth with ["email"] then you can also auth with ["pwd", "email"] by virtue of doing a password-reset loop, so they are equivalent in some sense. I wouldn't mind deferring that until we have a concrete ask for it from a relier (maybe AMO?) at which point it should be pretty trivial to add.
On many services we (Mozilla IAM/Auth0) equate the value of a password with the value of the email access due to the reason @rfk mentioned (password reset flow through email verification), though not always, if the service does not allow resetting through email.
We also validate the id_token
and would extract amr
then reason about the logic ourselves unless there is any benefit to using acr
instead (and checking acr
>= AAL2
, which is somewhat annoying with a string anyway)
In other words, sounds good to me / would work for us. In particular, we'd request AAL2
by default so in most cases the users won't get an error message from us "asking to enable 2FA" which is nice, and if our logic of what we'd consider an "AAL2
equivalent" is different from yours, we'd still fail with a message, but that would probably be a rare case.
(and checking acr >= AAL2, which is somewhat annoying with a string anyway)
Yeah, trying to do anything other than an ==
comparison on these strings sounds like it could get awkward fast.
we'd request AAL2 by default so in most cases the users won't get an error message from us "asking to enable 2FA" which is nice
To clarify - if you request AAL2 , and the user doesn't have 2FA enabled on their account, what behavior would you expect from FxA? Do you want us to return an error saying we couldn't meet the requested level, or succeed at a lower level and let you check that and show your own messaging? (Noting that you'd have to do the check anyway for security reasons).
To clarify - if you request AAL2 , and the user doesn't have 2FA enabled on their account, what behavior would you expect from FxA? Do you want us to return an error saying we couldn't meet the requested level, or succeed at a lower level and let you check that and show your own messaging? (Noting that you'd have to do the check anyway for security reasons).
I expect FxA would come up with some UI, such as "This application/relier requests you to authenticate with Firefox Accounts using strong authentication/2FA. Please setup 2FA
This is nicer, because it gives the user a chance to setup 2FA, then click ok and actually login - vs hitting an error and having to go to FxA site, setup 2FA, then try again (painful)
I like it; also a good opportunity to drive some more users to enabling 2FA :-)
To make informed security decisions, reliers need to know whether an OAuth flow was completed using MFA and may want to inspect the details of what kind of MFA took place. We need to decide on an API for exposing this data, most likely as part of the OIDC "id_token".
First action here is for @rfk to review OIDC and related specs for prior art on this topic.