Skip to content

Commit 340eb46

Browse files
authored
Adds endpoint for non-revocable session token upgrade (#2646)
1 parent c5fdd91 commit 340eb46

File tree

4 files changed

+156
-3
lines changed

4 files changed

+156
-3
lines changed

spec/RevocableSessionsUpgrade.spec.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
const Config = require('../src/Config');
2+
const sessionToken = 'legacySessionToken';
3+
const rp = require('request-promise');
4+
const Parse = require('parse/node');
5+
6+
function createUser() {
7+
const config = new Config(Parse.applicationId);
8+
const user = {
9+
objectId: '1234567890',
10+
username: 'hello',
11+
password: 'pass',
12+
_session_token: sessionToken
13+
}
14+
return config.database.create('_User', user);
15+
}
16+
17+
describe('revocable sessions', () => {
18+
19+
beforeEach((done) => {
20+
// Create 1 user with the legacy
21+
createUser().then(done);
22+
});
23+
24+
it('should upgrade legacy session token', done => {
25+
let user = Parse.Object.fromJSON({
26+
className: '_User',
27+
objectId: '1234567890',
28+
sessionToken: sessionToken
29+
});
30+
user._upgradeToRevocableSession().then((res) => {
31+
expect(res.getSessionToken().indexOf('r:')).toBe(0);
32+
const config = new Config(Parse.applicationId);
33+
// use direct access to the DB to make sure we're not
34+
// getting the session token stripped
35+
return config.database.loadSchema().then(schemaController => {
36+
return schemaController.getOneSchema('_User', true)
37+
}).then((schema) => {
38+
return config.database.adapter.find('_User', schema, {objectId: '1234567890'}, {})
39+
}).then((results) => {
40+
expect(results.length).toBe(1);
41+
expect(results[0].sessionToken).toBeUndefined();
42+
});
43+
}).then(() => {
44+
done();
45+
}, (err) => {
46+
jfail(err);
47+
done();
48+
});
49+
});
50+
51+
it('should be able to become with revocable session token', done => {
52+
let user = Parse.Object.fromJSON({
53+
className: '_User',
54+
objectId: '1234567890',
55+
sessionToken: sessionToken
56+
});
57+
user._upgradeToRevocableSession().then((res) => {
58+
expect(res.getSessionToken().indexOf('r:')).toBe(0);
59+
return Parse.User.logOut().then(() => {
60+
return Parse.User.become(res.getSessionToken())
61+
}).then((user) => {
62+
expect(user.id).toEqual('1234567890');
63+
});
64+
}).then(() => {
65+
done();
66+
}, (err) => {
67+
jfail(err);
68+
done();
69+
});
70+
});
71+
72+
it('should not upgrade bad legacy session token', done => {
73+
rp.post({
74+
url: Parse.serverURL+'/upgradeToRevocableSession',
75+
headers: {
76+
'X-Parse-Application-Id': Parse.applicationId,
77+
'X-Parse-Rest-API-Key': 'rest',
78+
'X-Parse-Session-Token': 'badSessionToken'
79+
},
80+
json: true
81+
}).then((res) => {
82+
fail('should not be able to upgrade a bad token');
83+
}, (response) => {
84+
expect(response.statusCode).toBe(400);
85+
expect(response.error).not.toBeUndefined();
86+
expect(response.error.code).toBe(Parse.Error.INVALID_SESSION_TOKEN);
87+
expect(response.error.error).toEqual('invalid legacy session token');
88+
}).then(() => {
89+
done();
90+
});
91+
});
92+
})

src/Auth.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,23 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } =
7878
});
7979
};
8080

81+
var getAuthForLegacySessionToken = function({config, sessionToken, installationId } = {}) {
82+
var restOptions = {
83+
limit: 1
84+
};
85+
var query = new RestQuery(config, master(config), '_User', { sessionToken: sessionToken}, restOptions);
86+
return query.execute().then((response) => {
87+
var results = response.results;
88+
if (results.length !== 1) {
89+
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid legacy session token');
90+
}
91+
let obj = results[0];
92+
obj.className = '_User';
93+
let userObject = Parse.Object.fromJSON(obj);
94+
return new Auth({config, isMaster: false, installationId, user: userObject});
95+
});
96+
}
97+
8198
// Returns a promise that resolves to an array of role names
8299
Auth.prototype.getUserRoles = function() {
83100
if (this.isMaster || !this.user) {
@@ -195,5 +212,6 @@ module.exports = {
195212
Auth: Auth,
196213
master: master,
197214
nobody: nobody,
198-
getAuthForSessionToken: getAuthForSessionToken
215+
getAuthForSessionToken,
216+
getAuthForLegacySessionToken
199217
};

src/Routers/SessionsRouter.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import ClassesRouter from './ClassesRouter';
33
import PromiseRouter from '../PromiseRouter';
44
import rest from '../rest';
55
import Auth from '../Auth';
6+
import RestWrite from '../RestWrite';
7+
import { newToken } from '../cryptoUtils';
68

79
export class SessionsRouter extends ClassesRouter {
810
handleFind(req) {
@@ -51,12 +53,45 @@ export class SessionsRouter extends ClassesRouter {
5153
});
5254
}
5355

56+
handleUpdateToRevocableSession(req) {
57+
const config = req.config;
58+
const masterAuth = Auth.master(config)
59+
const user = req.auth.user;
60+
const expiresAt = config.generateSessionExpiresAt();
61+
const sessionData = {
62+
sessionToken: 'r:' + newToken(),
63+
user: {
64+
__type: 'Pointer',
65+
className: '_User',
66+
objectId: user.id
67+
},
68+
createdWith: {
69+
'action': 'upgrade',
70+
},
71+
restricted: false,
72+
installationId: req.auth.installationId,
73+
expiresAt: Parse._encode(expiresAt)
74+
};
75+
const create = new RestWrite(config, masterAuth, '_Session', null, sessionData);
76+
return create.execute().then(() => {
77+
// delete the session token, use the db to skip beforeSave
78+
return config.database.update('_User', {
79+
objectId: user.id
80+
}, {
81+
sessionToken: {__op: 'Delete'}
82+
});
83+
}).then((res) => {
84+
return Promise.resolve({ response: sessionData });
85+
});
86+
}
87+
5488
mountRoutes() {
5589
this.route('GET', '/sessions', req => { return this.handleFind(req); });
5690
this.route('GET', '/sessions/:objectId', req => { return this.handleGet(req); });
5791
this.route('POST', '/sessions', req => { return this.handleCreate(req); });
5892
this.route('PUT', '/sessions/:objectId', req => { return this.handleUpdate(req); });
5993
this.route('DELETE', '/sessions/:objectId', req => { return this.handleDelete(req); });
94+
this.route('POST', '/upgradeToRevocableSession', req => { return this.handleUpdateToRevocableSession(req); })
6095
}
6196
}
6297

src/middlewares.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,16 @@ export function handleParseHeaders(req, res, next) {
147147
return;
148148
}
149149

150-
return auth.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken })
151-
.then((auth) => {
150+
return Promise.resolve().then(() => {
151+
// handle the upgradeToRevocableSession path on it's own
152+
if (info.sessionToken &&
153+
req.url === '/upgradeToRevocableSession' &&
154+
info.sessionToken.indexOf('r:') != 0) {
155+
return auth.getAuthForLegacySessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken })
156+
} else {
157+
return auth.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken })
158+
}
159+
}).then((auth) => {
152160
if (auth) {
153161
req.auth = auth;
154162
next();

0 commit comments

Comments
 (0)