feat(devices): Add ability to associate a device record with a refesh token.

This commit is contained in:
Ryan Kelly 2019-02-27 16:45:01 +11:00
Родитель 75aba96bd7
Коммит 1123e32805
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: FB70C973A037D258
9 изменённых файлов: 617 добавлений и 110 удалений

Просмотреть файл

@ -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';