From 6f292059badc2aad5056931a024f80eb10ee407d Mon Sep 17 00:00:00 2001 From: Diwakar Cherukumilli Date: Tue, 19 Jul 2016 01:10:36 -0500 Subject: [PATCH] Adds ability to expire email verify token (#2216) --- README.md | 11 + spec/EmailVerificationToken.spec.js | 515 ++++++++++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 14 + .../Postgres/PostgresStorageAdapter.js | 3 + src/Config.js | 22 +- src/Controllers/DatabaseController.js | 4 +- src/Controllers/UserController.js | 22 +- src/ParseServer.js | 2 + src/Routers/UsersRouter.js | 1 + src/cli/cli-definitions.js | 5 + 10 files changed, 591 insertions(+), 8 deletions(-) create mode 100644 spec/EmailVerificationToken.spec.js diff --git a/README.md b/README.md index 210b2bce83..cb496b615e 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,17 @@ var server = ParseServer({ // Enable email verification verifyUserEmails: true, + // if `verifyUserEmails` is `true` and + // if `emailVerifyTokenValidityDuration` is `undefined` then + // email verify token never expires + // else + // email verify token expires after `emailVerifyTokenValidityDuration` + // + // `emailVerifyTokenValidityDuration` defaults to `undefined` + // + // email verify token below expires in 2 hours (= 2 * 60 * 60 == 7200 seconds) + emailVerifyTokenValidityDuration = 2 * 60 * 60, // in seconds (2 hours = 7200 seconds) + // set preventLoginWithUnverifiedEmail to false to allow user to login without verifying their email // set preventLoginWithUnverifiedEmail to true to prevent user from login if their email is not verified preventLoginWithUnverifiedEmail: false, // defaults to false diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js new file mode 100644 index 0000000000..93acc2ea82 --- /dev/null +++ b/spec/EmailVerificationToken.spec.js @@ -0,0 +1,515 @@ +"use strict"; + +const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); +const request = require('request'); +const MongoClient = require("mongodb").MongoClient; + +describe("Email Verification Token Expiration: ", () => { + + it_exclude_dbs(['postgres'])('show the invalid link page, if the user clicks on the verify email link after the email verify token expires', done => { + var user = new Parse.User(); + var sendEmailOptions; + var emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + } + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 0.5, // 0.5 second + publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + user.setUsername("testEmailVerifyTokenValidity"); + user.setPassword("expiringToken"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }).then(() => { + // wait for 1 second - simulate user behavior to some extent + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + done(); + }); + }, 1000); + }); + }); + + it_exclude_dbs(['postgres'])('emailVerified should set to false, if the user does not verify their email before the email verify token expires', done => { + var user = new Parse.User(); + var sendEmailOptions; + var emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + } + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 0.5, // 0.5 second + publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + user.setUsername("testEmailVerifyTokenValidity"); + user.setPassword("expiringToken"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }).then(() => { + // wait for 1 second - simulate user behavior to some extent + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + user.fetch() + .then(() => { + expect(user.get('emailVerified')).toEqual(false); + done(); + }) + .catch((err) => { + fail("this should not fail"); + done(); + }); + }); + }, 1000); + }); + }); + + it_exclude_dbs(['postgres'])('if user clicks on the email verify link before email verification token expiration then show the verify email success page', done => { + var user = new Parse.User(); + var sendEmailOptions; + var emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + } + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + user.setUsername("testEmailVerifyTokenValidity"); + user.setPassword("expiringToken"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }).then(() => { + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity'); + done(); + }); + }); + }); + + it_exclude_dbs(['postgres'])('if user clicks on the email verify link before email verification token expiration then emailVerified should be true', done => { + var user = new Parse.User(); + var sendEmailOptions; + var emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + } + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + user.setUsername("testEmailVerifyTokenValidity"); + user.setPassword("expiringToken"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }).then(() => { + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + user.fetch() + .then(() => { + expect(user.get('emailVerified')).toEqual(true); + done(); + }) + .catch((err) => { + fail("this should not fail"); + done(); + }); + }); + }); + }); + + it_exclude_dbs(['postgres'])('if user clicks on the email verify link before email verification token expiration then user should be able to login', done => { + var user = new Parse.User(); + var sendEmailOptions; + var emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + } + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + user.setUsername("testEmailVerifyTokenValidity"); + user.setPassword("expiringToken"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }).then(() => { + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + Parse.User.logIn("testEmailVerifyTokenValidity", "expiringToken") + .then(user => { + expect(typeof user).toBe('object'); + expect(user.get('emailVerified')).toBe(true); + done(); + }) + .catch((error) => { + fail('login should have succeeded'); + done(); + }); + }); + }); + }); + + it_exclude_dbs(['postgres'])('sets the _email_verify_token_expires_at and _email_verify_token fields after user SignUp', done => { + var user = new Parse.User(); + var sendEmailOptions; + var emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + } + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1' + }) + .then(() => { + user.setUsername('sets_email_verify_token_expires_at'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; + return MongoClient.connect(databaseURI); + }) + .then(database => { + expect(typeof database).toBe('object'); + return database.collection('test__User').findOne({username: 'sets_email_verify_token_expires_at'}); + }) + .then(user => { + expect(typeof user).toBe('object'); + expect(user.emailVerified).toEqual(false); + expect(typeof user._email_verify_token).toBe('string'); + expect(typeof user._email_verify_token_expires_at).toBe('object'); + done(); + }) + .catch(error => { + fail("this should not fail"); + done(); + }); + }); + + it_exclude_dbs(['postgres'])('unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', done => { + var user = new Parse.User(); + var sendEmailOptions; + var emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + } + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + user.setUsername("unsets_email_verify_token_expires_at"); + user.setPassword("expiringToken"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + + const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; + MongoClient.connect(databaseURI) + .then(database => { + expect(typeof database).toBe('object'); + return database.collection('test__User').findOne({username: 'unsets_email_verify_token_expires_at'}); + }) + .then(user => { + expect(typeof user).toBe('object'); + expect(user.emailVerified).toEqual(true); + expect(typeof user._email_verify_token).toBe('undefined'); + expect(typeof user._email_verify_token_expires_at).toBe('undefined'); + done(); + }) + .catch(error => { + fail("this should not fail"); + done(); + }); + }); + }) + .catch(error => { + fail("this should not fail"); + done(); + }); + }); + + it_exclude_dbs(['postgres'])('clicking on the email verify link by an email VERIFIED user that was setup before enabling the expire email verify token should show an invalid link', done => { + var user = new Parse.User(); + var sendEmailOptions; + var emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + } + var serverConfig = { + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + publicServerURL: "http://localhost:8378/1" + }; + + // setup server WITHOUT enabling the expire email verify token flag + reconfigureServer(serverConfig) + .then(() => { + user.setUsername("testEmailVerifyTokenValidity"); + user.setPassword("expiringToken"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + return new Promise((resolve, reject) => { + request.get(sendEmailOptions.link, { followRedirect: false, }) + .on('error', error => reject(error)) + .on('response', (response) => { + expect(response.statusCode).toEqual(302); + resolve(user.fetch()); + }); + }); + }) + .then(() => { + expect(user.get('emailVerified')).toEqual(true); + // RECONFIGURE the server i.e., ENABLE the expire email verify token flag + serverConfig.emailVerifyTokenValidityDuration = 5; // 5 seconds + return reconfigureServer(serverConfig); + }) + .then(() => { + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + done(); + }); + }) + .catch((err) => { + fail("this should not fail"); + done(); + }); + }); + + it_exclude_dbs(['postgres'])('clicking on the email verify link by an email UNVERIFIED user that was setup before enabling the expire email verify token should show an invalid link', done => { + var user = new Parse.User(); + var sendEmailOptions; + var emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + } + var serverConfig = { + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + publicServerURL: "http://localhost:8378/1" + }; + + // setup server WITHOUT enabling the expire email verify token flag + reconfigureServer(serverConfig) + .then(() => { + user.setUsername("testEmailVerifyTokenValidity"); + user.setPassword("expiringToken"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + // just get the user again - DO NOT email verify the user + return user.fetch(); + }) + .then(() => { + expect(user.get('emailVerified')).toEqual(false); + // RECONFIGURE the server i.e., ENABLE the expire email verify token flag + serverConfig.emailVerifyTokenValidityDuration = 5; // 5 seconds + return reconfigureServer(serverConfig); + }) + .then(() => { + request.get(sendEmailOptions.link, { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + done(); + }); + }) + .catch((err) => { + fail("this should not fail"); + done(); + }); + }); + + it_exclude_dbs(['postgres'])('setting the email on the user should set a new email verification token and new expiration date for the token when expire email verify token flag is set', done => { + + const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; + let db; + + let user = new Parse.User(); + let userBeforeEmailReset; + + let sendEmailOptions; + let emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + }; + let serverConfig = { + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: "http://localhost:8378/1" + }; + + reconfigureServer(serverConfig) + .then(() => { + user.setUsername("newEmailVerifyTokenOnEmailReset"); + user.setPassword("expiringToken"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + return MongoClient.connect(databaseURI); + }) + .then(database => { + expect(typeof database).toBe('object'); + db = database; //save the db object for later use + return db.collection('test__User').findOne({username: 'newEmailVerifyTokenOnEmailReset'}); + }) + .then(userFromDb => { + expect(typeof userFromDb).toBe('object'); + userBeforeEmailReset = userFromDb; + + // trigger another token generation by setting the email + user.set('email', 'user@parse.com'); + return new Promise((resolve, reject) => { + // wait for half a sec to get a new expiration time + setTimeout( () => resolve(user.save()), 500 ); + }); + }) + .then(() => { + // get user data after email reset and new token generation + return db.collection('test__User').findOne({username: 'newEmailVerifyTokenOnEmailReset'}); + }) + .then(userAfterEmailReset => { + expect(typeof userAfterEmailReset).toBe('object'); + expect(userBeforeEmailReset._email_verify_token).not.toEqual(userAfterEmailReset._email_verify_token); + expect(userBeforeEmailReset._email_verify_token_expires_at).not.toEqual(userAfterEmailReset.__email_verify_token_expires_at); + done(); + }) + .catch((err) => { + fail("this should not fail"); + done(); + }); + }); + + it_exclude_dbs(['postgres'])('client should not see the _email_verify_token_expires_at field', done => { + var user = new Parse.User(); + var sendEmailOptions; + var emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {} + } + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + user.setUsername("testEmailVerifyTokenValidity"); + user.setPassword("expiringToken"); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + + user.fetch() + .then(() => { + expect(user.get('emailVerified')).toEqual(false); + expect(typeof user.get('_email_verify_token_expires_at')).toBe('undefined'); + done(); + }) + .catch(error => { + fail("this should not fail"); + done(); + }); + + }); + }); + +}) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 13279f71a6..cd7408f1fb 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -47,6 +47,10 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc key = 'expiresAt'; timeField = true; break; + case '_email_verify_token_expires_at': + key = '_email_verify_token_expires_at'; + timeField = true; + break; case '_rperm': case '_wperm': return {key: key, value: restValue}; @@ -134,6 +138,11 @@ function transformQueryKeyValue(className, key, value, schema) { return {key: 'expiresAt', value: valueAsDate(value)} } break; + case '_email_verify_token_expires_at': + if (valueAsDate(value)) { + return {key: '_email_verify_token_expires_at', value: valueAsDate(value)} + } + break; case 'objectId': return {key: '_id', value} case 'sessionToken': return {key: '_session_token', value} case '_rperm': @@ -207,6 +216,10 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) => transformedValue = transformTopLevelAtom(restValue); coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue return {key: 'expiresAt', value: coercedToDate}; + case '_email_verify_token_expires_at': + transformedValue = transformTopLevelAtom(restValue); + coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue + return {key: '_email_verify_token_expires_at', value: coercedToDate}; case '_rperm': case '_wperm': case '_email_verify_token': @@ -706,6 +719,7 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => { case '_email_verify_token': case '_perishable_token': case '_tombstone': + case '_email_verify_token_expires_at': break; case '_session_token': restObject['sessionToken'] = mongoObject[key]; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index d5b8e85f29..cf886cdcd8 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -379,6 +379,9 @@ export class PostgresStorageAdapter { if (object.expiresAt) { object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() }; } + if (object._email_verify_token_expires_at) { + object._email_verify_token_expires_at = { __type: 'Date', iso: object._email_verify_token_expires_at.toISOString() }; + } for (let fieldName in object) { if (object[fieldName] === null) { diff --git a/src/Config.js b/src/Config.js index 9eacfda8a7..89a4753258 100644 --- a/src/Config.js +++ b/src/Config.js @@ -38,6 +38,7 @@ export class Config { this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL); this.verifyUserEmails = cacheInfo.verifyUserEmails; this.preventLoginWithUnverifiedEmail = cacheInfo.preventLoginWithUnverifiedEmail; + this.emailVerifyTokenValidityDuration = cacheInfo.emailVerifyTokenValidityDuration; this.appName = cacheInfo.appName; this.cacheController = cacheInfo.cacheController; @@ -53,6 +54,7 @@ export class Config { this.sessionLength = cacheInfo.sessionLength; this.expireInactiveSessions = cacheInfo.expireInactiveSessions; this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this); + this.generateEmailVerifyTokenExpiresAt = this.generateEmailVerifyTokenExpiresAt.bind(this); this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset; } @@ -64,10 +66,11 @@ export class Config { revokeSessionOnPasswordReset, expireInactiveSessions, sessionLength, + emailVerifyTokenValidityDuration }) { const emailAdapter = userController.adapter; if (verifyUserEmails) { - this.validateEmailConfiguration({emailAdapter, appName, publicServerURL}); + this.validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration}); } if (typeof revokeSessionOnPasswordReset !== 'boolean') { @@ -83,7 +86,7 @@ export class Config { this.validateSessionConfiguration(sessionLength, expireInactiveSessions); } - static validateEmailConfiguration({emailAdapter, appName, publicServerURL}) { +static validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration}) { if (!emailAdapter) { throw 'An emailAdapter is required for e-mail verification and password resets.'; } @@ -93,6 +96,13 @@ export class Config { if (typeof publicServerURL !== 'string') { throw 'A public server url is required for e-mail verification and password resets.'; } + if (emailVerifyTokenValidityDuration) { + if (isNaN(emailVerifyTokenValidityDuration)) { + throw 'Email verify token validity duration must be a valid number.'; + } else if (emailVerifyTokenValidityDuration <= 0) { + throw 'Email verify token validity duration must be a value greater than 0.' + } + } } get mount() { @@ -118,6 +128,14 @@ export class Config { } } + generateEmailVerifyTokenExpiresAt() { + if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) { + return undefined; + } + var now = new Date(); + return new Date(now.getTime() + (this.emailVerifyTokenValidityDuration*1000)); + } + generateSessionExpiresAt() { if (!this.expireInactiveSessions) { return undefined; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 09891a3c98..5272f9bfd0 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -44,7 +44,7 @@ const transformObjectACL = ({ ACL, ...result }) => { return result; } -const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token']; +const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at']; const validateQuery = query => { if (query.ACL) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); @@ -176,7 +176,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => { // acl: a list of strings. If the object to be updated has an ACL, // one of the provided strings must provide the caller with // write permissions. -const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token']; +const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at']; DatabaseController.prototype.update = function(className, query, update, { acl, many, diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 98070fcafd..27ecad7100 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -36,6 +36,10 @@ export class UserController extends AdaptableController { if (this.shouldVerifyEmails) { user._email_verify_token = randomString(25); user.emailVerified = false; + + if (this.config.emailVerifyTokenValidityDuration) { + user._email_verify_token_expires_at = Parse._encode(this.config.generateEmailVerifyTokenExpiresAt()); + } } } @@ -45,10 +49,20 @@ export class UserController extends AdaptableController { // TODO: Better error here. throw undefined; } - return this.config.database.update('_User', { - username: username, - _email_verify_token: token - }, {emailVerified: true}).then(document => { + + let query = {username: username, _email_verify_token: token}; + let updateFields = { emailVerified: true, _email_verify_token: {__op: 'Delete'}}; + + // if the email verify token needs to be validated then + // add additional query params and additional fields that need to be updated + if (this.config.emailVerifyTokenValidityDuration) { + query.emailVerified = false; + query._email_verify_token_expires_at = { $gt: Parse._encode(new Date()) }; + + updateFields._email_verify_token_expires_at = {__op: 'Delete'}; + } + + return this.config.database.update('_User', query, updateFields).then((document) => { if (!document) { throw undefined; } diff --git a/src/ParseServer.js b/src/ParseServer.js index f5ff5ff75a..cd028d5784 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -120,6 +120,7 @@ class ParseServer { maxUploadSize = '20mb', verifyUserEmails = false, preventLoginWithUnverifiedEmail = false, + emailVerifyTokenValidityDuration, cacheAdapter, emailAdapter, publicServerURL, @@ -234,6 +235,7 @@ class ParseServer { userController: userController, verifyUserEmails: verifyUserEmails, preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail, + emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration, allowClientClassCreation: allowClientClassCreation, authDataManager: authDataManager(oauth, enableAnonymousUsers), appName: appName, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 02e9c3b434..d6477b10f1 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -166,6 +166,7 @@ export class UsersRouter extends ClassesRouter { emailAdapter: req.config.userController.adapter, appName: req.config.appName, publicServerURL: req.config.publicServerURL, + emailVerifyTokenValidityDuration: req.config.emailVerifyTokenValidityDuration }); } catch (e) { if (typeof e === 'string') { diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index 237108a426..3fc1e5dccd 100644 --- a/src/cli/cli-definitions.js +++ b/src/cli/cli-definitions.js @@ -151,6 +151,11 @@ export default { help: "Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false", action: booleanParser }, + "emailVerifyTokenValidityDuration": { + env: "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION", + help: "Email verification token validity duration", + action: numberParser("emailVerifyTokenValidityDuration") + }, "appName": { env: "PARSE_SERVER_APP_NAME", help: "Sets the app name"