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

Commit 7253d09

Browse files
authored
feat(email): Add change email (#254), r=@philbooth
1 parent 1a7967a commit 7253d09

File tree

9 files changed

+358
-10
lines changed

9 files changed

+358
-10
lines changed

docs/API.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ There are a number of methods that a DB storage backend should implement:
1717
* .resetTokens(uid)
1818
* Accounts (using `email`)
1919
* .emailRecord(emailBuffer)
20+
* .accountRecord(emailBuffer)
2021
* .accountExists(emailBuffer)
2122
* .getSecondaryEmail(emailBuffer)
23+
* .setPrimaryEmail(uid, emailBuffer)
2224
* Session Tokens
2325
* .createSessionToken(tokenId, sessionToken)
2426
* .updateSessionToken(tokenId, sessionToken)
@@ -296,6 +298,9 @@ Returns:
296298

297299
## .emailRecord(emailBuffer) ##
298300

301+
Note: Using this method will emit a deprecation warning, please use `.accountRecord` instead.
302+
This method only reads from the account table whereas `.accountRecord` checks the emails table and returns correct account record.
303+
299304
Gets the account record related to this (normalized) email address. The email is provided in a Buffer.
300305

301306
Parameters:
@@ -321,6 +326,36 @@ Returns:
321326
* rejects: with one of:
322327
* `error.notFound()` if no account exists for this email address
323328
* any error from the underlying storage engine
329+
330+
## .accountRecord(emailBuffer) ##
331+
332+
Gets the account record related to this (normalized) email address by checking for email on emails table.
333+
The email is provided in a Buffer.
334+
335+
Parameters:
336+
337+
* emailBuffer: the email address will be a hex encoded string, which is converted back to a string, then
338+
`.toLowerCase()`. In the MySql backend we use `LOWER(?)` which uses the current locale for case-folding.
339+
340+
Returns:
341+
342+
* resolves with:
343+
* `account` - consisting of:
344+
* uid - (Buffer16)
345+
* email - (string)
346+
* normalizedEmail - (string)
347+
* emailVerified - 0|1
348+
* emailCode - (Buffer16)
349+
* kA - (Buffer32)
350+
* wrapWrapKb - (Buffer32)
351+
* verifierVersion - (number)
352+
* verifyHash - (Buffer32)
353+
* authSalt - (Buffer32)
354+
* verifierSetAt - (number) an epoch
355+
* primaryEmail - (string)
356+
* rejects: with one of:
357+
* `error.notFound()` if no account exists for this email address
358+
* any error from the underlying storage engine
324359

325360
## .accountExists(email) ##
326361

@@ -363,6 +398,23 @@ Returns:
363398
* `error.notFound()` if no email address exists on emails table
364399
* any error from the underlying storage engine
365400

401+
## .setPrimaryEmail(uid, emailBuffer) ##
402+
403+
Sets the primary email address as `emailBuffer` for account with `uid`.
404+
405+
Parameters:
406+
407+
* uid: the uid of the account
408+
* email: the normalized email address that will be the new primary email
409+
410+
Returns:
411+
412+
* resolves with:
413+
* an empty object `{}`
414+
* rejects: with one of:
415+
* `error.notFound()` if no email address exists on emails table
416+
* any error from the underlying storage engine
417+
366418
## Tokens ##
367419

368420
All tokens (sessionTokens, keyFetchTokens, passwordForgotTokens, passwordChangeTokens, accountResetTokens) have three

fxa-auth-db-server/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ function createServer(db) {
9696
return db.getSecondaryEmail(Buffer(req.params.email, 'hex'))
9797
})
9898
)
99+
api.get('/email/:email/account',
100+
op(function (req) {
101+
return db.accountRecord(Buffer(req.params.email, 'hex'))
102+
})
103+
)
104+
api.post('/email/:email/account/:id',
105+
op(function (req) {
106+
return db.setPrimaryEmail(req.params.id, Buffer(req.params.email, 'hex'))
107+
})
108+
)
99109

100110
api.get('/sessionToken/:id', withIdAndBody(db.sessionToken))
101111
api.del('/sessionToken/:id', withIdAndBody(db.deleteSessionToken))

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

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function createEmail(data) {
4444
email: ('' + Math.random()).substr(2) + '@bar.com',
4545
uid: data.uid,
4646
emailCode: data.emailCode || crypto.randomBytes(16),
47-
isVerified: false,
47+
isVerified: data.isVerified || false,
4848
isPrimary: false,
4949
createdAt: Date.now()
5050
}
@@ -120,6 +120,23 @@ const ACCOUNT_RESET_TOKEN = {
120120
createdAt: now + 5
121121
}
122122

123+
function makeMockSessionToken(uid) {
124+
var sessionToken = {
125+
data : hex32(),
126+
uid : uid,
127+
createdAt : now + 1,
128+
uaBrowser : 'mock browser',
129+
uaBrowserVersion : 'mock browser version',
130+
uaOS : 'mock OS',
131+
uaOSVersion : 'mock OS version',
132+
uaDeviceType : 'mock device type',
133+
mustVerify: true,
134+
tokenVerificationId : hex16()
135+
}
136+
137+
return sessionToken
138+
}
139+
123140
// To run these tests from a new backend, pass the config and an already created
124141
// DB API for them to be run against.
125142
module.exports = function(config, DB) {
@@ -2468,6 +2485,79 @@ module.exports = function(config, DB) {
24682485
})
24692486
})
24702487

2488+
describe('change email', () => {
2489+
let account, secondEmail
2490+
2491+
before(() => {
2492+
account = createAccount()
2493+
account.emailVerified = true
2494+
secondEmail = createEmail({
2495+
uid: account.uid,
2496+
isVerified: true
2497+
})
2498+
return db.createAccount(account.uid, account)
2499+
.then(function () {
2500+
return db.createEmail(account.uid, secondEmail)
2501+
})
2502+
.then(function (result) {
2503+
assert.deepEqual(result, {}, 'Returned an empty object on email creation')
2504+
return db.accountEmails(account.uid)
2505+
})
2506+
.then(function (res) {
2507+
assert.deepEqual(res.length, 2, 'Returns correct amount of emails')
2508+
assert.equal(res[0].email, account.email, 'primary email is the address that was used to create account')
2509+
assert.deepEqual(res[0].emailCode, account.emailCode, 'correct emailCode')
2510+
assert.equal(!! res[0].isVerified, true, 'correct verification set')
2511+
assert.equal(!! res[0].isPrimary, true, 'isPrimary is true')
2512+
2513+
assert.equal(res[1].email, secondEmail.email, 'primary email is the address that was used to create account')
2514+
assert.deepEqual(res[1].emailCode, secondEmail.emailCode, 'correct emailCode')
2515+
assert.equal(!! res[1].isVerified, true, 'correct verification set')
2516+
assert.equal(!! res[1].isPrimary, false, 'isPrimary is false')
2517+
})
2518+
})
2519+
2520+
it('should change a user\'s email', () => {
2521+
return db.setPrimaryEmail(account.uid, secondEmail.email)
2522+
.then(function (res) {
2523+
assert.deepEqual(res, {}, 'Returned an empty object on email change')
2524+
return db.accountEmails(account.uid)
2525+
})
2526+
.then(function (res) {
2527+
assert.deepEqual(res.length, 2, 'Returns correct amount of emails')
2528+
2529+
assert.equal(res[0].email, secondEmail.email, 'primary email is the secondary email address')
2530+
assert.deepEqual(res[0].emailCode, secondEmail.emailCode, 'correct emailCode')
2531+
assert.equal(!! res[0].isVerified, secondEmail.isVerified, 'correct verification set')
2532+
assert.equal(!! res[0].isPrimary, true, 'isPrimary is true')
2533+
2534+
assert.equal(res[1].email, account.email, 'should equal account email')
2535+
assert.deepEqual(res[1].emailCode, account.emailCode, 'correct emailCode')
2536+
assert.equal(!! res[1].isVerified, account.emailVerified, 'correct verification set')
2537+
assert.equal(!! res[1].isPrimary, false, 'isPrimary is false')
2538+
2539+
// Verify correct email set in session
2540+
const sessionToken = makeMockSessionToken(account.uid)
2541+
return db.createSessionToken(SESSION_TOKEN_ID, sessionToken)
2542+
.then(() => {
2543+
return P.all([db.sessionToken(SESSION_TOKEN_ID), db.sessionTokenWithVerificationStatus(SESSION_TOKEN_ID)])
2544+
})
2545+
})
2546+
.then((res) => {
2547+
res.forEach((session) => {
2548+
assert.equal(session.email, secondEmail.email, 'should equal new primary email')
2549+
assert.deepEqual(session.emailCode, secondEmail.emailCode, 'should equal new primary emailCode')
2550+
assert.deepEqual(session.uid, account.uid, 'should equal account uid')
2551+
})
2552+
return P.all([db.accountRecord(secondEmail.email), db.accountRecord(account.email)])
2553+
})
2554+
.then((res) => {
2555+
assert.deepEqual(res[0], res[1], 'should return the same account record regardless of email used')
2556+
assert.deepEqual(res[0].primaryEmail, secondEmail.email, 'primary email should be set to update email')
2557+
})
2558+
})
2559+
})
2560+
24712561
after(() => db.close())
24722562
})
24732563
}

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,67 @@ module.exports = function(cfg, makeServer) {
14671467
}
14681468
)
14691469

1470+
describe(
1471+
'add account, add email, change email',
1472+
() => {
1473+
let user, secondEmailRecord
1474+
1475+
before(() => {
1476+
user = fake.newUserDataHex()
1477+
secondEmailRecord = user.email
1478+
1479+
// Create account
1480+
return client.putThen('/account/' + user.accountId, user.account)
1481+
.then(function (r) {
1482+
respOkEmpty(r)
1483+
// Create secondary email
1484+
return client.postThen('/account/' + user.accountId + '/emails', user.email)
1485+
})
1486+
.then(function (r) {
1487+
respOk(r)
1488+
const emailCodeHex = secondEmailRecord.emailCode.toString('hex')
1489+
// Verify secondary email
1490+
return client.postThen('/account/' + user.accountId + '/verifyEmail/' + emailCodeHex)
1491+
})
1492+
.then(function (r) {
1493+
respOkEmpty(r)
1494+
return client.getThen('/account/' + user.accountId + '/emails')
1495+
})
1496+
.then(function (r) {
1497+
respOk(r)
1498+
const result = r.obj
1499+
assert.equal(result[0].email, user.account.email, 'matches account email')
1500+
assert.equal(!! result[0].isPrimary, true, 'isPrimary is true on account email')
1501+
assert.equal(!! result[0].isVerified, !! user.account.emailVerified, 'matches account emailVerified')
1502+
1503+
assert.equal(result[1].email, secondEmailRecord.email, 'matches secondEmail email')
1504+
assert.equal(!! result[1].isPrimary, false, 'isPrimary is false on secondEmail email')
1505+
assert.equal(!! result[1].isVerified, true, 'matches secondEmail isVerified')
1506+
})
1507+
})
1508+
1509+
it('should change email', () => {
1510+
return client.postThen('/email/' + emailToHex(secondEmailRecord.email) + '/account/' + user.accountId)
1511+
.then((r) => {
1512+
respOkEmpty(r)
1513+
return client.getThen('/account/' + user.accountId + '/emails')
1514+
})
1515+
.then(function (r) {
1516+
respOk(r)
1517+
const result = r.obj
1518+
1519+
assert.equal(result[0].email, secondEmailRecord.email, 'matches secondEmail email')
1520+
assert.equal(!! result[0].isPrimary, true, 'isPrimary is true on secondEmail email')
1521+
assert.equal(!! result[0].isVerified, true, 'matches secondEmail isVerified')
1522+
1523+
assert.equal(result[1].email, user.account.email, 'matches account email')
1524+
assert.equal(!! result[1].isPrimary, false, 'isPrimary is false on account email')
1525+
assert.equal(!! result[1].isVerified, !! user.account.emailVerified, 'matches account emailVerified')
1526+
})
1527+
})
1528+
}
1529+
)
1530+
14701531
after(() => server.close())
14711532

14721533
})

lib/db/mem.js

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const extend = require('util')._extend
99
const ip = require('ip')
1010
const dbUtil = require('./util')
1111
const config = require('../../config')
12+
const nodeUtil = require('util')
1213

1314
// our data stores
1415
var accounts = {}
@@ -536,13 +537,13 @@ module.exports = function (log, error) {
536537
// Returns:
537538
// - the account if found
538539
// - throws 'notFound' if not found
539-
Memory.prototype.emailRecord = function (email) {
540+
Memory.prototype.emailRecord = nodeUtil.deprecate(function (email) {
540541
email = email.toString('utf8').toLowerCase()
541542
return getAccountByUid(uidByNormalizedEmail[email])
542543
.then(function (account) {
543544
return filterAccount(account)
544545
})
545-
}
546+
}, 'DeprecationWarning for mem.emailRecord: Use mem.accountRecord')
546547

547548
Memory.prototype.sessions = function (uid) {
548549
return this.accountDevices(uid).then(function (devices) {
@@ -613,14 +614,27 @@ module.exports = function (log, error) {
613614

614615
var accountId = sessionTokens[id].uid.toString('hex')
615616
var account = accounts[accountId]
616-
item.emailVerified = account.emailVerified
617-
item.email = account.email
618-
item.emailCode = account.emailCode
617+
619618
item.verifierSetAt = account.verifierSetAt
620619
item.locale = account.locale
621620
item.accountCreatedAt = account.createdAt
622621

623-
return P.resolve(item)
622+
return this.accountEmails(accountId)
623+
.then((emails) => {
624+
625+
// Set the primary email on the sessionToken, which
626+
// could be different from the email on the account object
627+
emails.some((email) => {
628+
if (email.isPrimary) {
629+
item.emailVerified = email.isVerified
630+
item.email = email.email
631+
item.emailCode = email.emailCode
632+
return true
633+
}
634+
})
635+
636+
return item
637+
})
624638
}
625639

626640
Memory.prototype.sessionTokenWithVerificationStatus = function (tokenId) {
@@ -1042,6 +1056,28 @@ module.exports = function (log, error) {
10421056
}
10431057
}
10441058

1059+
Memory.prototype.accountRecord = function (emailBuffer) {
1060+
const normalizedEmail = emailBuffer.toString('utf8').toLowerCase()
1061+
1062+
if (! emails[normalizedEmail]) {
1063+
return P.reject(error.notFound())
1064+
}
1065+
1066+
const uid = emails[normalizedEmail].uid
1067+
return P.all([this.accountEmails(uid), this.account(uid)])
1068+
.spread((emails, account) => {
1069+
1070+
Object.keys(emails).some((key) => {
1071+
var emailRecord = emails[key]
1072+
if (emailRecord.uid.toString('hex') === uid.toString('hex') && emailRecord.isPrimary) {
1073+
account.primaryEmail = emailRecord.normalizedEmail
1074+
}
1075+
})
1076+
1077+
return account
1078+
})
1079+
}
1080+
10451081
Memory.prototype.accountEmails = function (uid) {
10461082
const userEmails = []
10471083

@@ -1052,9 +1088,31 @@ module.exports = function (log, error) {
10521088
}
10531089
})
10541090

1091+
// Sort emails so that primary email is first
1092+
userEmails.sort((a, b) => {
1093+
return b.isPrimary - a.isPrimary
1094+
})
1095+
10551096
return P.resolve(userEmails)
10561097
}
10571098

1099+
Memory.prototype.setPrimaryEmail = function (uid, email) {
1100+
if (! emails[email]) {
1101+
return P.reject(error.notFound())
1102+
}
1103+
1104+
Object.keys(emails).forEach(function (key) {
1105+
var emailRecord = emails[key]
1106+
if (emailRecord.uid.toString('hex') === uid.toString('hex') && emailRecord.isPrimary) {
1107+
emailRecord.isPrimary = false
1108+
}
1109+
})
1110+
1111+
emails[email].isPrimary = true
1112+
1113+
return P.resolve({})
1114+
}
1115+
10581116
Memory.prototype.deleteEmail = function (uid, email) {
10591117
var emailRecord = emails[email]
10601118

0 commit comments

Comments
 (0)