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

Commit af0eb33

Browse files
rfkphilbooth
authored andcommitted
fix(tokens): Do not override the createdAt field on existing tokens.
Previously this code could overwrite `createdAt` to the current timestamp when reading an existing token from the database. https://github.com/mozilla/fxa-auth-server-private/pull/45 r=philbooth
1 parent 6bc39a6 commit af0eb33

File tree

4 files changed

+102
-48
lines changed

4 files changed

+102
-48
lines changed

lib/tokens/session_token.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,7 @@ module.exports = (log, inherits, Token, config) => {
2323
this.verifierSetAt = details.verifierSetAt
2424
this.locale = details.locale || null
2525
this.mustVerify = !!details.mustVerify || false
26-
27-
if (details.createdAt > 0) {
28-
this.accountCreatedAt = details.createdAt
29-
} else {
30-
this.accountCreatedAt = null
31-
}
26+
this.accountCreatedAt = details.accountCreatedAt || null
3227

3328
// Tokens are considered verified if no tokenVerificationId exists
3429
this.tokenVerificationId = details.tokenVerificationId || null
@@ -39,8 +34,9 @@ module.exports = (log, inherits, Token, config) => {
3934
SessionToken.tokenTypeID = 'sessionToken'
4035

4136
SessionToken.create = function (details) {
42-
log.trace({ op: 'SessionToken.create', uid: details && details.uid })
43-
return Token.createNewToken(SessionToken, details || {})
37+
details = details || {}
38+
log.trace({ op: 'SessionToken.create', uid: details.uid })
39+
return Token.createNewToken(SessionToken, details)
4440
}
4541

4642
SessionToken.fromHex = function (string, details) {

lib/tokens/token.js

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,22 @@ module.exports = function (log, random, P, hkdf, Bundle, error) {
4040
this.algorithm = 'sha256'
4141
this.uid = details.uid || null
4242
this.lifetime = details.lifetime || Infinity
43-
this.createdAt = optionallyOverrideCreatedAt(details.createdAt)
43+
this.createdAt = details.createdAt || 0
4444
}
4545

4646
function optionallyOverrideCreatedAt (timestamp) {
4747
var now = Date.now()
4848

49-
if (! config.isProduction && timestamp >= 0 && timestamp < now) {
50-
// In the wild, all tokens should have a fresh createdAt timestamp.
51-
// For testing purposes only, allow createdAt to be overridden.
52-
return timestamp
49+
// In the wild, all tokens should have a fresh createdAt timestamp.
50+
// For testing purposes only, allow createdAt to be overridden.
51+
if (timestamp !== undefined) {
52+
if (config.isProduction) {
53+
throw new Error('unexpected value for createdAt')
54+
}
55+
56+
if (timestamp >= 0 && timestamp <= now) {
57+
return timestamp
58+
}
5359
}
5460

5561
return now
@@ -61,28 +67,21 @@ module.exports = function (log, random, P, hkdf, Bundle, error) {
6167
Token.createNewToken = function(TokenType, details) {
6268
return random(32)
6369
.then(bytes => Token.deriveTokenKeys(TokenType, bytes))
64-
.then(keys => new TokenType(keys, details || {}))
70+
.then(keys => {
71+
details = details || {}
72+
details.createdAt = optionallyOverrideCreatedAt(details.createdAt)
73+
return new TokenType(keys, details)
74+
})
6575
}
6676

6777

6878
// Re-create an existing token of the given type.
6979
// This uses known seed data to derive the keys.
7080
//
7181
Token.createTokenFromHexData = function(TokenType, hexData, details) {
72-
var d = P.defer()
7382
var data = Buffer(hexData, 'hex')
74-
Token.deriveTokenKeys(TokenType, data)
75-
.then(
76-
function (keys) {
77-
d.resolve(new TokenType(keys, details || {}))
78-
}
79-
)
80-
.catch(
81-
function (err) {
82-
d.reject(err)
83-
}
84-
)
85-
return d.promise
83+
return Token.deriveTokenKeys(TokenType, data)
84+
.then(keys => new TokenType(keys, details || {}))
8685
}
8786

8887

test/local/session_token_tests.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,22 @@ var SessionToken = tokens.SessionToken
1717

1818
var TOKEN_FRESHNESS_THRESHOLD = require('../../lib/tokens/session_token').TOKEN_FRESHNESS_THREADHOLD
1919

20-
var ACCOUNT = {
20+
var TOKEN = {
2121
createdAt: Date.now(),
22+
accountCreatedAt: Date.now(),
2223
uid: 'xxx',
2324
email: Buffer('test@example.com').toString('hex'),
2425
emailCode: '123456',
2526
emailVerified: true,
2627
tokenVerificationId: crypto.randomBytes(16)
2728
}
2829

30+
2931
describe('SessionToken', () => {
3032
it(
3133
'interface is correct',
3234
() => {
33-
return SessionToken.create(ACCOUNT)
35+
return SessionToken.create(TOKEN)
3436
.then(function (token) {
3537
assert.equal(typeof token.lastAuthAt, 'function', 'lastAuthAt method is defined')
3638
assert.equal(typeof token.update, 'function', 'update method is defined')
@@ -44,16 +46,16 @@ describe('SessionToken', () => {
4446
're-creation from tokenData works',
4547
() => {
4648
var token = null
47-
return SessionToken.create(ACCOUNT)
49+
return SessionToken.create(TOKEN)
4850
.then(
4951
function (x) {
5052
token = x
51-
assert.equal(token.accountCreatedAt, ACCOUNT.createdAt)
53+
assert.equal(token.accountCreatedAt, TOKEN.accountCreatedAt)
5254
}
5355
)
5456
.then(
5557
function () {
56-
return SessionToken.fromHex(token.data, ACCOUNT)
58+
return SessionToken.fromHex(token.data, TOKEN)
5759
}
5860
)
5961
.then(
@@ -96,7 +98,7 @@ describe('SessionToken', () => {
9698
() => {
9799
var token = null
98100
var tokenData = 'a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf'
99-
return SessionToken.fromHex(tokenData, ACCOUNT)
101+
return SessionToken.fromHex(tokenData, TOKEN)
100102
.then(
101103
function (x) {
102104
token = x
@@ -111,7 +113,7 @@ describe('SessionToken', () => {
111113
it(
112114
'SessionToken.setUserAgentInfo',
113115
() => {
114-
return SessionToken.create(ACCOUNT)
116+
return SessionToken.create(TOKEN)
115117
.then(function (token) {
116118
token.setUserAgentInfo({
117119
data: 'foo',

test/local/token_tests.js

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
'use strict'
66

77
const assert = require('insist')
8-
var crypto = require('crypto')
8+
var random = require('../../lib/crypto/random')
99
var hkdf = require('../../lib/crypto/hkdf')
1010
var mocks = require('../mocks')
1111
var P = require('../../lib/promise')
@@ -26,7 +26,7 @@ describe('Token', () => {
2626
delete require.cache[require.resolve(modulePath)]
2727
delete require.cache[require.resolve('../../config')]
2828
process.env.NODE_ENV = 'dev'
29-
Token = require(modulePath)(log, crypto, P, hkdf, Bundle, null)
29+
Token = require(modulePath)(log, random, P, hkdf, Bundle, null)
3030
})
3131

3232
it('Token constructor was exported', () => {
@@ -35,25 +35,61 @@ describe('Token', () => {
3535
assert.equal(Token.length, 2, 'function expects two arguments')
3636
})
3737

38+
it('Token constructor has expected factory methods', () => {
39+
assert.equal(typeof Token.createNewToken, 'function', 'Token.createNewToken is function')
40+
assert.equal(Token.createNewToken.length, 2, 'function expects two arguments')
41+
assert.equal(typeof Token.createTokenFromHexData, 'function', 'Token.createTokenFromHexData is function')
42+
assert.equal(Token.createTokenFromHexData.length, 3, 'function expects three arguments')
43+
})
44+
3845
it('Token constructor sets createdAt', () => {
3946
var now = Date.now() - 1
4047
var token = new Token({}, { createdAt: now })
4148

4249
assert.equal(token.createdAt, now, 'token.createdAt is correct')
4350
})
4451

45-
it('Token constructor does not set createdAt if it is negative', () => {
46-
var notNow = -Date.now()
47-
var token = new Token({}, { createdAt: notNow })
52+
it('Token.createNewToken defaults createdAt to the current time', () => {
53+
var now = Date.now()
54+
return Token.createNewToken(Token, {}).then(token => {
55+
assert.ok(token.createdAt >= now && token.createdAt <= Date.now(), 'token.createdAt seems correct')
56+
})
57+
})
58+
59+
it('Token.createNewToken accepts an override for createdAt', () => {
60+
var now = Date.now() - 1
61+
return Token.createNewToken(Token, { createdAt: now }).then(token => {
62+
assert.equal(token.createdAt, now, 'token.createdAt is correct')
63+
})
64+
})
4865

49-
assert.ok(token.createdAt > 0, 'token.createdAt seems correct')
66+
it('Token.createNewToken ignores a negative value for createdAt', () => {
67+
var now = Date.now()
68+
var notNow = -now
69+
return Token.createNewToken(Token, { createdAt: notNow }).then(token => {
70+
assert.ok(token.createdAt >= now && token.createdAt <= Date.now(), 'token.createdAt seems correct')
71+
})
5072
})
5173

52-
it('Token constructor does not set createdAt if it is in the future', () => {
74+
it('Token.createNewToken ignores a createdAt timestamp in the future', () => {
75+
var now = Date.now()
5376
var notNow = Date.now() + 1000
54-
var token = new Token({}, { createdAt: notNow })
77+
return Token.createNewToken(Token, { createdAt: notNow }).then(token => {
78+
assert.ok(token.createdAt >= now && token.createdAt <= Date.now(), 'token.createdAt seems correct')
79+
})
80+
})
5581

56-
assert.ok(token.createdAt > 0 && token.createdAt < notNow, 'token.createdAt seems correct')
82+
it('Token.createTokenFromHexData accepts a value for createdAt', () => {
83+
var now = Date.now() - 20
84+
return Token.createTokenFromHexData(Token, 'ABCD', { createdAt: now }).then(token => {
85+
assert.equal(token.createdAt, now, 'token.createdAt is correct')
86+
})
87+
})
88+
89+
it('Token.createTokenFromHexData fails if not given a value for createdAt', () => {
90+
return Token.createTokenFromHexData(Token, 'ABCD', { other: 'data' }).then(token => {
91+
assert.equal(token.createdAt, 0, 'token.createdAt is correct')
92+
})
5793
})
5894
})
5995

@@ -63,14 +99,35 @@ describe('Token', () => {
6399
delete require.cache[require.resolve(modulePath)]
64100
delete require.cache[require.resolve('../../config')]
65101
process.env.NODE_ENV = 'prod'
66-
Token = require(modulePath)(log, crypto, P, hkdf, Bundle, null)
102+
Token = require(modulePath)(log, random, P, hkdf, Bundle, null)
103+
})
104+
105+
it('Token.createNewToken defaults createdAt to the current time', () => {
106+
var now = Date.now()
107+
return Token.createNewToken(Token, {}).then(token => {
108+
assert.ok(token.createdAt >= now && token.createdAt <= Date.now(), 'token.createdAt seems correct')
109+
})
67110
})
68111

69-
it('Token constructor does not set createdAt', () => {
70-
var notNow = Date.now() - 1
71-
var token = new Token({}, { createdAt: notNow })
112+
it('Token.createNewToken does not accept an override for createdAt', () => {
113+
var now = Date.now() - 1
114+
return Token.createNewToken(Token, { createdAt: now }).then(
115+
() => assert.fail('should have thrown'),
116+
(err) => assert.equal(err.message, 'unexpected value for createdAt')
117+
)
118+
})
119+
120+
it('Token.createTokenFromHexData accepts a value for createdAt', () => {
121+
var now = Date.now() - 20
122+
return Token.createTokenFromHexData(Token, 'ABCD', { createdAt: now }).then(token => {
123+
assert.equal(token.createdAt, now, 'token.createdAt is correct')
124+
})
125+
})
72126

73-
assert.ok(token.createdAt > notNow, 'token.createdAt seems correct')
127+
it('Token.createTokenFromHexData defaults to zero if not given a value for createdAt', () => {
128+
return Token.createTokenFromHexData(Token, 'ABCD', { other: 'data' }).then(token => {
129+
assert.equal(token.createdAt, 0, 'token.createdAt is correct')
130+
})
74131
})
75132
})
76133

0 commit comments

Comments
 (0)