Skip to content
This repository was archived by the owner on Apr 3, 2019. It is now read-only.

Commit ac0b814

Browse files
vbudhramvladikoff
authored andcommitted
feat(codes): add support for verifying token short code (#287) r=@vladikoff,@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
1 parent b06a8ed commit ac0b814

File tree

12 files changed

+465
-28
lines changed

12 files changed

+465
-28
lines changed

docs/API.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ There are a number of methods that a DB storage backend should implement:
3535
* .deleteKeyFetchToken(tokenId)
3636
* Unverified session tokens and key fetch tokens
3737
* .verifyTokens(tokenVerificationId, accountData)
38+
* .verifyTokenCode(code, accountData)
3839
* Password Forgot Tokens
3940
* .createPasswordForgotToken(tokenId, passwordForgotToken)
4041
* .deletePasswordForgotToken(tokenId)
@@ -430,7 +431,7 @@ Parameters.
430431
Each token takes the following fields for it's create method respectively:
431432

432433
* sessionToken : data, uid, createdAt, uaBrowser, uaBrowserVersion, uaOS, uaOSVersion, uaDeviceType,
433-
uaFormFactor, mustVerify, tokenVerificationId
434+
uaFormFactor, mustVerify, tokenVerificationId, tokenVerificationCodeHash, tokenVerificationCodeExpiresAt
434435
* keyFetchToken : authKey, uid, keyBundle, createdAt, tokenVerificationId
435436
* passwordChangeToken : data, uid, createdAt
436437
* passwordForgotToken : data, uid, passCode, createdAt, triesxb
@@ -480,7 +481,8 @@ These fields are represented as
480481
* sessionTokenWithVerificationStatus : t.tokenData, t.uid, t.createdAt, t.uaBrowser, t.uaBrowserVersion,
481482
t.uaOS, t.uaOSVersion, t.uaDeviceType, t.uaFormFactor, t.lastAccessTime,
482483
a.emailVerified, a.email, a.emailCode, a.verifierSetAt,
483-
a.createdAt AS accountCreatedAt, ut.mustVerify, ut.tokenVerificationId
484+
a.createdAt AS accountCreatedAt, ut.mustVerify, ut.tokenVerificationId,
485+
ut.tokenVerificationCodeHash, ut.tokenVerificationCodeExpiresAt
484486
* keyFetchToken : t.authKey, t.uid, t.keyBundle, t.createdAt, a.emailVerified, a.verifierSetAt
485487
* keyFetchTokenWithVerificationStatus : t.authKey, t.uid, t.keyBundle, t.createdAt, a.emailVerified,
486488
a.verifierSetAt, ut.mustVerify, ut.tokenVerificationId
@@ -561,6 +563,27 @@ Returns a promise that:
561563
from the underlying storage system
562564
(wrapped in `error.wrap()`).
563565

566+
## .verifyTokenCode(code, accountData)
567+
568+
Verifies sessionTokens and keyFetchTokens.
569+
Note that it takes the code (separate from tokenVerificationId)
570+
specified when creating the token,
571+
NOT the tokenId.
572+
`accountData` is an object
573+
with a `uid` property.
574+
575+
Returns a promise that:
576+
577+
* Resolves with an object `{}`
578+
if a token was verified.
579+
* Rejects with error `{ code: 404, errno: 116 }`
580+
if there was no matching token.
581+
* Rejects with error `{ code: 400, errno: 137 }`
582+
if token expired.
583+
* Rejects with any error
584+
from the underlying storage system
585+
(wrapped in `error.wrap()`).
586+
564587
## .forgotPasswordVerified(tokenId, accountResetToken) ##
565588

566589
An extra function for `passwordForgotTokens`. This performs three operations:
@@ -613,7 +636,7 @@ The deviceCallbackPublicKey and deviceCallbackAuthKey fields are urlsafe-base64
613636
d.callbackPublicKey AS deviceCallbackPublicKey,
614637
d.callbackAuthKey AS deviceCallbackAuthKey,
615638
d.callbackIsExpired AS deviceCallbackIsExpired,
616-
ut.mustVerify, ut.tokenVerificationId
639+
ut.mustVerify, ut.tokenVerificationId, ut.tokenVerificationCodeHash, ut.tokenVerificationCodeExpiresAt
617640

618641
## .createVerificationReminder(body) ##
619642

fxa-auth-db-server/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ function createServer(db) {
4141
})
4242
}
4343

44+
function withParamsAndBody(fn) {
45+
return reply(function (params, body, query) {
46+
return fn.call(db, params, body)
47+
})
48+
}
49+
4450
var api = restify.createServer({
4551
formatters: {
4652
'application/json; q=0.9': safeJsonFormatter
@@ -120,6 +126,7 @@ function createServer(db) {
120126
api.get('/sessionToken/:id/verified', withIdAndBody(db.sessionTokenWithVerificationStatus))
121127
api.get('/keyFetchToken/:id/verified', withIdAndBody(db.keyFetchTokenWithVerificationStatus))
122128
api.post('/tokens/:id/verify', withIdAndBody(db.verifyTokens))
129+
api.post('/tokens/:code/verifyCode', withParamsAndBody(db.verifyTokenCode))
123130

124131
api.get('/accountResetToken/:id', withIdAndBody(db.accountResetToken))
125132
api.del('/accountResetToken/:id', withIdAndBody(db.deleteAccountResetToken))

fxa-auth-db-server/lib/error.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ AppError.cannotDeletePrimaryEmail = function () {
6161
)
6262
}
6363

64+
AppError.expiredTokenVerificationCode = function () {
65+
return new AppError(
66+
{
67+
code: 400,
68+
error: 'Bad request',
69+
errno: 137,
70+
message: 'Expired token verification code'
71+
}
72+
)
73+
}
74+
6475
AppError.wrap = function (err) {
6576
return new AppError(
6677
{

fxa-auth-db-server/test/backend/db_tests.js

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@ function makeMockSessionToken(uid) {
132132
uaOSVersion : 'mock OS version',
133133
uaDeviceType : 'mock device type',
134134
mustVerify: true,
135-
tokenVerificationId : hex16()
135+
tokenVerificationId : hex16(),
136+
tokenVerificationCode: unblockCode(),
137+
tokenVerificationCodeExpiresAt: Date.now() + 20000
136138
}
137139

138140
return sessionToken
@@ -2580,6 +2582,97 @@ module.exports = function(config, DB) {
25802582
})
25812583
})
25822584

2585+
describe('db.verifyTokenCode', () => {
2586+
let account, anotherAccount, sessionToken, tokenVerificationCode, tokenId
2587+
before(() => {
2588+
account = createAccount()
2589+
account.emailVerified = true
2590+
return db.createAccount(account.uid, account)
2591+
})
2592+
2593+
it('should verify tokenVerificationCode', () => {
2594+
tokenId = hex32()
2595+
sessionToken = makeMockSessionToken(account.uid)
2596+
tokenVerificationCode = sessionToken.tokenVerificationCode
2597+
return db.createSessionToken(tokenId, sessionToken)
2598+
.then(() => {
2599+
return db.sessionTokenWithVerificationStatus(tokenId)
2600+
})
2601+
.then((session) => {
2602+
// Returns unverified session
2603+
assert.equal(session.mustVerify, sessionToken.mustVerify, 'mustVerify must match sessionToken')
2604+
assert.equal(session.tokenVerificationId.toString('hex'), sessionToken.tokenVerificationId.toString('hex'), 'tokenVerificationId must match sessionToken')
2605+
assert.ok(session.tokenVerificationCodeHash, 'tokenVerificationCodeHash exists')
2606+
assert.equal(session.tokenVerificationCodeExpiresAt, sessionToken.tokenVerificationCodeExpiresAt, 'tokenVerificationCodeExpiresAt must match sessionToken')
2607+
2608+
// Verify the session
2609+
return db.verifyTokenCode({code: tokenVerificationCode}, account)
2610+
})
2611+
.then(() => {
2612+
return db.sessionTokenWithVerificationStatus(tokenId)
2613+
})
2614+
.then((session) => {
2615+
// Returns verified session
2616+
assert.equal(session.mustVerify, null, 'mustVerify is not set')
2617+
assert.equal(session.tokenVerificationId, null, 'tokenVerificationId is not set')
2618+
assert.equal(session.tokenVerificationCodeHash, null, 'tokenVerificationCodeHash is not set')
2619+
assert.equal(session.tokenVerificationCodeExpiresAt, null, 'tokenVerificationCodeExpiresAt is not set')
2620+
})
2621+
})
2622+
2623+
it('shouldn\'t verify expired tokenVerificationCode', () => {
2624+
tokenId = hex32()
2625+
sessionToken = makeMockSessionToken(account.uid)
2626+
sessionToken.tokenVerificationCodeExpiresAt = Date.now() - 2000000000
2627+
tokenVerificationCode = sessionToken.tokenVerificationCode
2628+
return db.createSessionToken(tokenId, sessionToken)
2629+
.then(() => {
2630+
return db.verifyTokenCode({code: tokenVerificationCode}, account)
2631+
.then(() => {
2632+
assert.fail('should not have verified expired token')
2633+
}, (err) => {
2634+
assert.equal(err.errno, 137, 'correct errno, not found')
2635+
})
2636+
})
2637+
})
2638+
2639+
it('shouldn\'t verify unknown tokenVerificationCode', () => {
2640+
tokenId = hex32()
2641+
sessionToken = makeMockSessionToken(account.uid)
2642+
tokenVerificationCode = 'iamzinvalidz'
2643+
return db.createSessionToken(tokenId, sessionToken)
2644+
.then(() => {
2645+
return db.verifyTokenCode({code: tokenVerificationCode}, account)
2646+
.then(() => {
2647+
assert.fail('should not have verified unknown token')
2648+
}, (err) => {
2649+
assert.equal(err.errno, 116, 'correct errno, not found')
2650+
})
2651+
})
2652+
})
2653+
2654+
it('shouldn\'t verify tokenVerificationCode and uid mismatch', () => {
2655+
tokenId = hex32()
2656+
sessionToken = makeMockSessionToken(account.uid)
2657+
tokenVerificationCode = sessionToken.tokenVerificationCode
2658+
anotherAccount = createAccount()
2659+
anotherAccount.emailVerified = true
2660+
return db.createAccount(anotherAccount.uid, anotherAccount)
2661+
.then(() => {
2662+
return db.createSessionToken(tokenId, sessionToken)
2663+
})
2664+
.then(() => {
2665+
return db.verifyTokenCode({code: tokenVerificationCode}, anotherAccount)
2666+
.then(() => {
2667+
assert.fail('should not have verified unknown token')
2668+
}, (err) => {
2669+
assert.equal(err.errno, 116, 'correct errno, not found')
2670+
})
2671+
})
2672+
})
2673+
2674+
})
2675+
25832676
after(() => db.close())
25842677
})
25852678
}

fxa-auth-db-server/test/backend/remote.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,6 +1540,49 @@ module.exports = function(cfg, makeServer) {
15401540
}
15411541
)
15421542

1543+
describe(
1544+
'add account, verify session with tokenVerificationCode',
1545+
() => {
1546+
let user
1547+
1548+
before(() => {
1549+
user = fake.newUserDataHex()
1550+
1551+
return client.putThen('/account/' + user.accountId, user.account)
1552+
.then(function (r) {
1553+
respOkEmpty(r)
1554+
})
1555+
})
1556+
1557+
it('should verify session with tokenVerificationCode', () => {
1558+
return client.putThen('/sessionToken/' + user.sessionTokenId, user.sessionToken)
1559+
.then((r) => {
1560+
respOkEmpty(r)
1561+
return client.getThen('/sessionToken/' + user.sessionTokenId + '/verified')
1562+
})
1563+
.then(function (r) {
1564+
respOk(r)
1565+
const result = r.obj
1566+
assert.ok(result.tokenVerificationCodeHash, 'tokenVerificationCodeHash exists')
1567+
assert.equal(result.tokenVerificationCodeExpiresAt, user.sessionToken.tokenVerificationCodeExpiresAt, 'tokenVerificationCodeExpiresAt set')
1568+
return client.postThen('/tokens/' + user.sessionToken.tokenVerificationCode + '/verifyCode', {
1569+
uid: user.accountId
1570+
})
1571+
})
1572+
.then(function (r) {
1573+
respOk(r)
1574+
return client.getThen('/sessionToken/' + user.sessionTokenId + '/verified')
1575+
})
1576+
.then(function (r) {
1577+
respOk(r)
1578+
const result = r.obj
1579+
assert.equal(result.tokenVerificationCodeHash, null, 'tokenVerificationCodeHash not set')
1580+
assert.equal(result.tokenVerificationCodeExpiresAt, null, 'tokenVerificationCodeExpiresAt not set')
1581+
})
1582+
})
1583+
}
1584+
)
1585+
15431586
after(() => server.close())
15441587

15451588
})

fxa-auth-db-server/test/fake.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ module.exports.newUserDataHex = function() {
5959
uaDeviceType: 'fake device type',
6060
uaFormFactor: 'fake form factor',
6161
mustVerify: true,
62-
tokenVerificationId: hex16()
62+
tokenVerificationId: hex16(),
63+
tokenVerificationCode: crypto.randomBytes(4).toString('hex'),
64+
tokenVerificationCodeExpiresAt: Date.now() + 20000
6365
}
6466

6567
// device

lib/db/mem.js

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,13 @@ module.exports = function (log, error) {
120120
}
121121

122122
if (sessionToken.tokenVerificationId) {
123+
const tokenVerificationCodeHash = sessionToken.tokenVerificationCode ? dbUtil.createHash(sessionToken.tokenVerificationCode): null
123124
unverifiedTokens[tokenId] = {
124-
tokenVerificationId: sessionToken.tokenVerificationId,
125125
mustVerify: !! sessionToken.mustVerify,
126-
uid: sessionToken.uid
126+
tokenVerificationId: sessionToken.tokenVerificationId,
127+
tokenVerificationCodeHash: tokenVerificationCodeHash,
128+
tokenVerificationCodeExpiresAt: sessionToken.tokenVerificationCodeExpiresAt,
129+
uid: sessionToken.uid,
127130
}
128131
}
129132

@@ -363,6 +366,54 @@ module.exports = function (log, error) {
363366
return P.resolve({})
364367
}
365368

369+
Memory.prototype.verifyTokenCode = function (tokenData, accountData) {
370+
const uid = accountData.uid.toString('hex')
371+
const tokenVerificationCodeHash = dbUtil.createHash(tokenData.code)
372+
let expired = false
373+
374+
const tokenCount = Object.keys(unverifiedTokens).reduce((count, tokenId) => {
375+
const t = unverifiedTokens[tokenId]
376+
377+
if (t.uid.toString('hex') !== uid) {
378+
return count
379+
}
380+
381+
if (! t.tokenVerificationCodeHash || ! t.tokenVerificationCodeExpiresAt) {
382+
return count
383+
}
384+
385+
// Is code expired?
386+
if (t.tokenVerificationCodeHash.toString('hex') === tokenVerificationCodeHash.toString('hex')) {
387+
if (t.tokenVerificationCodeExpiresAt <= Date.now()) {
388+
expired = true
389+
390+
return count
391+
}
392+
393+
// Remove token and update security table
394+
(securityEvents[uid] || []).forEach(function (ev) {
395+
if (ev.tokenId && ev.tokenId.toString('hex') === tokenId) {
396+
ev.verified = true
397+
}
398+
})
399+
delete unverifiedTokens[tokenId]
400+
401+
return count + 1
402+
}
403+
return count
404+
}, 0)
405+
406+
if (expired) {
407+
return P.reject(error.expiredTokenVerificationCode())
408+
}
409+
410+
if (tokenCount === 0) {
411+
return P.reject(error.notFound())
412+
}
413+
414+
return P.resolve({})
415+
}
416+
366417
Memory.prototype.deleteAccountResetToken = function (tokenId) {
367418
delete accountResetTokens[tokenId.toString('hex')]
368419
return P.resolve({})
@@ -653,9 +704,14 @@ module.exports = function (log, error) {
653704
if (unverifiedTokens[tokenId]) {
654705
sessionToken.mustVerify = unverifiedTokens[tokenId].mustVerify
655706
sessionToken.tokenVerificationId = unverifiedTokens[tokenId].tokenVerificationId
707+
sessionToken.tokenVerificationCodeHash = unverifiedTokens[tokenId].tokenVerificationCodeHash
708+
sessionToken.tokenVerificationCodeExpiresAt = unverifiedTokens[tokenId].tokenVerificationCodeExpiresAt
709+
656710
} else {
657711
sessionToken.mustVerify = null
658712
sessionToken.tokenVerificationId = null
713+
sessionToken.tokenVerificationCodeHash = null
714+
sessionToken.tokenVerificationCodeExpiresAt = null
659715
}
660716
return sessionToken
661717
})

0 commit comments

Comments
 (0)