Adds ability to expire email verify token (#2216)
This commit is contained in:
committed by
Drew
parent
033bc317e6
commit
6f292059ba
@@ -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];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user