feat(server): try to verify emails on the server (#4794) r=vbudhram
This commit is contained in:
Родитель
5268e3ba79
Коммит
005549a9a7
|
@ -482,14 +482,22 @@ define(function (require, exports, module) {
|
||||||
* @param {String} code - the verification code
|
* @param {String} code - the verification code
|
||||||
* @param {Object} [options]
|
* @param {Object} [options]
|
||||||
* @param {Object} [options.service] - the service issuing signup request
|
* @param {Object} [options.service] - the service issuing signup request
|
||||||
|
* @param {String} [options.serverVerificationStatus] - the status of server verification
|
||||||
* @returns {Promise} - resolves when complete
|
* @returns {Promise} - resolves when complete
|
||||||
*/
|
*/
|
||||||
verifySignUp (code, options = {}) {
|
verifySignUp (code, options = {}) {
|
||||||
|
return p()
|
||||||
|
.then(() => {
|
||||||
|
if (options.serverVerificationStatus !== 'verified') {
|
||||||
|
// if server verification was not present or not successful
|
||||||
|
// then attempt client verification
|
||||||
return this._fxaClient.verifyCode(
|
return this._fxaClient.verifyCode(
|
||||||
this.get('uid'),
|
this.get('uid'),
|
||||||
code,
|
code,
|
||||||
options
|
options
|
||||||
)
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.set('verified', true);
|
this.set('verified', true);
|
||||||
|
|
||||||
|
|
|
@ -482,6 +482,7 @@ define(function (require, exports, module) {
|
||||||
* @param {String} code - verification code
|
* @param {String} code - verification code
|
||||||
* @param {Object} [options]
|
* @param {Object} [options]
|
||||||
* @param {Object} [options.service] - the service issuing signup request
|
* @param {Object} [options.service] - the service issuing signup request
|
||||||
|
* @param {String} [options.serverVerificationStatus] - the status of server verification
|
||||||
* @returns {Promise} - resolves with the account when complete
|
* @returns {Promise} - resolves with the account when complete
|
||||||
*/
|
*/
|
||||||
completeAccountSignUp (account, code, options) {
|
completeAccountSignUp (account, code, options) {
|
||||||
|
|
|
@ -74,6 +74,7 @@ define(function (require, exports, module) {
|
||||||
const code = verificationInfo.get('code');
|
const code = verificationInfo.get('code');
|
||||||
const options = {
|
const options = {
|
||||||
reminder: verificationInfo.get('reminder'),
|
reminder: verificationInfo.get('reminder'),
|
||||||
|
serverVerificationStatus: this.getSearchParam('server_verification') || null,
|
||||||
service: this.relier.get('service')
|
service: this.relier.get('service')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -691,6 +691,48 @@ define(function (require, exports, module) {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('verifySignUp', function () {
|
describe('verifySignUp', function () {
|
||||||
|
describe('with custom server verification value', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
sinon.stub(fxaClient, 'verifyCode', function () {
|
||||||
|
return p();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not call verifyCode with verified', function () {
|
||||||
|
account.set('uid', UID);
|
||||||
|
|
||||||
|
return account.verifySignUp('CODE', {
|
||||||
|
serverVerificationStatus: 'verified'
|
||||||
|
}).then(() => {
|
||||||
|
assert.isFalse(fxaClient.verifyCode.called);
|
||||||
|
assert.isTrue(account.get('verified'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls verifyCode with other status', function () {
|
||||||
|
account.set('uid', UID);
|
||||||
|
|
||||||
|
return account.verifySignUp('CODE', {
|
||||||
|
serverVerificationStatus: 'test'
|
||||||
|
}).then(() => {
|
||||||
|
assert.isTrue(fxaClient.verifyCode.called);
|
||||||
|
assert.isTrue(account.get('verified'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls verifyCode with undefined status', function () {
|
||||||
|
account.set('uid', UID);
|
||||||
|
|
||||||
|
return account.verifySignUp('CODE', {
|
||||||
|
serverVerificationStatus: undefined
|
||||||
|
}).then(() => {
|
||||||
|
assert.isTrue(fxaClient.verifyCode.called);
|
||||||
|
assert.isTrue(account.get('verified'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('without email opt-in', function () {
|
describe('without email opt-in', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
sinon.stub(fxaClient, 'verifyCode', function () {
|
sinon.stub(fxaClient, 'verifyCode', function () {
|
||||||
|
|
|
@ -198,7 +198,7 @@ define(function (require, exports, module) {
|
||||||
var args = account.verifySignUp.getCall(0).args;
|
var args = account.verifySignUp.getCall(0).args;
|
||||||
assert.isTrue(account.verifySignUp.called);
|
assert.isTrue(account.verifySignUp.called);
|
||||||
assert.ok(args[0]);
|
assert.ok(args[0]);
|
||||||
assert.deepEqual(args[1], {reminder: null, service: validService});
|
assert.deepEqual(args[1], {reminder: null, serverVerificationStatus: null, service: validService});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -217,7 +217,7 @@ define(function (require, exports, module) {
|
||||||
var args = account.verifySignUp.getCall(0).args;
|
var args = account.verifySignUp.getCall(0).args;
|
||||||
assert.isTrue(account.verifySignUp.called);
|
assert.isTrue(account.verifySignUp.called);
|
||||||
assert.ok(args[0]);
|
assert.ok(args[0]);
|
||||||
assert.deepEqual(args[1], {reminder: validReminder, service: null});
|
assert.deepEqual(args[1], {reminder: validReminder, serverVerificationStatus: null, service: null});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -237,7 +237,27 @@ define(function (require, exports, module) {
|
||||||
var args = account.verifySignUp.getCall(0).args;
|
var args = account.verifySignUp.getCall(0).args;
|
||||||
assert.isTrue(account.verifySignUp.called);
|
assert.isTrue(account.verifySignUp.called);
|
||||||
assert.ok(args[0]);
|
assert.ok(args[0]);
|
||||||
assert.deepEqual(args[1], {reminder: validReminder, service: validService});
|
assert.deepEqual(args[1], {reminder: validReminder, serverVerificationStatus: null, service: validService});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if server_verification is in the url', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
windowMock.location.search = '?code=' + validCode + '&uid=' + validUid +
|
||||||
|
'&server_verification=verified';
|
||||||
|
relier = new Relier({}, {
|
||||||
|
window: windowMock
|
||||||
|
});
|
||||||
|
relier.fetch();
|
||||||
|
initView(account);
|
||||||
|
return view.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('attempt to pass server_verification to verifySignUp', function () {
|
||||||
|
var args = account.verifySignUp.getCall(0).args;
|
||||||
|
assert.isTrue(account.verifySignUp.called);
|
||||||
|
assert.ok(args[0]);
|
||||||
|
assert.deepEqual(args[1], {reminder: null, serverVerificationStatus: 'verified', service: null});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ module.exports = function (config, i18n) {
|
||||||
redirectVersionedToUnversioned('complete_reset_password'),
|
redirectVersionedToUnversioned('complete_reset_password'),
|
||||||
redirectVersionedToUnversioned('reset_password'),
|
redirectVersionedToUnversioned('reset_password'),
|
||||||
redirectVersionedToUnversioned('verify_email'),
|
redirectVersionedToUnversioned('verify_email'),
|
||||||
|
require('./routes/get-verify-email')(),
|
||||||
require('./routes/get-frontend')(),
|
require('./routes/get-frontend')(),
|
||||||
require('./routes/get-terms-privacy')(i18n),
|
require('./routes/get-terms-privacy')(i18n),
|
||||||
require('./routes/get-index')(config),
|
require('./routes/get-index')(config),
|
||||||
|
|
|
@ -51,8 +51,7 @@ module.exports = function () {
|
||||||
'signup_verified',
|
'signup_verified',
|
||||||
'sms',
|
'sms',
|
||||||
'sms/sent',
|
'sms/sent',
|
||||||
'sms/why',
|
'sms/why'
|
||||||
'verify_email'
|
|
||||||
].join('|'); // prepare for use in a RegExp
|
].join('|'); // prepare for use in a RegExp
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
const url = require('url');
|
||||||
|
const got = require('got');
|
||||||
|
const logger = require('mozlog')('server.get-verify-email');
|
||||||
|
const config = require('../configuration');
|
||||||
|
const ravenClient = require('../../lib/raven').ravenMiddleware;
|
||||||
|
const joi = require('joi');
|
||||||
|
const validation = require('../validation');
|
||||||
|
|
||||||
|
const fxaAccountUrl = config.get('fxaccount_url');
|
||||||
|
const STRING_TYPE = validation.TYPES.STRING;
|
||||||
|
|
||||||
|
const VERIFICATION_ENDPOINT = `${fxaAccountUrl}/v1/recovery_email/verify_code`;
|
||||||
|
const VERIFICATION_TIMEOUT = 5000;
|
||||||
|
// Sentry combines both Info and Error into one if same name
|
||||||
|
const VERIFICATION_LEVEL_ERROR = 'VerificationValidationError';
|
||||||
|
const VERIFICATION_LEVEL_INFO = 'VerificationValidationInfo';
|
||||||
|
|
||||||
|
const REQUIRED_SCHEMA = {
|
||||||
|
'code': joi.string().hex().min(32).max(32).required(),
|
||||||
|
'uid': joi.string().hex().min(32).max(32).required()
|
||||||
|
};
|
||||||
|
|
||||||
|
const REPORT_ONLY_SCHEMA = {
|
||||||
|
'code': STRING_TYPE.alphanum().min(32).max(32).required(),
|
||||||
|
// resume token can be long, do not use the limited STRING_TYPE
|
||||||
|
'resume': joi.string().alphanum().optional(),
|
||||||
|
'service': STRING_TYPE.alphanum().max(100).optional(),
|
||||||
|
'uid': STRING_TYPE.alphanum().min(32).max(32).required(),
|
||||||
|
'utm_campaign': STRING_TYPE.alphanum().optional(),
|
||||||
|
'utm_content': STRING_TYPE.alphanum().optional(),
|
||||||
|
'utm_medium': STRING_TYPE.alphanum().optional(),
|
||||||
|
'utm_source': STRING_TYPE.alphanum().optional()
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = function () {
|
||||||
|
return {
|
||||||
|
method: 'get',
|
||||||
|
path: '/verify_email',
|
||||||
|
process: function (req, res, next) {
|
||||||
|
const rawQuery = url.parse(req.url).query;
|
||||||
|
|
||||||
|
// reset the url for the front-end router
|
||||||
|
req.url = '/';
|
||||||
|
|
||||||
|
if (req.query.server_verification) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
code: req.query.code,
|
||||||
|
uid: req.query.uid
|
||||||
|
};
|
||||||
|
|
||||||
|
joi.validate(data, REQUIRED_SCHEMA, (err) => {
|
||||||
|
if (err) {
|
||||||
|
ravenClient.captureMessage(VERIFICATION_LEVEL_ERROR, {
|
||||||
|
extra: {
|
||||||
|
details: err.details
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// if cannot validate required params then just forward to front-end
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.service) {
|
||||||
|
data.service = req.query.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.query.reminder) {
|
||||||
|
data.reminder = req.query.reminder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
body: data,
|
||||||
|
retries: 0,
|
||||||
|
timeout: {
|
||||||
|
connect: VERIFICATION_TIMEOUT,
|
||||||
|
socket: VERIFICATION_TIMEOUT
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
got.post(VERIFICATION_ENDPOINT, options)
|
||||||
|
.then(() => {
|
||||||
|
// In some cases the code can only be used once.
|
||||||
|
// Here we add an extra query param to signal the front-end that verification succeeded.
|
||||||
|
// See issue #4800
|
||||||
|
return res.redirect(`/verify_email?${rawQuery}&server_verification=verified`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
ravenClient.captureError(err);
|
||||||
|
logger.error(err);
|
||||||
|
// failed to verify, continue to front-end
|
||||||
|
next();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Passive validation and error reporting, could be made required in the future.
|
||||||
|
joi.validate(req.query, REPORT_ONLY_SCHEMA, (err) => {
|
||||||
|
if (err) {
|
||||||
|
ravenClient.captureMessage(VERIFICATION_LEVEL_INFO, {
|
||||||
|
extra: {
|
||||||
|
details: err.details
|
||||||
|
},
|
||||||
|
level: 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -4,3 +4,4 @@ extends: ../.eslintrc
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
id-blacklist: 0
|
id-blacklist: 0
|
||||||
|
strict: 0
|
||||||
|
|
|
@ -30,6 +30,7 @@ define([
|
||||||
'tests/server/statsd-collector',
|
'tests/server/statsd-collector',
|
||||||
'tests/server/raven',
|
'tests/server/raven',
|
||||||
'tests/server/routes/get-config',
|
'tests/server/routes/get-config',
|
||||||
|
'tests/server/routes/get-verify-email',
|
||||||
'tests/server/routes/get-fxa-client-configuration',
|
'tests/server/routes/get-fxa-client-configuration',
|
||||||
'tests/server/routes/get-openid-configuration',
|
'tests/server/routes/get-openid-configuration',
|
||||||
'tests/server/routes/get-index',
|
'tests/server/routes/get-index',
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
define([
|
||||||
|
'intern',
|
||||||
|
'intern!object',
|
||||||
|
'intern/chai!assert',
|
||||||
|
'intern/dojo/node!../../../server/lib/configuration',
|
||||||
|
'intern/dojo/Promise',
|
||||||
|
'intern/dojo/node!got',
|
||||||
|
'intern/dojo/node!fs',
|
||||||
|
'intern/dojo/node!path',
|
||||||
|
'intern/dojo/node!proxyquire',
|
||||||
|
'intern/dojo/node!sinon',
|
||||||
|
], function (intern, registerSuite, assert, config, dojoPromise, got, fs, path, proxyquire, sinon) {
|
||||||
|
|
||||||
|
function mockModule(mocks) {
|
||||||
|
return proxyquire(path.join(process.cwd(), 'server', 'lib', 'routes', 'get-verify-email'), mocks)();
|
||||||
|
}
|
||||||
|
|
||||||
|
let logger;
|
||||||
|
let mocks;
|
||||||
|
let ravenMock;
|
||||||
|
let gotMock;
|
||||||
|
const res = {
|
||||||
|
json: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
registerSuite({
|
||||||
|
name: 'verify_email',
|
||||||
|
|
||||||
|
beforeEach() {
|
||||||
|
gotMock = {
|
||||||
|
post: sinon.spy()
|
||||||
|
};
|
||||||
|
logger = {
|
||||||
|
error: sinon.spy()
|
||||||
|
};
|
||||||
|
ravenMock = {
|
||||||
|
ravenMiddleware: {
|
||||||
|
captureError: sinon.spy(),
|
||||||
|
captureMessage: sinon.spy()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mocks = {
|
||||||
|
'../../lib/raven': ravenMock,
|
||||||
|
'got': gotMock,
|
||||||
|
mozlog: () => {
|
||||||
|
return logger;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
'logs error without query params' () {
|
||||||
|
const dfd = new dojoPromise.Deferred();
|
||||||
|
const req = {
|
||||||
|
query: {
|
||||||
|
code: '',
|
||||||
|
uid: ''
|
||||||
|
},
|
||||||
|
url: '/verify_email'
|
||||||
|
};
|
||||||
|
|
||||||
|
mockModule(mocks).process(req, res, () => {
|
||||||
|
var c = ravenMock.ravenMiddleware.captureMessage;
|
||||||
|
var arg = c.args[0];
|
||||||
|
assert.equal(c.calledOnce, true);
|
||||||
|
assert.equal(arg[0], 'VerificationValidationError');
|
||||||
|
assert.equal(arg[1].extra.details[0].message, '"code" is not allowed to be empty');
|
||||||
|
dfd.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
return dfd.dojoPromise;
|
||||||
|
},
|
||||||
|
|
||||||
|
'no logs if successful' () {
|
||||||
|
const dfd = new dojoPromise.Deferred();
|
||||||
|
mocks.got = {
|
||||||
|
post: (req, res, next) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
resolve({
|
||||||
|
'statusCode': 200,
|
||||||
|
'statusMessage': 'OK'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
query: {
|
||||||
|
code: '12345678912345678912345678912312',
|
||||||
|
uid: '12345678912345678912345678912312'
|
||||||
|
},
|
||||||
|
url: '/verify_email'
|
||||||
|
};
|
||||||
|
|
||||||
|
mockModule(mocks).process(req, res, () => {
|
||||||
|
assert.equal(logger.error.callCount, 0);
|
||||||
|
assert.equal(ravenMock.ravenMiddleware.captureMessage.callCount, 0);
|
||||||
|
assert.equal(ravenMock.ravenMiddleware.captureError.callCount, 0);
|
||||||
|
|
||||||
|
req.query.something = 'else';
|
||||||
|
|
||||||
|
mockModule(mocks).process(req, res, () => {
|
||||||
|
assert.equal(logger.error.callCount, 0);
|
||||||
|
assert.equal(ravenMock.ravenMiddleware.captureMessage.callCount, 0);
|
||||||
|
assert.equal(ravenMock.ravenMiddleware.captureError.callCount, 0);
|
||||||
|
dfd.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return dfd.dojoPromise;
|
||||||
|
},
|
||||||
|
|
||||||
|
'logs errors when post fails' () {
|
||||||
|
const dfd = new dojoPromise.Deferred();
|
||||||
|
mocks.got = {
|
||||||
|
post: (req, res, next) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reject({
|
||||||
|
'host': '127.0.0.1:9000',
|
||||||
|
'hostname': '127.0.0.1',
|
||||||
|
'message': 'Response code 400 (Bad Request)',
|
||||||
|
'method': 'POST',
|
||||||
|
'path': '/v1/recovery_email/verify_code',
|
||||||
|
'statusCode': 400,
|
||||||
|
'statusMessage': 'Bad Request'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
query: {
|
||||||
|
code: '12345678912345678912345678912312',
|
||||||
|
uid: '12345678912345678912345678912312'
|
||||||
|
},
|
||||||
|
url: '/verify_email'
|
||||||
|
};
|
||||||
|
|
||||||
|
mockModule(mocks).process(req, res, () => {
|
||||||
|
assert.equal(logger.error.callCount, 1);
|
||||||
|
assert.equal(ravenMock.ravenMiddleware.captureMessage.callCount, 0);
|
||||||
|
assert.equal(ravenMock.ravenMiddleware.captureError.callCount, 1);
|
||||||
|
const result = ravenMock.ravenMiddleware.captureError.args[0][0];
|
||||||
|
assert.equal(result.statusCode, 400);
|
||||||
|
assert.equal(result.statusMessage, 'Bad Request');
|
||||||
|
|
||||||
|
dfd.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
return dfd.dojoPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
Загрузка…
Ссылка в новой задаче