From 340eb46fe169ede52dbb3f903864329daea0d324 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 9 Sep 2016 14:48:06 -0400 Subject: [PATCH] Adds endpoint for non-revocable session token upgrade (#2646) --- spec/RevocableSessionsUpgrade.spec.js | 92 +++++++++++++++++++++++++++ src/Auth.js | 20 +++++- src/Routers/SessionsRouter.js | 35 ++++++++++ src/middlewares.js | 12 +++- 4 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 spec/RevocableSessionsUpgrade.spec.js diff --git a/spec/RevocableSessionsUpgrade.spec.js b/spec/RevocableSessionsUpgrade.spec.js new file mode 100644 index 00000000..f3f1a0a4 --- /dev/null +++ b/spec/RevocableSessionsUpgrade.spec.js @@ -0,0 +1,92 @@ +const Config = require('../src/Config'); +const sessionToken = 'legacySessionToken'; +const rp = require('request-promise'); +const Parse = require('parse/node'); + +function createUser() { + const config = new Config(Parse.applicationId); + const user = { + objectId: '1234567890', + username: 'hello', + password: 'pass', + _session_token: sessionToken + } + return config.database.create('_User', user); +} + +describe('revocable sessions', () => { + + beforeEach((done) => { + // Create 1 user with the legacy + createUser().then(done); + }); + + it('should upgrade legacy session token', done => { + let user = Parse.Object.fromJSON({ + className: '_User', + objectId: '1234567890', + sessionToken: sessionToken + }); + user._upgradeToRevocableSession().then((res) => { + expect(res.getSessionToken().indexOf('r:')).toBe(0); + const config = new Config(Parse.applicationId); + // use direct access to the DB to make sure we're not + // getting the session token stripped + return config.database.loadSchema().then(schemaController => { + return schemaController.getOneSchema('_User', true) + }).then((schema) => { + return config.database.adapter.find('_User', schema, {objectId: '1234567890'}, {}) + }).then((results) => { + expect(results.length).toBe(1); + expect(results[0].sessionToken).toBeUndefined(); + }); + }).then(() => { + done(); + }, (err) => { + jfail(err); + done(); + }); + }); + + it('should be able to become with revocable session token', done => { + let user = Parse.Object.fromJSON({ + className: '_User', + objectId: '1234567890', + sessionToken: sessionToken + }); + user._upgradeToRevocableSession().then((res) => { + expect(res.getSessionToken().indexOf('r:')).toBe(0); + return Parse.User.logOut().then(() => { + return Parse.User.become(res.getSessionToken()) + }).then((user) => { + expect(user.id).toEqual('1234567890'); + }); + }).then(() => { + done(); + }, (err) => { + jfail(err); + done(); + }); + }); + + it('should not upgrade bad legacy session token', done => { + rp.post({ + url: Parse.serverURL+'/upgradeToRevocableSession', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Rest-API-Key': 'rest', + 'X-Parse-Session-Token': 'badSessionToken' + }, + json: true + }).then((res) => { + fail('should not be able to upgrade a bad token'); + }, (response) => { + expect(response.statusCode).toBe(400); + expect(response.error).not.toBeUndefined(); + expect(response.error.code).toBe(Parse.Error.INVALID_SESSION_TOKEN); + expect(response.error.error).toEqual('invalid legacy session token'); + }).then(() => { + done(); + }); + }); +}) \ No newline at end of file diff --git a/src/Auth.js b/src/Auth.js index efceb626..c9936dee 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -78,6 +78,23 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } = }); }; +var getAuthForLegacySessionToken = function({config, sessionToken, installationId } = {}) { + var restOptions = { + limit: 1 + }; + var query = new RestQuery(config, master(config), '_User', { sessionToken: sessionToken}, restOptions); + return query.execute().then((response) => { + var results = response.results; + if (results.length !== 1) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid legacy session token'); + } + let obj = results[0]; + obj.className = '_User'; + let userObject = Parse.Object.fromJSON(obj); + return new Auth({config, isMaster: false, installationId, user: userObject}); + }); +} + // Returns a promise that resolves to an array of role names Auth.prototype.getUserRoles = function() { if (this.isMaster || !this.user) { @@ -195,5 +212,6 @@ module.exports = { Auth: Auth, master: master, nobody: nobody, - getAuthForSessionToken: getAuthForSessionToken + getAuthForSessionToken, + getAuthForLegacySessionToken }; diff --git a/src/Routers/SessionsRouter.js b/src/Routers/SessionsRouter.js index ce590fa3..0d47f23d 100644 --- a/src/Routers/SessionsRouter.js +++ b/src/Routers/SessionsRouter.js @@ -3,6 +3,8 @@ import ClassesRouter from './ClassesRouter'; import PromiseRouter from '../PromiseRouter'; import rest from '../rest'; import Auth from '../Auth'; +import RestWrite from '../RestWrite'; +import { newToken } from '../cryptoUtils'; export class SessionsRouter extends ClassesRouter { handleFind(req) { @@ -51,12 +53,45 @@ export class SessionsRouter extends ClassesRouter { }); } + handleUpdateToRevocableSession(req) { + const config = req.config; + const masterAuth = Auth.master(config) + const user = req.auth.user; + const expiresAt = config.generateSessionExpiresAt(); + const sessionData = { + sessionToken: 'r:' + newToken(), + user: { + __type: 'Pointer', + className: '_User', + objectId: user.id + }, + createdWith: { + 'action': 'upgrade', + }, + restricted: false, + installationId: req.auth.installationId, + expiresAt: Parse._encode(expiresAt) + }; + const create = new RestWrite(config, masterAuth, '_Session', null, sessionData); + return create.execute().then(() => { + // delete the session token, use the db to skip beforeSave + return config.database.update('_User', { + objectId: user.id + }, { + sessionToken: {__op: 'Delete'} + }); + }).then((res) => { + return Promise.resolve({ response: sessionData }); + }); + } + mountRoutes() { this.route('GET', '/sessions', req => { return this.handleFind(req); }); this.route('GET', '/sessions/:objectId', req => { return this.handleGet(req); }); this.route('POST', '/sessions', req => { return this.handleCreate(req); }); this.route('PUT', '/sessions/:objectId', req => { return this.handleUpdate(req); }); this.route('DELETE', '/sessions/:objectId', req => { return this.handleDelete(req); }); + this.route('POST', '/upgradeToRevocableSession', req => { return this.handleUpdateToRevocableSession(req); }) } } diff --git a/src/middlewares.js b/src/middlewares.js index 4fa8c9ad..ca054c0a 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -147,8 +147,16 @@ export function handleParseHeaders(req, res, next) { return; } - return auth.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }) - .then((auth) => { + return Promise.resolve().then(() => { + // handle the upgradeToRevocableSession path on it's own + if (info.sessionToken && + req.url === '/upgradeToRevocableSession' && + info.sessionToken.indexOf('r:') != 0) { + return auth.getAuthForLegacySessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }) + } else { + return auth.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }) + } + }).then((auth) => { if (auth) { req.auth = auth; next();