From 825de481768031d1d9bfed299b11f9c6e21d8fb8 Mon Sep 17 00:00:00 2001 From: ettore Panini Date: Fri, 18 Jan 2019 18:17:13 +0100 Subject: [PATCH 1/3] add session management for two factor authentication --- package-lock.json | 100 ++++++++-------------------- src/Auth.js | 81 +++++++++++++++++++++- src/Controllers/SchemaController.js | 1 + src/Options/Definitions.js | 5 ++ src/Options/docs.js | 2 + src/Options/index.js | 13 ++++ src/Routers/UsersRouter.js | 98 ++++++++++++++++++++++++++- src/cryptoUtils.js | 8 ++- src/index.js | 2 + src/middlewares.js | 28 ++++++++ 10 files changed, 259 insertions(+), 79 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ba8b36ddf..5b3347ba34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1730,8 +1730,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -1749,13 +1748,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1768,18 +1765,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -1882,8 +1876,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -1893,7 +1886,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1906,20 +1898,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.4", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -1927,13 +1916,11 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.2", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -1948,7 +1935,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2021,8 +2007,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -2032,7 +2017,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -2108,8 +2092,7 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -2139,7 +2122,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2157,7 +2139,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2208,8 +2189,7 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -4223,8 +4203,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -4245,14 +4224,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4267,20 +4244,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -4397,8 +4371,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4410,7 +4383,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4425,7 +4397,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4433,14 +4404,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4459,7 +4428,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4540,8 +4508,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -4553,7 +4520,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4639,8 +4605,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -4676,7 +4641,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4696,7 +4660,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4740,14 +4703,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -7127,7 +7088,6 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, - "optional": true, "requires": { "remove-trailing-separator": "^1.0.1" } @@ -7241,7 +7201,6 @@ "version": "0.1.4", "bundled": true, "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -7566,8 +7525,7 @@ "is-buffer": { "version": "1.1.6", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "is-builtin-module": { "version": "1.0.0", @@ -7651,7 +7609,6 @@ "version": "3.2.2", "bundled": true, "dev": true, - "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -7698,8 +7655,7 @@ "longest": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "lru-cache": { "version": "4.1.3", @@ -7965,8 +7921,7 @@ "repeat-string": { "version": "1.6.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "require-directory": { "version": "2.1.1", @@ -9157,8 +9112,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true, - "optional": true + "dev": true }, "repeat-element": { "version": "1.1.3", diff --git a/src/Auth.js b/src/Auth.js index ebb5debfd2..f312a06d26 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -60,10 +60,22 @@ const getAuthForSessionToken = async function({ cacheController, sessionToken, installationId, + sessionTwoFactorToken, }) { + const { + twoFactorAuthentication: { token, mustUsed }, + } = config; + cacheController = cacheController || (config && config.cacheController); if (cacheController) { - const userJSON = await cacheController.user.get(sessionToken); + let redisUserKey = sessionToken; + if (token && sessionTwoFactorToken) { + redisUserKey = `${sessionToken}-${cryptoUtils.createHashHmac( + token, + sessionTwoFactorToken + )}`; + } + const userJSON = await cacheController.user.get(redisUserKey); if (userJSON) { const cachedUser = Parse.Object.fromJSON(userJSON); return Promise.resolve( @@ -107,6 +119,7 @@ const getAuthForSessionToken = async function({ 'Invalid session token' ); } + const now = new Date(), expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) @@ -118,12 +131,55 @@ const getAuthForSessionToken = async function({ ); } const obj = results[0]['user']; + let encryptTwoFactorToken = ''; + if (config) { + //check if 2FA is enabled + if (token) { + //check if 2FA is optional or must be used. default false + if ((mustUsed || obj.twoFactorActive) && !sessionTwoFactorToken) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + '2FA hash not found.' + ); + } + + const { twoFactorHash } = results[0]; + if (!twoFactorHash) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + '2FA hash not found on session.' + ); + } + // encrypt two factor token with the two factor config token + encryptTwoFactorToken = cryptoUtils.createHashHmac( + token, + sessionTwoFactorToken + ); + + // invalid session if it not match with token saved on session + + if (encryptTwoFactorToken !== twoFactorHash) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + '2FA hash not match.' + ); + } + } + } delete obj.password; obj['className'] = '_User'; obj['sessionToken'] = sessionToken; + + // if two factor Auth is enabled set user on cache + // with key generated merging sessionToken and encryptTwoFactorToken + const redisUserKey = encryptTwoFactorToken + ? `${sessionToken}-${encryptTwoFactorToken}` + : sessionToken; + if (cacheController) { - cacheController.user.put(sessionToken, obj); + cacheController.user.put(redisUserKey, obj); } + const userObject = Parse.Object.fromJSON(obj); return new Auth({ config, @@ -345,10 +401,17 @@ Auth.prototype._getAllRolesNamesForRoleIds = function( const createSession = function( config, - { userId, createdWith, installationId, additionalSessionData } + { + userId, + createdWith, + installationId, + additionalSessionData, + twoFactorActive, + } ) { const token = 'r:' + cryptoUtils.newToken(); const expiresAt = config.generateSessionExpiresAt(); + const sessionData = { sessionToken: token, user: { @@ -361,6 +424,18 @@ const createSession = function( expiresAt: Parse._encode(expiresAt), }; + if (config) { + const { + twoFactorAuthentication: { mustUsed, token, firstSessionExpireTime }, + } = config; + if (mustUsed || (twoFactorActive && token)) { + const now = new Date(); + sessionData.expiresAt = Parse._encode( + new Date(now.getTime() + firstSessionExpireTime * 60000) + ); + } + } + if (installationId) { sessionData.installationId = installationId; } diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 40e240ce26..ffe7b441c4 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -72,6 +72,7 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({ sessionToken: { type: 'String' }, expiresAt: { type: 'Date' }, createdWith: { type: 'Object' }, + twoFactorHash: { type: 'String' }, }, _Product: { productIdentifier: { type: 'String' }, diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 0d0b715400..bf002a002a 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -28,6 +28,11 @@ module.exports.ParseServerOptions = { action: parsers.arrayParser, default: [], }, + twoFactorAuthentication: { + env: 'PARSE_SERVER_TWO_FACTOR_AUTH', + help: 'used for validate Session after two factor auth', + default: { firstSessionExpireTime: 4, mustUsed: false }, + }, appName: { env: 'PARSE_SERVER_APP_NAME', help: 'Sets the app name', diff --git a/src/Options/docs.js b/src/Options/docs.js index 2bfac7e70c..df0fd8d05f 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -1,10 +1,12 @@ /** * @interface ParseServerOptions * @property {String} appId Your Parse Application ID + * * @property {String} masterKey Your Parse Master Key * @property {String} serverURL URL to your parse server with http:// or https://. * @property {String[]} masterKeyIps Restrict masterKey to be used by only these ips, defaults to [] (allow all ips) * @property {String} appName Sets the app name + * @property {Object} twoFactorAuthentication Object to enable two factor authentication * @property {Adapter} analyticsAdapter Adapter module for the analytics * @property {Adapter} filesAdapter Adapter module for the files sub-system * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications diff --git a/src/Options/index.js b/src/Options/index.js index 8ff3d371cd..ad36c523cc 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -24,6 +24,8 @@ export interface ParseServerOptions { masterKeyIps: ?(string[]); /* Sets the app name */ appName: ?string; + /* Set two factor authentication options */ + twoFactorAuthentication: TwoFactorAuthentication; /* Adapter module for the analytics */ analyticsAdapter: ?Adapter; /* Adapter module for the files sub-system */ @@ -184,6 +186,17 @@ export interface CustomPagesOptions { passwordResetSuccess: ?string; } +export interface TwoFactorAuthentication { + /* token to encrypt two factor hash on session */ + token: ?(string[]); + /* if true all user have to be authenticated with 2FA. default false */ + mustUsed: boolean; + /* minutes between creation and expiration of + session generated on first authentication step. + default 4 */ + firstSessionExpireTime: number; +} + export interface LiveQueryOptions { /* parse-server's LiveQuery classNames :ENV: PARSE_SERVER_LIVEQUERY_CLASSNAMES */ diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index b7c02fa2f5..82cfa7e2a2 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -7,6 +7,8 @@ import ClassesRouter from './ClassesRouter'; import rest from '../rest'; import Auth from '../Auth'; import passwordCrypto from '../password'; +import * as cryptoUtils from '../cryptoUtils'; +import RestWrite from '../RestWrite'; export class UsersRouter extends ClassesRouter { className() { @@ -163,13 +165,26 @@ export class UsersRouter extends ClassesRouter { } handleMe(req) { + const { + twoFactorAuthentication: { mustUsed }, + } = req.config; + if (!req.info || !req.info.sessionToken) { throw new Parse.Error( Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token' ); } + + if (mustUsed && !req.info.sessionTwoFactorToken) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token' + ); + } + const sessionToken = req.info.sessionToken; + const sessionTwoFactorToken = req.info.sessionTwoFactorToken || ''; return rest .find( req.config, @@ -193,6 +208,9 @@ export class UsersRouter extends ClassesRouter { const user = response.results[0].user; // Send token back on the login, because SDKs expect that. user.sessionToken = sessionToken; + if (sessionTwoFactorToken) { + user.sessionTwoFactorToken = sessionTwoFactorToken; + } // Remove hidden properties. UsersRouter.removeHiddenProperties(user); @@ -202,12 +220,80 @@ export class UsersRouter extends ClassesRouter { }); } + handleTwoFactorValidation(req) { + if (!req.info || !req.info.sessionToken) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token' + ); + } + const sessionToken = req.info.sessionToken; + if (!req.auth.isMaster) { + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + 'Need masterKey' + ); + } + + return rest + .find( + req.config, + Auth.master(req.config), + '_Session', + { sessionToken }, + { include: 'user' }, + req.info.clientSDK + ) + .then(response => { + const { results } = response; + const { + twoFactorAuthentication: { mustUsed, token }, + } = req.config; + + if (!results || results.length == 0 || !results[0].user) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token' + ); + } + + if (!mustUsed && !results[0].user.twoFactorActive) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + '2FA not active' + ); + } + + const hashToEncrypt = 'tf:' + cryptoUtils.newToken(); + + const expiresAt = req.config.generateSessionExpiresAt(); + const sessionData = { + expiresAt: Parse._encode(expiresAt), + twoFactorHash: cryptoUtils.createHashHmac(token, hashToEncrypt), + }; + const session = { sessionToken: results[0].sessionToken }; + + return new RestWrite( + req.config, + Auth.master(req.config), + '_Session', + session, + sessionData + ) + .execute() + .then(() => { + //send back hashToEncrypt to parse server + return { response: hashToEncrypt }; + //it will be attach on user request like session token + }); + }); + } + handleLogIn(req) { let user; return this._authenticateUserFromRequest(req) .then(res => { user = res; - // handle password expiry policy if ( req.config.passwordPolicy && @@ -248,6 +334,7 @@ export class UsersRouter extends ClassesRouter { const { sessionData, createSession } = Auth.createSession(req.config, { userId: user.objectId, + twoFactorActive: user.twoFactorActive, createdWith: { action: 'login', authProvider: 'password', @@ -258,7 +345,6 @@ export class UsersRouter extends ClassesRouter { user.sessionToken = sessionData.sessionToken; req.config.filesController.expandFilesInObject(req.config, user); - return createSession(); }) .then(() => { @@ -438,6 +524,14 @@ export class UsersRouter extends ClassesRouter { this.route('POST', '/login', req => { return this.handleLogIn(req); }); + + this.route('GET', '/login/two-factor-validation', req => { + return this.handleTwoFactorValidation(req); + }); + this.route('POST', '/login/two-factor-validation', req => { + return this.handleTwoFactorValidation(req); + }); + this.route('POST', '/logout', req => { return this.handleLogOut(req); }); diff --git a/src/cryptoUtils.js b/src/cryptoUtils.js index 580b843ad6..e21ae6bd5f 100644 --- a/src/cryptoUtils.js +++ b/src/cryptoUtils.js @@ -1,6 +1,6 @@ /* @flow */ -import { randomBytes, createHash } from 'crypto'; +import { randomBytes, createHash, createHmac } from 'crypto'; // Returns a new random hex string of the given even size. export function randomHexString(size: number): string { @@ -48,3 +48,9 @@ export function md5Hash(string: string): string { .update(string) .digest('hex'); } + +export function createHashHmac(token: string, string: string): string { + return createHmac('sha256', token) + .update(string, 'utf8', 'hex') + .digest('base64'); +} diff --git a/src/index.js b/src/index.js index 5082eb3a26..0a960b84cb 100644 --- a/src/index.js +++ b/src/index.js @@ -14,8 +14,10 @@ import { ParseServerOptions } from './Options'; // Factory function const _ParseServer = function(options: ParseServerOptions) { const server = new ParseServer(options); + return server.app; }; + // Mount the create liveQueryServer _ParseServer.createLiveQueryServer = ParseServer.createLiveQueryServer; _ParseServer.start = ParseServer.start; diff --git a/src/middlewares.js b/src/middlewares.js index d7c8cb41a3..1097c87faa 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -19,6 +19,7 @@ export function handleParseHeaders(req, res, next) { var info = { appId: req.get('X-Parse-Application-Id'), sessionToken: req.get('X-Parse-Session-Token'), + sessionTwoFactorToken: req.get('X-Parse-Session-Two-Factor-Token'), masterKey: req.get('X-Parse-Master-Key'), installationId: req.get('X-Parse-Installation-Id'), clientKey: req.get('X-Parse-Client-Key'), @@ -85,6 +86,10 @@ export function handleParseHeaders(req, res, next) { info.sessionToken = req.body._SessionToken; delete req.body._SessionToken; } + if (req.body._SessionTwoFactorToken) { + info.sessionTwoFactorToken = req.body._SessionTwoFactorToken; + delete req.body._SessionTwoFactorToken; + } if (req.body._MasterKey) { info.masterKey = req.body._MasterKey; delete req.body._MasterKey; @@ -166,6 +171,27 @@ export function handleParseHeaders(req, res, next) { if (oneKeyConfigured && !oneKeyMatches) { return invalidRequest(req, res); } + if ( + req.url === '/login/two-factor-validation' && + (!info.sessionToken || info.masterKey !== req.config.masterKey) + ) { + return invalidRequest(req, res); + } + + if ( + req.url === '/login/two-factor-validation' && + info.sessionToken && + info.masterKey !== req.config.masterKey + ) { + req.auth = new auth.Auth({ + config: req.config, + installationId: info.installationId, + sessionToken: info.sessionToken, + isMaster: true, + }); + next(); + return; + } if (req.url == '/login') { delete info.sessionToken; @@ -199,12 +225,14 @@ export function handleParseHeaders(req, res, next) { config: req.config, installationId: info.installationId, sessionToken: info.sessionToken, + sessionTwoFactorToken: info.sessionTwoFactorToken, }); } }) .then(auth => { if (auth) { req.auth = auth; + next(); } }) From 6d641ea9b5bc1c7701e2d6bb189764e0c134b598 Mon Sep 17 00:00:00 2001 From: ettore Panini Date: Sat, 19 Jan 2019 15:20:43 +0100 Subject: [PATCH 2/3] change mustUsed option name - fix some test --- spec/Schema.spec.js | 1 + spec/schemas.spec.js | 1 + src/Auth.js | 35 +++++++++++++++++++---------- src/Controllers/SchemaController.js | 1 + src/Options/Definitions.js | 2 +- src/Options/index.js | 2 +- src/Routers/UsersRouter.js | 10 ++++----- 7 files changed, 33 insertions(+), 19 deletions(-) diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 5a7c90363d..15f9cb1ff0 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -806,6 +806,7 @@ describe('SchemaController', () => { sessionToken: { type: 'String' }, expiresAt: { type: 'Date' }, createdWith: { type: 'Object' }, + twoFactorHash: { type: 'String' }, ACL: { type: 'ACL' }, }, classLevelPermissions: { diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 190f52d905..f603ed8df2 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -749,6 +749,7 @@ describe('schemas', () => { password: { type: 'String' }, email: { type: 'String' }, emailVerified: { type: 'Boolean' }, + twoFactorActive: { type: 'Boolean' }, authData: { type: 'Object' }, newField: { type: 'String' }, ACL: { type: 'ACL' }, diff --git a/src/Auth.js b/src/Auth.js index f312a06d26..b0748147f8 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -62,18 +62,19 @@ const getAuthForSessionToken = async function({ installationId, sessionTwoFactorToken, }) { - const { - twoFactorAuthentication: { token, mustUsed }, - } = config; - cacheController = cacheController || (config && config.cacheController); if (cacheController) { let redisUserKey = sessionToken; - if (token && sessionTwoFactorToken) { - redisUserKey = `${sessionToken}-${cryptoUtils.createHashHmac( - token, - sessionTwoFactorToken - )}`; + if (config) { + const { + twoFactorAuthentication: { token }, + } = config; + if (token && sessionTwoFactorToken) { + redisUserKey = `${sessionToken}-${cryptoUtils.createHashHmac( + token, + sessionTwoFactorToken + )}`; + } } const userJSON = await cacheController.user.get(redisUserKey); if (userJSON) { @@ -133,10 +134,16 @@ const getAuthForSessionToken = async function({ const obj = results[0]['user']; let encryptTwoFactorToken = ''; if (config) { + const { + twoFactorAuthentication: { token, twoFactorAlwaysRequired }, + } = config; //check if 2FA is enabled if (token) { //check if 2FA is optional or must be used. default false - if ((mustUsed || obj.twoFactorActive) && !sessionTwoFactorToken) { + if ( + (twoFactorAlwaysRequired || obj.twoFactorActive) && + !sessionTwoFactorToken + ) { throw new Parse.Error( Parse.Error.INVALID_SESSION_TOKEN, '2FA hash not found.' @@ -426,9 +433,13 @@ const createSession = function( if (config) { const { - twoFactorAuthentication: { mustUsed, token, firstSessionExpireTime }, + twoFactorAuthentication: { + twoFactorAlwaysRequired, + token, + firstSessionExpireTime, + }, } = config; - if (mustUsed || (twoFactorActive && token)) { + if (twoFactorAlwaysRequired || (twoFactorActive && token)) { const now = new Date(); sessionData.expiresAt = Parse._encode( new Date(now.getTime() + firstSessionExpireTime * 60000) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index ffe7b441c4..9cdf58ae2c 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -41,6 +41,7 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({ email: { type: 'String' }, emailVerified: { type: 'Boolean' }, authData: { type: 'Object' }, + twoFactorActive: { type: 'Boolean' }, }, // The additional default columns for the _Installation collection (in addition to DefaultCols) _Installation: { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index bf002a002a..6d5c3ad111 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -31,7 +31,7 @@ module.exports.ParseServerOptions = { twoFactorAuthentication: { env: 'PARSE_SERVER_TWO_FACTOR_AUTH', help: 'used for validate Session after two factor auth', - default: { firstSessionExpireTime: 4, mustUsed: false }, + default: { firstSessionExpireTime: 4, twoFactorAlwaysRequired: false }, }, appName: { env: 'PARSE_SERVER_APP_NAME', diff --git a/src/Options/index.js b/src/Options/index.js index ad36c523cc..6c786eb17e 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -190,7 +190,7 @@ export interface TwoFactorAuthentication { /* token to encrypt two factor hash on session */ token: ?(string[]); /* if true all user have to be authenticated with 2FA. default false */ - mustUsed: boolean; + twoFactorAlwaysRequired: boolean; /* minutes between creation and expiration of session generated on first authentication step. default 4 */ diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 82cfa7e2a2..a8d17ce41d 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -166,7 +166,7 @@ export class UsersRouter extends ClassesRouter { handleMe(req) { const { - twoFactorAuthentication: { mustUsed }, + twoFactorAuthentication: { twoFactorAlwaysRequired }, } = req.config; if (!req.info || !req.info.sessionToken) { @@ -176,7 +176,7 @@ export class UsersRouter extends ClassesRouter { ); } - if (mustUsed && !req.info.sessionTwoFactorToken) { + if (twoFactorAlwaysRequired && !req.info.sessionTwoFactorToken) { throw new Parse.Error( Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token' @@ -231,7 +231,7 @@ export class UsersRouter extends ClassesRouter { if (!req.auth.isMaster) { throw new Parse.Error( Parse.Error.INTERNAL_SERVER_ERROR, - 'Need masterKey' + 'MasterKey is required' ); } @@ -247,7 +247,7 @@ export class UsersRouter extends ClassesRouter { .then(response => { const { results } = response; const { - twoFactorAuthentication: { mustUsed, token }, + twoFactorAuthentication: { twoFactorAlwaysRequired, token }, } = req.config; if (!results || results.length == 0 || !results[0].user) { @@ -257,7 +257,7 @@ export class UsersRouter extends ClassesRouter { ); } - if (!mustUsed && !results[0].user.twoFactorActive) { + if (!twoFactorAlwaysRequired && !results[0].user.twoFactorActive) { throw new Parse.Error( Parse.Error.INVALID_SESSION_TOKEN, '2FA not active' From 8e0690c5a415380afe74d437cb9b88761efbd602 Mon Sep 17 00:00:00 2001 From: ettore Panini Date: Sat, 19 Jan 2019 15:41:23 +0100 Subject: [PATCH 3/3] fix middleware condition when 2fa is not required --- src/Auth.js | 56 ++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Auth.js b/src/Auth.js index b0748147f8..9c02d788e8 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -140,36 +140,36 @@ const getAuthForSessionToken = async function({ //check if 2FA is enabled if (token) { //check if 2FA is optional or must be used. default false - if ( - (twoFactorAlwaysRequired || obj.twoFactorActive) && - !sessionTwoFactorToken - ) { - throw new Parse.Error( - Parse.Error.INVALID_SESSION_TOKEN, - '2FA hash not found.' - ); - } - - const { twoFactorHash } = results[0]; - if (!twoFactorHash) { - throw new Parse.Error( - Parse.Error.INVALID_SESSION_TOKEN, - '2FA hash not found on session.' + if (twoFactorAlwaysRequired || obj.twoFactorActive) { + //check if sessionTwoFactorToken is set + if (!sessionTwoFactorToken) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + '2FA hash not found.' + ); + } + // check if session parse object has twoFactorHash; + const { twoFactorHash } = results[0]; + if (!twoFactorHash) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + '2FA hash not found on session.' + ); + } + + // encrypt two factor token with the two factor config token + encryptTwoFactorToken = cryptoUtils.createHashHmac( + token, + sessionTwoFactorToken ); - } - // encrypt two factor token with the two factor config token - encryptTwoFactorToken = cryptoUtils.createHashHmac( - token, - sessionTwoFactorToken - ); - // invalid session if it not match with token saved on session - - if (encryptTwoFactorToken !== twoFactorHash) { - throw new Parse.Error( - Parse.Error.INVALID_SESSION_TOKEN, - '2FA hash not match.' - ); + // invalid session if it not match with token saved on session + if (encryptTwoFactorToken !== twoFactorHash) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + '2FA hash not match.' + ); + } } } }