feat(devices): Add ability to associate a device record with a refesh token.
This commit is contained in:
Родитель
75aba96bd7
Коммит
1123e32805
|
@ -92,6 +92,7 @@ function createServer(db) {
|
|||
'passCode',
|
||||
'recoveryKeyId',
|
||||
'sessionTokenId',
|
||||
'refreshTokenId',
|
||||
'tokenId',
|
||||
'tokenVerificationId',
|
||||
'uid',
|
||||
|
|
|
@ -109,6 +109,32 @@ function makeMockDevice(tokenId) {
|
|||
return device
|
||||
}
|
||||
|
||||
function makeMockRefreshToken(uid) {
|
||||
const refreshToken = {
|
||||
tokenId: hex32(),
|
||||
uid: uid
|
||||
}
|
||||
return refreshToken
|
||||
}
|
||||
|
||||
function makeMockOAuthDevice(tokenId) {
|
||||
const device = {
|
||||
refreshTokenId: tokenId,
|
||||
name: 'Test OAuth Device',
|
||||
type: 'mobile',
|
||||
createdAt: Date.now(),
|
||||
callbackURL: 'https://push.server',
|
||||
callbackPublicKey: 'foo',
|
||||
callbackAuthKey: 'bar',
|
||||
callbackIsExpired: false,
|
||||
availableCommands: {
|
||||
'https://identity.mozilla.com/cmd/display-uri': 'metadata-bundle'
|
||||
}
|
||||
}
|
||||
device.deviceId = newUuid()
|
||||
return device
|
||||
}
|
||||
|
||||
function makeMockForgotPasswordToken(uid) {
|
||||
const token = {
|
||||
data: hex32(),
|
||||
|
@ -883,55 +909,86 @@ module.exports = function (config, DB) {
|
|||
.then(() => {
|
||||
return db.deviceFromTokenVerificationId(accountData.uid, sessionTokenData.tokenVerificationId)
|
||||
})
|
||||
.then((deviceInfo) => {
|
||||
assert.deepEqual(deviceInfo.id, deviceData.deviceId, 'We found our device id back')
|
||||
assert.equal(deviceInfo.name, deviceData.name, 'We found our device name back')
|
||||
.then((sessionDeviceInfo) => {
|
||||
assert.deepEqual(sessionDeviceInfo.id, deviceData.deviceId, 'We found our device id back')
|
||||
assert.equal(sessionDeviceInfo.name, deviceData.name, 'We found our device name back')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('db.accountDevices', () => {
|
||||
let deviceInfo, sessionTokenData
|
||||
let sessionDeviceInfo, oauthDeviceInfo, sessionTokenData, refreshTokenData
|
||||
|
||||
// A little helper function for finding a specific device record in a list.
|
||||
function matchById(field, value) {
|
||||
return d => d[field] && d[field].equals(value)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
sessionTokenData = makeMockSessionToken(accountData.uid)
|
||||
deviceInfo = makeMockDevice(sessionTokenData.tokenId)
|
||||
refreshTokenData = makeMockRefreshToken(accountData.uid)
|
||||
|
||||
sessionDeviceInfo = makeMockDevice(sessionTokenData.tokenId)
|
||||
oauthDeviceInfo = makeMockOAuthDevice(refreshTokenData.tokenId)
|
||||
|
||||
return db.createSessionToken(sessionTokenData.tokenId, sessionTokenData)
|
||||
.then(() => db.createDevice(accountData.uid, deviceInfo.deviceId, deviceInfo))
|
||||
.then(() => db.createDevice(accountData.uid, sessionDeviceInfo.deviceId, sessionDeviceInfo))
|
||||
.then((result) => {
|
||||
return assert.deepEqual(result, {}, 'returned empty object')
|
||||
})
|
||||
.then(() => db.createDevice(accountData.uid, oauthDeviceInfo.deviceId, oauthDeviceInfo))
|
||||
.then((result) => {
|
||||
return assert.deepEqual(result, {}, 'returned empty object')
|
||||
})
|
||||
})
|
||||
|
||||
it('should have created device', () => {
|
||||
return db.device(sessionTokenData.uid, deviceInfo.deviceId)
|
||||
it('should have created devices', () => {
|
||||
return P.resolve()
|
||||
.then(() => {
|
||||
return db.device(sessionTokenData.uid, sessionDeviceInfo.deviceId)
|
||||
})
|
||||
.then((d) => {
|
||||
assert.deepEqual(d.uid, sessionTokenData.uid, 'uid')
|
||||
assert.deepEqual(d.id, deviceInfo.deviceId, 'id')
|
||||
assert.equal(d.name, deviceInfo.name, 'name')
|
||||
assert.equal(d.type, deviceInfo.type, 'type')
|
||||
assert.equal(d.createdAt, deviceInfo.createdAt, 'createdAt')
|
||||
assert.equal(d.callbackURL, deviceInfo.callbackURL, 'callbackURL')
|
||||
assert.equal(d.callbackPublicKey, deviceInfo.callbackPublicKey, 'callbackPublicKey')
|
||||
assert.equal(d.callbackAuthKey, deviceInfo.callbackAuthKey, 'callbackAuthKey')
|
||||
assert.equal(d.callbackIsExpired, deviceInfo.callbackIsExpired, 'callbackIsExpired')
|
||||
assert.deepEqual(d.availableCommands, deviceInfo.availableCommands, 'availableCommands')
|
||||
assert.deepEqual(d.id, sessionDeviceInfo.deviceId, 'id')
|
||||
assert.equal(d.name, sessionDeviceInfo.name, 'name')
|
||||
assert.equal(d.type, sessionDeviceInfo.type, 'type')
|
||||
assert.equal(d.createdAt, sessionDeviceInfo.createdAt, 'createdAt')
|
||||
assert.equal(d.callbackURL, sessionDeviceInfo.callbackURL, 'callbackURL')
|
||||
assert.equal(d.callbackPublicKey, sessionDeviceInfo.callbackPublicKey, 'callbackPublicKey')
|
||||
assert.equal(d.callbackAuthKey, sessionDeviceInfo.callbackAuthKey, 'callbackAuthKey')
|
||||
assert.equal(d.callbackIsExpired, sessionDeviceInfo.callbackIsExpired, 'callbackIsExpired')
|
||||
assert.deepEqual(d.availableCommands, sessionDeviceInfo.availableCommands, 'availableCommands')
|
||||
})
|
||||
.then(() => {
|
||||
return db.device(refreshTokenData.uid, oauthDeviceInfo.deviceId)
|
||||
})
|
||||
.then((d) => {
|
||||
assert.deepEqual(d.uid, refreshTokenData.uid, 'uid')
|
||||
assert.deepEqual(d.id, oauthDeviceInfo.deviceId, 'id')
|
||||
assert.equal(d.name, oauthDeviceInfo.name, 'name')
|
||||
assert.equal(d.type, oauthDeviceInfo.type, 'type')
|
||||
assert.equal(d.createdAt, oauthDeviceInfo.createdAt, 'createdAt')
|
||||
assert.equal(d.callbackURL, oauthDeviceInfo.callbackURL, 'callbackURL')
|
||||
assert.equal(d.callbackPublicKey, oauthDeviceInfo.callbackPublicKey, 'callbackPublicKey')
|
||||
assert.equal(d.callbackAuthKey, oauthDeviceInfo.callbackAuthKey, 'callbackAuthKey')
|
||||
assert.equal(d.callbackIsExpired, oauthDeviceInfo.callbackIsExpired, 'callbackIsExpired')
|
||||
assert.deepEqual(d.availableCommands, oauthDeviceInfo.availableCommands, 'availableCommands')
|
||||
})
|
||||
})
|
||||
|
||||
it('should have linked device to session token', () => {
|
||||
it('should have linked one device to session token', () => {
|
||||
return db.sessionToken(sessionTokenData.tokenId)
|
||||
.then((s) => {
|
||||
assert.deepEqual(s.deviceId, deviceInfo.deviceId, 'id')
|
||||
assert.deepEqual(s.deviceId, sessionDeviceInfo.deviceId, 'id')
|
||||
assert.deepEqual(s.uid, sessionTokenData.uid, 'uid')
|
||||
assert.equal(s.deviceName, deviceInfo.name, 'name')
|
||||
assert.equal(s.deviceType, deviceInfo.type, 'type')
|
||||
assert.equal(s.deviceCreatedAt, deviceInfo.createdAt, 'createdAt')
|
||||
assert.equal(s.deviceCallbackURL, deviceInfo.callbackURL, 'callbackURL')
|
||||
assert.equal(s.deviceCallbackPublicKey, deviceInfo.callbackPublicKey, 'callbackPublicKey')
|
||||
assert.equal(s.deviceCallbackAuthKey, deviceInfo.callbackAuthKey, 'callbackAuthKey')
|
||||
assert.equal(s.deviceCallbackIsExpired, deviceInfo.callbackIsExpired, 'callbackIsExpired')
|
||||
assert.deepEqual(s.deviceAvailableCommands, deviceInfo.availableCommands, 'availableCommands')
|
||||
assert.equal(s.deviceName, sessionDeviceInfo.name, 'name')
|
||||
assert.equal(s.deviceType, sessionDeviceInfo.type, 'type')
|
||||
assert.equal(s.deviceCreatedAt, sessionDeviceInfo.createdAt, 'createdAt')
|
||||
assert.equal(s.deviceCallbackURL, sessionDeviceInfo.callbackURL, 'callbackURL')
|
||||
assert.equal(s.deviceCallbackPublicKey, sessionDeviceInfo.callbackPublicKey, 'callbackPublicKey')
|
||||
assert.equal(s.deviceCallbackAuthKey, sessionDeviceInfo.callbackAuthKey, 'callbackAuthKey')
|
||||
assert.equal(s.deviceCallbackIsExpired, sessionDeviceInfo.callbackIsExpired, 'callbackIsExpired')
|
||||
assert.deepEqual(s.deviceAvailableCommands, sessionDeviceInfo.availableCommands, 'availableCommands')
|
||||
assert.equal(!! s.mustVerify, !! sessionTokenData.mustVerify, 'mustVerify is correct')
|
||||
assert.deepEqual(s.tokenVerificationId, sessionTokenData.tokenVerificationId, 'tokenVerificationId is correct')
|
||||
})
|
||||
|
@ -940,46 +997,93 @@ module.exports = function (config, DB) {
|
|||
it('should get all devices', () => {
|
||||
return db.accountDevices(accountData.uid)
|
||||
.then((devices) => {
|
||||
assert.equal(devices.length, 1, 'devices length 1')
|
||||
const device = devices[0]
|
||||
assert.equal(devices.length, 2, 'devices length 2')
|
||||
|
||||
let device = devices.find(matchById('sessionTokenId', sessionTokenData.tokenId))
|
||||
assert.deepEqual(device.sessionTokenId, sessionTokenData.tokenId, 'sessionTokenId')
|
||||
assert.equal(device.name, deviceInfo.name, 'name')
|
||||
assert.deepEqual(device.id, deviceInfo.deviceId, 'id')
|
||||
assert.equal(device.createdAt, deviceInfo.createdAt, 'createdAt')
|
||||
assert.equal(device.type, deviceInfo.type, 'type')
|
||||
assert.equal(device.callbackURL, deviceInfo.callbackURL, 'callbackURL')
|
||||
assert.equal(device.callbackPublicKey, deviceInfo.callbackPublicKey, 'callbackPublicKey')
|
||||
assert.equal(device.callbackAuthKey, deviceInfo.callbackAuthKey, 'callbackAuthKey')
|
||||
assert.equal(device.callbackIsExpired, deviceInfo.callbackIsExpired, 'callbackIsExpired')
|
||||
assert.deepEqual(device.availableCommands, deviceInfo.availableCommands, 'availableCommands')
|
||||
assert.deepEqual(device.refreshTokenId, null, 'refreshTokenId')
|
||||
assert.equal(device.name, sessionDeviceInfo.name, 'name')
|
||||
assert.deepEqual(device.id, sessionDeviceInfo.deviceId, 'id')
|
||||
assert.equal(device.createdAt, sessionDeviceInfo.createdAt, 'createdAt')
|
||||
assert.equal(device.type, sessionDeviceInfo.type, 'type')
|
||||
assert.equal(device.callbackURL, sessionDeviceInfo.callbackURL, 'callbackURL')
|
||||
assert.equal(device.callbackPublicKey, sessionDeviceInfo.callbackPublicKey, 'callbackPublicKey')
|
||||
assert.equal(device.callbackAuthKey, sessionDeviceInfo.callbackAuthKey, 'callbackAuthKey')
|
||||
assert.equal(device.callbackIsExpired, sessionDeviceInfo.callbackIsExpired, 'callbackIsExpired')
|
||||
assert.deepEqual(device.availableCommands, sessionDeviceInfo.availableCommands, 'availableCommands')
|
||||
assert(device.lastAccessTime > 0, 'has a lastAccessTime')
|
||||
|
||||
device = devices.find(matchById('refreshTokenId', refreshTokenData.tokenId))
|
||||
assert.deepEqual(device.sessionTokenId, null, 'sessionTokenId')
|
||||
assert.deepEqual(device.refreshTokenId, refreshTokenData.tokenId, 'refreshTokenId')
|
||||
assert.equal(device.name, oauthDeviceInfo.name, 'name')
|
||||
assert.deepEqual(device.id, oauthDeviceInfo.deviceId, 'id')
|
||||
assert.equal(device.createdAt, oauthDeviceInfo.createdAt, 'createdAt')
|
||||
assert.equal(device.type, oauthDeviceInfo.type, 'type')
|
||||
assert.equal(device.callbackURL, oauthDeviceInfo.callbackURL, 'callbackURL')
|
||||
assert.equal(device.callbackPublicKey, oauthDeviceInfo.callbackPublicKey, 'callbackPublicKey')
|
||||
assert.equal(device.callbackAuthKey, oauthDeviceInfo.callbackAuthKey, 'callbackAuthKey')
|
||||
assert.equal(device.callbackIsExpired, oauthDeviceInfo.callbackIsExpired, 'callbackIsExpired')
|
||||
assert.deepEqual(device.availableCommands, oauthDeviceInfo.availableCommands, 'availableCommands')
|
||||
assert.equal(device.lastAccessTime, null, 'does not have lastAccessTime')
|
||||
})
|
||||
})
|
||||
|
||||
it('should update device', () => {
|
||||
deviceInfo.name = 'New New Device'
|
||||
deviceInfo.type = 'desktop'
|
||||
deviceInfo.callbackURL = ''
|
||||
deviceInfo.callbackPublicKey = ''
|
||||
deviceInfo.callbackAuthKey = ''
|
||||
deviceInfo.callbackIsExpired = true
|
||||
deviceInfo.availableCommands = {}
|
||||
it('should update device by sessionTokenId', () => {
|
||||
sessionDeviceInfo.name = 'New New Device'
|
||||
sessionDeviceInfo.type = 'desktop'
|
||||
sessionDeviceInfo.callbackURL = ''
|
||||
sessionDeviceInfo.callbackPublicKey = ''
|
||||
sessionDeviceInfo.callbackAuthKey = ''
|
||||
sessionDeviceInfo.callbackIsExpired = true
|
||||
sessionDeviceInfo.availableCommands = {}
|
||||
|
||||
const newSessionTokenData = makeMockSessionToken(accountData.uid)
|
||||
deviceInfo.sessionTokenId = newSessionTokenData.tokenId
|
||||
sessionDeviceInfo.sessionTokenId = newSessionTokenData.tokenId
|
||||
|
||||
return db.createSessionToken(newSessionTokenData.tokenId, newSessionTokenData)
|
||||
.then(() => {
|
||||
return db.updateDevice(accountData.uid, deviceInfo.deviceId, deviceInfo)
|
||||
return db.updateDevice(accountData.uid, sessionDeviceInfo.deviceId, sessionDeviceInfo)
|
||||
})
|
||||
.then((result) => {
|
||||
assert.deepEqual(result, {}, 'returned empty object')
|
||||
return db.accountDevices(accountData.uid)
|
||||
})
|
||||
.then((devices) => {
|
||||
assert.equal(devices.length, 1, 'devices length 1')
|
||||
const device = devices[0]
|
||||
assert.deepEqual(device.sessionTokenId, newSessionTokenData.tokenId, 'sessionTokenId updated')
|
||||
assert.equal(devices.length, 2, 'devices length 2')
|
||||
const device = devices.find(matchById('sessionTokenId', newSessionTokenData.tokenId))
|
||||
assert.ok(device, 'device found under new token id')
|
||||
assert.equal(device.name, 'New New Device', 'name updated')
|
||||
assert.equal(device.type, 'desktop', 'type unchanged')
|
||||
assert.equal(device.callbackURL, '', 'callbackURL unchanged')
|
||||
assert.equal(device.callbackPublicKey, '', 'callbackPublicKey unchanged')
|
||||
assert.equal(device.callbackAuthKey, '', 'callbackAuthKey unchanged')
|
||||
assert.equal(device.callbackIsExpired, true, 'callbackIsExpired unchanged')
|
||||
assert.deepEqual(device.availableCommands, {}, 'availableCommands updated')
|
||||
})
|
||||
})
|
||||
|
||||
it('should update device by refreshTokenId', () => {
|
||||
oauthDeviceInfo.name = 'New New Device'
|
||||
oauthDeviceInfo.type = 'desktop'
|
||||
oauthDeviceInfo.callbackURL = ''
|
||||
oauthDeviceInfo.callbackPublicKey = ''
|
||||
oauthDeviceInfo.callbackAuthKey = ''
|
||||
oauthDeviceInfo.callbackIsExpired = true
|
||||
oauthDeviceInfo.availableCommands = {}
|
||||
|
||||
const newRefreshTokenData = makeMockRefreshToken(accountData.uid)
|
||||
oauthDeviceInfo.refreshTokenId = newRefreshTokenData.tokenId
|
||||
|
||||
return db.updateDevice(accountData.uid, oauthDeviceInfo.deviceId, oauthDeviceInfo)
|
||||
.then((result) => {
|
||||
assert.deepEqual(result, {}, 'returned empty object')
|
||||
return db.accountDevices(accountData.uid)
|
||||
})
|
||||
.then((devices) => {
|
||||
assert.equal(devices.length, 2, 'devices length 2')
|
||||
const device = devices.find(matchById('refreshTokenId', newRefreshTokenData.tokenId))
|
||||
assert.ok(device, 'device found under new token id')
|
||||
assert.equal(device.name, 'New New Device', 'name updated')
|
||||
assert.equal(device.type, 'desktop', 'type unchanged')
|
||||
assert.equal(device.callbackURL, '', 'callbackURL unchanged')
|
||||
|
@ -992,18 +1096,20 @@ module.exports = function (config, DB) {
|
|||
|
||||
it('should fail to return zombie session', () => {
|
||||
// zombie devices don't have an associated session
|
||||
deviceInfo.sessionTokenId = hex16()
|
||||
return db.updateDevice(accountData.uid, deviceInfo.deviceId, deviceInfo)
|
||||
sessionDeviceInfo.sessionTokenId = hex16()
|
||||
return db.updateDevice(accountData.uid, sessionDeviceInfo.deviceId, sessionDeviceInfo)
|
||||
.then((result) => {
|
||||
assert.deepEqual(result, {}, 'returned empty object')
|
||||
return db.accountDevices(accountData.uid)
|
||||
})
|
||||
.then((devices) => {
|
||||
assert.equal(devices.length, 0, 'devices length 0')
|
||||
assert.equal(devices.length, 1, 'devices length 1')
|
||||
assert.equal(devices[0].sessionTokenId, null, 'sessionTokenId')
|
||||
assert.deepEqual(devices[0].refreshTokenId, refreshTokenData.tokenId, 'refreshTokenId')
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail add multiple device to session', () => {
|
||||
it('should fail to add multiple devices to session', () => {
|
||||
const anotherDevice = makeMockDevice(sessionTokenData.tokenId)
|
||||
return db.createDevice(accountData.uid, anotherDevice.deviceId, anotherDevice)
|
||||
.then(assert.fail, (err) => {
|
||||
|
@ -1012,8 +1118,32 @@ module.exports = function (config, DB) {
|
|||
})
|
||||
})
|
||||
|
||||
it('should fail to add multiple devices to refreshToken', () => {
|
||||
const anotherDevice = makeMockOAuthDevice(refreshTokenData.tokenId)
|
||||
return db.createDevice(accountData.uid, anotherDevice.deviceId, anotherDevice)
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.code, 409, 'err.code')
|
||||
assert.equal(err.errno, 101, 'err.errno')
|
||||
})
|
||||
})
|
||||
|
||||
it('can associate a device record with both sessionToken and refreshToken', () => {
|
||||
const anotherRefreshToken = makeMockRefreshToken(accountData.uid)
|
||||
sessionDeviceInfo.refreshTokenId = anotherRefreshToken.tokenId
|
||||
return db.updateDevice(accountData.uid, sessionDeviceInfo.deviceId, sessionDeviceInfo)
|
||||
.then(() => {
|
||||
return db.accountDevices(accountData.uid)
|
||||
})
|
||||
.then((devices) => {
|
||||
assert.equal(devices.length, 2, 'devices length 2')
|
||||
const comboDeviceInfo = devices.find(matchById('sessionTokenId', sessionTokenData.tokenId))
|
||||
assert.ok(comboDeviceInfo, 'found device record')
|
||||
assert.deepEqual(comboDeviceInfo.refreshTokenId, anotherRefreshToken.tokenId)
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail to update non-existent device', () => {
|
||||
return db.updateDevice(accountData.uid, hex16(), deviceInfo)
|
||||
return db.updateDevice(accountData.uid, hex16(), sessionDeviceInfo)
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.code, 404, 'err.code')
|
||||
assert.equal(err.errno, 116, 'err.errno')
|
||||
|
@ -1021,11 +1151,11 @@ module.exports = function (config, DB) {
|
|||
})
|
||||
|
||||
it('availableCommands are not cleared if not specified', () => {
|
||||
const newDevice = Object.assign({}, deviceInfo)
|
||||
const newDevice = Object.assign({}, sessionDeviceInfo)
|
||||
delete newDevice.availableCommands
|
||||
return db.updateDevice(accountData.uid, deviceInfo.deviceId, newDevice)
|
||||
return db.updateDevice(accountData.uid, sessionDeviceInfo.deviceId, newDevice)
|
||||
.then(() => {
|
||||
return db.device(accountData.uid, deviceInfo.deviceId)
|
||||
return db.device(accountData.uid, sessionDeviceInfo.deviceId)
|
||||
})
|
||||
.then(device => assert.deepEqual(device.availableCommands, {
|
||||
'https://identity.mozilla.com/cmd/display-uri': 'metadata-bundle'
|
||||
|
@ -1033,15 +1163,15 @@ module.exports = function (config, DB) {
|
|||
})
|
||||
|
||||
it('availableCommands are overwritten on update', () => {
|
||||
const newDevice = Object.assign({}, deviceInfo, {
|
||||
const newDevice = Object.assign({}, sessionDeviceInfo, {
|
||||
availableCommands: {
|
||||
foo: 'bar',
|
||||
second: 'command'
|
||||
}
|
||||
})
|
||||
return db.updateDevice(accountData.uid, deviceInfo.deviceId, newDevice)
|
||||
return db.updateDevice(accountData.uid, sessionDeviceInfo.deviceId, newDevice)
|
||||
.then(() => {
|
||||
return db.device(accountData.uid, deviceInfo.deviceId)
|
||||
return db.device(accountData.uid, sessionDeviceInfo.deviceId)
|
||||
})
|
||||
.then(device => assert.deepEqual(device.availableCommands, {
|
||||
foo: 'bar',
|
||||
|
@ -1050,14 +1180,14 @@ module.exports = function (config, DB) {
|
|||
})
|
||||
|
||||
it('availableCommands can update metadata on an existing command', () => {
|
||||
const newDevice = Object.assign({}, deviceInfo, {
|
||||
const newDevice = Object.assign({}, sessionDeviceInfo, {
|
||||
availableCommands: {
|
||||
'https://identity.mozilla.com/cmd/display-uri': 'new-metadata'
|
||||
}
|
||||
})
|
||||
return db.updateDevice(accountData.uid, deviceInfo.deviceId, newDevice)
|
||||
return db.updateDevice(accountData.uid, sessionDeviceInfo.deviceId, newDevice)
|
||||
.then(() => {
|
||||
return db.device(accountData.uid, deviceInfo.deviceId)
|
||||
return db.device(accountData.uid, sessionDeviceInfo.deviceId)
|
||||
})
|
||||
.then(device => assert.deepEqual(device.availableCommands, {
|
||||
'https://identity.mozilla.com/cmd/display-uri': 'new-metadata'
|
||||
|
@ -1074,42 +1204,46 @@ module.exports = function (config, DB) {
|
|||
|
||||
it('should correctly handle multiple devices with different availableCommands maps', () => {
|
||||
const sessionToken2 = makeMockSessionToken(accountData.uid)
|
||||
const deviceInfo2 = Object.assign(makeMockDevice(sessionToken2.tokenId), {
|
||||
const sessionDeviceInfo2 = Object.assign(makeMockDevice(sessionToken2.tokenId), {
|
||||
availableCommands: {
|
||||
'https://identity.mozilla.com/cmd/display-uri': 'device-two-metadata',
|
||||
'extra-command': 'extra-data'
|
||||
}
|
||||
})
|
||||
const sessionToken3 = makeMockSessionToken(accountData.uid)
|
||||
const deviceInfo3 = Object.assign(makeMockDevice(sessionToken3.tokenId), {
|
||||
const sessionDeviceInfo3 = Object.assign(makeMockDevice(sessionToken3.tokenId), {
|
||||
availableCommands: {}
|
||||
})
|
||||
|
||||
return db.createSessionToken(sessionToken2.tokenId, sessionToken2)
|
||||
.then(() => db.createDevice(accountData.uid, deviceInfo2.deviceId, deviceInfo2))
|
||||
.then(() => db.createDevice(accountData.uid, sessionDeviceInfo2.deviceId, sessionDeviceInfo2))
|
||||
.then(() => db.createSessionToken(sessionToken3.tokenId, sessionToken3))
|
||||
.then(() => db.createDevice(accountData.uid, deviceInfo3.deviceId, deviceInfo3))
|
||||
.then(() => db.createDevice(accountData.uid, sessionDeviceInfo3.deviceId, sessionDeviceInfo3))
|
||||
.then(() => db.accountDevices(accountData.uid))
|
||||
.then(devices => {
|
||||
assert.equal(devices.length, 3, 'devices length 3')
|
||||
assert.equal(devices.length, 4, 'devices length 4')
|
||||
|
||||
const device1 = devices.find(d => d.sessionTokenId.equals(sessionTokenData.tokenId))
|
||||
const device1 = devices.find(matchById('sessionTokenId', sessionTokenData.tokenId))
|
||||
assert.ok(device1, 'found first device')
|
||||
assert.deepEqual(device1.availableCommands, deviceInfo.availableCommands, 'device1 availableCommands')
|
||||
assert.deepEqual(device1.availableCommands, sessionDeviceInfo.availableCommands, 'device1 availableCommands')
|
||||
|
||||
const device2 = devices.find(d => d.sessionTokenId.equals(sessionToken2.tokenId))
|
||||
const device2 = devices.find(matchById('sessionTokenId', sessionToken2.tokenId))
|
||||
assert.ok(device2, 'found second device')
|
||||
assert.deepEqual(device2.availableCommands, deviceInfo2.availableCommands, 'device2 availableCommands')
|
||||
assert.deepEqual(device2.availableCommands, sessionDeviceInfo2.availableCommands, 'device2 availableCommands')
|
||||
|
||||
const device3 = devices.find(d => d.sessionTokenId.equals(sessionToken3.tokenId))
|
||||
const device3 = devices.find(matchById('sessionTokenId', sessionToken3.tokenId))
|
||||
assert.ok(device3, 'found third device')
|
||||
assert.deepEqual(device3.availableCommands, deviceInfo3.availableCommands, 'device3 availableCommands')
|
||||
assert.deepEqual(device3.availableCommands, sessionDeviceInfo3.availableCommands, 'device3 availableCommands')
|
||||
|
||||
const device4 = devices.find(matchById('refreshTokenId', refreshTokenData.tokenId))
|
||||
assert.ok(device4, 'found fourth device')
|
||||
assert.deepEqual(device4.availableCommands, oauthDeviceInfo.availableCommands, 'device4 availableCommands')
|
||||
})
|
||||
})
|
||||
|
||||
it('should correctly handle multiple sessions with different availableCommands maps', () => {
|
||||
const sessionToken2 = makeMockSessionToken(accountData.uid)
|
||||
const deviceInfo2 = Object.assign(makeMockDevice(sessionToken2.tokenId), {
|
||||
const sessionDeviceInfo2 = Object.assign(makeMockDevice(sessionToken2.tokenId), {
|
||||
availableCommands: {
|
||||
'https://identity.mozilla.com/cmd/display-uri': 'device-two-metadata',
|
||||
'extra-command': 'extra-data'
|
||||
|
@ -1118,7 +1252,7 @@ module.exports = function (config, DB) {
|
|||
const sessionToken3 = makeMockSessionToken(accountData.uid)
|
||||
|
||||
return db.createSessionToken(sessionToken2.tokenId, sessionToken2)
|
||||
.then(() => db.createDevice(accountData.uid, deviceInfo2.deviceId, deviceInfo2))
|
||||
.then(() => db.createDevice(accountData.uid, sessionDeviceInfo2.deviceId, sessionDeviceInfo2))
|
||||
.then(() => db.createSessionToken(sessionToken3.tokenId, sessionToken3))
|
||||
.then(() => db.sessions(accountData.uid))
|
||||
.then(sessions => {
|
||||
|
@ -1126,11 +1260,11 @@ module.exports = function (config, DB) {
|
|||
|
||||
const session1 = sessions.find(s => s.tokenId.equals(sessionTokenData.tokenId))
|
||||
assert.ok(session1, 'found first session')
|
||||
assert.deepEqual(session1.deviceAvailableCommands, deviceInfo.availableCommands, 'session1 availableCommands')
|
||||
assert.deepEqual(session1.deviceAvailableCommands, sessionDeviceInfo.availableCommands, 'session1 availableCommands')
|
||||
|
||||
const session2 = sessions.find(s => s.tokenId.equals(sessionToken2.tokenId))
|
||||
assert.ok(session2, 'found second session')
|
||||
assert.deepEqual(session2.deviceAvailableCommands, deviceInfo2.availableCommands, 'session2 availableCommands')
|
||||
assert.deepEqual(session2.deviceAvailableCommands, sessionDeviceInfo2.availableCommands, 'session2 availableCommands')
|
||||
|
||||
const session3 = sessions.find(s => s.tokenId.equals(sessionToken3.tokenId))
|
||||
assert.ok(session3, 'found third session')
|
||||
|
@ -1140,26 +1274,42 @@ module.exports = function (config, DB) {
|
|||
})
|
||||
|
||||
it('should delete session when device is deleted', () => {
|
||||
return db.deleteDevice(accountData.uid, deviceInfo.deviceId)
|
||||
return db.deleteDevice(accountData.uid, sessionDeviceInfo.deviceId)
|
||||
.then(result => {
|
||||
assert.deepEqual(result, {sessionTokenId: sessionTokenData.tokenId})
|
||||
assert.deepEqual(result, {sessionTokenId: sessionTokenData.tokenId, refreshTokenId: null})
|
||||
|
||||
// Fetch all of the devices for the account
|
||||
return db.accountDevices(accountData.uid)
|
||||
})
|
||||
.then((devices) => assert.equal(devices.length, 0, 'devices length 0'))
|
||||
.then((devices) => assert.equal(devices.length, 1, 'devices length 1'))
|
||||
.then(() => db.sessionToken(sessionTokenData.tokenId))
|
||||
.then(assert.fail, (err) => {
|
||||
assert.equal(err.code, 404, 'err.code')
|
||||
assert.equal(err.errno, 116, 'err.errno')
|
||||
})
|
||||
})
|
||||
|
||||
it('should return refreshTokenId when device is deleted, so that calling code can delete it', () => {
|
||||
return db.deleteDevice(accountData.uid, oauthDeviceInfo.deviceId)
|
||||
.then(result => {
|
||||
assert.deepEqual(result, { sessionTokenId: null, refreshTokenId: refreshTokenData.tokenId })
|
||||
|
||||
// Fetch all of the devices for the account
|
||||
return db.accountDevices(accountData.uid)
|
||||
})
|
||||
.then((devices) => assert.equal(devices.length, 1, 'devices length 1'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('db.resetAccount', () => {
|
||||
let passwordForgotTokenData, sessionTokenData, deviceInfo
|
||||
let passwordForgotTokenData, sessionTokenData, sessionDeviceInfo
|
||||
beforeEach(() => {
|
||||
sessionTokenData = makeMockSessionToken(accountData.uid, true)
|
||||
passwordForgotTokenData = makeMockForgotPasswordToken(accountData.uid)
|
||||
deviceInfo = makeMockDevice(sessionTokenData.tokenId)
|
||||
sessionDeviceInfo = makeMockDevice(sessionTokenData.tokenId)
|
||||
|
||||
return db.createSessionToken(sessionTokenData.tokenId, sessionTokenData)
|
||||
.then(() => db.createDevice(accountData.uid, deviceInfo.deviceId, deviceInfo))
|
||||
.then(() => db.createDevice(accountData.uid, sessionDeviceInfo.deviceId, sessionDeviceInfo))
|
||||
.then(() => db.createPasswordForgotToken(passwordForgotTokenData.tokenId, passwordForgotTokenData))
|
||||
})
|
||||
|
||||
|
|
|
@ -580,8 +580,8 @@ module.exports = function(cfg, makeServer) {
|
|||
it(
|
||||
'device handling',
|
||||
() => {
|
||||
var user = fake.newUserDataHex()
|
||||
var zombieUser = fake.newUserDataHex()
|
||||
const user = fake.newUserDataHex()
|
||||
const zombieUser = fake.newUserDataHex()
|
||||
return client.getThen('/account/' + user.accountId + '/devices')
|
||||
.then(function(r) {
|
||||
respOk(r)
|
||||
|
@ -606,10 +606,11 @@ module.exports = function(cfg, makeServer) {
|
|||
respOk(r)
|
||||
var devices = r.obj
|
||||
assert.equal(devices.length, 1, 'devices contains one item')
|
||||
assert.equal(Object.keys(devices[0]).length, 18, 'device has eighteen properties')
|
||||
assert.equal(Object.keys(devices[0]).length, 19, 'device has nineteen properties')
|
||||
assert.equal(devices[0].uid, user.accountId, 'uid is correct')
|
||||
assert.equal(devices[0].id, user.deviceId, 'id is correct')
|
||||
assert.equal(devices[0].sessionTokenId, user.sessionTokenId, 'sessionTokenId is correct')
|
||||
assert.equal(devices[0].refreshTokenId, null, 'refreshTokenId is correct')
|
||||
assert.equal(devices[0].createdAt, user.device.createdAt, 'createdAt is correct')
|
||||
assert.equal(devices[0].name, user.device.name, 'name is correct')
|
||||
assert.equal(devices[0].type, user.device.type, 'type is correct')
|
||||
|
@ -735,11 +736,75 @@ module.exports = function(cfg, makeServer) {
|
|||
assert.equal(devices.length, 1, 'devices contains one item again')
|
||||
assert.equal(devices[0].name, '4a6f686e', 'name was not automagically bufferized')
|
||||
|
||||
return client.putThen('/account/' + user.accountId + '/device/' + user.oauthDeviceId, user.oauthDevice)
|
||||
})
|
||||
.then(function (r) {
|
||||
return client.getThen('/account/' + user.accountId + '/devices')
|
||||
})
|
||||
.then(function (r) {
|
||||
respOk(r)
|
||||
var devices = r.obj
|
||||
assert.equal(devices.length, 2, 'devices now contains two items')
|
||||
const sessionDevice = devices.find(d => d.sessionTokenId)
|
||||
const oauthDevice = devices.find(d => d.refreshTokenId)
|
||||
|
||||
assert.equal(sessionDevice.uid, user.accountId, 'uid is correct')
|
||||
assert.equal(sessionDevice.sessionTokenId, user.sessionTokenId, 'sessionTokenId is correct')
|
||||
assert.equal(sessionDevice.refreshTokenId, null, 'refreshTokenId is correct')
|
||||
|
||||
assert.equal(Object.keys(oauthDevice).length, 19, 'device has nineteen properties')
|
||||
assert.equal(oauthDevice.uid, user.accountId, 'uid is correct')
|
||||
assert.equal(oauthDevice.id, user.oauthDeviceId, 'id is correct')
|
||||
assert.equal(oauthDevice.sessionTokenId, null, 'sessionTokenId is correct')
|
||||
assert.equal(oauthDevice.refreshTokenId, user.refreshTokenId, 'refreshTokenId is correct')
|
||||
assert.equal(oauthDevice.createdAt, user.oauthDevice.createdAt, 'createdAt is correct')
|
||||
assert.equal(oauthDevice.name, user.oauthDevice.name, 'name is correct')
|
||||
assert.equal(oauthDevice.type, user.oauthDevice.type, 'type is correct')
|
||||
assert.equal(oauthDevice.callbackURL, user.oauthDevice.callbackURL, 'callbackURL is correct')
|
||||
assert.equal(oauthDevice.callbackPublicKey, user.oauthDevice.callbackPublicKey, 'callbackPublicKey is correct')
|
||||
assert.equal(oauthDevice.callbackAuthKey, user.oauthDevice.callbackAuthKey, 'callbackAuthKey is correct')
|
||||
assert.equal(oauthDevice.callbackIsExpired, user.oauthDevice.callbackIsExpired, 'callbackIsExpired is correct')
|
||||
assert.deepEqual(oauthDevice.availableCommands, {}, 'availableCommands is correct')
|
||||
assert.equal(oauthDevice.uaBrowser, null, 'uaBrowser is correct')
|
||||
assert.equal(oauthDevice.uaBrowserVersion, null, 'uaBrowserVersion is correct')
|
||||
assert.equal(oauthDevice.uaOS, null, 'uaOS is correct')
|
||||
assert.equal(oauthDevice.uaOSVersion, null, 'uaOSVersion is correct')
|
||||
assert.equal(oauthDevice.uaDeviceType, null, 'uaDeviceType is correct')
|
||||
assert.equal(oauthDevice.uaFormFactor, null, 'uaFormFactor is correct')
|
||||
assert.equal(oauthDevice.lastAccessTime, null, 'lastAccessTime is correct')
|
||||
|
||||
return client.postThen('/account/' + user.accountId + '/device/' + oauthDevice.id + '/update', {
|
||||
name: 'a new device name'
|
||||
})
|
||||
})
|
||||
.then(function (r) {
|
||||
return client.getThen('/account/' + user.accountId + '/devices')
|
||||
})
|
||||
.then(function (r) {
|
||||
respOk(r)
|
||||
var devices = r.obj
|
||||
assert.equal(devices.length, 2, 'devices still contains two items')
|
||||
const sessionDevice = devices.find(d => d.sessionTokenId)
|
||||
const oauthDevice = devices.find(d => d.refreshTokenId)
|
||||
|
||||
assert.equal(sessionDevice.sessionTokenId, user.sessionTokenId, 'sessionTokenId is correct')
|
||||
assert.equal(sessionDevice.refreshTokenId, null, 'refreshTokenId is correct')
|
||||
|
||||
assert.equal(oauthDevice.sessionTokenId, null, 'sessionTokenId is correct')
|
||||
assert.equal(oauthDevice.refreshTokenId, oauthDevice.refreshTokenId, 'refreshTokenId is correct')
|
||||
assert.equal(oauthDevice.name, 'a new device name', 'name is correct')
|
||||
|
||||
return client.delThen('/account/' + user.accountId + '/device/' + user.oauthDeviceId)
|
||||
})
|
||||
.then(function (r) {
|
||||
respOk(r)
|
||||
assert.deepEqual(r.obj, { sessionTokenId: null, refreshTokenId: user.refreshTokenId })
|
||||
|
||||
return client.delThen('/account/' + user.accountId + '/device/' + user.deviceId)
|
||||
})
|
||||
.then(function(r) {
|
||||
respOk(r)
|
||||
assert.deepEqual(r.obj, { sessionTokenId: user.sessionTokenId })
|
||||
assert.deepEqual(r.obj, { sessionTokenId: user.sessionTokenId, refreshTokenId: null })
|
||||
return client.getThen('/account/' + user.accountId + '/devices')
|
||||
})
|
||||
.then(function(r) {
|
||||
|
|
|
@ -69,14 +69,30 @@ module.exports.newUserDataHex = function() {
|
|||
data.device = {
|
||||
uid: data.accountId,
|
||||
sessionTokenId: data.sessionTokenId,
|
||||
refreshTokenId: null,
|
||||
createdAt: Date.now(),
|
||||
name: 'fake device name',
|
||||
type: 'fake device type',
|
||||
callbackURL: 'fake callback URL',
|
||||
callbackPublicKey: base64_65(),
|
||||
callbackAuthKey: base64_16(),
|
||||
callbackIsExpired: false,
|
||||
capabilities: ['messages']
|
||||
callbackIsExpired: false
|
||||
}
|
||||
|
||||
// oauth device
|
||||
data.refreshTokenId = hex32()
|
||||
data.oauthDeviceId = hex16()
|
||||
data.oauthDevice = {
|
||||
uid: data.accountId,
|
||||
sessionTokenId: null,
|
||||
refreshTokenId: data.refreshTokenId,
|
||||
createdAt: Date.now(),
|
||||
name: 'fake oauth device name',
|
||||
type: 'oauth device',
|
||||
callbackURL: 'fake oauth callback URL',
|
||||
callbackPublicKey: base64_65(),
|
||||
callbackAuthKey: base64_16(),
|
||||
callbackIsExpired: false
|
||||
}
|
||||
|
||||
// keyFetchToken
|
||||
|
|
|
@ -28,9 +28,11 @@ var signinCodes = {}
|
|||
const totpTokens = {}
|
||||
const recoveryCodes = {}
|
||||
const recoveryKeys = {}
|
||||
const devicesByRefreshTokenId = {}
|
||||
|
||||
var DEVICE_FIELDS = [
|
||||
'sessionTokenId',
|
||||
'refreshTokenId',
|
||||
'name',
|
||||
'type',
|
||||
'createdAt',
|
||||
|
@ -231,9 +233,9 @@ module.exports = function (log, error) {
|
|||
}
|
||||
|
||||
function updateDeviceRecord (device, deviceInfo, deviceKey) {
|
||||
var session
|
||||
var sessionKey = (deviceInfo.sessionTokenId || '').toString('hex')
|
||||
|
||||
// Prevent multiple device records from linking to the same sessionToken.
|
||||
let session
|
||||
const sessionKey = (deviceInfo.sessionTokenId || '').toString('hex')
|
||||
if (sessionKey) {
|
||||
session = sessionTokens[sessionKey]
|
||||
if (session && session.deviceKey && session.deviceKey !== deviceKey) {
|
||||
|
@ -241,6 +243,16 @@ module.exports = function (log, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Prevent multiple device records from linking to the same refreshToken.
|
||||
const refreshTokenId = (deviceInfo.refreshTokenId || '').toString('hex')
|
||||
if (refreshTokenId) {
|
||||
const existingDevice = devicesByRefreshTokenId[refreshTokenId]
|
||||
if (existingDevice && existingDevice !== deviceKey) {
|
||||
throw error.duplicate()
|
||||
}
|
||||
devicesByRefreshTokenId[refreshTokenId] = deviceKey
|
||||
}
|
||||
|
||||
DEVICE_FIELDS.forEach(function (key) {
|
||||
var field = deviceInfo[key]
|
||||
if (field === undefined || field === null) {
|
||||
|
@ -257,6 +269,10 @@ module.exports = function (log, error) {
|
|||
device[key] = session[key]
|
||||
})
|
||||
session.deviceKey = deviceKey
|
||||
} else {
|
||||
SESSION_DEVICE_FIELDS.forEach(function (key) {
|
||||
device[key] = null
|
||||
})
|
||||
}
|
||||
|
||||
return device
|
||||
|
@ -271,6 +287,7 @@ module.exports = function (log, error) {
|
|||
throw error.notFound()
|
||||
}
|
||||
var device = account.devices[deviceKey]
|
||||
// If changing sessionTokenId, the old token loses its device record.
|
||||
if (device.sessionTokenId) {
|
||||
if (deviceInfo.sessionTokenId) {
|
||||
var oldSessionKey = device.sessionTokenId.toString('hex')
|
||||
|
@ -284,6 +301,17 @@ module.exports = function (log, error) {
|
|||
deviceInfo.sessionTokenId = device.sessionTokenId
|
||||
}
|
||||
}
|
||||
// If changing refreshTokenId, the old token loses its device record.
|
||||
if (device.refreshTokenId) {
|
||||
if (deviceInfo.refreshTokenId) {
|
||||
const oldRefreshTokenId = device.refreshTokenId.toString('hex')
|
||||
if (oldRefreshTokenId !== deviceInfo.refreshTokenId.toString('hex')) {
|
||||
delete devicesByRefreshTokenId[oldRefreshTokenId]
|
||||
}
|
||||
} else {
|
||||
deviceInfo.refreshTokenId = device.refreshTokenId
|
||||
}
|
||||
}
|
||||
account.devices[deviceKey] = updateDeviceRecord(device, deviceInfo, deviceKey)
|
||||
return {}
|
||||
}
|
||||
|
@ -418,7 +446,7 @@ module.exports = function (log, error) {
|
|||
|
||||
Memory.prototype.deleteDevice = function (uid, deviceId) {
|
||||
const deviceKey = deviceId.toString('hex')
|
||||
let sessionTokenId
|
||||
let sessionTokenId, refreshTokenId
|
||||
|
||||
return getAccountByUid(uid)
|
||||
.then(account => {
|
||||
|
@ -428,12 +456,15 @@ module.exports = function (log, error) {
|
|||
|
||||
const device = account.devices[deviceKey]
|
||||
sessionTokenId = device.sessionTokenId
|
||||
refreshTokenId = device.refreshTokenId
|
||||
|
||||
delete account.devices[deviceKey]
|
||||
|
||||
return Memory.prototype.deleteSessionToken(sessionTokenId)
|
||||
if (sessionTokenId) {
|
||||
return Memory.prototype.deleteSessionToken(sessionTokenId)
|
||||
}
|
||||
})
|
||||
.then(() => ({ sessionTokenId }))
|
||||
.then(() => ({ sessionTokenId, refreshTokenId }))
|
||||
}
|
||||
|
||||
// READ
|
||||
|
@ -477,6 +508,10 @@ module.exports = function (log, error) {
|
|||
})
|
||||
return device
|
||||
}
|
||||
if (device.refreshTokenId) {
|
||||
device.sessionTokenId = null
|
||||
return device
|
||||
}
|
||||
}
|
||||
)
|
||||
.filter(
|
||||
|
@ -521,7 +556,7 @@ module.exports = function (log, error) {
|
|||
function (devices) {
|
||||
var device = devices.filter(
|
||||
function (d) {
|
||||
return d.sessionTokenId.toString('hex') === sessionTokenId
|
||||
return d.sessionTokenId && d.sessionTokenId.toString('hex') === sessionTokenId
|
||||
}
|
||||
)[0]
|
||||
if (! device) {
|
||||
|
@ -604,7 +639,7 @@ module.exports = function (log, error) {
|
|||
})
|
||||
|
||||
const device = devices.filter((d) => {
|
||||
return d.sessionTokenId.toString('hex') === id.toString('hex')
|
||||
return d.sessionTokenId && d.sessionTokenId.toString('hex') === id.toString('hex')
|
||||
})[0]
|
||||
|
||||
if (device) {
|
||||
|
@ -667,7 +702,7 @@ module.exports = function (log, error) {
|
|||
var sessionToken = sessionTokens[key]
|
||||
|
||||
var deviceInfo = devices.find(function (device) {
|
||||
return device.sessionTokenId.toString('hex') === key
|
||||
return device.sessionTokenId && device.sessionTokenId.toString('hex') === key
|
||||
})
|
||||
|
||||
if (! deviceInfo) {
|
||||
|
|
|
@ -322,7 +322,7 @@ module.exports = function (log, error) {
|
|||
}, [])
|
||||
}
|
||||
|
||||
const CREATE_DEVICE = 'CALL createDevice_4(?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
const CREATE_DEVICE = 'CALL createDevice_5(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
|
||||
MySql.prototype.createDevice = function (uid, deviceId, deviceInfo) {
|
||||
const statements = [{
|
||||
|
@ -331,6 +331,7 @@ module.exports = function (log, error) {
|
|||
uid,
|
||||
deviceId,
|
||||
deviceInfo.sessionTokenId,
|
||||
deviceInfo.refreshTokenId,
|
||||
deviceInfo.name, // inNameUtf8
|
||||
deviceInfo.type,
|
||||
deviceInfo.createdAt,
|
||||
|
@ -345,7 +346,7 @@ module.exports = function (log, error) {
|
|||
return this.writeMultiple(statements)
|
||||
}
|
||||
|
||||
const UPDATE_DEVICE = 'CALL updateDevice_5(?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
const UPDATE_DEVICE = 'CALL updateDevice_6(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
|
||||
MySql.prototype.updateDevice = function (uid, deviceId, deviceInfo) {
|
||||
const statements = [{
|
||||
|
@ -354,6 +355,7 @@ module.exports = function (log, error) {
|
|||
uid,
|
||||
deviceId,
|
||||
deviceInfo.sessionTokenId,
|
||||
deviceInfo.refreshTokenId,
|
||||
deviceInfo.name, // inNameUtf8
|
||||
deviceInfo.type,
|
||||
deviceInfo.callbackURL,
|
||||
|
@ -407,12 +409,12 @@ module.exports = function (log, error) {
|
|||
}
|
||||
|
||||
// Select : devices d, sessionTokens s, deviceAvailableCommands dc, deviceCommandIdentifiers ci
|
||||
// Fields : d.uid, d.id, d.sessionTokenId, d.name, d.type, d.createdAt, d.callbackURL,
|
||||
// Fields : d.uid, d.id, d.sessionTokenId, d.refreshTokenId, d.name, d.type, d.createdAt, d.callbackURL,
|
||||
// d.callbackPublicKey, d.callbackAuthKey, d.callbackIsExpired,
|
||||
// s.uaBrowser, s.uaBrowserVersion, s.uaOS, s.uaOSVersion, s.uaDeviceType,
|
||||
// s.uaFormFactor, s.lastAccessTime, { ci.commandName : dc.commandData }
|
||||
// Where : d.uid = $1
|
||||
var ACCOUNT_DEVICES = 'CALL accountDevices_15(?)'
|
||||
var ACCOUNT_DEVICES = 'CALL accountDevices_16(?)'
|
||||
|
||||
MySql.prototype.accountDevices = function (uid) {
|
||||
return this.readAllResults(ACCOUNT_DEVICES, [uid])
|
||||
|
@ -420,12 +422,12 @@ module.exports = function (log, error) {
|
|||
}
|
||||
|
||||
// Select : devices d, sessionTokens s, deviceAvailableCommands dc, deviceCommandIdentifiers ci
|
||||
// Fields : d.uid, d.id, d.sessionTokenId, d.name, d.type, d.createdAt, d.callbackURL,
|
||||
// Fields : d.uid, d.id, d.sessionTokenId, d.refreshTokenId, d.name, d.type, d.createdAt, d.callbackURL,
|
||||
// d.callbackPublicKey, d.callbackAuthKey, d.callbackIsExpired,
|
||||
// s.uaBrowser, s.uaBrowserVersion, s.uaOS, s.uaOSVersion, s.uaDeviceType,
|
||||
// s.uaFormFactor, s.lastAccessTime, { ci.commandName : dc.commandData }
|
||||
// Where : d.uid = $1 AND d.id = $2
|
||||
var DEVICE = 'CALL device_2(?, ?)'
|
||||
var DEVICE = 'CALL device_3(?, ?)'
|
||||
|
||||
MySql.prototype.device = function (uid, id) {
|
||||
return this.readAllResults(DEVICE, [uid, id])
|
||||
|
@ -683,10 +685,10 @@ module.exports = function (log, error) {
|
|||
}
|
||||
|
||||
// Select : devices
|
||||
// Fields : sessionTokenId
|
||||
// Fields : sessionTokenId, refreshTokenId
|
||||
// Delete : devices, sessionTokens, unverifiedTokens
|
||||
// Where : uid = $1, deviceId = $2
|
||||
var DELETE_DEVICE = 'CALL deleteDevice_3(?, ?)'
|
||||
var DELETE_DEVICE = 'CALL deleteDevice_4(?, ?)'
|
||||
|
||||
MySql.prototype.deleteDevice = function (uid, deviceId) {
|
||||
return this.write(DELETE_DEVICE, [ uid, deviceId ], results => {
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
|
||||
// The expected patch level of the database. Update if you add a new
|
||||
// patch in the ./schema/ directory.
|
||||
module.exports.level = 96
|
||||
module.exports.level = 97
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
SET NAMES utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
CALL assertPatchLevel('96');
|
||||
|
||||
-- This migration adds an optional `refreshTokenId` column to the `devices` table,
|
||||
-- allowing OAuth clients to participate in the sync device ecosystem.
|
||||
|
||||
-- First, in order for the migration to apply cleanly via migration tooling,
|
||||
-- we need to drop `FOREIGN KEY` constraints on the `devices` table. There's
|
||||
-- one instance of this in the old `deviceCapabilities` table which is no
|
||||
-- longer used and should be safe to drop entirely.
|
||||
|
||||
DROP TABLE IF EXISTS deviceCapabilities;
|
||||
|
||||
-- And there are two foreign keys on the `deviceCommands` table, so we might
|
||||
-- as well remove both while we're here. The first links to the `deviceCommandIdentifiers`
|
||||
-- table, from which we never delete and so which will not be affected by removal.
|
||||
-- The second links to `devices` with `ON DELETE CASCADE`, and a previous migration
|
||||
-- has added explicit deletions as a replacement. So they're both safe to drop.
|
||||
|
||||
ALTER TABLE deviceCommands
|
||||
DROP FOREIGN KEY deviceCommands_ibfk_1,
|
||||
DROP FOREIGN KEY deviceCommands_ibfk_2,
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- With that, we can actually add the new columns.
|
||||
|
||||
ALTER TABLE devices
|
||||
ADD COLUMN refreshTokenId BINARY(32) DEFAULT NULL,
|
||||
ADD CONSTRAINT UQ_devices_refreshTokenId UNIQUE (uid, refreshTokenId),
|
||||
ALGORITHM = INPLACE, LOCK = NONE;
|
||||
|
||||
-- And the stored procedures to use them.
|
||||
|
||||
CREATE PROCEDURE `createDevice_5` (
|
||||
IN `inUid` BINARY(16),
|
||||
IN `inId` BINARY(16),
|
||||
IN `inSessionTokenId` BINARY(32),
|
||||
IN `inRefreshTokenId` BINARY(32),
|
||||
IN `inNameUtf8` VARCHAR(255),
|
||||
IN `inType` VARCHAR(16),
|
||||
IN `inCreatedAt` BIGINT UNSIGNED,
|
||||
IN `inCallbackURL` VARCHAR(255),
|
||||
IN `inCallbackPublicKey` CHAR(88),
|
||||
IN `inCallbackAuthKey` CHAR(24)
|
||||
)
|
||||
BEGIN
|
||||
INSERT INTO devices(
|
||||
uid,
|
||||
id,
|
||||
sessionTokenId,
|
||||
refreshTokenId,
|
||||
nameUtf8,
|
||||
type,
|
||||
createdAt,
|
||||
callbackURL,
|
||||
callbackPublicKey,
|
||||
callbackAuthKey
|
||||
)
|
||||
VALUES (
|
||||
inUid,
|
||||
inId,
|
||||
inSessionTokenId,
|
||||
inRefreshTokenId,
|
||||
inNameUtf8,
|
||||
inType,
|
||||
inCreatedAt,
|
||||
inCallbackURL,
|
||||
inCallbackPublicKey,
|
||||
inCallbackAuthKey
|
||||
);
|
||||
END;
|
||||
|
||||
|
||||
CREATE PROCEDURE `updateDevice_6` (
|
||||
IN `inUid` BINARY(16),
|
||||
IN `inId` BINARY(16),
|
||||
IN `inSessionTokenId` BINARY(32),
|
||||
IN `inRefreshTokenId` BINARY(32),
|
||||
IN `inNameUtf8` VARCHAR(255),
|
||||
IN `inType` VARCHAR(16),
|
||||
IN `inCallbackURL` VARCHAR(255),
|
||||
IN `inCallbackPublicKey` CHAR(88),
|
||||
IN `inCallbackAuthKey` CHAR(24),
|
||||
IN `inCallbackIsExpired` BOOLEAN
|
||||
)
|
||||
BEGIN
|
||||
UPDATE devices
|
||||
SET
|
||||
sessionTokenId = COALESCE(inSessionTokenId, sessionTokenId),
|
||||
refreshTokenId = COALESCE(inRefreshTokenId, refreshTokenId),
|
||||
nameUtf8 = COALESCE(inNameUtf8, nameUtf8),
|
||||
type = COALESCE(inType, type),
|
||||
callbackURL = COALESCE(inCallbackURL, callbackURL),
|
||||
callbackPublicKey = COALESCE(inCallbackPublicKey, callbackPublicKey),
|
||||
callbackAuthKey = COALESCE(inCallbackAuthKey, callbackAuthKey),
|
||||
callbackIsExpired = COALESCE(inCallbackIsExpired, callbackIsExpired)
|
||||
WHERE uid = inUid AND id = inId;
|
||||
END;
|
||||
|
||||
|
||||
-- Return the sessionTokenId and refreshTokenId from deleteDevice
|
||||
-- so the auth server can remove them from other data stores.
|
||||
CREATE PROCEDURE `deleteDevice_4` (
|
||||
IN `uidArg` BINARY(16),
|
||||
IN `idArg` BINARY(16)
|
||||
)
|
||||
BEGIN
|
||||
SELECT devices.sessionTokenId, devices.refreshTokenId FROM devices
|
||||
WHERE devices.uid = uidArg AND devices.id = idArg;
|
||||
|
||||
DELETE devices, deviceCommands, sessionTokens, unverifiedTokens
|
||||
FROM devices
|
||||
LEFT JOIN deviceCommands
|
||||
ON (deviceCommands.uid = devices.uid AND deviceCommands.deviceId = devices.id)
|
||||
LEFT JOIN sessionTokens
|
||||
ON devices.sessionTokenId = sessionTokens.tokenId
|
||||
LEFT JOIN unverifiedTokens
|
||||
ON sessionTokens.tokenId = unverifiedTokens.tokenId
|
||||
WHERE devices.uid = uidArg
|
||||
AND devices.id = idArg;
|
||||
END;
|
||||
|
||||
|
||||
CREATE PROCEDURE `accountDevices_16` (
|
||||
IN `uidArg` BINARY(16)
|
||||
)
|
||||
BEGIN
|
||||
SELECT
|
||||
d.uid,
|
||||
d.id,
|
||||
s.tokenId AS sessionTokenId, -- Ensure we only return valid sessionToken ids
|
||||
d.refreshTokenId,
|
||||
d.nameUtf8 AS name,
|
||||
d.type,
|
||||
d.createdAt,
|
||||
d.callbackURL,
|
||||
d.callbackPublicKey,
|
||||
d.callbackAuthKey,
|
||||
d.callbackIsExpired,
|
||||
s.uaBrowser,
|
||||
s.uaBrowserVersion,
|
||||
s.uaOS,
|
||||
s.uaOSVersion,
|
||||
s.uaDeviceType,
|
||||
s.uaFormFactor,
|
||||
s.lastAccessTime,
|
||||
ci.commandName,
|
||||
dc.commandData
|
||||
FROM devices AS d
|
||||
-- Left join, because it might not have a sessionToken.
|
||||
LEFT JOIN sessionTokens AS s
|
||||
ON d.sessionTokenId = s.tokenId
|
||||
LEFT JOIN (
|
||||
deviceCommands AS dc FORCE INDEX (PRIMARY)
|
||||
INNER JOIN deviceCommandIdentifiers AS ci FORCE INDEX (PRIMARY)
|
||||
ON ci.commandId = dc.commandId
|
||||
) ON (dc.uid = d.uid AND dc.deviceId = d.id)
|
||||
WHERE d.uid = uidArg
|
||||
-- We don't want to return 'zombie' device records where the sessionToken
|
||||
-- no longer exists in the sessionTokens table.
|
||||
AND (s.tokenId IS NOT NULL OR d.refreshTokenId IS NOT NULL)
|
||||
-- For easy flattening, ensure rows are ordered by device id.
|
||||
ORDER BY 1, 2;
|
||||
END;
|
||||
|
||||
|
||||
CREATE PROCEDURE `device_3` (
|
||||
IN `uidArg` BINARY(16),
|
||||
IN `idArg` BINARY(16)
|
||||
)
|
||||
BEGIN
|
||||
SELECT
|
||||
d.uid,
|
||||
d.id,
|
||||
s.tokenId AS sessionTokenId, -- Ensure we only return valid sessionToken ids
|
||||
d.refreshTokenId,
|
||||
d.nameUtf8 AS name,
|
||||
d.type,
|
||||
d.createdAt,
|
||||
d.callbackURL,
|
||||
d.callbackPublicKey,
|
||||
d.callbackAuthKey,
|
||||
d.callbackIsExpired,
|
||||
s.uaBrowser,
|
||||
s.uaBrowserVersion,
|
||||
s.uaOS,
|
||||
s.uaOSVersion,
|
||||
s.uaDeviceType,
|
||||
s.uaFormFactor,
|
||||
s.lastAccessTime,
|
||||
ci.commandName,
|
||||
dc.commandData
|
||||
FROM devices AS d
|
||||
-- Left join, because it might not have a sessionToken.
|
||||
LEFT JOIN sessionTokens AS s
|
||||
ON d.sessionTokenId = s.tokenId
|
||||
LEFT JOIN (
|
||||
deviceCommands AS dc FORCE INDEX (PRIMARY)
|
||||
INNER JOIN deviceCommandIdentifiers AS ci FORCE INDEX (PRIMARY)
|
||||
ON ci.commandId = dc.commandId
|
||||
) ON (dc.uid = d.uid AND dc.deviceId = d.id)
|
||||
WHERE d.uid = uidArg
|
||||
AND d.id = idArg
|
||||
-- We don't want to return 'zombie' device records where the sessionToken
|
||||
-- no longer exists in the sessionTokens table.
|
||||
AND (s.tokenId IS NOT NULL OR d.refreshTokenId IS NOT NULL);
|
||||
END;
|
||||
|
||||
UPDATE dbMetadata SET value = '97' WHERE name = 'schema-patch-level';
|
|
@ -0,0 +1,28 @@
|
|||
-- SET NAMES utf8mb4 COLLATE utf8mb4_bin;
|
||||
|
||||
-- This migration adds an optional `refreshTokenId` column to the `devices` table,
|
||||
-- allowing OAuth clients to participate in the sync device ecosystem.
|
||||
|
||||
-- DROP PROCEDURE `createDevice_5`;
|
||||
-- DROP PROCEDURE `updateDevice_6`;
|
||||
-- DROP PROCEDURE `deleteDevice_4`;
|
||||
-- DROP PROCEDURE `accountDevices_16`;
|
||||
-- DROP PROCEDURE `device_3`;
|
||||
|
||||
-- ALTER TABLE devices
|
||||
-- DROP COLUMN refreshTokenId,
|
||||
-- DROP INDEX UQ_devices_refreshTokenId;
|
||||
|
||||
-- ALTER TABLE deviceCommands
|
||||
-- ADD CONSTRAINT `deviceCommands_ibfk_1` FOREIGN KEY (`commandId`) REFERENCES `deviceCommandIdentifiers` (`commandId`) ON DELETE CASCADE,
|
||||
-- ADD CONSTRAINT `deviceCommands_ibfk_2` FOREIGN KEY (`uid`, `deviceId`) REFERENCES `devices` (`uid`, `id`) ON DELETE CASCADE;
|
||||
|
||||
-- CREATE TABLE `deviceCapabilities` (
|
||||
-- `uid` binary(16) NOT NULL,
|
||||
-- `deviceId` binary(16) NOT NULL,
|
||||
-- `capability` tinyint(3) unsigned NOT NULL,
|
||||
-- PRIMARY KEY (`uid`,`deviceId`,`capability`),
|
||||
-- CONSTRAINT `devicecapabilities_ibfk_1` FOREIGN KEY (`uid`, `deviceId`) REFERENCES `devices` (`uid`, `id`) ON DELETE CASCADE
|
||||
-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||
|
||||
-- UPDATE dbMetadata SET value = '96' WHERE name = 'schema-patch-level';
|
Загрузка…
Ссылка в новой задаче