Adds ability to expire email verify token (#2216)
This commit is contained in:
committed by
Drew
parent
033bc317e6
commit
6f292059ba
11
README.md
11
README.md
@@ -226,6 +226,17 @@ var server = ParseServer({
|
|||||||
// Enable email verification
|
// Enable email verification
|
||||||
verifyUserEmails: true,
|
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 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
|
// set preventLoginWithUnverifiedEmail to true to prevent user from login if their email is not verified
|
||||||
preventLoginWithUnverifiedEmail: false, // defaults to false
|
preventLoginWithUnverifiedEmail: false, // defaults to false
|
||||||
|
|||||||
515
spec/EmailVerificationToken.spec.js
Normal file
515
spec/EmailVerificationToken.spec.js
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
||||||
@@ -47,6 +47,10 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
|
|||||||
key = 'expiresAt';
|
key = 'expiresAt';
|
||||||
timeField = true;
|
timeField = true;
|
||||||
break;
|
break;
|
||||||
|
case '_email_verify_token_expires_at':
|
||||||
|
key = '_email_verify_token_expires_at';
|
||||||
|
timeField = true;
|
||||||
|
break;
|
||||||
case '_rperm':
|
case '_rperm':
|
||||||
case '_wperm':
|
case '_wperm':
|
||||||
return {key: key, value: restValue};
|
return {key: key, value: restValue};
|
||||||
@@ -134,6 +138,11 @@ function transformQueryKeyValue(className, key, value, schema) {
|
|||||||
return {key: 'expiresAt', value: valueAsDate(value)}
|
return {key: 'expiresAt', value: valueAsDate(value)}
|
||||||
}
|
}
|
||||||
break;
|
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 'objectId': return {key: '_id', value}
|
||||||
case 'sessionToken': return {key: '_session_token', value}
|
case 'sessionToken': return {key: '_session_token', value}
|
||||||
case '_rperm':
|
case '_rperm':
|
||||||
@@ -207,6 +216,10 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) =>
|
|||||||
transformedValue = transformTopLevelAtom(restValue);
|
transformedValue = transformTopLevelAtom(restValue);
|
||||||
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
|
coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue
|
||||||
return {key: 'expiresAt', value: coercedToDate};
|
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 '_rperm':
|
||||||
case '_wperm':
|
case '_wperm':
|
||||||
case '_email_verify_token':
|
case '_email_verify_token':
|
||||||
@@ -706,6 +719,7 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => {
|
|||||||
case '_email_verify_token':
|
case '_email_verify_token':
|
||||||
case '_perishable_token':
|
case '_perishable_token':
|
||||||
case '_tombstone':
|
case '_tombstone':
|
||||||
|
case '_email_verify_token_expires_at':
|
||||||
break;
|
break;
|
||||||
case '_session_token':
|
case '_session_token':
|
||||||
restObject['sessionToken'] = mongoObject[key];
|
restObject['sessionToken'] = mongoObject[key];
|
||||||
|
|||||||
@@ -379,6 +379,9 @@ export class PostgresStorageAdapter {
|
|||||||
if (object.expiresAt) {
|
if (object.expiresAt) {
|
||||||
object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() };
|
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) {
|
for (let fieldName in object) {
|
||||||
if (object[fieldName] === null) {
|
if (object[fieldName] === null) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export class Config {
|
|||||||
this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);
|
this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);
|
||||||
this.verifyUserEmails = cacheInfo.verifyUserEmails;
|
this.verifyUserEmails = cacheInfo.verifyUserEmails;
|
||||||
this.preventLoginWithUnverifiedEmail = cacheInfo.preventLoginWithUnverifiedEmail;
|
this.preventLoginWithUnverifiedEmail = cacheInfo.preventLoginWithUnverifiedEmail;
|
||||||
|
this.emailVerifyTokenValidityDuration = cacheInfo.emailVerifyTokenValidityDuration;
|
||||||
this.appName = cacheInfo.appName;
|
this.appName = cacheInfo.appName;
|
||||||
|
|
||||||
this.cacheController = cacheInfo.cacheController;
|
this.cacheController = cacheInfo.cacheController;
|
||||||
@@ -53,6 +54,7 @@ export class Config {
|
|||||||
this.sessionLength = cacheInfo.sessionLength;
|
this.sessionLength = cacheInfo.sessionLength;
|
||||||
this.expireInactiveSessions = cacheInfo.expireInactiveSessions;
|
this.expireInactiveSessions = cacheInfo.expireInactiveSessions;
|
||||||
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
|
this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this);
|
||||||
|
this.generateEmailVerifyTokenExpiresAt = this.generateEmailVerifyTokenExpiresAt.bind(this);
|
||||||
this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset;
|
this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +66,11 @@ export class Config {
|
|||||||
revokeSessionOnPasswordReset,
|
revokeSessionOnPasswordReset,
|
||||||
expireInactiveSessions,
|
expireInactiveSessions,
|
||||||
sessionLength,
|
sessionLength,
|
||||||
|
emailVerifyTokenValidityDuration
|
||||||
}) {
|
}) {
|
||||||
const emailAdapter = userController.adapter;
|
const emailAdapter = userController.adapter;
|
||||||
if (verifyUserEmails) {
|
if (verifyUserEmails) {
|
||||||
this.validateEmailConfiguration({emailAdapter, appName, publicServerURL});
|
this.validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof revokeSessionOnPasswordReset !== 'boolean') {
|
if (typeof revokeSessionOnPasswordReset !== 'boolean') {
|
||||||
@@ -83,7 +86,7 @@ export class Config {
|
|||||||
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
|
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateEmailConfiguration({emailAdapter, appName, publicServerURL}) {
|
static validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration}) {
|
||||||
if (!emailAdapter) {
|
if (!emailAdapter) {
|
||||||
throw 'An emailAdapter is required for e-mail verification and password resets.';
|
throw 'An emailAdapter is required for e-mail verification and password resets.';
|
||||||
}
|
}
|
||||||
@@ -93,6 +96,13 @@ export class Config {
|
|||||||
if (typeof publicServerURL !== 'string') {
|
if (typeof publicServerURL !== 'string') {
|
||||||
throw 'A public server url is required for e-mail verification and password resets.';
|
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() {
|
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() {
|
generateSessionExpiresAt() {
|
||||||
if (!this.expireInactiveSessions) {
|
if (!this.expireInactiveSessions) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const transformObjectACL = ({ ACL, ...result }) => {
|
|||||||
return 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 => {
|
const validateQuery = query => {
|
||||||
if (query.ACL) {
|
if (query.ACL) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on 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,
|
// acl: a list of strings. If the object to be updated has an ACL,
|
||||||
// one of the provided strings must provide the caller with
|
// one of the provided strings must provide the caller with
|
||||||
// write permissions.
|
// 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, {
|
DatabaseController.prototype.update = function(className, query, update, {
|
||||||
acl,
|
acl,
|
||||||
many,
|
many,
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export class UserController extends AdaptableController {
|
|||||||
if (this.shouldVerifyEmails) {
|
if (this.shouldVerifyEmails) {
|
||||||
user._email_verify_token = randomString(25);
|
user._email_verify_token = randomString(25);
|
||||||
user.emailVerified = false;
|
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.
|
// TODO: Better error here.
|
||||||
throw undefined;
|
throw undefined;
|
||||||
}
|
}
|
||||||
return this.config.database.update('_User', {
|
|
||||||
username: username,
|
let query = {username: username, _email_verify_token: token};
|
||||||
_email_verify_token: token
|
let updateFields = { emailVerified: true, _email_verify_token: {__op: 'Delete'}};
|
||||||
}, {emailVerified: true}).then(document => {
|
|
||||||
|
// 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) {
|
if (!document) {
|
||||||
throw undefined;
|
throw undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class ParseServer {
|
|||||||
maxUploadSize = '20mb',
|
maxUploadSize = '20mb',
|
||||||
verifyUserEmails = false,
|
verifyUserEmails = false,
|
||||||
preventLoginWithUnverifiedEmail = false,
|
preventLoginWithUnverifiedEmail = false,
|
||||||
|
emailVerifyTokenValidityDuration,
|
||||||
cacheAdapter,
|
cacheAdapter,
|
||||||
emailAdapter,
|
emailAdapter,
|
||||||
publicServerURL,
|
publicServerURL,
|
||||||
@@ -234,6 +235,7 @@ class ParseServer {
|
|||||||
userController: userController,
|
userController: userController,
|
||||||
verifyUserEmails: verifyUserEmails,
|
verifyUserEmails: verifyUserEmails,
|
||||||
preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail,
|
preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail,
|
||||||
|
emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration,
|
||||||
allowClientClassCreation: allowClientClassCreation,
|
allowClientClassCreation: allowClientClassCreation,
|
||||||
authDataManager: authDataManager(oauth, enableAnonymousUsers),
|
authDataManager: authDataManager(oauth, enableAnonymousUsers),
|
||||||
appName: appName,
|
appName: appName,
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export class UsersRouter extends ClassesRouter {
|
|||||||
emailAdapter: req.config.userController.adapter,
|
emailAdapter: req.config.userController.adapter,
|
||||||
appName: req.config.appName,
|
appName: req.config.appName,
|
||||||
publicServerURL: req.config.publicServerURL,
|
publicServerURL: req.config.publicServerURL,
|
||||||
|
emailVerifyTokenValidityDuration: req.config.emailVerifyTokenValidityDuration
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (typeof e === 'string') {
|
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",
|
help: "Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false",
|
||||||
action: booleanParser
|
action: booleanParser
|
||||||
},
|
},
|
||||||
|
"emailVerifyTokenValidityDuration": {
|
||||||
|
env: "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION",
|
||||||
|
help: "Email verification token validity duration",
|
||||||
|
action: numberParser("emailVerifyTokenValidityDuration")
|
||||||
|
},
|
||||||
"appName": {
|
"appName": {
|
||||||
env: "PARSE_SERVER_APP_NAME",
|
env: "PARSE_SERVER_APP_NAME",
|
||||||
help: "Sets the app name"
|
help: "Sets the app name"
|
||||||
|
|||||||
Reference in New Issue
Block a user