diff --git a/fxa-auth-db-server/docs/DB_API.md b/fxa-auth-db-server/docs/DB_API.md index a8cfa868..2e78b523 100644 --- a/fxa-auth-db-server/docs/DB_API.md +++ b/fxa-auth-db-server/docs/DB_API.md @@ -15,6 +15,7 @@ There are a number of methods that a DB storage backend should implement: * .accountExists(emailBuffer) * Session Tokens * .createSessionToken(tokenId, sessionToken) + * .updateSessionToken(tokenId, sessionToken) * .sessionToken(id) * .deleteSessionToken(tokenId) * Key Fetch Tokens @@ -249,7 +250,7 @@ Parameters. Each token takes the following fields for it's create method respectively: -* sessionToken : data, uid, createdAt +* sessionToken : data, uid, createdAt, userAgent * keyFetchToken : authKey, uid, keyBundle, createdAt * passwordChangeToken : data, uid, createdAt * passwordForgotToken : data, uid, passCode, createdAt, triesxb @@ -288,7 +289,7 @@ Returns: Each token returns different fields. These fields are represented as `t.*` for a field from the token and `a.*` for a field from the corresponding account. -* sessionToken : t.tokenData, t.uid, t.createdAt, a.emailVerified, a.email, a.emailCode, a.verifierSetAt +* sessionToken : t.tokenData, t.uid, t.createdAt, t.userAgent, t.lastAccessTime, a.emailVerified, a.email, a.emailCode, a.verifierSetAt * keyFetchToken : t.authKey, t.uid, t.keyBundle, t.createdAt, a.emailVerified, a.verifierSetAt * passwordChangeToken : t.tokenData, t.uid, t.createdAt, a.verifierSetAt * passwordForgotToken : t.tokenData, t.uid, t.createdAt, t.passCode, t.tries, a.email, a.verifierSetAt @@ -302,6 +303,24 @@ from the token and `a.*` for a field from the corresponding account. Will delete the token of the correct type designated by the given `tokenId`. +## .updateSessionToken(tokenId, token) ## + +An extra function for `sessionTokens`. Just updates the `userAgent` and `lastAccessTime` fields of the token. + +Parameters. + +* tokenId : (Buffer32) the unique id for this token +* token : (Object) - + * userAgent : (string) + * lastAccessTime : (number) + +Returns: + +* resolves with: + * an object `{}` (whether a row was updated or not, ie. even if `tokenId` does not exist.) +* rejects with: + * any error from the underlying storage system (wrapped in `error.wrap()`) + ## .updatePasswordForgotToken(tokenId, token) ## An extra function for `passwordForgotTokens`. Just updates the `tries` field of the token. @@ -317,7 +336,7 @@ Returns: * resolves with: * an object `{}` (whether a row was updated or not, ie. even if `tokenId` does not exist.) * rejects with: - * any error from the underlying storage system (wrapped in `error.wrap()` + * any error from the underlying storage system (wrapped in `error.wrap()`) ## .forgotPasswordVerified(tokenId, accountResetToken) ## diff --git a/fxa-auth-db-server/docs/Server_API.md b/fxa-auth-db-server/docs/Server_API.md index 841dafab..60bb293e 100644 --- a/fxa-auth-db-server/docs/Server_API.md +++ b/fxa-auth-db-server/docs/Server_API.md @@ -61,6 +61,7 @@ The following datatypes are used throughout this document: * sessionToken : `GET /sessionToken/:id` * deleteSessionToken : `DEL /sessionToken/:id` * createSessionToken : `PUT /sessionToken/:id` + * updateSessionToken : `POST /sessionToken/:id/update` * Account Reset Tokens: * accountResetToken : `GET /accountResetToken/:id` * deleteAccountResetToken : `DEL /accountResetToken/:id` @@ -482,6 +483,54 @@ Content-Length: 2 * Content-Type : 'application/json' * Body : {"code":"InternalError","message":"......"} +## updateSessionToken : `POST /sessionToken//update` + +This updates the user agent and last-access time for a particular token. + +### Example + +``` +curl \ + -v \ + -X POST \ + -H "Content-Type: application/json" \ + -d '{ + "userAgent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:41.0) Gecko/20100101 Firefox/41.0", + "lastAccessTime": 1437992394186 + }' \ + http://localhost:8000/sessionToken/522c251a1623e1f1db1f4fe68b9594d26772d6e77e04cb68e110c58600f97a77/update +``` + +### Request + +* Method : POST +* Path : `/sessionToken//update` + * tokenId : hex256 +* Params: + * userAgent : string + * lastAccessTime : epoch + +### Response + +``` +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 2 + +{} +``` + +* Status Code : 200 OK + * Content-Type : 'application/json' + * Body : {} +* Status Code : 404 Not Found + * Conditions: if this session `tokenId` doesn't exist + * Content-Type : 'application/json' + * Body : `{"message":"Not Found"}` +* Status Code : 500 Internal Server Error + * Conditions: if something goes wrong on the server + * Content-Type : 'application/json' + * Body : `{"code":"InternalError","message":"......"}` ## accountDevices : `GET /account//devices` @@ -1199,7 +1248,7 @@ curl \ ### Request -* Method : PUT +* Method : POST * Path : `/passwordForgotToken//update` * tokenId : hex256 * Params: diff --git a/fxa-auth-db-server/index.js b/fxa-auth-db-server/index.js index cdc42d05..f0b65c43 100644 --- a/fxa-auth-db-server/index.js +++ b/fxa-auth-db-server/index.js @@ -83,6 +83,7 @@ function createServer(db) { api.get('/sessionToken/:id', reply(db.sessionToken)) api.del('/sessionToken/:id', reply(db.deleteSessionToken)) api.put('/sessionToken/:id', reply(db.createSessionToken)) + api.post('/sessionToken/:id/update', reply(db.updateSessionToken)) api.get('/keyFetchToken/:id', reply(db.keyFetchToken)) api.del('/keyFetchToken/:id', reply(db.deleteKeyFetchToken)) diff --git a/fxa-auth-db-server/test/backend/remote.js b/fxa-auth-db-server/test/backend/remote.js index 8fe3eed8..8203a0f5 100644 --- a/fxa-auth-db-server/test/backend/remote.js +++ b/fxa-auth-db-server/test/backend/remote.js @@ -180,7 +180,7 @@ module.exports = function(cfg, server) { test( 'session token handling', function (t) { - t.plan(14) + t.plan(23) var user = fake.newUserDataHex() client.putThen('/account/' + user.accountId, user.account) .then(function() { @@ -202,13 +202,35 @@ module.exports = function(cfg, server) { // tokenId is not returned from db.sessionToken() t.deepEqual(token.tokenData, user.sessionToken.data, 'token data matches') t.deepEqual(token.uid, user.accountId, 'token belongs to this account') - t.ok(token.createdAt, 'Got a createdAt') + t.equal(token.createdAt, user.sessionToken.createdAt, 'createdAt matches') + t.equal(token.userAgent, user.sessionToken.userAgent, 'userAgent matches') + t.equal(token.lastAccessTime, token.createdAt, 'lastAccessTime was set') t.equal(!!token.emailVerified, user.account.emailVerified, 'emailVerified same as account emailVerified') t.equal(token.email, user.account.email, 'token.email same as account email') t.deepEqual(token.emailCode, user.account.emailCode, 'token emailCode same as account emailCode') t.ok(token.verifierSetAt, 'verifierSetAt is set to a truthy value') - // now delete it + // update the session token + return client.postThen('/sessionToken/' + user.sessionTokenId + '/update', { + userAgent: 'foo', + lastAccessTime: 42 + }) + }) + .then(function(r) { + respOk(t, r) + return client.getThen('/sessionToken/' + user.sessionTokenId) + }) + .then(function(r) { + var token = r.obj + + // tokenId is not returned from db.sessionToken() + t.deepEqual(token.tokenData, user.sessionToken.data, 'token data matches') + t.deepEqual(token.uid, user.accountId, 'token belongs to this account') + t.equal(token.createdAt, user.sessionToken.createdAt, 'createdAt was not updated') + t.equal(token.userAgent, 'foo', 'userAgent was updated') + t.equal(token.lastAccessTime, 42, 'lastAccessTime was updated') + + // delete the session token return client.delThen('/sessionToken/' + user.sessionTokenId) }) .then(function(r) { diff --git a/fxa-auth-db-server/test/fake.js b/fxa-auth-db-server/test/fake.js index d64c7d34..b0447e1a 100644 --- a/fxa-auth-db-server/test/fake.js +++ b/fxa-auth-db-server/test/fake.js @@ -43,7 +43,8 @@ module.exports.newUserDataHex = function() { data.sessionToken = { data : hex32(), uid : data.accountId, - createdAt: Date.now() + createdAt: Date.now(), + userAgent: 'fake user agent string' } // keyFetchToken @@ -110,7 +111,8 @@ module.exports.newUserDataBuffer = function() { data.sessionToken = { data : buf32(), uid : data.accountId, - createdAt: Date.now() + createdAt: Date.now(), + userAgent: '' } // keyFetchToken diff --git a/lib/db/mysql.js b/lib/db/mysql.js index a2b7b762..c9c431e3 100644 --- a/lib/db/mysql.js +++ b/lib/db/mysql.js @@ -179,8 +179,9 @@ module.exports = function (log, error) { } // Insert : sessionTokens - // Values : tokenId = $1, tokenData = $2, uid = $3, createdAt = $4 - var CREATE_SESSION_TOKEN = 'CALL createSessionToken_1(?, ?, ?, ?)' + // Values : tokenId = $1, tokenData = $2, uid = $3, + // createdAt = $4, userAgent = $5, lastAccessTime = $6 + var CREATE_SESSION_TOKEN = 'CALL createSessionToken_2(?, ?, ?, ?, ?, ?)' MySql.prototype.createSessionToken = function (tokenId, sessionToken) { return this.write( @@ -189,7 +190,9 @@ module.exports = function (log, error) { tokenId, sessionToken.data, sessionToken.uid, - sessionToken.createdAt + sessionToken.createdAt, + sessionToken.userAgent, + sessionToken.lastAccessTime ] ) } @@ -301,9 +304,10 @@ module.exports = function (log, error) { } // Select : sessionTokens t, accounts a - // Fields : t.tokenData, t.uid, t.createdAt, a.emailVerified, a.email, a.emailCode, a.verifierSetAt, a.locale + // Fields : t.tokenData, t.uid, t.createdAt, t.userAgent, t.lastAccessTime, + // a.emailVerified, a.email, a.emailCode, a.verifierSetAt, a.locale // Where : t.tokenId = $1 AND t.uid = a.uid - var SESSION_TOKEN = 'CALL sessionToken_1(?)' + var SESSION_TOKEN = 'CALL sessionToken_2(?)' MySql.prototype.sessionToken = function (id) { return this.readFirstResult(SESSION_TOKEN, [id]) @@ -373,6 +377,15 @@ module.exports = function (log, error) { return this.write(UPDATE_PASSWORD_FORGOT_TOKEN, [token.tries, tokenId]) } + // Update : sessionTokens + // Set : userAgent = $1, lastAccessTime = $2 + // Where : tokenId = $3 + var UPDATE_SESSION_TOKEN = 'CALL updateSessionToken_1(?, ?, ?)' + + MySql.prototype.updateSessionToken = function (tokenId, token) { + return this.write(UPDATE_SESSION_TOKEN, [token.userAgent, token.lastAccessTime, tokenId]) + } + // DELETE // Delete : sessionTokens, keyFetchTokens, accountResetTokens, passwordChangeTokens, passwordForgotTokens, accountUnlockCodes, accounts diff --git a/lib/db/patch.js b/lib/db/patch.js index 2d669f8c..1b2aed94 100644 --- a/lib/db/patch.js +++ b/lib/db/patch.js @@ -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 = 14 +module.exports.level = 15 diff --git a/lib/db/schema/patch-014-015.sql b/lib/db/schema/patch-014-015.sql new file mode 100644 index 00000000..fe210fd5 --- /dev/null +++ b/lib/db/schema/patch-014-015.sql @@ -0,0 +1,71 @@ +-- Add userAgent and lastAccessTime fields to sessionTokens table + +ALTER TABLE sessionTokens ADD COLUMN userAgent VARCHAR(255) NOT NULL DEFAULT ''; +ALTER TABLE sessionTokens ADD COLUMN lastAccessTime BIGINT UNSIGNED NOT NULL DEFAULT 0; + +CREATE PROCEDURE `createSessionToken_2` ( + IN tokenId BINARY(32), + IN tokenData BINARY(32), + IN uid BINARY(16), + IN createdAt BIGINT UNSIGNED, + IN userAgent VARCHAR(255), + IN lastAccessTime BIGINT UNSIGNED +) +BEGIN + INSERT INTO sessionTokens( + tokenId, + tokenData, + uid, + createdAt, + userAgent, + lastAccessTime + ) + VALUES( + tokenId, + tokenData, + uid, + createdAt, + userAgent, + lastAccessTime + ); +END; + +CREATE PROCEDURE `sessionToken_2` ( + IN `inTokenId` BINARY(32) +) +BEGIN + SELECT + t.tokenData, + t.uid, + t.createdAt, + t.userAgent, + t.lastAccessTime, + a.emailVerified, + a.email, + a.emailCode, + a.verifierSetAt, + a.locale + FROM + sessionTokens t, + accounts a + WHERE + t.tokenId = inTokenId + AND + t.uid = a.uid + ; +END; + +CREATE PROCEDURE `updateSessionToken_1` ( + IN userAgentArg VARCHAR(255), + IN lastAccessTimeArg BIGINT UNSIGNED, + IN tokenIdArg BINARY(32) +) +BEGIN + UPDATE sessionTokens + SET userAgent = userAgentArg, + lastAccessTime = lastAccessTimeArg + WHERE tokenId = tokenIdArg; +END; + +UPDATE dbMetadata SET value = '15' WHERE name = 'schema-patch-level'; + diff --git a/lib/db/schema/patch-015-014.sql b/lib/db/schema/patch-015-014.sql new file mode 100644 index 00000000..233f8b18 --- /dev/null +++ b/lib/db/schema/patch-015-014.sql @@ -0,0 +1,11 @@ +-- -- drop new stored procedures +-- DROP PROCEDURE `createSessionToken_2`; +-- DROP PROCEDURE `sessionToken_2`; +-- DROP PROCEDURE `updateSessionToken_1`; + +-- -- drop new columns +-- ALTER TABLE sessionTokens DROP COLUMN userAgent; +-- ALTER TABLE sessionTokens DROP COLUMN lastAccessTime; + +-- -- Schema patch-level decrement. +-- UPDATE dbMetadata SET value = '14' WHERE name = 'schema-patch-level';