Add ability to verify login with token code#2218
Conversation
c228a7a to
47a1fe9
Compare
…dikoff,@rfk This PR exposes new APIs to create and verify sessionTokens using a short code. Couple notes - Each code has an expiration date - Codes are stored as hashes in database - Support for resending (aka regenerating) a code to come in later PR. - New endpoint `verifyTokensShortCode` used to explicitly verify shortCodes. Connects with mozilla/fxa-auth-server#2218 Feature doc: https://docs.google.com/document/d/1xQtCGBJ9XZuF1q9faZV3YIiOnxomQMHKiTP62YSf3v8/edit
dc51a86 to
cb68dcf
Compare
73323b6 to
da3798d
Compare
|
@mozilla/fxa-devs I think this is ready, r? |
rfk
left a comment
There was a problem hiding this comment.
This is good stuff @vbudhram! I left a bunch of smaller comments, but I also have two larger ones.
First, can you please generate a screenshot of two of the resulting emails for @ryanfeeley's explicit review?
Second, I don't see anything in here about re-sending the email-2fa email, but the design for the front-end in [1] does include a "resend" link. Do we need a new endpoint to trigger that resend? (And some additional security checks to ensure an attacker doesn't try to use it for old sessions that don't have a short-code attached?).
config/dev.json
Outdated
| "signinConfirmation": { | ||
| "skipForNewAccounts": { | ||
| "enabled": false | ||
| } |
There was a problem hiding this comment.
Is this necessary to e.g. get the tests to pass properly, or was it for local dev?
There was a problem hiding this comment.
Just for local dev, but I don't think it is needed.
| format: 'duration', | ||
| default: '1 hour', | ||
| env: 'SIGNIN_TOKEN_CODE_LIFETIME' | ||
| }, |
There was a problem hiding this comment.
For my own sense of thoroughness, noting that these are the same parameters used by the similarly-powerful "unblock codes" feature 👍
lib/db.js
Outdated
| '/tokens/' + code + '/verifyCode', | ||
| { uid: accountData.uid } | ||
| ) | ||
| .then( |
There was a problem hiding this comment.
nit: the indentation of .then( here is mis-aligned with the paren on the line above, which doesn't match the style of other code in this file.
| }, | ||
| details | ||
| ) | ||
| } |
There was a problem hiding this comment.
Just flagging that we'll need to add support in fxa-content-server for handling these new codes.
lib/routes/account.js
Outdated
|
|
||
| const tokenCodeConfig = config.signinConfirmation.tokenVerificationCode | ||
| const tokenCodeLifetime = tokenCodeConfig && tokenCodeConfig.codeLifetime || 0 | ||
| const tokenCodeLength = tokenCodeConfig && tokenCodeConfig.codeLength || 0 |
There was a problem hiding this comment.
Zero doesn't seem like a safe fallback value for the code length here. A missing length should either disable the feature entirely, or throw as a hard config error of some sort.
There was a problem hiding this comment.
Updated to use default value of 1 hour.
| path: '/tokenCodes/verify', | ||
| config: { | ||
| validate: { | ||
| payload: { |
There was a problem hiding this comment.
The tests suggest this should take a metricsContext payload param as well, but it doesn't appear to.
There was a problem hiding this comment.
I removed the metricsContext requirement and emit metrics using request.emit
test/mail_helper.js
Outdated
| console.log('\x1B[32m', link, '\x1B[39m') | ||
| } | ||
| else if (sc) { | ||
| console.log('\x1B[32mSign-in code: ', sc, '\x1B[39m') |
There was a problem hiding this comment.
As above, this may be a little confusing because "signin code" is already the name of something else. Could we call this e.g. "verification code" or similar?
| assert.equal(status.emailVerified, true, 'email is verified') | ||
| assert.equal(status.sessionVerified, false, 'session is not verified') | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Should this also assert that the expected email was sent, like the previous test does?
test/remote/token_code_tests.js
Outdated
| const metricsContext = { | ||
| flowId: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', | ||
| flowBeginTime: Date.now() | ||
| } |
There was a problem hiding this comment.
ISTM the tests should assert something about this metricsContext, e.g. that the correct flow-id header was present in the outgoing email.
| }) | ||
| }) | ||
|
|
||
| it('should consume valid code', () => { |
There was a problem hiding this comment.
For completeness, can you please add a remote test to assert that invalid token codes are rejected, and leave the session unverified? It'll be good to double-check that end-to-end.
224d14d to
16ab206
Compare
|
@rfk Updated! Opted to use |
rfk
left a comment
There was a problem hiding this comment.
r+ with nits, thanks @vbudhram
I left a couple of small nits, but also realized one broader concern w.r.t metrics. IIUC users who end up in the treatment group for this experiment would no longer emit an account.confirmed metrics event. I suspect that they should, and that we should just add one in the /session/verify/token endpoint. But it's possible I don't understand all the nuance involved. Thoughts?
lib/routes/account.js
Outdated
|
|
||
| const tokenCodeConfig = config.signinConfirmation.tokenVerificationCode | ||
| const tokenCodeLifetime = tokenCodeConfig && tokenCodeConfig.codeLifetime || MS_ONE_HOUR | ||
| const tokenCodeLength = tokenCodeConfig && tokenCodeConfig.codeLength || 0 |
There was a problem hiding this comment.
IIUC, you changed this to make the default for tokenCodeLifetime be 1 hour, but by concern was about the default for tokenCodeLength - we don't want a configuration error to accidentally make "" a valid verification code for all sessions. It may also be worth changing the defaults for unblockCode* above to match whatever you choose here, for consistency.
lib/routes/token-codes.js
Outdated
| const uid = request.payload.uid | ||
| const code = request.payload.code.toUpperCase() | ||
|
|
||
| customs.checkIpOnly(request, 'verifyTokenCode') |
There was a problem hiding this comment.
Since we've authenticated with the sessionToken here, we have the user's email address available in request.auth.credentials.email, meaning it could use customs.check here rather than customs.checkIpOnly. It's probably worth doing so, to avoid incorrectly rate-limited people who happen to be behind a shared IP.
|
|
||
| return request.emitMetricsEvent('tokenCodes.verified', {uid: uid}) | ||
| .then(() => ({})) | ||
| } |
There was a problem hiding this comment.
I suspect we will need to emit an account.confirmed metrics event here, for at least two reasons:
- This event is used by amplitude to generate
fxa_login - email_confirmedevents, so users in this experiment might otherwise disappear from our login funnels in amplitude - This is used as the flow-complete signal for non-sync logins, so users in this experiment might incorrectly appear to have failed to complete their login flow.
test/remote/token_code_tests.js
Outdated
| return Client.login(config.publicUrl, email, password, { | ||
| verificationMethod: 'email-2fa' | ||
| }) | ||
| .then((res) => { |
test/remote/token_code_tests.js
Outdated
| return Client.login(config.publicUrl, email, password, { | ||
| verificationMethod: 'email-2fa' | ||
| }) | ||
| .then((res) => { |
|
@rfk Thank you! Added updated metrics |
This exposes the ability for a client to specify how it wants to verify the login.