diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index a44926caa4..3502cdd50d 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -12,6 +12,7 @@ const request = require('../lib/request'); const passwordCrypto = require('../lib/password'); const Config = require('../lib/Config'); const cryptoUtils = require('../lib/cryptoUtils'); +const UsersRouter = require('../lib/Routers/UsersRouter'); function verifyACL(user) { const ACL = user.getACL(); @@ -3925,6 +3926,45 @@ describe('Parse.User testing', () => { } }); + describe('UsersRouter.handleLogIn', () => { + it('should work with valid userFromJWT', async done => { + const user = await Parse.User.signUp('some_user', 'some_password'); + + const fakeReq = { + userFromJWT: user, + config: Config.get('test'), + info: {}, + }; + + const { response = {} } = await new UsersRouter.UsersRouter().handleLogIn(fakeReq); + + expect(user.id).toEqual(response.objectId); + expect(response.sessionToken).toBeTruthy(); + done(); + }); + + it('should fail with non-existing userFromJWT', async done => { + const user = new Parse.User({ + username: 'not_a_real_user', + id: '1234567', + }); + + const fakeReq = { + userFromJWT: user, + config: Config.get('test'), + info: {}, + }; + + try { + await new UsersRouter.UsersRouter().handleLogIn(fakeReq); + fail('User login should not have succeeded'); + } catch (error) { + expect(error.code).toEqual(101); + } + done(); + }); + }); + describe('issue #4897', () => { it_only_db('mongo')('should be able to login with a legacy user (no ACL)', async () => { // This issue is a side effect of the locked users and legacy users which don't have ACL's diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 7843cf4674..6c0347f501 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -139,6 +139,34 @@ export class UsersRouter extends ClassesRouter { }); } + /** + * Validates JWT bearer token and looks up user `req.userFromJWT`. CRITICALLY IMPORTANT that the JWT has already been validated by this point (eg: express middleware, AWS API Gateway Authorizer) + * @param {Object} req The request + * @returns {Object} User object + */ + _authenticateUserFromRequestWithJwt(req) { + return new Promise((resolve, reject) => { + if (!req.userFromJWT) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid credentials.'); + } + + const query = { + objectId: req.userFromJWT.id, + }; + return req.config.database + .find('_User', query) + .then(results => { + if (!results.length) + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid credentials.'); + const user = results[0]; + resolve(user); + }) + .catch(error => { + return reject(error); + }); + }); + } + handleMe(req) { if (!req.info || !req.info.sessionToken) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); @@ -171,10 +199,17 @@ export class UsersRouter extends ClassesRouter { } async handleLogIn(req) { - const user = await this._authenticateUserFromRequest(req); + let userFromJWT; + if (req.userFromJWT) { + // Could be just used `req.userFromJWT`, but the forced lookup + // Ensures the user hasn't been deleted since the the JWT was granted + userFromJWT = await this._authenticateUserFromRequestWithJwt(req); + } + + const user = userFromJWT || (await this._authenticateUserFromRequest(req)); - // handle password expiry policy - if (req.config.passwordPolicy && req.config.passwordPolicy.maxPasswordAge) { + // handle password expiry policy - ignore if user is managed in SSO (provided by JWT) + if (!userFromJWT && req.config.passwordPolicy && req.config.passwordPolicy.maxPasswordAge) { let changedAt = user._password_changed_at; if (!changedAt) {