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

Commit 1123e32

Browse files
committed
feat(devices): Add ability to associate a device record with a refesh token.
1 parent 75aba96 commit 1123e32

File tree

9 files changed

+619
-112
lines changed

9 files changed

+619
-112
lines changed

db-server/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ function createServer(db) {
9292
'passCode',
9393
'recoveryKeyId',
9494
'sessionTokenId',
95+
'refreshTokenId',
9596
'tokenId',
9697
'tokenVerificationId',
9798
'uid',

db-server/test/backend/db_tests.js

Lines changed: 238 additions & 88 deletions
Large diffs are not rendered by default.

db-server/test/backend/remote.js

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -580,8 +580,8 @@ module.exports = function(cfg, makeServer) {
580580
it(
581581
'device handling',
582582
() => {
583-
var user = fake.newUserDataHex()
584-
var zombieUser = fake.newUserDataHex()
583+
const user = fake.newUserDataHex()
584+
const zombieUser = fake.newUserDataHex()
585585
return client.getThen('/account/' + user.accountId + '/devices')
586586
.then(function(r) {
587587
respOk(r)
@@ -606,10 +606,11 @@ module.exports = function(cfg, makeServer) {
606606
respOk(r)
607607
var devices = r.obj
608608
assert.equal(devices.length, 1, 'devices contains one item')
609-
assert.equal(Object.keys(devices[0]).length, 18, 'device has eighteen properties')
609+
assert.equal(Object.keys(devices[0]).length, 19, 'device has nineteen properties')
610610
assert.equal(devices[0].uid, user.accountId, 'uid is correct')
611611
assert.equal(devices[0].id, user.deviceId, 'id is correct')
612612
assert.equal(devices[0].sessionTokenId, user.sessionTokenId, 'sessionTokenId is correct')
613+
assert.equal(devices[0].refreshTokenId, null, 'refreshTokenId is correct')
613614
assert.equal(devices[0].createdAt, user.device.createdAt, 'createdAt is correct')
614615
assert.equal(devices[0].name, user.device.name, 'name is correct')
615616
assert.equal(devices[0].type, user.device.type, 'type is correct')
@@ -735,11 +736,75 @@ module.exports = function(cfg, makeServer) {
735736
assert.equal(devices.length, 1, 'devices contains one item again')
736737
assert.equal(devices[0].name, '4a6f686e', 'name was not automagically bufferized')
737738

739+
return client.putThen('/account/' + user.accountId + '/device/' + user.oauthDeviceId, user.oauthDevice)
740+
})
741+
.then(function (r) {
742+
return client.getThen('/account/' + user.accountId + '/devices')
743+
})
744+
.then(function (r) {
745+
respOk(r)
746+
var devices = r.obj
747+
assert.equal(devices.length, 2, 'devices now contains two items')
748+
const sessionDevice = devices.find(d => d.sessionTokenId)
749+
const oauthDevice = devices.find(d => d.refreshTokenId)
750+
751+
assert.equal(sessionDevice.uid, user.accountId, 'uid is correct')
752+
assert.equal(sessionDevice.sessionTokenId, user.sessionTokenId, 'sessionTokenId is correct')
753+
assert.equal(sessionDevice.refreshTokenId, null, 'refreshTokenId is correct')
754+
755+
assert.equal(Object.keys(oauthDevice).length, 19, 'device has nineteen properties')
756+
assert.equal(oauthDevice.uid, user.accountId, 'uid is correct')
757+
assert.equal(oauthDevice.id, user.oauthDeviceId, 'id is correct')
758+
assert.equal(oauthDevice.sessionTokenId, null, 'sessionTokenId is correct')
759+
assert.equal(oauthDevice.refreshTokenId, user.refreshTokenId, 'refreshTokenId is correct')
760+
assert.equal(oauthDevice.createdAt, user.oauthDevice.createdAt, 'createdAt is correct')
761+
assert.equal(oauthDevice.name, user.oauthDevice.name, 'name is correct')
762+
assert.equal(oauthDevice.type, user.oauthDevice.type, 'type is correct')
763+
assert.equal(oauthDevice.callbackURL, user.oauthDevice.callbackURL, 'callbackURL is correct')
764+
assert.equal(oauthDevice.callbackPublicKey, user.oauthDevice.callbackPublicKey, 'callbackPublicKey is correct')
765+
assert.equal(oauthDevice.callbackAuthKey, user.oauthDevice.callbackAuthKey, 'callbackAuthKey is correct')
766+
assert.equal(oauthDevice.callbackIsExpired, user.oauthDevice.callbackIsExpired, 'callbackIsExpired is correct')
767+
assert.deepEqual(oauthDevice.availableCommands, {}, 'availableCommands is correct')
768+
assert.equal(oauthDevice.uaBrowser, null, 'uaBrowser is correct')
769+
assert.equal(oauthDevice.uaBrowserVersion, null, 'uaBrowserVersion is correct')
770+
assert.equal(oauthDevice.uaOS, null, 'uaOS is correct')
771+
assert.equal(oauthDevice.uaOSVersion, null, 'uaOSVersion is correct')
772+
assert.equal(oauthDevice.uaDeviceType, null, 'uaDeviceType is correct')
773+
assert.equal(oauthDevice.uaFormFactor, null, 'uaFormFactor is correct')
774+
assert.equal(oauthDevice.lastAccessTime, null, 'lastAccessTime is correct')
775+
776+
return client.postThen('/account/' + user.accountId + '/device/' + oauthDevice.id + '/update', {
777+
name: 'a new device name'
778+
})
779+
})
780+
.then(function (r) {
781+
return client.getThen('/account/' + user.accountId + '/devices')
782+
})
783+
.then(function (r) {
784+
respOk(r)
785+
var devices = r.obj
786+
assert.equal(devices.length, 2, 'devices still contains two items')
787+
const sessionDevice = devices.find(d => d.sessionTokenId)
788+
const oauthDevice = devices.find(d => d.refreshTokenId)
789+
790+
assert.equal(sessionDevice.sessionTokenId, user.sessionTokenId, 'sessionTokenId is correct')
791+
assert.equal(sessionDevice.refreshTokenId, null, 'refreshTokenId is correct')
792+
793+
assert.equal(oauthDevice.sessionTokenId, null, 'sessionTokenId is correct')
794+
assert.equal(oauthDevice.refreshTokenId, oauthDevice.refreshTokenId, 'refreshTokenId is correct')
795+
assert.equal(oauthDevice.name, 'a new device name', 'name is correct')
796+
797+
return client.delThen('/account/' + user.accountId + '/device/' + user.oauthDeviceId)
798+
})
799+
.then(function (r) {
800+
respOk(r)
801+
assert.deepEqual(r.obj, { sessionTokenId: null, refreshTokenId: user.refreshTokenId })
802+
738803
return client.delThen('/account/' + user.accountId + '/device/' + user.deviceId)
739804
})
740805
.then(function(r) {
741806
respOk(r)
742-
assert.deepEqual(r.obj, { sessionTokenId: user.sessionTokenId })
807+
assert.deepEqual(r.obj, { sessionTokenId: user.sessionTokenId, refreshTokenId: null })
743808
return client.getThen('/account/' + user.accountId + '/devices')
744809
})
745810
.then(function(r) {

db-server/test/fake.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,30 @@ module.exports.newUserDataHex = function() {
6969
data.device = {
7070
uid: data.accountId,
7171
sessionTokenId: data.sessionTokenId,
72+
refreshTokenId: null,
7273
createdAt: Date.now(),
7374
name: 'fake device name',
7475
type: 'fake device type',
7576
callbackURL: 'fake callback URL',
7677
callbackPublicKey: base64_65(),
7778
callbackAuthKey: base64_16(),
78-
callbackIsExpired: false,
79-
capabilities: ['messages']
79+
callbackIsExpired: false
80+
}
81+
82+
// oauth device
83+
data.refreshTokenId = hex32()
84+
data.oauthDeviceId = hex16()
85+
data.oauthDevice = {
86+
uid: data.accountId,
87+
sessionTokenId: null,
88+
refreshTokenId: data.refreshTokenId,
89+
createdAt: Date.now(),
90+
name: 'fake oauth device name',
91+
type: 'oauth device',
92+
callbackURL: 'fake oauth callback URL',
93+
callbackPublicKey: base64_65(),
94+
callbackAuthKey: base64_16(),
95+
callbackIsExpired: false
8096
}
8197

8298
// keyFetchToken

lib/db/mem.js

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ var signinCodes = {}
2828
const totpTokens = {}
2929
const recoveryCodes = {}
3030
const recoveryKeys = {}
31+
const devicesByRefreshTokenId = {}
3132

3233
var DEVICE_FIELDS = [
3334
'sessionTokenId',
35+
'refreshTokenId',
3436
'name',
3537
'type',
3638
'createdAt',
@@ -231,16 +233,26 @@ module.exports = function (log, error) {
231233
}
232234

233235
function updateDeviceRecord (device, deviceInfo, deviceKey) {
234-
var session
235-
var sessionKey = (deviceInfo.sessionTokenId || '').toString('hex')
236-
236+
// Prevent multiple device records from linking to the same sessionToken.
237+
let session
238+
const sessionKey = (deviceInfo.sessionTokenId || '').toString('hex')
237239
if (sessionKey) {
238240
session = sessionTokens[sessionKey]
239241
if (session && session.deviceKey && session.deviceKey !== deviceKey) {
240242
throw error.duplicate()
241243
}
242244
}
243245

246+
// Prevent multiple device records from linking to the same refreshToken.
247+
const refreshTokenId = (deviceInfo.refreshTokenId || '').toString('hex')
248+
if (refreshTokenId) {
249+
const existingDevice = devicesByRefreshTokenId[refreshTokenId]
250+
if (existingDevice && existingDevice !== deviceKey) {
251+
throw error.duplicate()
252+
}
253+
devicesByRefreshTokenId[refreshTokenId] = deviceKey
254+
}
255+
244256
DEVICE_FIELDS.forEach(function (key) {
245257
var field = deviceInfo[key]
246258
if (field === undefined || field === null) {
@@ -257,6 +269,10 @@ module.exports = function (log, error) {
257269
device[key] = session[key]
258270
})
259271
session.deviceKey = deviceKey
272+
} else {
273+
SESSION_DEVICE_FIELDS.forEach(function (key) {
274+
device[key] = null
275+
})
260276
}
261277

262278
return device
@@ -271,6 +287,7 @@ module.exports = function (log, error) {
271287
throw error.notFound()
272288
}
273289
var device = account.devices[deviceKey]
290+
// If changing sessionTokenId, the old token loses its device record.
274291
if (device.sessionTokenId) {
275292
if (deviceInfo.sessionTokenId) {
276293
var oldSessionKey = device.sessionTokenId.toString('hex')
@@ -284,6 +301,17 @@ module.exports = function (log, error) {
284301
deviceInfo.sessionTokenId = device.sessionTokenId
285302
}
286303
}
304+
// If changing refreshTokenId, the old token loses its device record.
305+
if (device.refreshTokenId) {
306+
if (deviceInfo.refreshTokenId) {
307+
const oldRefreshTokenId = device.refreshTokenId.toString('hex')
308+
if (oldRefreshTokenId !== deviceInfo.refreshTokenId.toString('hex')) {
309+
delete devicesByRefreshTokenId[oldRefreshTokenId]
310+
}
311+
} else {
312+
deviceInfo.refreshTokenId = device.refreshTokenId
313+
}
314+
}
287315
account.devices[deviceKey] = updateDeviceRecord(device, deviceInfo, deviceKey)
288316
return {}
289317
}
@@ -418,7 +446,7 @@ module.exports = function (log, error) {
418446

419447
Memory.prototype.deleteDevice = function (uid, deviceId) {
420448
const deviceKey = deviceId.toString('hex')
421-
let sessionTokenId
449+
let sessionTokenId, refreshTokenId
422450

423451
return getAccountByUid(uid)
424452
.then(account => {
@@ -428,12 +456,15 @@ module.exports = function (log, error) {
428456

429457
const device = account.devices[deviceKey]
430458
sessionTokenId = device.sessionTokenId
459+
refreshTokenId = device.refreshTokenId
431460

432461
delete account.devices[deviceKey]
433462

434-
return Memory.prototype.deleteSessionToken(sessionTokenId)
463+
if (sessionTokenId) {
464+
return Memory.prototype.deleteSessionToken(sessionTokenId)
465+
}
435466
})
436-
.then(() => ({ sessionTokenId }))
467+
.then(() => ({ sessionTokenId, refreshTokenId }))
437468
}
438469

439470
// READ
@@ -477,6 +508,10 @@ module.exports = function (log, error) {
477508
})
478509
return device
479510
}
511+
if (device.refreshTokenId) {
512+
device.sessionTokenId = null
513+
return device
514+
}
480515
}
481516
)
482517
.filter(
@@ -521,7 +556,7 @@ module.exports = function (log, error) {
521556
function (devices) {
522557
var device = devices.filter(
523558
function (d) {
524-
return d.sessionTokenId.toString('hex') === sessionTokenId
559+
return d.sessionTokenId && d.sessionTokenId.toString('hex') === sessionTokenId
525560
}
526561
)[0]
527562
if (! device) {
@@ -604,7 +639,7 @@ module.exports = function (log, error) {
604639
})
605640

606641
const device = devices.filter((d) => {
607-
return d.sessionTokenId.toString('hex') === id.toString('hex')
642+
return d.sessionTokenId && d.sessionTokenId.toString('hex') === id.toString('hex')
608643
})[0]
609644

610645
if (device) {
@@ -667,7 +702,7 @@ module.exports = function (log, error) {
667702
var sessionToken = sessionTokens[key]
668703

669704
var deviceInfo = devices.find(function (device) {
670-
return device.sessionTokenId.toString('hex') === key
705+
return device.sessionTokenId && device.sessionTokenId.toString('hex') === key
671706
})
672707

673708
if (! deviceInfo) {

lib/db/mysql.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ module.exports = function (log, error) {
322322
}, [])
323323
}
324324

325-
const CREATE_DEVICE = 'CALL createDevice_4(?, ?, ?, ?, ?, ?, ?, ?, ?)'
325+
const CREATE_DEVICE = 'CALL createDevice_5(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
326326

327327
MySql.prototype.createDevice = function (uid, deviceId, deviceInfo) {
328328
const statements = [{
@@ -331,6 +331,7 @@ module.exports = function (log, error) {
331331
uid,
332332
deviceId,
333333
deviceInfo.sessionTokenId,
334+
deviceInfo.refreshTokenId,
334335
deviceInfo.name, // inNameUtf8
335336
deviceInfo.type,
336337
deviceInfo.createdAt,
@@ -345,7 +346,7 @@ module.exports = function (log, error) {
345346
return this.writeMultiple(statements)
346347
}
347348

348-
const UPDATE_DEVICE = 'CALL updateDevice_5(?, ?, ?, ?, ?, ?, ?, ?, ?)'
349+
const UPDATE_DEVICE = 'CALL updateDevice_6(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
349350

350351
MySql.prototype.updateDevice = function (uid, deviceId, deviceInfo) {
351352
const statements = [{
@@ -354,6 +355,7 @@ module.exports = function (log, error) {
354355
uid,
355356
deviceId,
356357
deviceInfo.sessionTokenId,
358+
deviceInfo.refreshTokenId,
357359
deviceInfo.name, // inNameUtf8
358360
deviceInfo.type,
359361
deviceInfo.callbackURL,
@@ -407,25 +409,25 @@ module.exports = function (log, error) {
407409
}
408410

409411
// Select : devices d, sessionTokens s, deviceAvailableCommands dc, deviceCommandIdentifiers ci
410-
// Fields : d.uid, d.id, d.sessionTokenId, d.name, d.type, d.createdAt, d.callbackURL,
412+
// Fields : d.uid, d.id, d.sessionTokenId, d.refreshTokenId, d.name, d.type, d.createdAt, d.callbackURL,
411413
// d.callbackPublicKey, d.callbackAuthKey, d.callbackIsExpired,
412414
// s.uaBrowser, s.uaBrowserVersion, s.uaOS, s.uaOSVersion, s.uaDeviceType,
413415
// s.uaFormFactor, s.lastAccessTime, { ci.commandName : dc.commandData }
414416
// Where : d.uid = $1
415-
var ACCOUNT_DEVICES = 'CALL accountDevices_15(?)'
417+
var ACCOUNT_DEVICES = 'CALL accountDevices_16(?)'
416418

417419
MySql.prototype.accountDevices = function (uid) {
418420
return this.readAllResults(ACCOUNT_DEVICES, [uid])
419421
.then(rows => dbUtil.aggregateNameValuePairs(rows, 'id', 'commandName', 'commandData', 'availableCommands'))
420422
}
421423

422424
// Select : devices d, sessionTokens s, deviceAvailableCommands dc, deviceCommandIdentifiers ci
423-
// Fields : d.uid, d.id, d.sessionTokenId, d.name, d.type, d.createdAt, d.callbackURL,
425+
// Fields : d.uid, d.id, d.sessionTokenId, d.refreshTokenId, d.name, d.type, d.createdAt, d.callbackURL,
424426
// d.callbackPublicKey, d.callbackAuthKey, d.callbackIsExpired,
425427
// s.uaBrowser, s.uaBrowserVersion, s.uaOS, s.uaOSVersion, s.uaDeviceType,
426428
// s.uaFormFactor, s.lastAccessTime, { ci.commandName : dc.commandData }
427429
// Where : d.uid = $1 AND d.id = $2
428-
var DEVICE = 'CALL device_2(?, ?)'
430+
var DEVICE = 'CALL device_3(?, ?)'
429431

430432
MySql.prototype.device = function (uid, id) {
431433
return this.readAllResults(DEVICE, [uid, id])
@@ -683,10 +685,10 @@ module.exports = function (log, error) {
683685
}
684686

685687
// Select : devices
686-
// Fields : sessionTokenId
688+
// Fields : sessionTokenId, refreshTokenId
687689
// Delete : devices, sessionTokens, unverifiedTokens
688690
// Where : uid = $1, deviceId = $2
689-
var DELETE_DEVICE = 'CALL deleteDevice_3(?, ?)'
691+
var DELETE_DEVICE = 'CALL deleteDevice_4(?, ?)'
690692

691693
MySql.prototype.deleteDevice = function (uid, deviceId) {
692694
return this.write(DELETE_DEVICE, [ uid, deviceId ], results => {

lib/db/patch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
// The expected patch level of the database. Update if you add a new
66
// patch in the ./schema/ directory.
7-
module.exports.level = 96
7+
module.exports.level = 97

0 commit comments

Comments
 (0)