refactor(fxa-auth-server): Added semicolons(semi rule)

This commit is contained in:
hritvi 2019-03-25 19:25:37 +05:30
Родитель 8a6490e22c
Коммит 1b910f0af9
263 изменённых файлов: 22601 добавлений и 22600 удалений

Просмотреть файл

@ -1,11 +1,12 @@
plugins:
- fxa
extends: plugin:fxa/server
rules:
handle-callback-err: 0
strict: 2
semi: 0
semi: [2, "always"]
indent: [0, 2]
parserOptions:

Просмотреть файл

@ -2,17 +2,17 @@
* 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'
'use strict';
module.exports = function (grunt) {
require('load-grunt-tasks')(grunt)
require('load-grunt-tasks')(grunt);
grunt.initConfig({
pkg: grunt.file.readJSON('package.json')
})
});
grunt.loadTasks('grunttasks')
grunt.loadTasks('grunttasks');
grunt.registerTask('default', ['eslint', 'copyright'])
grunt.registerTask('mailer', ['templates', 'copy:strings', 'l10n-extract'])
}
grunt.registerTask('default', ['eslint', 'copyright']);
grunt.registerTask('mailer', ['templates', 'copy:strings', 'l10n-extract']);
};

Просмотреть файл

@ -2,27 +2,27 @@
* 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'
'use strict';
// This MUST be the first require in the program.
// Only `require()` the newrelic module if explicity enabled.
// If required, modules will be instrumented.
require('../lib/newrelic')()
require('../lib/newrelic')();
const config = require('../config').getProperties()
const log = require('../lib/log')(config.log.level, 'fxa-email-bouncer')
const error = require('../lib/error')
const Token = require('../lib/tokens')(log, config)
const SQSReceiver = require('../lib/sqs')(log)
const bounces = require('../lib/email/bounces')(log, error)
const delivery = require('../lib/email/delivery')(log)
const notifications = require('../lib/email/notifications')(log, error)
const config = require('../config').getProperties();
const log = require('../lib/log')(config.log.level, 'fxa-email-bouncer');
const error = require('../lib/error');
const Token = require('../lib/tokens')(log, config);
const SQSReceiver = require('../lib/sqs')(log);
const bounces = require('../lib/email/bounces')(log, error);
const delivery = require('../lib/email/delivery')(log);
const notifications = require('../lib/email/notifications')(log, error);
const DB = require('../lib/db')(
config,
log,
Token
)
);
const {
bounceQueueUrl,
@ -30,17 +30,17 @@ const {
deliveryQueueUrl,
notificationQueueUrl,
region
} = config.emailNotifications
} = config.emailNotifications;
const bounceQueue = new SQSReceiver(region, [ bounceQueueUrl, complaintQueueUrl ])
const deliveryQueue = new SQSReceiver(region, [ deliveryQueueUrl ])
const notificationQueue = new SQSReceiver(region, [ notificationQueueUrl ])
const bounceQueue = new SQSReceiver(region, [ bounceQueueUrl, complaintQueueUrl ]);
const deliveryQueue = new SQSReceiver(region, [ deliveryQueueUrl ]);
const notificationQueue = new SQSReceiver(region, [ notificationQueueUrl ]);
DB.connect(config[config.db.backend])
.then(db => {
// bounces and delivery are now deprecated, we'll delete them
// as soon as we're 100% confident in fxa-email-service
bounces(bounceQueue, db)
delivery(deliveryQueue)
notifications(notificationQueue, db)
})
bounces(bounceQueue, db);
delivery(deliveryQueue);
notifications(notificationQueue, db);
});

Просмотреть файл

@ -2,42 +2,42 @@
* 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'
'use strict';
// This MUST be the first require in the program.
// Only `require()` the newrelic module if explicity enabled.
// If required, modules will be instrumented.
require('../lib/newrelic')()
require('../lib/newrelic')();
var jwtool = require('fxa-jwtool')
var P = require('../lib/promise')
var jwtool = require('fxa-jwtool');
var P = require('../lib/promise');
function run(config) {
var log = require('../lib/log')(config.log)
var getGeoData = require('../lib/geodb')(log)
var log = require('../lib/log')(config.log);
var getGeoData = require('../lib/geodb')(log);
// Force the geo to load and run at startup, not waiting for it to run on
// some route later.
const knownIp = '63.245.221.32' // Mozilla MTV
const location = getGeoData(knownIp)
log.info({ op: 'geodb.check', result: location })
const knownIp = '63.245.221.32'; // Mozilla MTV
const location = getGeoData(knownIp);
log.info({ op: 'geodb.check', result: location });
// RegExp instances serialise to empty objects, display regex strings instead.
const stringifiedConfig =
JSON.stringify(config, (k, v) =>
v && v.constructor === RegExp ? v.toString() : v
)
);
if (config.env !== 'prod') {
log.info(stringifiedConfig, 'starting config')
log.info(stringifiedConfig, 'starting config');
}
var error = require('../lib/error')
var Token = require('../lib/tokens')(log, config)
var Password = require('../lib/crypto/password')(log, config)
var UnblockCode = require('../lib/crypto/random').base32(config.signinUnblock.codeLength)
var error = require('../lib/error');
var Token = require('../lib/tokens')(log, config);
var Password = require('../lib/crypto/password')(log, config);
var UnblockCode = require('../lib/crypto/random').base32(config.signinUnblock.codeLength);
var signer = require('../lib/signer')(config.secretKeyFile, config.domain)
var signer = require('../lib/signer')(config.secretKeyFile, config.domain);
var serverPublicKeys = {
primary: jwtool.JWK.fromFile(
config.publicKeyFile,
@ -57,21 +57,21 @@ function run(config) {
}
)
: null
}
};
var Customs = require('../lib/customs')(log, error)
var Customs = require('../lib/customs')(log, error);
const Server = require('../lib/server')
let server = null
let senders = null
let statsInterval = null
let database = null
let customs = null
let oauthdb = null
const Server = require('../lib/server');
let server = null;
let senders = null;
let statsInterval = null;
let database = null;
let customs = null;
let oauthdb = null;
function logStatInfo() {
log.stat(server.stat())
log.stat(Password.stat())
log.stat(server.stat());
log.stat(Password.stat());
}
var DB = require('../lib/db')(
@ -79,7 +79,7 @@ function run(config) {
log,
Token,
UnblockCode
)
);
return P.all([
DB.connect(config[config.db.backend]),
@ -87,14 +87,14 @@ function run(config) {
])
.spread(
(db, translator) => {
database = db
const bounces = require('../lib/bounces')(config, db)
oauthdb = require('../lib/oauthdb')(log, config)
database = db;
const bounces = require('../lib/bounces')(config, db);
oauthdb = require('../lib/oauthdb')(log, config);
return require('../lib/senders')(log, config, error, bounces, translator, oauthdb)
.then(result => {
senders = result
customs = new Customs(config.customsUrl)
senders = result;
customs = new Customs(config.customsUrl);
var routes = require('../lib/routes')(
log,
serverPublicKeys,
@ -106,32 +106,32 @@ function run(config) {
Password,
config,
customs
)
);
statsInterval = setInterval(logStatInfo, 15000)
statsInterval = setInterval(logStatInfo, 15000);
async function init() {
server = await Server.create(log, error, config, routes, db, oauthdb, translator)
server = await Server.create(log, error, config, routes, db, oauthdb, translator);
try {
await server.start()
log.info({op: 'server.start.1', msg: 'running on ' + server.info.uri})
await server.start();
log.info({op: 'server.start.1', msg: 'running on ' + server.info.uri});
} catch (err) {
log.error(
{
op: 'server.start.1', msg: 'failed startup with error',
err: {message: err.message}
}
)
);
}
}
init()
init();
})
});
},
function (err) {
log.error({ op: 'DB.connect', err: { message: err.message } })
process.exit(1)
log.error({ op: 'DB.connect', err: { message: err.message } });
process.exit(1);
}
)
.then(() => {
@ -139,56 +139,56 @@ function run(config) {
log: log,
close() {
return new P((resolve) => {
log.info({ op: 'shutdown' })
clearInterval(statsInterval)
log.info({ op: 'shutdown' });
clearInterval(statsInterval);
server.stop().then(() => {
customs.close()
oauthdb.close()
customs.close();
oauthdb.close();
try {
senders.email.stop()
senders.email.stop();
} catch (e) {
// XXX: simplesmtp module may quit early and set socket to `false`, stopping it may fail
log.warn({op: 'shutdown', message: 'Mailer client already disconnected'})
log.warn({op: 'shutdown', message: 'Mailer client already disconnected'});
}
database.close()
resolve()
})
})
database.close();
resolve();
});
});
}
}
})
};
});
}
function main() {
const config = require('../config').getProperties()
const config = require('../config').getProperties();
run(config).then(server => {
process.on('uncaughtException', (err) => {
server.log.fatal(err)
process.exit(8)
})
server.log.fatal(err);
process.exit(8);
});
process.on('unhandledRejection', (reason, promise) => {
server.log.fatal({
op: 'promise.unhandledRejection',
error: reason
})
})
process.on('SIGINT', shutdown)
server.log.on('error', shutdown)
});
});
process.on('SIGINT', shutdown);
server.log.on('error', shutdown);
function shutdown() {
server.close().then(() => {
process.exit() //XXX: because of openid dep ಠ_ಠ
})
process.exit(); //XXX: because of openid dep ಠ_ಠ
});
}
})
.catch((err) => {
console.error(err) // eslint-disable-line no-console
process.exit(8)
})
console.error(err); // eslint-disable-line no-console
process.exit(8);
});
}
if (require.main === module) {
main()
main();
} else {
module.exports = run
module.exports = run;
}

Просмотреть файл

@ -2,33 +2,33 @@
* 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'
'use strict';
// This MUST be the first require in the program.
// Only `require()` the newrelic module if explicity enabled.
// If required, modules will be instrumented.
require('../lib/newrelic')()
require('../lib/newrelic')();
var config = require('../config').getProperties()
var log = require('../lib/log')(config.log.level, 'profile-server-messaging')
var Token = require('../lib/tokens')(log, config)
var SQSReceiver = require('../lib/sqs')(log)
var profileUpdates = require('../lib/profile/updates')(log)
var push = require('../lib/push')
var config = require('../config').getProperties();
var log = require('../lib/log')(config.log.level, 'profile-server-messaging');
var Token = require('../lib/tokens')(log, config);
var SQSReceiver = require('../lib/sqs')(log);
var profileUpdates = require('../lib/profile/updates')(log);
var push = require('../lib/push');
var DB = require('../lib/db')(
config,
log,
Token
)
);
var profileUpdatesQueue = new SQSReceiver(config.profileServerMessaging.region, [
config.profileServerMessaging.profileUpdatesQueueUrl
])
]);
DB.connect(config[config.db.backend])
.then(
function (db) {
profileUpdates(profileUpdatesQueue, push(log, db, config), db)
profileUpdates(profileUpdatesQueue, push(log, db, config), db);
}
)
);

Просмотреть файл

@ -2,17 +2,17 @@
* 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'
'use strict';
var fs = require('fs')
var path = require('path')
var url = require('url')
var convict = require('convict')
var DEFAULT_SUPPORTED_LANGUAGES = require('./supportedLanguages')
var fs = require('fs');
var path = require('path');
var url = require('url');
var convict = require('convict');
var DEFAULT_SUPPORTED_LANGUAGES = require('./supportedLanguages');
const ONE_DAY = 1000 * 60 * 60 * 24
const ONE_YEAR = ONE_DAY * 365
const FIVE_MINUTES = 1000 * 60 * 5
const ONE_DAY = 1000 * 60 * 60 * 24;
const ONE_YEAR = ONE_DAY * 365;
const FIVE_MINUTES = 1000 * 60 * 5;
var conf = convict({
env: {
@ -877,48 +877,48 @@ var conf = convict({
}
}
}
})
});
// handle configuration files. you can specify a CSV list of configuration
// files to process, which will be overlayed in order, in the CONFIG_FILES
// environment variable.
var envConfig = path.join(__dirname, conf.get('env') + '.json')
envConfig = envConfig + ',' + (process.env.CONFIG_FILES || '')
var files = envConfig.split(',').filter(fs.existsSync)
conf.loadFile(files)
conf.validate({ allowed: 'strict' })
var envConfig = path.join(__dirname, conf.get('env') + '.json');
envConfig = envConfig + ',' + (process.env.CONFIG_FILES || '');
var files = envConfig.split(',').filter(fs.existsSync);
conf.loadFile(files);
conf.validate({ allowed: 'strict' });
// set the public url as the issuer domain for assertions
conf.set('domain', url.parse(conf.get('publicUrl')).host)
conf.set('domain', url.parse(conf.get('publicUrl')).host);
// derive fxa-auth-mailer configuration from our content-server url
conf.set('smtp.accountSettingsUrl', conf.get('contentServer.url') + '/settings')
conf.set('smtp.accountRecoveryCodesUrl', conf.get('contentServer.url') + '/settings/two_step_authentication/recovery_codes')
conf.set('smtp.verificationUrl', conf.get('contentServer.url') + '/verify_email')
conf.set('smtp.passwordResetUrl', conf.get('contentServer.url') + '/complete_reset_password')
conf.set('smtp.initiatePasswordResetUrl', conf.get('contentServer.url') + '/reset_password')
conf.set('smtp.initiatePasswordChangeUrl', conf.get('contentServer.url') + '/settings/change_password')
conf.set('smtp.verifyLoginUrl', conf.get('contentServer.url') + '/complete_signin')
conf.set('smtp.reportSignInUrl', conf.get('contentServer.url') + '/report_signin')
conf.set('smtp.revokeAccountRecoveryUrl', conf.get('contentServer.url') + '/settings/account_recovery/confirm_revoke')
conf.set('smtp.createAccountRecoveryUrl', conf.get('contentServer.url') + '/settings/account_recovery/confirm_password')
conf.set('smtp.verifyPrimaryEmailUrl', conf.get('contentServer.url') + '/verify_primary_email')
conf.set('smtp.verifySecondaryEmailUrl', conf.get('contentServer.url') + '/verify_secondary_email')
conf.set('smtp.accountSettingsUrl', conf.get('contentServer.url') + '/settings');
conf.set('smtp.accountRecoveryCodesUrl', conf.get('contentServer.url') + '/settings/two_step_authentication/recovery_codes');
conf.set('smtp.verificationUrl', conf.get('contentServer.url') + '/verify_email');
conf.set('smtp.passwordResetUrl', conf.get('contentServer.url') + '/complete_reset_password');
conf.set('smtp.initiatePasswordResetUrl', conf.get('contentServer.url') + '/reset_password');
conf.set('smtp.initiatePasswordChangeUrl', conf.get('contentServer.url') + '/settings/change_password');
conf.set('smtp.verifyLoginUrl', conf.get('contentServer.url') + '/complete_signin');
conf.set('smtp.reportSignInUrl', conf.get('contentServer.url') + '/report_signin');
conf.set('smtp.revokeAccountRecoveryUrl', conf.get('contentServer.url') + '/settings/account_recovery/confirm_revoke');
conf.set('smtp.createAccountRecoveryUrl', conf.get('contentServer.url') + '/settings/account_recovery/confirm_password');
conf.set('smtp.verifyPrimaryEmailUrl', conf.get('contentServer.url') + '/verify_primary_email');
conf.set('smtp.verifySecondaryEmailUrl', conf.get('contentServer.url') + '/verify_secondary_email');
conf.set('isProduction', conf.get('env') === 'prod')
conf.set('isProduction', conf.get('env') === 'prod');
//sns endpoint is not to be set in production
if (conf.has('snsTopicEndpoint') && conf.get('env') !== 'dev') {
throw new Error('snsTopicEndpoint is only allowed in dev env')
throw new Error('snsTopicEndpoint is only allowed in dev env');
}
if (conf.get('env') === 'dev'){
if (! process.env.AWS_ACCESS_KEY_ID) {
process.env.AWS_ACCESS_KEY_ID = 'DEV_KEY_ID'
process.env.AWS_ACCESS_KEY_ID = 'DEV_KEY_ID';
}
if (! process.env.AWS_SECRET_ACCESS_KEY) {
process.env.AWS_SECRET_ACCESS_KEY = 'DEV_ACCESS_KEY'
process.env.AWS_SECRET_ACCESS_KEY = 'DEV_ACCESS_KEY';
}
}
@ -928,12 +928,12 @@ if (conf.get('isProduction')) {
'pushbox.key',
'metrics.flow_id_key',
'oauth.secretKey',
]
];
for (const key of SECRET_SETTINGS) {
if (conf.get(key) === conf.default(key)) {
throw new Error(`Config '${key}' must be set in production`)
throw new Error(`Config '${key}' must be set in production`);
}
}
}
module.exports = conf
module.exports = conf;

Просмотреть файл

@ -2,8 +2,8 @@
* 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'
'use strict';
const { popularDomains } = require('fxa-shared').email
const { popularDomains } = require('fxa-shared').email;
module.exports = new Set(popularDomains)
module.exports = new Set(popularDomains);

Просмотреть файл

@ -2,9 +2,9 @@
* 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'
'use strict';
// The list below should be kept in sync with:
// https://raw.githubusercontent.com/mozilla/fxa-content-server/master/server/config/production-locales.json
module.exports = require('fxa-shared').l10n.supportedLanguages
module.exports = require('fxa-shared').l10n.supportedLanguages;

Просмотреть файл

@ -4,7 +4,7 @@
// takes care of bumping the version number in package.json
'use strict'
'use strict';
module.exports = function (grunt) {
grunt.config('bump', {
@ -29,6 +29,6 @@ module.exports = function (grunt) {
pushTo: 'origin',
gitDescribeOptions: '--tags --always --abrev=1 --dirty=-d'
}
})
}
});
};

Просмотреть файл

@ -2,9 +2,9 @@
* 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'
'use strict';
var fxaChangelog = require('fxa-conventional-changelog')()
var fxaChangelog = require('fxa-conventional-changelog')();
module.exports = function (grunt) {
grunt.config('conventionalChangelog', {
@ -16,5 +16,5 @@ module.exports = function (grunt) {
release: {
src: 'CHANGELOG.md'
}
})
}
});
};

Просмотреть файл

@ -2,7 +2,7 @@
* 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'
'use strict';
module.exports = function (grunt) {
grunt.config('copyright', {
@ -14,5 +14,5 @@ module.exports = function (grunt) {
'<%= eslint.app.src %>'
]
}
})
}
});
};

Просмотреть файл

@ -2,7 +2,7 @@
* 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'
'use strict';
module.exports = function (grunt) {
grunt.config('eslint', {
@ -12,6 +12,6 @@ module.exports = function (grunt) {
'{fxa-oauth-server/bin/**/,fxa-oauth-server/lib/**/,fxa-oauth-server/test/**/,fxa-oauth-server/scripts/**/}*.js'
]
}
})
grunt.registerTask('quicklint', 'lint the modified files', 'newer:eslint')
}
});
grunt.registerTask('quicklint', 'lint the modified files', 'newer:eslint');
};

Просмотреть файл

@ -4,12 +4,12 @@
* 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'
'use strict';
const path = require('path')
const extract = require('jsxgettext-recursive-next')
const path = require('path');
const extract = require('jsxgettext-recursive-next');
const pkgroot = path.dirname(__dirname)
const pkgroot = path.dirname(__dirname);
module.exports = function (grunt) {
grunt.config('copy', {
@ -24,10 +24,10 @@ module.exports = function (grunt) {
]
}]
}
})
});
grunt.registerTask('l10n-extract', 'Extract strings from templates for localization.', function () {
var done = this.async()
var done = this.async();
var walker = extract({
'input-dir': path.join(pkgroot, 'lib/senders/templates'),
@ -39,7 +39,7 @@ module.exports = function (grunt) {
'.txt': 'handlebars',
'.html': 'handlebars'
}
})
});
walker.on('end', function () {
var jsWalker = extract({
@ -51,19 +51,19 @@ module.exports = function (grunt) {
parsers: {
'.js': 'javascript'
}
})
});
jsWalker.on('end', function () {
done()
})
})
})
done();
});
});
});
// load local Grunt tasks
grunt.registerTask('lint', 'Alias for eslint tasks', ['eslint'])
grunt.registerTask('templates', 'Alias for the template task', ['nunjucks'])
grunt.registerTask('lint', 'Alias for eslint tasks', ['eslint']);
grunt.registerTask('templates', 'Alias for the template task', ['nunjucks']);
grunt.registerTask('default', [ 'templates', 'copy:strings', 'l10n-extract' ])
grunt.registerTask('default', [ 'templates', 'copy:strings', 'l10n-extract' ]);
}
};

Просмотреть файл

@ -4,7 +4,7 @@
* 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'
'use strict';
module.exports = function (grunt) {
grunt.config('nunjucks', {
@ -31,7 +31,7 @@ module.exports = function (grunt) {
}
]
}
})
});
grunt.registerTask('templates', 'Alias for the template task', ['nunjucks'])
}
grunt.registerTask('templates', 'Alias for the template task', ['nunjucks']);
};

Просмотреть файл

@ -16,18 +16,18 @@
// NOTE: This task will not push this commit for you.
//
'use strict'
'use strict';
module.exports = function (grunt) {
grunt.registerTask('version', [
'bump-only:minor',
'conventionalChangelog:release',
'bump-commit'
])
]);
grunt.registerTask('version:patch', [
'bump-only:patch',
'conventionalChangelog:release',
'bump-commit'
])
}
]);
};

Просмотреть файл

@ -2,9 +2,9 @@
* 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'
'use strict';
const error = require('./error')
const error = require('./error');
// Maps our variety of verification methods down to a few short standard
// "authentication method reference" strings that we're happy to expose to
@ -19,7 +19,7 @@ const METHOD_TO_AMR = {
'email-2fa': 'email',
'totp-2fa': 'otp',
'recovery-code': 'otp'
}
};
// Maps AMR values to the type of authenticator they represent, e.g.
// "something you know" vs "something you have". This helps us determine
@ -28,7 +28,7 @@ const AMR_TO_TYPE = {
'pwd': 'know',
'email': 'know',
'otp': 'have'
}
};
module.exports = {
@ -37,28 +37,28 @@ module.exports = {
* for the given account, as amr value strings.
*/
availableAuthenticationMethods(db, account) {
const amrValues = new Set()
const amrValues = new Set();
// All accounts can authenticate with a password.
amrValues.add('pwd')
amrValues.add('pwd');
// All accounts can authenticate with an email confirmation loop.
amrValues.add('email')
amrValues.add('email');
// Some accounts have a TOTP token.
return db.totpToken(account.uid)
.then(
res => {
if (res && res.verified && res.enabled) {
amrValues.add('otp')
amrValues.add('otp');
}
},
err => {
if (err.errno !== error.ERRNO.TOTP_TOKEN_NOT_FOUND) {
throw err
throw err;
}
}
)
.then(() => {
return amrValues
})
return amrValues;
});
},
/**
@ -67,11 +67,11 @@ module.exports = {
* "email" while "totp-2fa" will map to "otp".
*/
verificationMethodToAMR(verificationMethod) {
const amr = METHOD_TO_AMR[verificationMethod]
const amr = METHOD_TO_AMR[verificationMethod];
if (! amr) {
throw new Error('unknown verificationMethod: ' + verificationMethod)
throw new Error('unknown verificationMethod: ' + verificationMethod);
}
return amr
return amr;
},
/**
@ -83,10 +83,10 @@ module.exports = {
* for level 3.
*/
maximumAssuranceLevel(amrValues) {
const types = new Set()
const types = new Set();
amrValues.forEach(amr => {
types.add(AMR_TO_TYPE[amr])
})
return types.size
types.add(AMR_TO_TYPE[amr]);
});
return types.size;
}
}
};

Просмотреть файл

@ -66,55 +66,55 @@
*
*/
'use strict'
'use strict';
const Joi = require('joi')
const Joi = require('joi');
const P = require('./promise')
const Pool = require('./pool')
const error = require('./error')
const P = require('./promise');
const Pool = require('./pool');
const error = require('./error');
module.exports = function createBackendServiceAPI(log, config, serviceName, methods) {
const SafeUrl = require('./safe-url')(log)
const SafeUrl = require('./safe-url')(log);
function Service(url, options = {}) {
this._headers = options.headers
this._pool = new Pool(url, options)
this._headers = options.headers;
this._pool = new Pool(url, options);
}
Service.prototype.close = function close() {
return this._pool.close()
}
return this._pool.close();
};
for (const methodName in methods) {
Service.prototype[methodName] = makeServiceMethod(methodName, methods[methodName])
Service.prototype[methodName] = makeServiceMethod(methodName, methods[methodName]);
}
return Service
return Service;
// Each declared service method gets turned into an async function
// that validates its inputs, makes the HTTP request using the
// connection pool, and validates the response.
function makeServiceMethod(methodName, opts) {
const path = new SafeUrl(opts.path)
const path = new SafeUrl(opts.path);
const validation = opts.validate || {}
const paramsSchema = Joi.compile(validation.params || Joi.object())
const querySchema = Joi.compile(validation.query || Joi.object())
const payloadSchema = Joi.compile(validation.payload || Joi.object())
const responseSchema = Joi.compile(validation.response || Joi.any())
const validation = opts.validate || {};
const paramsSchema = Joi.compile(validation.params || Joi.object());
const querySchema = Joi.compile(validation.query || Joi.object());
const payloadSchema = Joi.compile(validation.payload || Joi.object());
const responseSchema = Joi.compile(validation.response || Joi.any());
let expectedNumArgs = path.params().length
let expectedNumArgs = path.params().length;
if (validation.query) {
expectedNumArgs += 1
expectedNumArgs += 1;
}
if (validation.payload) {
expectedNumArgs += 1
expectedNumArgs += 1;
}
const fullMethodName = `${serviceName}.${methodName}`
const fullMethodName = `${serviceName}.${methodName}`;
// A thin wrapper around Joi.validate(), that logs the error and then
// wraps it in a generic "internal validation error" that can be returned
@ -124,33 +124,33 @@ module.exports = function createBackendServiceAPI(log, config, serviceName, meth
return new P((resolve, reject) => {
Joi.validate(value, schema, options, (err, value) => {
if (! err) {
return resolve(value)
return resolve(value);
}
log.error(fullMethodName, {
error: `${location} schema validation failed`,
message: err.message,
value
})
reject(error.internalValidationError(fullMethodName, { location, value }))
})
})
});
reject(error.internalValidationError(fullMethodName, { location, value }));
});
});
}
// A helper to make the request and return the response, or an error.
// This assumes you've done all the hard work of formulating params, body, etc.
async function sendRequest(pool, method, path, params, query, payload, headers) {
log.trace(fullMethodName, { params, query, payload })
log.trace(fullMethodName, { params, query, payload });
try {
return await pool.request(method, path, params, query, payload, headers)
return await pool.request(method, path, params, query, payload, headers);
} catch (err) {
// Re-throw 400-level errors, but wrap 500-level or generic errors
// into a "backend service failure" to propagate to the client.
if (err.errno || (err.statusCode && err.statusCode < 500)) {
throw err
throw err;
} else {
log.error(`${fullMethodName}.1`, { params, query, payload, err })
throw error.backendServiceFailure(serviceName, methodName)
log.error(`${fullMethodName}.1`, { params, query, payload, err });
throw error.backendServiceFailure(serviceName, methodName);
}
}
}
@ -160,28 +160,28 @@ module.exports = function createBackendServiceAPI(log, config, serviceName, meth
async function theServiceMethod(...args) {
// Interpret function arguments according to the declared schema.
if (args.length !== expectedNumArgs) {
throw new Error(`${fullMethodName} must be called with ${expectedNumArgs} arguments (${args.length} given)`)
throw new Error(`${fullMethodName} must be called with ${expectedNumArgs} arguments (${args.length} given)`);
}
let i = 0
let i = 0;
// The leading positional arguments correspond to individual path params,
// in the order they appear in the path template.
let params = {}
let params = {};
for (const param of path.params()) {
params[param] = args[i++]
params[param] = args[i++];
}
params = await validate('params', params, paramsSchema)
params = await validate('params', params, paramsSchema);
// Next are query params as a dict, if any.
const query = validation.query ? await validate('query', args[i++], querySchema) : {}
const query = validation.query ? await validate('query', args[i++], querySchema) : {};
// Next is request payload as a dict, if any.
const payload = validation.payload ? await validate('request', args[i++], payloadSchema) : {}
const payload = validation.payload ? await validate('request', args[i++], payloadSchema) : {};
// Unexpected extra fields in the service response should not be a fatal error,
// but we also don't want them polluting our code. So, stripUnknown=true.
const response = await sendRequest(this._pool, opts.method, path, params, query, payload, this._headers)
return await validate('response', response, responseSchema, { stripUnknown: true })
const response = await sendRequest(this._pool, opts.method, path, params, query, payload, this._headers);
return await validate('response', response, responseSchema, { stripUnknown: true });
}
// Expose the options for introspection by calling code if necessary.
theServiceMethod.opts = opts
return theServiceMethod
theServiceMethod.opts = opts;
return theServiceMethod;
}
}
};

Просмотреть файл

@ -2,35 +2,35 @@
* 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'
'use strict';
const error = require('./error')
const P = require('./promise')
const error = require('./error');
const P = require('./promise');
module.exports = (config, db) => {
const configBounces = config.smtp && config.smtp.bounces || {}
const BOUNCES_ENABLED = !! configBounces.enabled
const configBounces = config.smtp && config.smtp.bounces || {};
const BOUNCES_ENABLED = !! configBounces.enabled;
const BOUNCE_TYPE_HARD = 1
const BOUNCE_TYPE_SOFT = 2
const BOUNCE_TYPE_COMPLAINT = 3
const BOUNCE_TYPE_HARD = 1;
const BOUNCE_TYPE_SOFT = 2;
const BOUNCE_TYPE_COMPLAINT = 3;
const freeze = Object.freeze
const freeze = Object.freeze;
const BOUNCE_RULES = freeze({
[BOUNCE_TYPE_HARD]: freeze(configBounces.hard || {}),
[BOUNCE_TYPE_SOFT]: freeze(configBounces.soft || {}),
[BOUNCE_TYPE_COMPLAINT]: freeze(configBounces.complaint || {})
})
});
const ERRORS = {
[BOUNCE_TYPE_HARD]: error.emailBouncedHard,
[BOUNCE_TYPE_SOFT]: error.emailBouncedSoft,
[BOUNCE_TYPE_COMPLAINT]: error.emailComplaint
}
};
function checkBounces(email) {
return db.emailBounces(email)
.then(applyRules)
.then(applyRules);
}
// Relies on the order of the bounces array to be sorted by date,
@ -50,31 +50,31 @@ module.exports = (config, db) => {
count: 0,
latest: 0
}
}
const now = Date.now()
};
const now = Date.now();
bounces.forEach(bounce => {
const type = bounce.bounceType
const ruleSet = BOUNCE_RULES[type]
const type = bounce.bounceType;
const ruleSet = BOUNCE_RULES[type];
if (ruleSet) {
const tally = tallies[type]
const tier = ruleSet[tally.count]
const tally = tallies[type];
const tier = ruleSet[tally.count];
if (! tally.latest) {
tally.latest = bounce.createdAt
tally.latest = bounce.createdAt;
}
if (tier && bounce.createdAt > now - tier) {
throw ERRORS[type](tally.latest)
throw ERRORS[type](tally.latest);
}
tally.count++
tally.count++;
}
})
});
}
function disabled() {
return P.resolve()
return P.resolve();
}
return {
check: BOUNCES_ENABLED ? checkBounces : disabled
}
}
};
};

Просмотреть файл

@ -2,26 +2,26 @@
* 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'
'use strict';
const Memcached = require('memcached')
const P = require('./promise')
const Memcached = require('memcached');
const P = require('./promise');
P.promisifyAll(Memcached.prototype)
P.promisifyAll(Memcached.prototype);
const NOP = () => P.resolve()
const NOP = () => P.resolve();
const NULL_CACHE = {
addAsync: NOP,
delAsync: NOP,
getAsync: NOP
}
};
module.exports = (log, config, namespace) => {
let _cache
let _cache;
const CACHE_ADDRESS = config.memcached.address
const CACHE_IDLE = config.memcached.idle
const CACHE_LIFETIME = config.memcached.lifetime
const CACHE_ADDRESS = config.memcached.address;
const CACHE_IDLE = config.memcached.idle;
const CACHE_LIFETIME = config.memcached.lifetime;
return {
/**
@ -36,7 +36,7 @@ module.exports = (log, config, namespace) => {
*/
add (key, data) {
return getCache()
.then(cache => cache.addAsync(key, data, CACHE_LIFETIME))
.then(cache => cache.addAsync(key, data, CACHE_LIFETIME));
},
/**
@ -48,7 +48,7 @@ module.exports = (log, config, namespace) => {
*/
del (key) {
return getCache()
.then(cache => cache.delAsync(key))
.then(cache => cache.delAsync(key));
},
/**
@ -60,19 +60,19 @@ module.exports = (log, config, namespace) => {
*/
get (key) {
return getCache()
.then(cache => cache.getAsync(key))
}
.then(cache => cache.getAsync(key));
}
};
function getCache () {
return P.resolve()
.then(() => {
if (_cache) {
return _cache
return _cache;
}
if (CACHE_ADDRESS === 'none') {
_cache = NULL_CACHE
_cache = NULL_CACHE;
} else {
_cache = new Memcached(CACHE_ADDRESS, {
timeout: 500,
@ -81,14 +81,14 @@ module.exports = (log, config, namespace) => {
reconnect: 1000,
idle: CACHE_IDLE,
namespace
})
});
}
return _cache
return _cache;
})
.catch(err => {
log.error('cache.getCache', { err: err })
return NULL_CACHE
})
log.error('cache.getCache', { err: err });
return NULL_CACHE;
});
}
}
};

Просмотреть файл

@ -2,36 +2,36 @@
* 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'
'use strict';
module.exports.ONES = Buffer.from('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 'hex')
module.exports.ONES = Buffer.from('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 'hex');
module.exports.buffersAreEqual = function buffersAreEqual(buffer1, buffer2) {
buffer1 = Buffer.from(buffer1, 'hex')
buffer2 = Buffer.from(buffer2, 'hex')
var mismatch = buffer1.length - buffer2.length
buffer1 = Buffer.from(buffer1, 'hex');
buffer2 = Buffer.from(buffer2, 'hex');
var mismatch = buffer1.length - buffer2.length;
if (mismatch) {
return false
return false;
}
for (var i = 0; i < buffer1.length; i++) {
mismatch |= buffer1[i] ^ buffer2[i]
mismatch |= buffer1[i] ^ buffer2[i];
}
return mismatch === 0
}
return mismatch === 0;
};
module.exports.xorBuffers = function xorBuffers(buffer1, buffer2) {
buffer1 = Buffer.from(buffer1, 'hex')
buffer2 = Buffer.from(buffer2, 'hex')
buffer1 = Buffer.from(buffer1, 'hex');
buffer2 = Buffer.from(buffer2, 'hex');
if (buffer1.length !== buffer2.length) {
throw new Error(
'XOR buffers must be same length (%d != %d)',
buffer1.length,
buffer2.length
)
);
}
var result = Buffer.alloc(buffer1.length)
var result = Buffer.alloc(buffer1.length);
for (var i = 0; i < buffer1.length; i++) {
result[i] = buffer1[i] ^ buffer2[i]
result[i] = buffer1[i] ^ buffer2[i];
}
return result
}
return result;
};

Просмотреть файл

@ -2,35 +2,35 @@
* 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'
'use strict';
var HKDF = require('hkdf')
var P = require('../promise')
var HKDF = require('hkdf');
var P = require('../promise');
const NAMESPACE = 'identity.mozilla.com/picl/v1/'
const NAMESPACE = 'identity.mozilla.com/picl/v1/';
function KWE(name, email) {
return Buffer.from(NAMESPACE + name + ':' + email)
return Buffer.from(NAMESPACE + name + ':' + email);
}
function KW(name) {
return Buffer.from(NAMESPACE + name)
return Buffer.from(NAMESPACE + name);
}
function hkdf(km, info, salt, len) {
var d = P.defer()
var df = new HKDF('sha256', salt, km)
var d = P.defer();
var df = new HKDF('sha256', salt, km);
df.derive(
KW(info),
len,
function(key) {
d.resolve(key)
d.resolve(key);
}
)
return d.promise
);
return d.promise;
}
hkdf.KW = KW
hkdf.KWE = KWE
hkdf.KW = KW;
hkdf.KWE = KWE;
module.exports = hkdf
module.exports = hkdf;

Просмотреть файл

@ -2,80 +2,80 @@
* 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'
'use strict';
var P = require('../promise')
var hkdf = require('./hkdf')
var butil = require('./butil')
var P = require('../promise');
var hkdf = require('./hkdf');
var butil = require('./butil');
module.exports = function(log, config) {
var scrypt = require('./scrypt')(log, config)
var scrypt = require('./scrypt')(log, config);
var hashVersions = {
0: function (authPW, authSalt) {
return P.resolve(butil.xorBuffers(authPW, authSalt))
return P.resolve(butil.xorBuffers(authPW, authSalt));
},
1: function (authPW, authSalt) {
return scrypt.hash(authPW, authSalt, 65536, 8, 1, 32)
}
return scrypt.hash(authPW, authSalt, 65536, 8, 1, 32);
}
};
function Password(authPW, authSalt, version) {
version = typeof(version) === 'number' ? version : 1
this.authPW = Buffer.from(authPW, 'hex')
this.authSalt = Buffer.from(authSalt, 'hex')
this.version = version
this.stretchPromise = hashVersions[version](this.authPW, this.authSalt)
this.verifyHashPromise = this.stretchPromise.then(hkdfVerify)
version = typeof(version) === 'number' ? version : 1;
this.authPW = Buffer.from(authPW, 'hex');
this.authSalt = Buffer.from(authSalt, 'hex');
this.version = version;
this.stretchPromise = hashVersions[version](this.authPW, this.authSalt);
this.verifyHashPromise = this.stretchPromise.then(hkdfVerify);
}
Password.prototype.stretchedPassword = function () {
return this.stretchPromise
}
return this.stretchPromise;
};
Password.prototype.verifyHash = function () {
return this.verifyHashPromise
}
return this.verifyHashPromise;
};
Password.prototype.matches = function (verifyHash) {
return this.verifyHash().then(
function (hash) {
return butil.buffersAreEqual(hash, verifyHash)
}
)
return butil.buffersAreEqual(hash, verifyHash);
}
);
};
Password.prototype.unwrap = function (wrapped, context) {
context = context || 'wrapwrapKey'
context = context || 'wrapwrapKey';
return this.stretchedPassword().then(
function (stretched) {
return hkdf(stretched, context, null, 32)
.then(
function (wrapper) {
return butil.xorBuffers(wrapper, wrapped).toString('hex')
return butil.xorBuffers(wrapper, wrapped).toString('hex');
}
)
);
}
)
}
Password.prototype.wrap = Password.prototype.unwrap
);
};
Password.prototype.wrap = Password.prototype.unwrap;
function hkdfVerify(stretched) {
return hkdf(stretched, 'verifyHash', null, 32).then(buf => buf.toString('hex'))
return hkdf(stretched, 'verifyHash', null, 32).then(buf => buf.toString('hex'));
}
Password.stat = function () {
// Reset the high-water-mark whenever it is read.
var numPendingHWM = scrypt.numPendingHWM
scrypt.numPendingHWM = scrypt.numPending
var numPendingHWM = scrypt.numPendingHWM;
scrypt.numPendingHWM = scrypt.numPending;
return {
stat: 'scrypt',
maxPending: scrypt.maxPending,
numPending: scrypt.numPending,
numPendingHWM: numPendingHWM
}
}
};
};
return Password
}
return Password;
};

Просмотреть файл

@ -2,10 +2,10 @@
* 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'
'use strict';
var sjcl = require('sjcl')
var P = require('../promise')
var sjcl = require('sjcl');
var P = require('../promise');
/** pbkdf2 string creator
*
@ -14,11 +14,11 @@ var P = require('../promise')
* @return {Buffer} the derived key hex buffer.
*/
function derive(input, salt, iterations, len) {
var password = sjcl.codec.hex.toBits(input.toString('hex'))
var saltBits = sjcl.codec.hex.toBits(salt.toString('hex'))
var result = sjcl.misc.pbkdf2(password, saltBits, iterations, len * 8, sjcl.misc.hmac)
var password = sjcl.codec.hex.toBits(input.toString('hex'));
var saltBits = sjcl.codec.hex.toBits(salt.toString('hex'));
var result = sjcl.misc.pbkdf2(password, saltBits, iterations, len * 8, sjcl.misc.hmac);
return P.resolve(Buffer.from(sjcl.codec.hex.fromBits(result), 'hex'))
return P.resolve(Buffer.from(sjcl.codec.hex.fromBits(result), 'hex'));
}
module.exports.derive = derive
module.exports.derive = derive;

Просмотреть файл

@ -2,48 +2,48 @@
* 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'
'use strict';
const assert = require('assert')
const randomBytes = require('../promise').promisify(require('crypto').randomBytes)
const assert = require('assert');
const randomBytes = require('../promise').promisify(require('crypto').randomBytes);
const BASE32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
const BASE10 = '0123456789'
const BASE32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
const BASE10 = '0123456789';
// some sanity checks, hard to test, private to this mdoule
assert.equal(BASE32.length, 32, 'ALPHABET is 32 characters')
assert.equal(BASE32.indexOf('I'), -1, 'should not contain I')
assert.equal(BASE32.indexOf('L'), -1, 'should not contain L')
assert.equal(BASE32.indexOf('O'), -1, 'should not contain O')
assert.equal(BASE32.indexOf('U'), -1, 'should not contain U')
assert.equal(BASE32.length, 32, 'ALPHABET is 32 characters');
assert.equal(BASE32.indexOf('I'), -1, 'should not contain I');
assert.equal(BASE32.indexOf('L'), -1, 'should not contain L');
assert.equal(BASE32.indexOf('O'), -1, 'should not contain O');
assert.equal(BASE32.indexOf('U'), -1, 'should not contain U');
function random(bytes) {
if (arguments.length > 1) {
bytes = Array.from(arguments)
const sum = bytes.reduce((acc, val) => acc + val, 0)
bytes = Array.from(arguments);
const sum = bytes.reduce((acc, val) => acc + val, 0);
return randomBytes(sum).then(buf => {
let pos = 0
let pos = 0;
return bytes.map(num => {
const slice = buf.slice(pos, pos + num)
pos += num
return slice
})
})
const slice = buf.slice(pos, pos + num);
pos += num;
return slice;
});
});
} else {
return randomBytes(bytes)
return randomBytes(bytes);
}
}
random.hex = function hex() {
return random.apply(null, arguments).then(bufs => {
if (Array.isArray(bufs)) {
return bufs.map(buf => buf.toString('hex'))
return bufs.map(buf => buf.toString('hex'));
} else {
return bufs.toString('hex')
return bufs.toString('hex');
}
})
}
});
};
function randomValue(base, len) {
// To minimize bias in element selection, we generate a
@ -51,23 +51,23 @@ function randomValue(base, len) {
// This requires 4 bytes of randomness per element.
return random(len * 4)
.then(bytes => {
const out = []
const out = [];
for (let i = 0; i < len; i++) {
const r = bytes.readUInt32BE(4 * i) / 2**32
out.push(base[Math.floor(r * base.length)])
const r = bytes.readUInt32BE(4 * i) / 2**32;
out.push(base[Math.floor(r * base.length)]);
}
return out.join('')
})
return out.join('');
});
}
random.base10 = function(len) {
return () => randomValue(BASE10, len)
}
return () => randomValue(BASE10, len);
};
random.base32 = function(len) {
return () => randomValue(BASE32, len)
}
return () => randomValue(BASE32, len);
};
module.exports = random
module.exports = random;

Просмотреть файл

@ -2,22 +2,22 @@
* 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'
'use strict';
const crypto = require('crypto')
const P = require('../promise')
const crypto = require('crypto');
const P = require('../promise');
// Magic numbers from the node crypto docs:
// https://nodejs.org/api/crypto.html#crypto_crypto_scrypt_password_salt_keylen_options_callback
const DEFAULT_N = 16384
const DEFAULT_R = 8
const MAXMEM_MULTIPLIER = 256
const DEFAULT_MAXMEM = MAXMEM_MULTIPLIER * DEFAULT_N * DEFAULT_R
const DEFAULT_N = 16384;
const DEFAULT_R = 8;
const MAXMEM_MULTIPLIER = 256;
const DEFAULT_MAXMEM = MAXMEM_MULTIPLIER * DEFAULT_N * DEFAULT_R;
// The maximum numer of hash operations allowed concurrently.
// This can be customized by setting the `maxPending` attribute on the
// exported object, or by setting the `scrypt.maxPending` config option.
const DEFAULT_MAX_PENDING = 100
const DEFAULT_MAX_PENDING = 100;
module.exports = function(log, config) {
@ -29,9 +29,9 @@ module.exports = function(log, config) {
numPendingHWM: 0,
// The maximum number of hash operations that may be in progress.
maxPending: DEFAULT_MAX_PENDING
}
};
if (config.scrypt && config.scrypt.hasOwnProperty('maxPending')) {
scrypt.maxPending = config.scrypt.maxPending
scrypt.maxPending = config.scrypt.maxPending;
}
/** hash - Creates an scrypt hash asynchronously
@ -41,28 +41,28 @@ module.exports = function(log, config) {
* @returns {Object} d.promise Deferred promise
*/
function hash(input, salt, N, r, p, len) {
var d = P.defer()
var d = P.defer();
if (scrypt.maxPending > 0 && scrypt.numPending > scrypt.maxPending) {
log.warn('scrypt.maxPendingExceeded')
d.reject(new Error('too many pending scrypt hashes'))
log.warn('scrypt.maxPendingExceeded');
d.reject(new Error('too many pending scrypt hashes'));
} else {
scrypt.numPending += 1
scrypt.numPending += 1;
if (scrypt.numPending > scrypt.numPendingHWM) {
scrypt.numPendingHWM = scrypt.numPending
scrypt.numPendingHWM = scrypt.numPending;
}
let maxmem = DEFAULT_MAXMEM
let maxmem = DEFAULT_MAXMEM;
if (N > DEFAULT_N || r > DEFAULT_R) {
// Conservatively prevent `memory limit exceeded` errors. See the docs for more info:
// https://nodejs.org/api/crypto.html#crypto_crypto_scrypt_password_salt_keylen_options_callback
maxmem = MAXMEM_MULTIPLIER * (N || DEFAULT_N) * (r || DEFAULT_R)
maxmem = MAXMEM_MULTIPLIER * (N || DEFAULT_N) * (r || DEFAULT_R);
}
crypto.scrypt(input, salt, len, { N, r, p, maxmem }, (err, hash) => {
scrypt.numPending -= 1
return err ? d.reject(err) : d.resolve(hash.toString('hex'))
})
scrypt.numPending -= 1;
return err ? d.reject(err) : d.resolve(hash.toString('hex'));
});
}
return d.promise
return d.promise;
}
return scrypt
}
return scrypt;
};

Просмотреть файл

@ -2,15 +2,15 @@
* 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'
'use strict';
const Joi = require('joi')
const createBackendServiceAPI = require('./backendService')
const config = require('../config')
const Joi = require('joi');
const createBackendServiceAPI = require('./backendService');
const config = require('../config');
const localizeTimestamp = require('fxa-shared').l10n.localizeTimestamp({
supportedLanguages: config.get('i18n').supportedLanguages,
defaultLanguage: config.get('i18n').defaultLanguage
})
});
module.exports = function (log, error) {
@ -97,30 +97,30 @@ module.exports = function (log, error) {
}
},
})
});
// Perform a deep clone of payload and remove user password.
function sanitizePayload(payload) {
if (! payload) {
return
return;
}
const clonePayload = Object.assign({}, payload)
const clonePayload = Object.assign({}, payload);
if (clonePayload.authPW) {
delete clonePayload.authPW
delete clonePayload.authPW;
}
if (clonePayload.oldAuthPW) {
delete clonePayload.oldAuthPW
delete clonePayload.oldAuthPW;
}
return clonePayload
return clonePayload;
}
function Customs(url) {
if (url === 'none') {
const noblock = async function () { return { block: false }}
const noop = async function () {}
const noblock = async function () { return { block: false };};
const noop = async function () {};
this.api = {
check: noblock,
checkAuthenticated: noblock,
@ -128,9 +128,9 @@ module.exports = function (log, error) {
failedLoginAttempt: noop,
passwordReset: noop,
close: noop
}
};
} else {
this.api = new CustomsAPI(url, { timeout: 3000 })
this.api = new CustomsAPI(url, { timeout: 3000 });
}
}
@ -142,23 +142,23 @@ module.exports = function (log, error) {
headers: request.headers,
query: request.query,
payload: sanitizePayload(request.payload)
})
return handleCustomsResult(request, result)
}
});
return handleCustomsResult(request, result);
};
// Annotate the request and/or throw an error
// based on the check result returned by customs-server.
function handleCustomsResult (request, result) {
if (result.suspect) {
request.app.isSuspiciousRequest = true
request.app.isSuspiciousRequest = true;
}
if (result.block) {
// Log a flow event that the user got blocked.
request.emitMetricsEvent('customs.blocked')
request.emitMetricsEvent('customs.blocked');
const unblock = !! result.unblock
const unblock = !! result.unblock;
if (result.retryAfter) {
// Create a localized retryAfterLocalized value from retryAfter.
@ -166,12 +166,12 @@ module.exports = function (log, error) {
const retryAfterLocalized = localizeTimestamp.format(
Date.now() + result.retryAfter * 1000,
request.headers['accept-language']
)
);
throw error.tooManyRequests(result.retryAfter, retryAfterLocalized, unblock)
throw error.tooManyRequests(result.retryAfter, retryAfterLocalized, unblock);
}
throw error.requestBlocked(unblock)
throw error.requestBlocked(unblock);
}
}
@ -180,39 +180,39 @@ module.exports = function (log, error) {
action: action,
ip: request.app.clientAddress,
uid: uid
})
return handleCustomsResult(request, result)
}
});
return handleCustomsResult(request, result);
};
Customs.prototype.checkIpOnly = async function (request, action) {
const result = await this.api.checkIpOnly({
ip: request.app.clientAddress,
action: action
})
return handleCustomsResult(request, result)
}
});
return handleCustomsResult(request, result);
};
Customs.prototype.flag = async function (ip, info) {
var email = info.email
var errno = info.errno || error.ERRNO.UNEXPECTED_ERROR
var email = info.email;
var errno = info.errno || error.ERRNO.UNEXPECTED_ERROR;
// There's no useful information in the HTTP response, ignore it.
await this.api.failedLoginAttempt({
ip: ip,
email: email,
errno: errno
})
}
});
};
Customs.prototype.reset = async function (email) {
// There's no useful information in the HTTP response, ignore it.
await this.api.passwordReset({
email: email
})
}
});
};
Customs.prototype.close = function () {
return this.api.close()
}
return this.api.close();
};
return Customs
}
return Customs;
};

1088
lib/db.js

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -2,16 +2,16 @@
* 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'
'use strict';
const isA = require('joi')
const validators = require('./routes/validators')
const isA = require('joi');
const validators = require('./routes/validators');
const {
DISPLAY_SAFE_UNICODE_WITH_NON_BMP,
HEX_STRING,
URL_SAFE_BASE_64
} = validators
const PUSH_SERVER_REGEX = require('../config').get('push.allowedServerRegex')
} = validators;
const PUSH_SERVER_REGEX = require('../config').get('push.allowedServerRegex');
const SCHEMA = {
id: isA.string().length(32).regex(HEX_STRING),
@ -32,98 +32,98 @@ const SCHEMA = {
pushEndpointExpired: isA.boolean().strict(),
// An object mapping command names to metadata bundles.
availableCommands: isA.object().pattern(validators.DEVICE_COMMAND_NAME, isA.string().max(2048))
}
};
module.exports = (log, db, push) => {
return { isSpuriousUpdate, upsert, synthesizeName }
return { isSpuriousUpdate, upsert, synthesizeName };
// Clients have been known to send spurious device updates,
// which generates lots of unnecessary database load.
// Check if anything has actually changed.
function isSpuriousUpdate (payload, token) {
if (! token.deviceId || payload.id !== token.deviceId) {
return false
return false;
}
if (payload.name && payload.name !== token.deviceName) {
return false
return false;
}
if (payload.type && payload.type !== token.deviceType) {
return false
return false;
}
if (payload.pushCallback && payload.pushCallback !== token.deviceCallbackURL) {
return false
return false;
}
if (payload.pushPublicKey && payload.pushPublicKey !== token.deviceCallbackPublicKey) {
return false
return false;
}
if (payload.availableCommands) {
if (! token.deviceAvailableCommands) {
return false
return false;
}
if (! isLike(token.deviceAvailableCommands, payload.availableCommands)) {
return false
return false;
}
if (! isLike(payload.availableCommands, token.deviceAvailableCommands)) {
return false
return false;
}
}
return true
return true;
}
function upsert (request, credentials, deviceInfo) {
let operation, event, result
let operation, event, result;
if (deviceInfo.id) {
operation = 'updateDevice'
event = 'device.updated'
operation = 'updateDevice';
event = 'device.updated';
} else {
operation = 'createDevice'
event = 'device.created'
operation = 'createDevice';
event = 'device.created';
if (! deviceInfo.name) {
deviceInfo.name = credentials.client && credentials.client.name || ''
deviceInfo.name = credentials.client && credentials.client.name || '';
}
}
deviceInfo.sessionTokenId = credentials.id
deviceInfo.refreshTokenId = credentials.refreshTokenId
deviceInfo.sessionTokenId = credentials.id;
deviceInfo.refreshTokenId = credentials.refreshTokenId;
const isPlaceholderDevice = ! deviceInfo.id && ! deviceInfo.name && ! deviceInfo.type
const isPlaceholderDevice = ! deviceInfo.id && ! deviceInfo.name && ! deviceInfo.type;
return db[operation](credentials.uid, deviceInfo)
.then(device => {
result = device
result = device;
return request.emitMetricsEvent(event, {
uid: credentials.uid,
device_id: result.id,
is_placeholder: isPlaceholderDevice
})
});
})
.then(() => {
if (operation === 'createDevice') {
// Clients expect this notification to always include a name,
// so try to synthesize one if necessary.
let deviceName = result.name
let deviceName = result.name;
if (! deviceName) {
deviceName = synthesizeName(deviceInfo)
deviceName = synthesizeName(deviceInfo);
}
if (credentials.tokenVerified) {
request.app.devices.then(devices => {
const otherDevices = devices.filter(device => device.id !== result.id)
return push.notifyDeviceConnected(credentials.uid, otherDevices, deviceName)
})
const otherDevices = devices.filter(device => device.id !== result.id);
return push.notifyDeviceConnected(credentials.uid, otherDevices, deviceName);
});
}
if (isPlaceholderDevice) {
log.info('device:createPlaceholder', {
uid: credentials.uid,
id: result.id
})
});
}
return log.notifyAttachedServices('device:create', request, {
uid: credentials.uid,
@ -131,55 +131,55 @@ module.exports = (log, db, push) => {
type: result.type,
timestamp: result.createdAt,
isPlaceholder: isPlaceholderDevice
})
});
}
})
.then(function () {
delete result.sessionTokenId
delete result.refreshTokenId
return result
})
delete result.sessionTokenId;
delete result.refreshTokenId;
return result;
});
}
function synthesizeName (device) {
const uaBrowser = device.uaBrowser
const uaBrowserVersion = device.uaBrowserVersion
const uaOS = device.uaOS
const uaOSVersion = device.uaOSVersion
const uaFormFactor = device.uaFormFactor
let result = ''
const uaBrowser = device.uaBrowser;
const uaBrowserVersion = device.uaBrowserVersion;
const uaOS = device.uaOS;
const uaOSVersion = device.uaOSVersion;
const uaFormFactor = device.uaFormFactor;
let result = '';
if (uaBrowser) {
if (uaBrowserVersion) {
const splitIndex = uaBrowserVersion.indexOf('.')
result = `${uaBrowser} ${splitIndex === -1 ? uaBrowserVersion : uaBrowserVersion.substr(0, splitIndex)}`
const splitIndex = uaBrowserVersion.indexOf('.');
result = `${uaBrowser} ${splitIndex === -1 ? uaBrowserVersion : uaBrowserVersion.substr(0, splitIndex)}`;
} else {
result = uaBrowser
result = uaBrowser;
}
if (uaOS || uaFormFactor) {
result += ', '
result += ', ';
}
}
if (uaFormFactor) {
return `${result}${uaFormFactor}`
return `${result}${uaFormFactor}`;
}
if (uaOS) {
result += uaOS
result += uaOS;
if (uaOSVersion) {
result += ` ${uaOSVersion}`
result += ` ${uaOSVersion}`;
}
}
return result
return result;
}
}
};
module.exports.schema = SCHEMA
module.exports.schema = SCHEMA;
function isLike (object, archetype) {
return Object.entries(archetype).every(([ key, value ]) => object[key] === value)
return Object.entries(archetype).every(([ key, value ]) => object[key] === value);
}

Просмотреть файл

@ -2,32 +2,32 @@
* 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'
'use strict';
const eaddrs = require('email-addresses')
const P = require('./../promise')
const utils = require('./utils/helpers')
const isValidEmailAddress = require('./../routes/validators').isValidEmailAddress
const SIX_HOURS = 1000 * 60 * 60 * 6
const eaddrs = require('email-addresses');
const P = require('./../promise');
const utils = require('./utils/helpers');
const isValidEmailAddress = require('./../routes/validators').isValidEmailAddress;
const SIX_HOURS = 1000 * 60 * 60 * 6;
module.exports = function (log, error) {
return function start(bounceQueue, db) {
function accountDeleted(uid, email) {
log.info('accountDeleted', { uid: uid, email: email })
log.info('accountDeleted', { uid: uid, email: email });
}
function gotError(email, err) {
log.error('databaseError', { email: email, err: err })
log.error('databaseError', { email: email, err: err });
}
function findEmailRecord(email) {
return db.accountRecord(email)
return db.accountRecord(email);
}
function recordBounce(bounce) {
return db.createEmailBounce(bounce)
return db.createEmailBounce(bounce);
}
function deleteAccountIfUnverifiedNew(record) {
@ -37,43 +37,43 @@ module.exports = function (log, error) {
.then(
accountDeleted.bind(null, record.uid, record.email),
gotError.bind(null, record.email)
)
);
}
}
function handleBounce(message) {
utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'bounce')
utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'bounce');
var recipients = []
var recipients = [];
// According to the AWS SES docs, a notification will never
// include multiple types, so it's fine for us to check for
// EITHER bounce OR complaint here.
if (message.bounce) {
recipients = message.bounce.bouncedRecipients
recipients = message.bounce.bouncedRecipients;
} else if (message.complaint) {
recipients = message.complaint.complainedRecipients
recipients = message.complaint.complainedRecipients;
}
// SES can now send custom headers if enabled on topic.
// Headers are stored as an array of name/value pairs.
// Log the `X-Template-Name` header to help track the email template that bounced.
// Ref: http://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html
const templateName = utils.getHeaderValue('X-Template-Name', message)
const language = utils.getHeaderValue('Content-Language', message)
const templateName = utils.getHeaderValue('X-Template-Name', message);
const language = utils.getHeaderValue('Content-Language', message);
return P.each(recipients, function (recipient) {
// The email address in the bounce message has been handled by an external
// system, and depending on the system it can have had some strange things
// done to it. Try to normalize as best we can.
let email
let emailIsValid = true
const parsedAddress = eaddrs.parseOneAddress(recipient.emailAddress)
let email;
let emailIsValid = true;
const parsedAddress = eaddrs.parseOneAddress(recipient.emailAddress);
if (parsedAddress !== null) {
email = parsedAddress.address
email = parsedAddress.address;
} else {
email = recipient.emailAddress
email = recipient.emailAddress;
if (! isValidEmailAddress(email)) {
emailIsValid = false
emailIsValid = false;
// We couldn't make the recipient address look like a valid email.
// Log a warning but don't error out because we still want to
// emit flow metrics etc.
@ -81,10 +81,10 @@ module.exports = function (log, error) {
email: email,
action: recipient.action,
diagnosticCode: recipient.diagnosticCode
})
});
}
}
const emailDomain = utils.getAnonymizedEmailDomain(email)
const emailDomain = utils.getAnonymizedEmailDomain(email);
const logData = {
action: recipient.action,
email: email,
@ -92,47 +92,47 @@ module.exports = function (log, error) {
bounce: !! message.bounce,
diagnosticCode: recipient.diagnosticCode,
status: recipient.status
}
};
const bounce = {
email: email
}
};
// Template name corresponds directly with the email template that was used
if (templateName) {
logData.template = templateName
logData.template = templateName;
}
if (language) {
logData.lang = language
logData.lang = language;
}
// Log the type of bounce that occurred
// Ref: http://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#bounce-types
if (message.bounce && message.bounce.bounceType) {
bounce.bounceType = logData.bounceType = message.bounce.bounceType
bounce.bounceType = logData.bounceType = message.bounce.bounceType;
if (message.bounce.bounceSubType) {
bounce.bounceSubType = logData.bounceSubType = message.bounce.bounceSubType
bounce.bounceSubType = logData.bounceSubType = message.bounce.bounceSubType;
}
} else if (message.complaint) {
// Log the type of complaint and userAgent reported
logData.complaint = !! message.complaint
bounce.bounceType = 'Complaint'
logData.complaint = !! message.complaint;
bounce.bounceType = 'Complaint';
if (message.complaint.userAgent) {
logData.complaintUserAgent = message.complaint.userAgent
logData.complaintUserAgent = message.complaint.userAgent;
}
if (message.complaint.complaintFeedbackType) {
bounce.bounceSubType = logData.complaintFeedbackType = message.complaint.complaintFeedbackType
bounce.bounceSubType = logData.complaintFeedbackType = message.complaint.complaintFeedbackType;
}
}
// Log the bounced flowEvent and emailEvent metrics
utils.logFlowEventFromMessage(log, message, 'bounced')
utils.logEmailEventFromMessage(log, message, 'bounced', emailDomain)
log.info('handleBounce', logData)
utils.logFlowEventFromMessage(log, message, 'bounced');
utils.logEmailEventFromMessage(log, message, 'bounced', emailDomain);
log.info('handleBounce', logData);
/**
* Docs: https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#bounce-types
@ -142,36 +142,36 @@ module.exports = function (log, error) {
* Code below will fetch the email record and if it is an unverified new account then it will delete
* the account.
*/
const suggestAccountDeletion = !! bounce.bounceType
const work = []
const suggestAccountDeletion = !! bounce.bounceType;
const work = [];
if (emailIsValid) {
work.push(recordBounce(bounce)
.catch(gotError.bind(null, email)))
.catch(gotError.bind(null, email)));
if (suggestAccountDeletion) {
work.push(findEmailRecord(email)
.then(
deleteAccountIfUnverifiedNew,
gotError.bind(null, email)
))
));
}
}
return P.all(work)
return P.all(work);
}).then(
function () {
// We always delete the message, even if handling some addrs failed.
message.del()
message.del();
}
)
);
}
bounceQueue.on('data', handleBounce)
bounceQueue.start()
bounceQueue.on('data', handleBounce);
bounceQueue.start();
return {
bounceQueue: bounceQueue,
handleBounce: handleBounce
}
}
}
};
};
};

Просмотреть файл

@ -2,63 +2,63 @@
* 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'
'use strict';
var P = require('./../promise')
var utils = require('./utils/helpers')
var P = require('./../promise');
var utils = require('./utils/helpers');
module.exports = function (log) {
return function start(deliveryQueue) {
function handleDelivery(message) {
utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'delivery')
utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'delivery');
var recipients = []
var recipients = [];
if (message.delivery && message.notificationType === 'Delivery') {
recipients = message.delivery.recipients
recipients = message.delivery.recipients;
}
// SES can now send custom headers if enabled on topic.
// Headers are stored as an array of name/value pairs.
// Log the `X-Template-Name` header to help track the email template that delivered.
// Ref: http://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html
const templateName = utils.getHeaderValue('X-Template-Name', message)
const templateName = utils.getHeaderValue('X-Template-Name', message);
return P.each(recipients, function (recipient) {
var email = recipient
var emailDomain = utils.getAnonymizedEmailDomain(email)
var email = recipient;
var emailDomain = utils.getAnonymizedEmailDomain(email);
var logData = {
email: email,
domain: emailDomain,
processingTimeMillis: message.delivery.processingTimeMillis
}
};
// Template name corresponds directly with the email template that was used
if (templateName) {
logData.template = templateName
logData.template = templateName;
}
// Log the delivery flowEvent and emailEvent metrics if available
utils.logFlowEventFromMessage(log, message, 'delivered')
utils.logEmailEventFromMessage(log, message, 'delivered', emailDomain)
utils.logFlowEventFromMessage(log, message, 'delivered');
utils.logEmailEventFromMessage(log, message, 'delivered', emailDomain);
log.info('handleDelivery', logData)
log.info('handleDelivery', logData);
}).then(
function () {
// We always delete the message, even if handling some addrs failed.
message.del()
message.del();
}
)
);
}
deliveryQueue.on('data', handleDelivery)
deliveryQueue.start()
deliveryQueue.on('data', handleDelivery);
deliveryQueue.start();
return {
deliveryQueue: deliveryQueue,
handleDelivery: handleDelivery
}
}
}
};
};
};

Просмотреть файл

@ -2,58 +2,58 @@
* 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'
'use strict';
const P = require('../promise')
const utils = require('./utils/helpers')
const P = require('../promise');
const utils = require('./utils/helpers');
// Account deletion threshold for new unverified accounts that receive
// a bounce or complaint notification. Unverified accounts younger than
// 6 hours old will be deleted if a bounce or complaint occurs.
const SIX_HOURS = 1000 * 60 * 60 * 6
const SIX_HOURS = 1000 * 60 * 60 * 6;
module.exports = (log, error) => {
return (queue, db) => {
queue.start()
queue.start();
queue.on('data', async message => {
try {
utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'notification')
utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'notification');
let addresses = [], eventType = 'bounced', isDeletionCandidate = false
let addresses = [], eventType = 'bounced', isDeletionCandidate = false;
if (message.bounce) {
addresses = message.bounce.bouncedRecipients
isDeletionCandidate = true
addresses = message.bounce.bouncedRecipients;
isDeletionCandidate = true;
} else if (message.complaint) {
addresses = message.complaint.complainedRecipients
isDeletionCandidate = true
addresses = message.complaint.complainedRecipients;
isDeletionCandidate = true;
} else if (message.delivery) {
addresses = message.delivery.recipients
eventType = 'delivered'
addresses = message.delivery.recipients;
eventType = 'delivered';
}
await P.all(addresses.map(async address => {
const domain = utils.getAnonymizedEmailDomain(address)
const domain = utils.getAnonymizedEmailDomain(address);
utils.logFlowEventFromMessage(log, message, eventType)
utils.logEmailEventFromMessage(log, message, eventType, domain)
utils.logFlowEventFromMessage(log, message, eventType);
utils.logEmailEventFromMessage(log, message, eventType, domain);
if (isDeletionCandidate) {
const emailRecord = await db.accountRecord(address)
const emailRecord = await db.accountRecord(address);
if (! emailRecord.emailVerified && emailRecord.createdAt >= Date.now() - SIX_HOURS) {
// A bounce or complaint on a new unverified account is grounds for deletion
await db.deleteAccount(emailRecord)
await db.deleteAccount(emailRecord);
log.info('accountDeleted', { ...emailRecord })
log.info('accountDeleted', { ...emailRecord });
}
}
}))
}));
} catch (err) {
log.error('email.notification.error', { err })
log.error('email.notification.error', { err });
}
message.del()
})
}
}
message.del();
});
};
};

Просмотреть файл

@ -2,82 +2,82 @@
* 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'
'use strict';
const ROOT_DIR = '../../..'
const ROOT_DIR = '../../..';
const config = require(`${ROOT_DIR}/config`)
const emailDomains = require(`${ROOT_DIR}/config/popular-email-domains`)
const P = require('../../promise')
const config = require(`${ROOT_DIR}/config`);
const emailDomains = require(`${ROOT_DIR}/config/popular-email-domains`);
const P = require('../../promise');
let amplitude
let amplitude;
function getInsensitiveHeaderValueFromArray(headerName, headers) {
var value = ''
var headerNameNormalized = headerName.toLowerCase()
var value = '';
var headerNameNormalized = headerName.toLowerCase();
headers.some(function (header) {
if (header.name.toLowerCase() === headerNameNormalized) {
value = header.value
return true
value = header.value;
return true;
}
return false
})
return false;
});
return value
return value;
}
function getInsensitiveHeaderValueFromObject(headerName, headers) {
var headerNameNormalized = headerName.toLowerCase()
var value = ''
var headerNameNormalized = headerName.toLowerCase();
var value = '';
Object.keys(headers).some(function (name) {
if (name.toLowerCase() === headerNameNormalized) {
value = headers[name]
return true
value = headers[name];
return true;
}
return false
})
return value
return false;
});
return value;
}
function getHeaderValue(headerName, message){
const headers = getHeaders(message)
const headers = getHeaders(message);
if (Array.isArray(headers)) {
return getInsensitiveHeaderValueFromArray(headerName, headers)
return getInsensitiveHeaderValueFromArray(headerName, headers);
}
if (headers) {
return getInsensitiveHeaderValueFromObject(headerName, headers)
return getInsensitiveHeaderValueFromObject(headerName, headers);
}
return ''
return '';
}
function getHeaders (message) {
return message.mail && message.mail.headers || message.headers
return message.mail && message.mail.headers || message.headers;
}
function logErrorIfHeadersAreWeirdOrMissing (log, message, origin) {
const headers = getHeaders(message)
const headers = getHeaders(message);
if (headers) {
const type = typeof headers
const type = typeof headers;
if (type === 'object') {
const deviceId = getHeaderValue('X-Device-Id', message)
const uid = getHeaderValue('X-Uid', message)
const deviceId = getHeaderValue('X-Device-Id', message);
const uid = getHeaderValue('X-Uid', message);
if (! deviceId && ! uid) {
log.warn('emailHeaders.keys', {
keys: Object.keys(headers).join(','),
template: getHeaderValue('X-Template-Name', message),
origin
})
});
}
} else {
log.error('emailHeaders.weird', { type, origin })
log.error('emailHeaders.weird', { type, origin });
}
} else {
log.error('emailHeaders.missing', { origin })
log.error('emailHeaders.missing', { origin });
}
}
@ -87,26 +87,26 @@ function logEmailEventSent(log, message) {
templateVersion: message.templateVersion,
type: 'sent',
flow_id: message.flowId
}
};
emailEventInfo.locale = getHeaderValue('Content-Language', message)
emailEventInfo.locale = getHeaderValue('Content-Language', message);
const addrs = [message.email].concat(message.ccEmails || [])
const addrs = [message.email].concat(message.ccEmails || []);
addrs.forEach(addr => {
const msg = Object.assign({}, emailEventInfo)
msg.domain = getAnonymizedEmailDomain(addr)
log.info('emailEvent', msg)
})
const msg = Object.assign({}, emailEventInfo);
msg.domain = getAnonymizedEmailDomain(addr);
log.info('emailEvent', msg);
});
logAmplitudeEvent(log, message, Object.assign({
domain: getAnonymizedEmailDomain(message.email)
}, emailEventInfo))
}, emailEventInfo));
}
function logAmplitudeEvent (log, message, eventInfo) {
if (! amplitude) {
amplitude = require('../../metrics/amplitude')(log, config.getProperties())
amplitude = require('../../metrics/amplitude')(log, config.getProperties());
}
amplitude(`email.${eventInfo.template}.${eventInfo.type}`, {
@ -133,14 +133,14 @@ function logAmplitudeEvent (log, message, eventInfo) {
flowBeginTime: message.flowBeginTime || getHeaderValue('X-Flow-Begin-Time', message),
flow_id: eventInfo.flow_id,
time: Date.now()
})
});
}
function logEmailEventFromMessage(log, message, type, emailDomain) {
const templateName = getHeaderValue('X-Template-Name', message)
const templateVersion = getHeaderValue('X-Template-Version', message)
const flowId = getHeaderValue('X-Flow-Id', message)
const locale = getHeaderValue('Content-Language', message)
const templateName = getHeaderValue('X-Template-Name', message);
const templateVersion = getHeaderValue('X-Template-Version', message);
const flowId = getHeaderValue('X-Flow-Id', message);
const locale = getHeaderValue('Content-Language', message);
const emailEventInfo = {
domain: emailDomain,
@ -148,36 +148,36 @@ function logEmailEventFromMessage(log, message, type, emailDomain) {
template: templateName,
templateVersion,
type
}
};
if (flowId) {
emailEventInfo['flow_id'] = flowId
emailEventInfo['flow_id'] = flowId;
}
if (message.bounce) {
emailEventInfo.bounced = true
emailEventInfo.bounced = true;
}
if (message.complaint) {
emailEventInfo.complaint = true
emailEventInfo.complaint = true;
}
log.info('emailEvent', emailEventInfo)
log.info('emailEvent', emailEventInfo);
logAmplitudeEvent(log, message, emailEventInfo)
logAmplitudeEvent(log, message, emailEventInfo);
}
function logFlowEventFromMessage(log, message, type) {
const currentTime = Date.now()
const templateName = getHeaderValue('X-Template-Name', message)
const currentTime = Date.now();
const templateName = getHeaderValue('X-Template-Name', message);
// Log flow metrics if `flowId` and `flowBeginTime` specified in headers
const flowId = getHeaderValue('X-Flow-Id', message)
const flowBeginTime = getHeaderValue('X-Flow-Begin-Time', message)
const elapsedTime = currentTime - flowBeginTime
const flowId = getHeaderValue('X-Flow-Id', message);
const flowBeginTime = getHeaderValue('X-Flow-Begin-Time', message);
const elapsedTime = currentTime - flowBeginTime;
if (flowId && flowBeginTime && (elapsedTime > 0) && type && templateName) {
const eventName = `email.${templateName}.${type}`
const eventName = `email.${templateName}.${type}`;
// Flow events have a specific event and structure that must be emitted.
// Ref `gather` in https://github.com/mozilla/fxa-auth-server/blob/master/lib/metrics/context.js
@ -186,11 +186,11 @@ function logFlowEventFromMessage(log, message, type) {
time: currentTime,
flow_id: flowId,
flow_time: elapsedTime
}
};
log.flowEvent(flowEventInfo)
log.flowEvent(flowEventInfo);
} else {
log.error('handleBounce.flowEvent', { templateName, type, flowId, flowBeginTime, currentTime})
log.error('handleBounce.flowEvent', { templateName, type, flowId, flowBeginTime, currentTime});
}
}
@ -198,14 +198,14 @@ function getAnonymizedEmailDomain(email) {
// This function returns an email domain if it is considered a popular domain,
// which means it is in `./config/popular-email-domains.js`. Otherwise, it
// return `other` as domain.
const tokens = email.split('@')
const emailDomain = tokens[1]
var anonymizedEmailDomain = 'other'
const tokens = email.split('@');
const emailDomain = tokens[1];
var anonymizedEmailDomain = 'other';
if (emailDomain && emailDomains.has(emailDomain)) {
anonymizedEmailDomain = emailDomain
anonymizedEmailDomain = emailDomain;
}
return anonymizedEmailDomain
return anonymizedEmailDomain;
}
module.exports = {
@ -215,4 +215,4 @@ module.exports = {
logFlowEventFromMessage,
getHeaderValue,
getAnonymizedEmailDomain
}
};

Просмотреть файл

@ -2,10 +2,10 @@
* 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'
'use strict';
var inherits = require('util').inherits
var messages = require('joi/lib/language').errors
var inherits = require('util').inherits;
var messages = require('joi/lib/language').errors;
var ERRNO = {
SERVER_CONFIG_ERROR: 100,
@ -82,7 +82,7 @@ var ERRNO = {
INTERNAL_VALIDATION_ERROR: 998,
UNEXPECTED_ERROR: 999
}
};
const DEFAULTS = {
code: 500,
@ -90,16 +90,16 @@ const DEFAULTS = {
errno: ERRNO.UNEXPECTED_ERROR,
message: 'Unspecified error',
info: 'https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-format'
}
};
const TOO_LARGE = /^Payload (?:content length|size) greater than maximum allowed/
const TOO_LARGE = /^Payload (?:content length|size) greater than maximum allowed/;
const BAD_SIGNATURE_ERRORS = [
'Bad mac',
'Unknown algorithm',
'Missing required payload hash',
'Payload is invalid'
]
];
// Payload properties that might help us debug unexpected errors
// when they show up in production. Obviously we don't want to
@ -132,16 +132,16 @@ const DEBUGGABLE_PAYLOAD_KEYS = new Set([
'type',
'unblockCode',
'verificationMethod',
])
]);
function AppError(options, extra, headers) {
this.message = options.message || DEFAULTS.message
this.isBoom = true
this.stack = options.stack
this.message = options.message || DEFAULTS.message;
this.isBoom = true;
this.stack = options.stack;
if (! this.stack) {
Error.captureStackTrace(this, AppError)
Error.captureStackTrace(this, AppError);
}
this.errno = options.errno || DEFAULTS.errno
this.errno = options.errno || DEFAULTS.errno;
this.output = {
statusCode: options.code || DEFAULTS.code,
payload: {
@ -152,69 +152,69 @@ function AppError(options, extra, headers) {
info: options.info || DEFAULTS.info
},
headers: headers || {}
}
var keys = Object.keys(extra || {})
};
var keys = Object.keys(extra || {});
for (var i = 0; i < keys.length; i++) {
this.output.payload[keys[i]] = extra[keys[i]]
this.output.payload[keys[i]] = extra[keys[i]];
}
}
inherits(AppError, Error)
inherits(AppError, Error);
AppError.prototype.toString = function () {
return 'Error: ' + this.message
}
return 'Error: ' + this.message;
};
AppError.prototype.header = function (name, value) {
this.output.headers[name] = value
}
this.output.headers[name] = value;
};
AppError.prototype.backtrace = function (traced) {
this.output.payload.log = traced
}
this.output.payload.log = traced;
};
/**
Translates an error from Hapi format to our format
*/
AppError.translate = function (request, response) {
var error
var error;
if (response instanceof AppError) {
return response
return response;
}
var payload = response.output.payload
var reason = response.reason
var payload = response.output.payload;
var reason = response.reason;
if (! payload) {
error = AppError.unexpectedError(request)
error = AppError.unexpectedError(request);
} else if (payload.statusCode === 500 && /(socket hang up|ECONNREFUSED)/.test(reason)) {
// A connection to a remote service either was not made or timed out.
error = AppError.backendServiceFailure()
error = AppError.backendServiceFailure();
} else if (payload.statusCode === 401) {
// These are common errors generated by Hawk auth lib.
if (payload.message === 'Unknown credentials' ||
payload.message === 'Invalid credentials') {
error = AppError.invalidToken('Invalid authentication token: ' + payload.message)
error = AppError.invalidToken('Invalid authentication token: ' + payload.message);
}
else if (payload.message === 'Stale timestamp') {
error = AppError.invalidTimestamp()
error = AppError.invalidTimestamp();
}
else if (payload.message === 'Invalid nonce') {
error = AppError.invalidNonce()
error = AppError.invalidNonce();
}
else if (BAD_SIGNATURE_ERRORS.indexOf(payload.message) !== -1) {
error = AppError.invalidSignature(payload.message)
error = AppError.invalidSignature(payload.message);
}
else {
error = AppError.invalidToken('Invalid authentication token: ' + payload.message)
error = AppError.invalidToken('Invalid authentication token: ' + payload.message);
}
}
else if (payload.validation) {
if (payload.message && payload.message.indexOf(messages.any.required) >= 0) {
error = AppError.missingRequestParameter(payload.validation.keys[0])
error = AppError.missingRequestParameter(payload.validation.keys[0]);
} else {
error = AppError.invalidRequestParameter(payload.validation)
error = AppError.invalidRequestParameter(payload.validation);
}
}
else if (payload.statusCode === 413 && TOO_LARGE.test(payload.message)) {
error = AppError.requestBodyTooLarge()
error = AppError.requestBodyTooLarge();
}
else {
error = new AppError({
@ -224,13 +224,13 @@ AppError.translate = function (request, response) {
errno: payload.errno,
info: payload.info,
stack: response.stack
})
});
if (payload.statusCode >= 500) {
decorateErrorWithRequest(error, request)
decorateErrorWithRequest(error, request);
}
}
return error
}
return error;
};
// Helper functions for creating particular response types.
@ -246,8 +246,8 @@ AppError.dbIncorrectPatchLevel = function (level, levelRequired) {
level: level,
levelRequired: levelRequired
}
)
}
);
};
AppError.accountExists = function (email) {
return new AppError(
@ -260,8 +260,8 @@ AppError.accountExists = function (email) {
{
email: email
}
)
}
);
};
AppError.unknownAccount = function (email) {
return new AppError(
@ -274,8 +274,8 @@ AppError.unknownAccount = function (email) {
{
email: email
}
)
}
);
};
AppError.incorrectPassword = function (dbEmail, requestEmail) {
if (dbEmail !== requestEmail) {
@ -289,7 +289,7 @@ AppError.incorrectPassword = function (dbEmail, requestEmail) {
{
email: dbEmail
}
)
);
}
return new AppError(
{
@ -301,8 +301,8 @@ AppError.incorrectPassword = function (dbEmail, requestEmail) {
{
email: dbEmail
}
)
}
);
};
AppError.unverifiedAccount = function () {
return new AppError({
@ -310,8 +310,8 @@ AppError.unverifiedAccount = function () {
error: 'Bad Request',
errno: ERRNO.ACCOUNT_UNVERIFIED,
message: 'Unverified account'
})
}
});
};
AppError.invalidVerificationCode = function (details) {
return new AppError(
@ -322,8 +322,8 @@ AppError.invalidVerificationCode = function (details) {
message: 'Invalid verification code'
},
details
)
}
);
};
AppError.invalidRequestBody = function () {
return new AppError({
@ -331,8 +331,8 @@ AppError.invalidRequestBody = function () {
error: 'Bad Request',
errno: ERRNO.INVALID_JSON,
message: 'Invalid JSON in request body'
})
}
});
};
AppError.invalidRequestParameter = function (validation) {
return new AppError(
@ -345,8 +345,8 @@ AppError.invalidRequestParameter = function (validation) {
{
validation: validation
}
)
}
);
};
AppError.missingRequestParameter = function (param) {
return new AppError(
@ -359,8 +359,8 @@ AppError.missingRequestParameter = function (param) {
{
param: param
}
)
}
);
};
AppError.invalidSignature = function (message) {
return new AppError({
@ -368,8 +368,8 @@ AppError.invalidSignature = function (message) {
error: 'Unauthorized',
errno: ERRNO.INVALID_REQUEST_SIGNATURE,
message: message || 'Invalid request signature'
})
}
});
};
AppError.invalidToken = function (message) {
return new AppError({
@ -377,8 +377,8 @@ AppError.invalidToken = function (message) {
error: 'Unauthorized',
errno: ERRNO.INVALID_TOKEN,
message: message || 'Invalid authentication token in request signature'
})
}
});
};
AppError.invalidTimestamp = function () {
return new AppError(
@ -391,8 +391,8 @@ AppError.invalidTimestamp = function () {
{
serverTime: Math.floor(+new Date() / 1000)
}
)
}
);
};
AppError.invalidNonce = function () {
return new AppError({
@ -400,8 +400,8 @@ AppError.invalidNonce = function () {
error: 'Unauthorized',
errno: ERRNO.INVALID_NONCE,
message: 'Invalid nonce in request signature'
})
}
});
};
AppError.missingContentLength = function () {
return new AppError({
@ -409,8 +409,8 @@ AppError.missingContentLength = function () {
error: 'Length Required',
errno: ERRNO.MISSING_CONTENT_LENGTH_HEADER,
message: 'Missing content-length header'
})
}
});
};
AppError.requestBodyTooLarge = function () {
return new AppError({
@ -418,25 +418,25 @@ AppError.requestBodyTooLarge = function () {
error: 'Request Entity Too Large',
errno: ERRNO.REQUEST_TOO_LARGE,
message: 'Request body too large'
})
}
});
};
AppError.tooManyRequests = function (retryAfter, retryAfterLocalized, canUnblock) {
if (! retryAfter) {
retryAfter = 30
retryAfter = 30;
}
var extraData = {
retryAfter: retryAfter
}
};
if (retryAfterLocalized) {
extraData.retryAfterLocalized = retryAfterLocalized
extraData.retryAfterLocalized = retryAfterLocalized;
}
if (canUnblock) {
extraData.verificationMethod = 'email-captcha'
extraData.verificationReason = 'login'
extraData.verificationMethod = 'email-captcha';
extraData.verificationReason = 'login';
}
return new AppError(
@ -450,28 +450,28 @@ AppError.tooManyRequests = function (retryAfter, retryAfterLocalized, canUnblock
{
'retry-after': retryAfter
}
)
}
);
};
AppError.requestBlocked = function (canUnblock) {
var extra
var extra;
if (canUnblock) {
extra = {
verificationMethod: 'email-captcha',
verificationReason: 'login'
}
};
}
return new AppError({
code: 400,
error: 'Request blocked',
errno: ERRNO.REQUEST_BLOCKED,
message: 'The request was blocked for security reasons'
}, extra)
}
}, extra);
};
AppError.serviceUnavailable = function (retryAfter) {
if (! retryAfter) {
retryAfter = 30
retryAfter = 30;
}
return new AppError(
{
@ -486,12 +486,12 @@ AppError.serviceUnavailable = function (retryAfter) {
{
'retry-after': retryAfter
}
)
}
);
};
AppError.featureNotEnabled = function (retryAfter) {
if (! retryAfter) {
retryAfter = 30
retryAfter = 30;
}
return new AppError(
{
@ -506,8 +506,8 @@ AppError.featureNotEnabled = function (retryAfter) {
{
'retry-after': retryAfter
}
)
}
);
};
AppError.gone = function () {
return new AppError({
@ -515,8 +515,8 @@ AppError.gone = function () {
error: 'Gone',
errno: ERRNO.ENDPOINT_NOT_SUPPORTED,
message: 'This endpoint is no longer supported'
})
}
});
};
AppError.mustResetAccount = function (email) {
return new AppError(
@ -529,8 +529,8 @@ AppError.mustResetAccount = function (email) {
{
email: email
}
)
}
);
};
AppError.unknownDevice = function () {
return new AppError(
@ -540,8 +540,8 @@ AppError.unknownDevice = function () {
errno: ERRNO.DEVICE_UNKNOWN,
message: 'Unknown device'
}
)
}
);
};
AppError.deviceSessionConflict = function (deviceId) {
return new AppError({
@ -549,8 +549,8 @@ AppError.deviceSessionConflict = function (deviceId) {
error: 'Bad Request',
errno: ERRNO.DEVICE_CONFLICT,
message: 'Session already registered by another device'
}, { deviceId })
}
}, { deviceId });
};
AppError.invalidUnblockCode = function () {
return new AppError({
@ -558,8 +558,8 @@ AppError.invalidUnblockCode = function () {
error: 'Bad Request',
errno: ERRNO.INVALID_UNBLOCK_CODE,
message: 'Invalid unblock code'
})
}
});
};
AppError.invalidPhoneNumber = () => {
return new AppError({
@ -567,8 +567,8 @@ AppError.invalidPhoneNumber = () => {
error: 'Bad Request',
errno: ERRNO.INVALID_PHONE_NUMBER,
message: 'Invalid phone number'
})
}
});
};
AppError.invalidRegion = region => {
return new AppError({
@ -578,8 +578,8 @@ AppError.invalidRegion = region => {
message: 'Invalid region'
}, {
region
})
}
});
};
AppError.invalidMessageId = () => {
return new AppError({
@ -587,8 +587,8 @@ AppError.invalidMessageId = () => {
error: 'Bad Request',
errno: ERRNO.INVALID_MESSAGE_ID,
message: 'Invalid message id'
})
}
});
};
AppError.messageRejected = (reason, reasonCode) => {
return new AppError({
@ -599,8 +599,8 @@ AppError.messageRejected = (reason, reasonCode) => {
}, {
reason,
reasonCode
})
}
});
};
AppError.emailComplaint = (bouncedAt) => {
return new AppError({
@ -610,8 +610,8 @@ AppError.emailComplaint = (bouncedAt) => {
message: 'Email account sent complaint'
}, {
bouncedAt
})
}
});
};
AppError.emailBouncedHard = (bouncedAt) => {
return new AppError({
@ -621,8 +621,8 @@ AppError.emailBouncedHard = (bouncedAt) => {
message: 'Email account hard bounced'
}, {
bouncedAt
})
}
});
};
AppError.emailBouncedSoft = (bouncedAt) => {
return new AppError({
@ -632,8 +632,8 @@ AppError.emailBouncedSoft = (bouncedAt) => {
message: 'Email account soft bounced'
}, {
bouncedAt
})
}
});
};
AppError.emailExists = () => {
return new AppError({
@ -641,8 +641,8 @@ AppError.emailExists = () => {
error: 'Bad Request',
errno: ERRNO.EMAIL_EXISTS,
message: 'Email already exists'
})
}
});
};
AppError.cannotDeletePrimaryEmail = () => {
return new AppError({
@ -650,8 +650,8 @@ AppError.cannotDeletePrimaryEmail = () => {
error: 'Bad Request',
errno: ERRNO.EMAIL_DELETE_PRIMARY,
message: 'Can not delete primary email'
})
}
});
};
AppError.unverifiedSession = function () {
return new AppError({
@ -659,8 +659,8 @@ AppError.unverifiedSession = function () {
error: 'Bad Request',
errno: ERRNO.SESSION_UNVERIFIED,
message: 'Unverified session'
})
}
});
};
AppError.yourPrimaryEmailExists = () => {
return new AppError({
@ -668,8 +668,8 @@ AppError.yourPrimaryEmailExists = () => {
error: 'Bad Request',
errno: ERRNO.USER_PRIMARY_EMAIL_EXISTS,
message: 'Can not add secondary email that is same as your primary'
})
}
});
};
AppError.verifiedPrimaryEmailAlreadyExists = () => {
return new AppError({
@ -677,8 +677,8 @@ AppError.verifiedPrimaryEmailAlreadyExists = () => {
error: 'Bad Request',
errno: ERRNO.VERIFIED_PRIMARY_EMAIL_EXISTS,
message: 'Email already exists'
})
}
});
};
AppError.verifiedSecondaryEmailAlreadyExists = () => {
return new AppError({
@ -686,8 +686,8 @@ AppError.verifiedSecondaryEmailAlreadyExists = () => {
error: 'Bad Request',
errno: ERRNO.VERIFIED_SECONDARY_EMAIL_EXISTS,
message: 'Email already exists'
})
}
});
};
// This error is thrown when someone attempts to add a secondary email
// that is the same as the primary email of another account, but the account
@ -698,8 +698,8 @@ AppError.unverifiedPrimaryEmailNewlyCreated = () => {
error: 'Bad Request',
errno: ERRNO.UNVERIFIED_PRIMARY_EMAIL_NEWLY_CREATED,
message: 'Email already exists'
})
}
});
};
AppError.cannotLoginWithSecondaryEmail = () => {
return new AppError({
@ -707,8 +707,8 @@ AppError.cannotLoginWithSecondaryEmail = () => {
error: 'Bad Request',
errno: ERRNO.LOGIN_WITH_SECONDARY_EMAIL,
message: 'Sign in with this email type is not currently supported'
})
}
});
};
AppError.unknownSecondaryEmail = () => {
return new AppError({
@ -716,8 +716,8 @@ AppError.unknownSecondaryEmail = () => {
error: 'Bad Request',
errno: ERRNO.SECONDARY_EMAIL_UNKNOWN,
message: 'Unknown email'
})
}
});
};
AppError.cannotResetPasswordWithSecondaryEmail = () => {
return new AppError({
@ -725,8 +725,8 @@ AppError.cannotResetPasswordWithSecondaryEmail = () => {
error: 'Bad Request',
errno: ERRNO.RESET_PASSWORD_WITH_SECONDARY_EMAIL,
message: 'Reset password with this email type is not currently supported'
})
}
});
};
AppError.invalidSigninCode = function () {
return new AppError({
@ -734,8 +734,8 @@ AppError.invalidSigninCode = function () {
error: 'Bad Request',
errno: ERRNO.INVALID_SIGNIN_CODE,
message: 'Invalid signin code'
})
}
});
};
AppError.cannotChangeEmailToUnverifiedEmail = function () {
return new AppError({
@ -743,8 +743,8 @@ AppError.cannotChangeEmailToUnverifiedEmail = function () {
error: 'Bad Request',
errno: ERRNO.CHANGE_EMAIL_TO_UNVERIFIED_EMAIL,
message: 'Can not change primary email to an unverified email'
})
}
});
};
AppError.cannotChangeEmailToUnownedEmail = function () {
return new AppError({
@ -752,8 +752,8 @@ AppError.cannotChangeEmailToUnownedEmail = function () {
error: 'Bad Request',
errno: ERRNO.CHANGE_EMAIL_TO_UNOWNED_EMAIL,
message: 'Can not change primary email to an email that does not belong to this account'
})
}
});
};
AppError.cannotLoginWithEmail = function () {
return new AppError({
@ -761,8 +761,8 @@ AppError.cannotLoginWithEmail = function () {
error: 'Bad Request',
errno: ERRNO.LOGIN_WITH_INVALID_EMAIL,
message: 'This email can not currently be used to login'
})
}
});
};
AppError.cannotResendEmailCodeToUnownedEmail = function () {
return new AppError({
@ -770,8 +770,8 @@ AppError.cannotResendEmailCodeToUnownedEmail = function () {
error: 'Bad Request',
errno: ERRNO.RESEND_EMAIL_CODE_TO_UNOWNED_EMAIL,
message: 'Can not resend email code to an email that does not belong to this account'
})
}
});
};
AppError.cannotSendEmail = function (isNewAddress) {
if (! isNewAddress) {
@ -780,15 +780,15 @@ AppError.cannotSendEmail = function (isNewAddress) {
error: 'Internal Server Error',
errno: ERRNO.FAILED_TO_SEND_EMAIL,
message: 'Failed to send email'
})
});
}
return new AppError({
code: 422,
error: 'Unprocessable Entity',
errno: ERRNO.FAILED_TO_SEND_EMAIL,
message: 'Failed to send email'
})
}
});
};
AppError.invalidTokenVerficationCode = function (details) {
return new AppError(
@ -799,8 +799,8 @@ AppError.invalidTokenVerficationCode = function (details) {
message: 'Invalid token verification code'
},
details
)
}
);
};
AppError.expiredTokenVerficationCode = function (details) {
return new AppError(
@ -811,8 +811,8 @@ AppError.expiredTokenVerficationCode = function (details) {
message: 'Expired token verification code'
},
details
)
}
);
};
AppError.totpTokenAlreadyExists = () => {
return new AppError({
@ -820,8 +820,8 @@ AppError.totpTokenAlreadyExists = () => {
error: 'Bad Request',
errno: ERRNO.TOTP_TOKEN_EXISTS,
message: 'TOTP token already exists for this account.'
})
}
});
};
AppError.totpTokenNotFound = () => {
return new AppError({
@ -829,8 +829,8 @@ AppError.totpTokenNotFound = () => {
error: 'Bad Request',
errno: ERRNO.TOTP_TOKEN_NOT_FOUND,
message: 'TOTP token not found.'
})
}
});
};
AppError.recoveryCodeNotFound = () => {
return new AppError({
@ -838,8 +838,8 @@ AppError.recoveryCodeNotFound = () => {
error: 'Bad Request',
errno: ERRNO.RECOVERY_CODE_NOT_FOUND,
message: 'Recovery code not found.'
})
}
});
};
AppError.unavailableDeviceCommand = () => {
return new AppError({
@ -847,8 +847,8 @@ AppError.unavailableDeviceCommand = () => {
error: 'Bad Request',
errno: ERRNO.DEVICE_COMMAND_UNAVAILABLE,
message: 'Unavailable device command.'
})
}
});
};
AppError.recoveryKeyNotFound = () => {
return new AppError({
@ -856,8 +856,8 @@ AppError.recoveryKeyNotFound = () => {
error: 'Bad Request',
errno: ERRNO.RECOVERY_KEY_NOT_FOUND,
message: 'Recovery key not found.'
})
}
});
};
AppError.recoveryKeyInvalid = () => {
return new AppError({
@ -865,8 +865,8 @@ AppError.recoveryKeyInvalid = () => {
error: 'Bad Request',
errno: ERRNO.RECOVERY_KEY_INVALID,
message: 'Recovery key is not valid.'
})
}
});
};
AppError.totpRequired = () => {
return new AppError({
@ -874,8 +874,8 @@ AppError.totpRequired = () => {
error: 'Bad Request',
errno: ERRNO.TOTP_REQUIRED,
message: 'This request requires two step authentication enabled on your account.'
})
}
});
};
AppError.recoveryKeyExists = () => {
return new AppError({
@ -883,8 +883,8 @@ AppError.recoveryKeyExists = () => {
error: 'Bad Request',
errno: ERRNO.RECOVERY_KEY_EXISTS,
message: 'Recovery key already exists.'
})
}
});
};
AppError.unknownClientId = (clientId) => {
return new AppError({
@ -894,8 +894,8 @@ AppError.unknownClientId = (clientId) => {
message: 'Unknown client_id'
}, {
clientId
})
}
});
};
AppError.staleAuthAt = (authAt) => {
return new AppError({
@ -905,8 +905,8 @@ AppError.staleAuthAt = (authAt) => {
message: 'Stale auth timestamp'
}, {
authAt
})
}
});
};
AppError.notPublicClient = function notPublicClient() {
return new AppError({
@ -923,8 +923,8 @@ AppError.redisConflict = () => {
error: 'Conflict',
errno: ERRNO.REDIS_CONFLICT,
message: 'Redis WATCH detected a conflicting update'
})
}
});
};
AppError.backendServiceFailure = (service, operation) => {
return new AppError({
@ -935,8 +935,8 @@ AppError.backendServiceFailure = (service, operation) => {
}, {
service,
operation
})
}
});
};
AppError.internalValidationError = (op, data) => {
return new AppError({
@ -947,17 +947,17 @@ AppError.internalValidationError = (op, data) => {
}, {
op,
data
})
}
});
};
AppError.unexpectedError = request => {
const error = new AppError({})
decorateErrorWithRequest(error, request)
return error
}
const error = new AppError({});
decorateErrorWithRequest(error, request);
return error;
};
module.exports = AppError
module.exports.ERRNO = ERRNO
module.exports = AppError;
module.exports.ERRNO = ERRNO;
function decorateErrorWithRequest (error, request) {
if (request) {
@ -971,20 +971,20 @@ function decorateErrorWithRequest (error, request) {
query: request.query,
payload: scrubPii(request.payload),
headers: request.headers
}
};
}
}
function scrubPii (payload) {
if (! payload) {
return
return;
}
return Object.entries(payload).reduce((scrubbed, [ key, value ]) => {
if (DEBUGGABLE_PAYLOAD_KEYS.has(key)) {
scrubbed[key] = value
scrubbed[key] = value;
}
return scrubbed
}, {})
return scrubbed;
}, {});
}

Просмотреть файл

@ -2,15 +2,15 @@
* 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'
'use strict';
const crypto = require('crypto')
const isA = require('joi')
const crypto = require('crypto');
const isA = require('joi');
const SCHEMA = isA.array().items(isA.string()).optional()
const SCHEMA = isA.array().items(isA.string()).optional();
module.exports = config => {
const lastAccessTimeUpdates = config.lastAccessTimeUpdates || {}
const lastAccessTimeUpdates = config.lastAccessTimeUpdates || {};
return {
/**
@ -22,7 +22,7 @@ module.exports = config => {
*/
isLastAccessTimeEnabledForUser (uid) {
return lastAccessTimeUpdates.enabled &&
isSampledUser(lastAccessTimeUpdates.sampleRate, uid, 'lastAccessTimeUpdates')
isSampledUser(lastAccessTimeUpdates.sampleRate, uid, 'lastAccessTimeUpdates');
},
/**
@ -34,21 +34,21 @@ module.exports = config => {
* @param key String
*/
isSampledUser: isSampledUser
}
}
};
};
const HASH_LENGTH = hash('', '').length
const MAX_SAFE_HEX = Number.MAX_SAFE_INTEGER.toString(16)
const MAX_SAFE_HEX_LENGTH = MAX_SAFE_HEX.length - MAX_SAFE_HEX.indexOf('f')
const COHORT_DIVISOR = parseInt(Array(MAX_SAFE_HEX_LENGTH).fill('f').join(''), 16)
const HASH_LENGTH = hash('', '').length;
const MAX_SAFE_HEX = Number.MAX_SAFE_INTEGER.toString(16);
const MAX_SAFE_HEX_LENGTH = MAX_SAFE_HEX.length - MAX_SAFE_HEX.indexOf('f');
const COHORT_DIVISOR = parseInt(Array(MAX_SAFE_HEX_LENGTH).fill('f').join(''), 16);
function isSampledUser (sampleRate, uid, key) {
if (sampleRate === 1) {
return true
return true;
}
if (sampleRate === 0) {
return false
return false;
}
// Extract the maximum entropy we can safely handle as a number then reduce
@ -56,19 +56,19 @@ function isSampledUser (sampleRate, uid, key) {
const cohort = parseInt(
hash(uid, key).substr(HASH_LENGTH - MAX_SAFE_HEX_LENGTH),
16
) / COHORT_DIVISOR
return cohort < sampleRate
) / COHORT_DIVISOR;
return cohort < sampleRate;
}
function hash (uid, key) {
// Note that we don't need anything cryptographically secure here,
// speed and a good distribution are the requirements.
const h = crypto.createHash('sha1')
h.update(uid)
h.update(key)
return h.digest('hex')
const h = crypto.createHash('sha1');
h.update(uid);
h.update(key);
return h.digest('hex');
}
// Joi schema for endpoints that can take a `features` parameter.
module.exports.schema = SCHEMA
module.exports.schema = SCHEMA;

Просмотреть файл

@ -2,12 +2,12 @@
* 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'
'use strict';
const config = require('../config').get('geodb')
const geodb = require('fxa-geodb')(config)
const ACCURACY_MAX_KM = 200
const ACCURACY_MIN_KM = 25
const config = require('../config').get('geodb');
const geodb = require('fxa-geodb')(config);
const ACCURACY_MAX_KM = 200;
const ACCURACY_MIN_KM = 25;
/**
* Thin wrapper around geodb, to help log the accuracy
@ -15,30 +15,30 @@ const ACCURACY_MIN_KM = 25
* `location` data. On failure, returns an empty object
**/
module.exports = log => {
log.info('geodb.start', { enabled: config.enabled, dbPath: config.dbPath })
log.info('geodb.start', { enabled: config.enabled, dbPath: config.dbPath });
return ip => {
if (config.enabled === false) {
return {}
return {};
}
try {
const location = geodb(ip)
const accuracy = location.accuracy
let confidence = 'fxa.location.accuracy.'
const location = geodb(ip);
const accuracy = location.accuracy;
let confidence = 'fxa.location.accuracy.';
if (accuracy > ACCURACY_MAX_KM) {
confidence += 'unknown'
confidence += 'unknown';
} else if (accuracy > ACCURACY_MIN_KM && accuracy <= ACCURACY_MAX_KM) {
confidence += 'uncertain'
confidence += 'uncertain';
} else if (accuracy <= ACCURACY_MIN_KM) {
confidence += 'confident'
confidence += 'confident';
} else {
confidence += 'no_accuracy_data'
confidence += 'no_accuracy_data';
}
log.info('geodb.accuracy', { accuracy })
log.info('geodb.accuracy_confidence', { accuracy_confidence: confidence })
log.info('geodb.accuracy', { accuracy });
log.info('geodb.accuracy_confidence', { accuracy_confidence: confidence });
return {
location: {
@ -49,10 +49,10 @@ module.exports = log => {
stateCode: location.stateCode
},
timeZone: location.timeZone
}
};
} catch (err) {
log.error('geodb.1', { err: err.message })
return {}
log.error('geodb.1', { err: err.message });
return {};
}
}
}
};
};

Просмотреть файл

@ -2,41 +2,41 @@
* 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'
'use strict';
const EventEmitter = require('events').EventEmitter
const util = require('util')
const mozlog = require('mozlog')
const config = require('../config')
const logConfig = config.get('log')
const EventEmitter = require('events').EventEmitter;
const util = require('util');
const mozlog = require('mozlog');
const config = require('../config');
const logConfig = config.get('log');
const CLIENT_ID_TO_SERVICE_NAMES = config.get('oauth.clientIds') || {}
const CLIENT_ID_TO_SERVICE_NAMES = config.get('oauth.clientIds') || {};
function Lug(options) {
EventEmitter.call(this)
this.name = options.name || 'fxa-auth-server'
EventEmitter.call(this);
this.name = options.name || 'fxa-auth-server';
mozlog.config({
app: this.name,
level: options.level,
stream: options.stderr || process.stderr,
fmt: options.fmt
})
this.logger = mozlog()
});
this.logger = mozlog();
this.stdout = options.stdout || process.stdout
this.stdout = options.stdout || process.stdout;
this.notifier = require('./notifier')(this)
this.notifier = require('./notifier')(this);
}
util.inherits(Lug, EventEmitter)
util.inherits(Lug, EventEmitter);
Lug.prototype.close = function() {
}
};
// Expose the standard error/warn/info/debug/etc log methods.
Lug.prototype.trace = function (op, data) {
this.logger.debug(op, data)
}
this.logger.debug(op, data);
};
Lug.prototype.error = function (op, data) {
// If the error object contains an email address,
@ -44,32 +44,32 @@ Lug.prototype.error = function (op, data) {
// PII-scrubbing tool is able to find it.
if (data.err && data.err.email) {
if (! data.email) {
data.email = data.err.email
data.email = data.err.email;
}
data.err.email = null
data.err.email = null;
}
this.logger.error(op, data)
}
this.logger.error(op, data);
};
Lug.prototype.fatal = function (op, data) {
this.logger.critical(op, data)
}
this.logger.critical(op, data);
};
Lug.prototype.warn = function (op, data) {
this.logger.warn(op, data)
}
this.logger.warn(op, data);
};
Lug.prototype.info = function (op, data) {
this.logger.info(op, data)
}
this.logger.info(op, data);
};
Lug.prototype.begin = function (op, request) {
this.logger.debug(op)
}
this.logger.debug(op);
};
Lug.prototype.stat = function (stats) {
this.logger.info('stat', stats)
}
this.logger.info('stat', stats);
};
// Log a request summary line.
// This gets called once for each completed request.
@ -78,15 +78,15 @@ Lug.prototype.stat = function (stats) {
Lug.prototype.summary = function (request, response) {
if (request.method === 'options') {
return
return;
}
request.emitRouteFlowEvent(response)
request.emitRouteFlowEvent(response);
const payload = request.payload || {}
const query = request.query || {}
const credentials = (request.auth && request.auth.credentials) || {}
const responseBody = (response && response.source) || {}
const payload = request.payload || {};
const query = request.query || {};
const credentials = (request.auth && request.auth.credentials) || {};
const responseBody = (response && response.source) || {};
const line = {
status: (response.isBoom) ? response.output.statusCode : response.statusCode,
@ -111,17 +111,17 @@ Lug.prototype.summary = function (request, response) {
method: request.method,
email: credentials.email || payload.email || query.email,
phoneNumber: responseBody.formattedPhoneNumber,
}
};
if (line.status >= 500) {
line.trace = request.app.traced
line.stack = response.stack
this.error('request.summary', line, response.message)
line.trace = request.app.traced;
line.stack = response.stack;
this.error('request.summary', line, response.message);
}
else {
this.info('request.summary', line)
this.info('request.summary', line);
}
}
};
// Broadcast an event to attached services, such as sync.
@ -132,24 +132,24 @@ Lug.prototype.notifyAttachedServices = function (name, request, data) {
metricsContextData => {
// Add a timestamp that this event occurred to help attached services resolve any
// potential timing issues
data.ts = data.ts || Date.now() / 1000 // Convert to float seconds
data.ts = data.ts || Date.now() / 1000; // Convert to float seconds
// convert an oauth client-id to a human readable format, if a name is available.
// If no name is available, continue to use the client_id.
if (data.service && data.service !== 'sync') {
data.service = CLIENT_ID_TO_SERVICE_NAMES[data.service] || data.service
data.service = CLIENT_ID_TO_SERVICE_NAMES[data.service] || data.service;
}
const e = {
event: name,
data: data
};
e.data.metricsContext = metricsContextData;
this.info('notify.attached', e);
this.notifier.send(e);
}
e.data.metricsContext = metricsContextData
this.info('notify.attached', e)
this.notifier.send(e)
}
)
}
);
};
// Log an activity metrics event.
// These events indicate key points at which a particular
@ -157,53 +157,53 @@ Lug.prototype.notifyAttachedServices = function (name, request, data) {
Lug.prototype.activityEvent = function (data) {
if (! data || ! data.event || ! data.uid) {
this.error('log.activityEvent', { data })
return
this.error('log.activityEvent', { data });
return;
}
this.logger.info('activityEvent', data)
}
this.logger.info('activityEvent', data);
};
// Log a flow metrics event.
// These events help understand the user's sign-in or sign-up journey.
Lug.prototype.flowEvent = function (data) {
if (! data || ! data.event || ! data.flow_id || ! data.flow_time || ! data.time) {
this.error('flow.missingData', { data })
return
this.error('flow.missingData', { data });
return;
}
this.logger.info('flowEvent', data)
}
this.logger.info('flowEvent', data);
};
Lug.prototype.amplitudeEvent = function (data) {
if (! data || ! data.event_type || (! data.device_id && ! data.user_id)) {
this.error('amplitude.missingData', { data })
return
this.error('amplitude.missingData', { data });
return;
}
this.logger.info('amplitudeEvent', data)
}
this.logger.info('amplitudeEvent', data);
};
module.exports = function (level, name, options = {}) {
if (arguments.length === 1 && typeof level === 'object') {
options = level
level = options.level
name = options.name
options = level;
level = options.level;
name = options.name;
}
options.name = name
options.level = level
options.fmt = logConfig.fmt
var log = new Lug(options)
options.name = name;
options.level = level;
options.fmt = logConfig.fmt;
var log = new Lug(options);
log.stdout.on(
'error',
function (err) {
if (err.code === 'EPIPE') {
log.emit('error', err)
log.emit('error', err);
}
}
)
);
return log
}
return log;
};

Просмотреть файл

@ -10,10 +10,10 @@
//
// https://docs.google.com/spreadsheets/d/1G_8OJGOxeWXdGJ1Ugmykk33Zsl-qAQL05CONSeD4Uz4
'use strict'
'use strict';
const { GROUPS, initialize } = require('fxa-shared/metrics/amplitude')
const P = require('../promise')
const { GROUPS, initialize } = require('fxa-shared/metrics/amplitude');
const P = require('../promise');
// Maps template name to email type
const EMAIL_TYPES = {
@ -41,7 +41,7 @@ const EMAIL_TYPES = {
verifyPrimaryEmail: 'verify',
verifySyncEmail: 'registration',
verifySecondaryEmail: 'secondary_email'
}
};
const EVENTS = {
'account.confirmed': {
@ -80,7 +80,7 @@ const EVENTS = {
group: GROUPS.sms,
event: 'sent'
}
}
};
const FUZZY_EVENTS = new Map([
[ /^email\.(\w+)\.bounced$/, {
@ -95,34 +95,34 @@ const FUZZY_EVENTS = new Map([
group: eventCategory => GROUPS[eventCategory],
event: 'complete'
} ]
])
]);
const ACCOUNT_RESET_COMPLETE = `${GROUPS.login} - forgot_complete`
const LOGIN_COMPLETE = `${GROUPS.login} - complete`
const ACCOUNT_RESET_COMPLETE = `${GROUPS.login} - forgot_complete`;
const LOGIN_COMPLETE = `${GROUPS.login} - complete`;
module.exports = (log, config) => {
if (! log || ! config.oauth.clientIds) {
throw new TypeError('Missing argument')
throw new TypeError('Missing argument');
}
const transformEvent = initialize(config.oauth.clientIds, EVENTS, FUZZY_EVENTS)
const transformEvent = initialize(config.oauth.clientIds, EVENTS, FUZZY_EVENTS);
return receiveEvent
return receiveEvent;
function receiveEvent (event, request, data = {}, metricsContext = {}) {
if (! event || ! request) {
log.error('amplitude.badArgument', { err: 'Bad argument', event, hasRequest: !! request })
return P.resolve()
log.error('amplitude.badArgument', { err: 'Bad argument', event, hasRequest: !! request });
return P.resolve();
}
return request.app.devices
.catch(() => {})
.then(devices => {
const { formFactor } = request.app.ua
const { formFactor } = request.app.ua;
if (event === 'flow.complete') {
// HACK: Push flowType into the event so it can be parsed as eventCategory
event += `.${metricsContext.flowType}`
event += `.${metricsContext.flowType}`;
}
const amplitudeEvent = transformEvent({
@ -141,10 +141,10 @@ module.exports = (log, config) => {
emailService: data.email_service,
emailTypes: EMAIL_TYPES,
service: getService(request, data, metricsContext)
}, getOs(request), getBrowser(request), getLocation(request)))
}, getOs(request), getBrowser(request), getLocation(request)));
if (amplitudeEvent) {
log.amplitudeEvent(amplitudeEvent)
log.amplitudeEvent(amplitudeEvent);
// HACK: Account reset returns a session token so emit login complete too
if (amplitudeEvent.event_type === ACCOUNT_RESET_COMPLETE) {
@ -152,64 +152,64 @@ module.exports = (log, config) => {
...amplitudeEvent,
event_type: LOGIN_COMPLETE,
time: amplitudeEvent.time + 1
})
});
}
}
})
});
}
}
};
function getFromToken (request, key) {
if (request.auth && request.auth.credentials) {
return request.auth.credentials[key]
return request.auth.credentials[key];
}
}
function getFromMetricsContext (metricsContext, key, request, payloadKey) {
return metricsContext[key] ||
(request.payload && request.payload.metricsContext && request.payload.metricsContext[payloadKey])
(request.payload && request.payload.metricsContext && request.payload.metricsContext[payloadKey]);
}
function getOs (request) {
const { os, osVersion } = request.app.ua
const { os, osVersion } = request.app.ua;
if (os) {
return { os, osVersion }
return { os, osVersion };
}
}
function getBrowser (request) {
const { browser, browserVersion } = request.app.ua
const { browser, browserVersion } = request.app.ua;
if (browser) {
return { browser, browserVersion }
return { browser, browserVersion };
}
}
function getLocation (request) {
const { location } = request.app.geo
const { location } = request.app.geo;
if (location && (location.country || location.state)) {
return {
country: location.country,
region: location.state
}
};
}
}
function getService (request, data, metricsContext) {
if (data.service) {
return data.service
return data.service;
}
if (request.payload && request.payload.service) {
return request.payload.service
return request.payload.service;
}
if (request.query && request.query.service) {
return request.query.service
return request.query.service;
}
return metricsContext.service
return metricsContext.service;
}

Просмотреть файл

@ -2,21 +2,21 @@
* 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'
'use strict';
const bufferEqualConstantTime = require('buffer-equal-constant-time')
const crypto = require('crypto')
const HEX_STRING = require('../routes/validators').HEX_STRING
const isA = require('joi')
const P = require('../promise')
const bufferEqualConstantTime = require('buffer-equal-constant-time');
const crypto = require('crypto');
const HEX_STRING = require('../routes/validators').HEX_STRING;
const isA = require('joi');
const P = require('../promise');
const FLOW_ID_LENGTH = 64
const FLOW_ID_LENGTH = 64;
// These match validation in the content server backend.
// We should probably refactor them to fxa-shared.
const ENTRYPOINT_SCHEMA = isA.string().max(128).regex(/^[\w.:-]+$/)
const UTM_SCHEMA = isA.string().max(128).regex(/^[\w\/.%-]+$/)
const UTM_CAMPAIGN_SCHEMA = UTM_SCHEMA.allow('page+referral+-+not+part+of+a+campaign')
const ENTRYPOINT_SCHEMA = isA.string().max(128).regex(/^[\w.:-]+$/);
const UTM_SCHEMA = isA.string().max(128).regex(/^[\w\/.%-]+$/);
const UTM_CAMPAIGN_SCHEMA = UTM_SCHEMA.allow('page+referral+-+not+part+of+a+campaign');
const SCHEMA = isA.object({
// The metrics context device id is a client-generated property
@ -34,10 +34,10 @@ const SCHEMA = isA.object({
utmTerm: UTM_SCHEMA.optional()
})
.unknown(false)
.and('flowId', 'flowBeginTime')
.and('flowId', 'flowBeginTime');
module.exports = function (log, config) {
const cache = require('../cache')(log, config, 'fxa-metrics~')
const cache = require('../cache')(log, config, 'fxa-metrics~');
return {
stash,
@ -47,7 +47,7 @@ module.exports = function (log, config) {
clear,
validate,
setFlowCompleteSignal
}
};
/**
* Stashes metrics context metadata using a key derived from a token.
@ -62,25 +62,25 @@ module.exports = function (log, config) {
* @param token token to stash the metadata against
*/
function stash (token) {
const metadata = this.payload && this.payload.metricsContext
const metadata = this.payload && this.payload.metricsContext;
if (! metadata) {
return P.resolve()
return P.resolve();
}
metadata.service = this.payload.service || this.query.service
metadata.service = this.payload.service || this.query.service;
return P.resolve()
.then(() => {
return cache.add(getKey(token), metadata)
.catch(err => log.warn('metricsContext.stash.add', { err }))
.catch(err => log.warn('metricsContext.stash.add', { err }));
})
.catch(err => log.error('metricsContext.stash', {
err,
hasToken: !! token,
hasId: !! (token && token.id),
hasUid: !! (token && token.uid)
}))
}));
}
/**
@ -96,18 +96,18 @@ module.exports = function (log, config) {
* @param request
*/
async function get (request) {
let token
let token;
try {
const metadata = request.payload && request.payload.metricsContext
const metadata = request.payload && request.payload.metricsContext;
if (metadata) {
return metadata
return metadata;
}
token = getToken(request)
token = getToken(request);
if (token) {
return await cache.get(getKey(token)) || {}
return await cache.get(getKey(token)) || {};
}
} catch (err) {
log.error('metricsContext.get', {
@ -115,10 +115,10 @@ module.exports = function (log, config) {
hasToken: !! token,
hasId: !! (token && token.id),
hasUid: !! (token && token.uid)
})
});
}
return {}
return {};
}
/**
@ -132,45 +132,45 @@ module.exports = function (log, config) {
* @param data target object
*/
async function gather (data) {
const metadata = await this.app.metricsContext
const metadata = await this.app.metricsContext;
if (metadata) {
data.time = Date.now()
data.device_id = metadata.deviceId
data.flow_id = metadata.flowId
data.flow_time = calculateFlowTime(data.time, metadata.flowBeginTime)
data.flowBeginTime = metadata.flowBeginTime
data.flowCompleteSignal = metadata.flowCompleteSignal
data.flowType = metadata.flowType
data.time = Date.now();
data.device_id = metadata.deviceId;
data.flow_id = metadata.flowId;
data.flow_time = calculateFlowTime(data.time, metadata.flowBeginTime);
data.flowBeginTime = metadata.flowBeginTime;
data.flowCompleteSignal = metadata.flowCompleteSignal;
data.flowType = metadata.flowType;
if (metadata.service) {
data.service = metadata.service
data.service = metadata.service;
}
const doNotTrack = this.headers && this.headers.dnt === '1'
const doNotTrack = this.headers && this.headers.dnt === '1';
if (! doNotTrack) {
data.entrypoint = metadata.entrypoint
data.utm_campaign = metadata.utmCampaign
data.utm_content = metadata.utmContent
data.utm_medium = metadata.utmMedium
data.utm_source = metadata.utmSource
data.utm_term = metadata.utmTerm
data.entrypoint = metadata.entrypoint;
data.utm_campaign = metadata.utmCampaign;
data.utm_content = metadata.utmContent;
data.utm_medium = metadata.utmMedium;
data.utm_source = metadata.utmSource;
data.utm_term = metadata.utmTerm;
}
}
return data
return data;
}
function getToken (request) {
if (request.auth && request.auth.credentials) {
return request.auth.credentials
return request.auth.credentials;
}
if (request.payload && request.payload.uid && request.payload.code) {
return {
uid: request.payload.uid,
id: request.payload.code
}
};
}
}
@ -184,12 +184,12 @@ module.exports = function (log, config) {
* @param newToken token to stash the metadata against
*/
function propagate (oldToken, newToken) {
const oldKey = getKey(oldToken)
const oldKey = getKey(oldToken);
return cache.get(oldKey)
.then(metadata => {
if (metadata) {
return cache.add(getKey(newToken), metadata)
.catch(err => log.warn('metricsContext.propagate.add', { err }))
.catch(err => log.warn('metricsContext.propagate.add', { err }));
}
})
.catch(err => log.error('metricsContext.propagate', {
@ -200,7 +200,7 @@ module.exports = function (log, config) {
hasNewToken: !! newToken,
hasNewTokenId: !! (newToken && newToken.id),
hasNewTokenUid: !! (newToken && newToken.uid),
}))
}));
}
/**
@ -212,11 +212,11 @@ module.exports = function (log, config) {
function clear () {
return P.resolve()
.then(() => {
const token = getToken(this)
const token = getToken(this);
if (token) {
return cache.del(getKey(token))
return cache.del(getKey(token));
}
})
});
}
/**
@ -229,72 +229,72 @@ module.exports = function (log, config) {
*/
function validate() {
if (! this.payload) {
return logInvalidContext(this, 'missing payload')
return logInvalidContext(this, 'missing payload');
}
const metadata = this.payload.metricsContext
const metadata = this.payload.metricsContext;
if (! metadata) {
return logInvalidContext(this, 'missing context')
return logInvalidContext(this, 'missing context');
}
if (! metadata.flowId) {
return logInvalidContext(this, 'missing flowId')
return logInvalidContext(this, 'missing flowId');
}
if (! metadata.flowBeginTime) {
return logInvalidContext(this, 'missing flowBeginTime')
return logInvalidContext(this, 'missing flowBeginTime');
}
const age = Date.now() - metadata.flowBeginTime
const age = Date.now() - metadata.flowBeginTime;
if (age > config.metrics.flow_id_expiry || age <= 0) {
return logInvalidContext(this, 'expired flowBeginTime')
return logInvalidContext(this, 'expired flowBeginTime');
}
if (! HEX_STRING.test(metadata.flowId)) {
return logInvalidContext(this, 'invalid flowId')
return logInvalidContext(this, 'invalid flowId');
}
// The first half of the id is random bytes, the second half is a HMAC of
// additional contextual information about the request. It's a simple way
// to check that the metrics came from the right place, without having to
// share state between content-server and auth-server.
const flowSignature = metadata.flowId.substr(FLOW_ID_LENGTH / 2, FLOW_ID_LENGTH)
const flowSignatureBytes = Buffer.from(flowSignature, 'hex')
const expectedSignatureBytes = calculateFlowSignatureBytes(metadata)
const flowSignature = metadata.flowId.substr(FLOW_ID_LENGTH / 2, FLOW_ID_LENGTH);
const flowSignatureBytes = Buffer.from(flowSignature, 'hex');
const expectedSignatureBytes = calculateFlowSignatureBytes(metadata);
if (! bufferEqualConstantTime(flowSignatureBytes, expectedSignatureBytes)) {
return logInvalidContext(this, 'invalid signature')
return logInvalidContext(this, 'invalid signature');
}
log.info('metrics.context.validate', {
valid: true
})
return true
});
return true;
}
function logInvalidContext(request, reason) {
if (request.payload && request.payload.metricsContext) {
delete request.payload.metricsContext.flowId
delete request.payload.metricsContext.flowBeginTime
delete request.payload.metricsContext.flowId;
delete request.payload.metricsContext.flowBeginTime;
}
log.warn('metrics.context.validate', {
valid: false,
reason: reason
})
return false
});
return false;
}
function calculateFlowSignatureBytes (metadata) {
const hmacData = [
metadata.flowId.substr(0, FLOW_ID_LENGTH / 2),
metadata.flowBeginTime.toString(16)
]
];
// We want a digest that's half the length of a flowid,
// and we want the length in bytes rather than hex.
const signatureLength = FLOW_ID_LENGTH / 2 / 2
const key = config.metrics.flow_id_key
const signatureLength = FLOW_ID_LENGTH / 2 / 2;
const key = config.metrics.flow_id_key;
return crypto.createHmac('sha256', key)
.update(hmacData.join('\n'))
.digest()
.slice(0, signatureLength)
.slice(0, signatureLength);
}
/**
@ -306,35 +306,35 @@ module.exports = function (log, config) {
*/
function setFlowCompleteSignal (flowCompleteSignal, flowType) {
if (this.payload && this.payload.metricsContext) {
this.payload.metricsContext.flowCompleteSignal = flowCompleteSignal
this.payload.metricsContext.flowType = flowType
this.payload.metricsContext.flowCompleteSignal = flowCompleteSignal;
this.payload.metricsContext.flowType = flowType;
}
}
}
};
function calculateFlowTime (time, flowBeginTime) {
if (time <= flowBeginTime) {
return 0
return 0;
}
return time - flowBeginTime
return time - flowBeginTime;
}
function getKey (token) {
if (! token || ! token.uid || ! token.id) {
const err = new Error('Invalid token')
throw err
const err = new Error('Invalid token');
throw err;
}
const hash = crypto.createHash('sha256')
hash.update(token.uid)
hash.update(token.id)
const hash = crypto.createHash('sha256');
hash.update(token.uid);
hash.update(token.id);
return hash.digest('base64')
return hash.digest('base64');
}
// HACK: Force the API docs to expand SCHEMA inline
module.exports.SCHEMA = SCHEMA
module.exports.schema = SCHEMA.optional()
module.exports.requiredSchema = SCHEMA.required()
module.exports.SCHEMA = SCHEMA;
module.exports.schema = SCHEMA.optional();
module.exports.requiredSchema = SCHEMA.required();

Просмотреть файл

@ -2,10 +2,10 @@
* 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'
'use strict';
const error = require('../error')
const P = require('../promise')
const error = require('../error');
const P = require('../promise');
const ACTIVITY_EVENTS = new Set([
'account.changedPassword',
@ -21,7 +21,7 @@ const ACTIVITY_EVENTS = new Set([
'device.deleted',
'device.updated',
'sync.sentTabToDevice'
])
]);
// We plan to emit a vast number of flow events to cover all
// kinds of success and error steps of the sign-in/up journey.
@ -34,7 +34,7 @@ const NOT_FLOW_EVENTS = new Set([
'device.deleted',
'device.updated',
'sync.sentTabToDevice'
])
]);
// It's an error if a flow event doesn't have a flow_id
// but some events are also emitted outside of user flows.
@ -43,11 +43,11 @@ const OPTIONAL_FLOW_EVENTS = {
'account.keyfetch': true,
'account.reset': true,
'account.signed': true
}
};
const IGNORE_FLOW_EVENTS_FROM_SERVICES = {
'account.signed': 'content-server'
}
};
const IGNORE_ROUTE_FLOW_EVENTS_FOR_PATHS = new Set([
'/account/devices',
@ -56,14 +56,14 @@ const IGNORE_ROUTE_FLOW_EVENTS_FOR_PATHS = new Set([
'/certificate/sign',
'/password/forgot/status',
'/recovery_email/status'
])
]);
const IGNORE_ROUTE_FLOW_EVENTS_REGEX = /^\/recoveryKey\/[0-9A-Fa-f]+$/
const IGNORE_ROUTE_FLOW_EVENTS_REGEX = /^\/recoveryKey\/[0-9A-Fa-f]+$/;
const PATH_PREFIX = /^\/v1/
const PATH_PREFIX = /^\/v1/;
module.exports = (log, config) => {
const amplitude = require('./amplitude')(log, config)
const amplitude = require('./amplitude')(log, config);
return {
/**
@ -77,48 +77,48 @@ module.exports = (log, config) => {
*/
emit (event, data) {
if (! event) {
log.error('metricsEvents.emit', { missingEvent: true })
return P.resolve()
log.error('metricsEvents.emit', { missingEvent: true });
return P.resolve();
}
const request = this
let isFlowCompleteSignal = false
const request = this;
let isFlowCompleteSignal = false;
return P.resolve().then(() => {
if (ACTIVITY_EVENTS.has(event)) {
emitActivityEvent(event, request, data)
emitActivityEvent(event, request, data);
}
})
.then(() => {
if (NOT_FLOW_EVENTS.has(event)) {
return
return;
}
const service = request.query && request.query.service
const service = request.query && request.query.service;
if (service && IGNORE_FLOW_EVENTS_FROM_SERVICES[event] === service) {
return
return;
}
return emitFlowEvent(event, request, data)
return emitFlowEvent(event, request, data);
})
.then(metricsContext => {
if (metricsContext) {
isFlowCompleteSignal = event === metricsContext.flowCompleteSignal
return metricsContext
isFlowCompleteSignal = event === metricsContext.flowCompleteSignal;
return metricsContext;
}
return request.gatherMetricsContext({})
return request.gatherMetricsContext({});
})
.then(metricsContext => {
return amplitude(event, request, data, metricsContext)
.then(() => {
if (isFlowCompleteSignal) {
log.flowEvent(Object.assign({}, metricsContext, { event: 'flow.complete' }))
log.flowEvent(Object.assign({}, metricsContext, { event: 'flow.complete' }));
return amplitude('flow.complete', request, data, metricsContext)
.then(() => request.clearMetricsContext())
.then(() => request.clearMetricsContext());
}
})
})
});
});
},
/**
@ -130,22 +130,22 @@ module.exports = (log, config) => {
* @returns {Promise}
*/
emitRouteFlowEvent (response) {
const request = this
const path = request.path.replace(PATH_PREFIX, '')
let status = response.statusCode || response.output.statusCode
const request = this;
const path = request.path.replace(PATH_PREFIX, '');
let status = response.statusCode || response.output.statusCode;
if (status === 404 || IGNORE_ROUTE_FLOW_EVENTS_FOR_PATHS.has(path) || IGNORE_ROUTE_FLOW_EVENTS_REGEX.test(path)) {
return P.resolve()
return P.resolve();
}
if (status >= 400) {
const errno = response.errno || (response.output && response.output.errno)
const errno = response.errno || (response.output && response.output.errno);
if (errno === error.ERRNO.INVALID_PARAMETER && ! request.validateMetricsContext()) {
// Don't emit flow events if the metrics context failed validation
return P.resolve()
return P.resolve();
}
status = `${status}.${errno || 999}`
status = `${status}.${errno || 999}`;
}
return emitFlowEvent(`route.${path}.${status}`, request)
@ -153,33 +153,33 @@ module.exports = (log, config) => {
if (status >= 200 && status < 300) {
// Limit to success responses so that short-cut logic (e.g. errors, 304s)
// doesn't skew distribution of the performance data
return emitPerformanceEvent(path, request, data)
}
})
return emitPerformanceEvent(path, request, data);
}
});
}
};
function emitActivityEvent (event, request, data) {
const { location } = request.app.geo
const { location } = request.app.geo;
data = Object.assign({
country: location && location.country,
event,
region: location && location.state,
userAgent: request.headers['user-agent']
}, data)
}, data);
optionallySetService(data, request)
optionallySetService(data, request);
log.activityEvent(data)
log.activityEvent(data);
}
function emitFlowEvent (event, request, optionalData) {
if (! request || ! request.headers) {
log.error('metricsEvents.emitFlowEvent', { event, badRequest: true })
return P.resolve()
log.error('metricsEvents.emitFlowEvent', { event, badRequest: true });
return P.resolve();
}
const { location } = request.app.geo
const { location } = request.app.geo;
return request.gatherMetricsContext({
country: location && location.country,
event: event,
@ -188,45 +188,45 @@ module.exports = (log, config) => {
userAgent: request.headers['user-agent']
}).then(data => {
if (data.flow_id) {
const uid = coalesceUid(optionalData, request)
const uid = coalesceUid(optionalData, request);
if (uid) {
data.uid = uid
data.uid = uid;
}
log.flowEvent(data)
log.flowEvent(data);
} else if (! OPTIONAL_FLOW_EVENTS[event]) {
log.error('metricsEvents.emitFlowEvent', { event, missingFlowId: true })
log.error('metricsEvents.emitFlowEvent', { event, missingFlowId: true });
}
return data
})
return data;
});
}
function emitPerformanceEvent (path, request, data) {
return log.flowEvent(Object.assign({}, data, {
event: `route.performance.${path}`,
flow_time: Date.now() - request.info.received
}))
}));
}
}
};
function optionallySetService (data, request) {
if (data.service) {
return
return;
}
data.service =
(request.payload && request.payload.service) ||
(request.query && request.query.service)
(request.query && request.query.service);
}
function coalesceUid (data, request) {
if (data && data.uid) {
return data.uid
return data.uid;
}
return request.auth &&
request.auth.credentials &&
request.auth.credentials.uid
request.auth.credentials.uid;
}

Просмотреть файл

@ -2,21 +2,21 @@
* 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'
'use strict';
// To be enabled via the environment of stage or prod. NEW_RELIC_HIGH_SECURITY
// and NEW_RELIC_LOG should be set in addition to NEW_RELIC_APP_NAME and
// NEW_RELIC_LICENSE_KEY.
function maybeRequireNewRelic() {
var env = process.env
var env = process.env;
if (env.NEW_RELIC_APP_NAME && env.NEW_RELIC_LICENSE_KEY) {
return require('newrelic')
return require('newrelic');
}
return null
return null;
}
module.exports = maybeRequireNewRelic
module.exports = maybeRequireNewRelic;

Просмотреть файл

@ -2,52 +2,52 @@
* 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'
'use strict';
/**
* This notifier is called by the logger via `notifyAttachedServices`
* to send notifications to Amazon SNS/SQS.
*/
const AWS = require('aws-sdk')
const config = require('../config')
const AWS = require('aws-sdk');
const config = require('../config');
const notifierSnsTopicArn = config.get('snsTopicArn')
const notifierSnsTopicEndpoint = config.get('snsTopicEndpoint')
const notifierSnsTopicArn = config.get('snsTopicArn');
const notifierSnsTopicEndpoint = config.get('snsTopicEndpoint');
let sns = { publish: function (msg, cb) {
cb(null, {disabled: true})
}}
cb(null, {disabled: true});
}};
if (notifierSnsTopicArn !== 'disabled') {
// Pull the region info out of the topic arn.
// For some reason we need to pass this in explicitly.
// Format is "arn:aws:sns:<region>:<other junk>"
const region = notifierSnsTopicArn.split(':')[3]
const region = notifierSnsTopicArn.split(':')[3];
// This will pull in default credentials, region data etc
// from the metadata service available to the instance.
// It's magic, and it's awesome.
sns = new AWS.SNS({endpoint: notifierSnsTopicEndpoint, region: region})
sns = new AWS.SNS({endpoint: notifierSnsTopicEndpoint, region: region});
}
function formatMessageAttributes(msg) {
const attrs = {}
const attrs = {};
attrs.event_type = {
DataType: 'String',
StringValue: msg.event
}
};
if (msg.email) {
attrs.email_domain = {
DataType: 'String',
StringValue: msg.email.split('@')[1]
};
}
}
return attrs
return attrs;
}
module.exports = function notifierLog(log) {
return {
send: (event, callback) => {
const msg = event.data || {}
msg.event = event.event
const msg = event.data || {};
msg.event = event.event;
sns.publish({
TopicArn: notifierSnsTopicArn,
@ -55,18 +55,18 @@ module.exports = function notifierLog(log) {
MessageAttributes: formatMessageAttributes(msg)
}, (err, data) => {
if (err) {
log.error('Notifier.publish', { err: err})
log.error('Notifier.publish', { err: err});
} else {
log.trace('Notifier.publish', { success: true, data: data})
log.trace('Notifier.publish', { success: true, data: data});
}
if (callback) {
callback(err, data)
callback(err, data);
}
})
});
},
// exported for testing purposes
__sns: sns
}
}
};
};

Просмотреть файл

@ -2,10 +2,10 @@
* 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'
'use strict';
const Joi = require('joi')
const validators = require('../routes/validators')
const Joi = require('joi');
const validators = require('../routes/validators');
module.exports = (config) => {
return {
@ -30,5 +30,5 @@ module.exports = (config) => {
'fxa-lastUsedAt': Joi.number().optional()
}
}
}
}
};
};

Просмотреть файл

@ -2,10 +2,10 @@
* 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'
'use strict';
const Joi = require('joi')
const validators = require('../routes/validators')
const Joi = require('joi');
const validators = require('../routes/validators');
module.exports = (config) => {
return {
@ -23,5 +23,5 @@ module.exports = (config) => {
redirect_uri: Joi.string().required().allow('')
}
}
}
}
};
};

Просмотреть файл

@ -2,7 +2,7 @@
* 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'
'use strict';
/* Operations on OAuth database state.
*
@ -18,8 +18,8 @@
* Ref: https://docs.google.com/document/d/1CnTv0Eamy7Lnbmf1ALH00oTKMPhGu70elRivJYjx5v0/
*/
const createBackendServiceAPI = require('../backendService')
const { mapOAuthError, makeAssertionJWT } = require('./utils')
const createBackendServiceAPI = require('../backendService');
const { mapOAuthError, makeAssertionJWT } = require('./utils');
module.exports = (log, config) => {
@ -28,25 +28,25 @@ module.exports = (log, config) => {
revokeRefreshTokenById: require('./revoke-refresh-token-by-id')(config),
getClientInfo: require('./client-info')(config),
getScopedKeyData: require('./scoped-key-data')(config),
})
});
const api = new OAuthAPI(config.oauth.url, config.oauth.poolee)
const api = new OAuthAPI(config.oauth.url, config.oauth.poolee);
return {
api,
close() {
api.close()
api.close();
},
async checkRefreshToken(token) {
try {
return await api.checkRefreshToken({
token: token,
})
});
} catch (err) {
throw mapOAuthError(log, err)
throw mapOAuthError(log, err);
}
},
@ -54,26 +54,26 @@ module.exports = (log, config) => {
try {
return await api.revokeRefreshTokenById({
refresh_token_id: refreshTokenId,
})
});
} catch (err) {
throw mapOAuthError(log, err)
throw mapOAuthError(log, err);
}
},
async getClientInfo(clientId) {
try {
return await api.getClientInfo(clientId)
return await api.getClientInfo(clientId);
} catch (err) {
throw mapOAuthError(log, err)
throw mapOAuthError(log, err);
}
},
async getScopedKeyData(sessionToken, oauthParams) {
oauthParams.assertion = await makeAssertionJWT(config, sessionToken)
oauthParams.assertion = await makeAssertionJWT(config, sessionToken);
try {
return await api.getScopedKeyData(oauthParams)
return await api.getScopedKeyData(oauthParams);
} catch (err) {
throw mapOAuthError(log, err)
throw mapOAuthError(log, err);
}
},
@ -101,5 +101,5 @@ module.exports = (log, config) => {
*
*/
}
}
};
};

Просмотреть файл

@ -2,9 +2,9 @@
* 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'
'use strict';
const Joi = require('joi')
const Joi = require('joi');
module.exports = (config) => {
return {
@ -16,5 +16,5 @@ module.exports = (config) => {
},
response: {}
}
}
}
};
};

Просмотреть файл

@ -2,10 +2,10 @@
* 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'
'use strict';
const Joi = require('joi')
const validators = require('../routes/validators')
const Joi = require('joi');
const validators = require('../routes/validators');
module.exports = (config) => {
return {
@ -23,5 +23,5 @@ module.exports = (config) => {
keyRotationTimestamp: Joi.number().required(),
}))
}
}
}
};
};

Просмотреть файл

@ -2,12 +2,12 @@
* 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'
'use strict';
const P = require('../promise')
const signJWT = P.promisify(require('jsonwebtoken').sign)
const P = require('../promise');
const signJWT = P.promisify(require('jsonwebtoken').sign);
const error = require('../error')
const error = require('../error');
module.exports = {
@ -18,24 +18,24 @@ module.exports = {
// If it's already an instance of our internal error type,
// then just return it as-is.
if (err instanceof error) {
return err
return err;
}
switch (err.errno) {
case 101:
return error.unknownClientId(err.clientId)
return error.unknownClientId(err.clientId);
case 108:
return error.invalidToken()
return error.invalidToken();
case 116:
return error.notPublicClient()
return error.notPublicClient();
case 119:
return error.staleAuthAt(err.authAt)
return error.staleAuthAt(err.authAt);
default:
log.warn('oauthdb.mapOAuthError', {
err: err,
errno: err.errno,
warning: 'unmapped oauth-server errno'
})
return error.unexpectedError()
});
return error.unexpectedError();
}
},
@ -48,17 +48,17 @@ module.exports = {
makeAssertionJWT: function makeAssertionJWT(config, credentials) {
if (! credentials.emailVerified) {
throw error.unverifiedAccount()
throw error.unverifiedAccount();
}
if (credentials.mustVerify && ! credentials.tokenVerified) {
throw error.unverifiedSession()
throw error.unverifiedSession();
}
const opts = {
algorithm: 'HS256',
expiresIn: 60,
audience: config.oauth.url,
issuer: config.domain
}
};
const claims = {
'sub': credentials.uid,
'fxa-generation': credentials.verifierSetAt,
@ -67,7 +67,7 @@ module.exports = {
'fxa-tokenVerified': credentials.tokenVerified,
'fxa-amr': Array.from(credentials.authenticationMethods),
'fxa-aal': credentials.authenticatorAssuranceLevel
};
return signJWT(claims, config.oauth.secretKey, opts);
}
return signJWT(claims, config.oauth.secretKey, opts)
}
}
};

Просмотреть файл

@ -2,24 +2,24 @@
* 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'
'use strict';
const P = require('./promise')
const Poolee = require('poolee')
const url = require('url')
const P = require('./promise');
const Poolee = require('poolee');
const url = require('url');
const PROTOCOL_MODULES = {
'http': require('http'),
'https': require('https')
}
};
function Pool(uri, options = {}) {
const parsed = url.parse(uri)
const {protocol, host} = parsed
const protocolModule = PROTOCOL_MODULES[protocol.slice(0, -1)]
const parsed = url.parse(uri);
const {protocol, host} = parsed;
const protocolModule = PROTOCOL_MODULES[protocol.slice(0, -1)];
if (! protocolModule) {
throw new Error(`Protocol ${protocol} is not supported.`)
throw new Error(`Protocol ${protocol} is not supported.`);
}
const port = parsed.port || protocolModule.globalAgent.defaultPort
const port = parsed.port || protocolModule.globalAgent.defaultPort;
this.poolee = new Poolee(
protocolModule,
[`${host}:${port}`],
@ -29,23 +29,23 @@ function Pool(uri, options = {}) {
keepAlive: true,
maxRetries: 0
}
)
);
}
Pool.prototype.request = function (method, url, params, query, body, headers = {}) {
let path
let path;
try {
path = url.render(params, query)
path = url.render(params, query);
}
catch (err) {
return P.reject(err)
return P.reject(err);
}
var d = P.defer()
let data
var d = P.defer();
let data;
if (body) {
headers['Content-Type'] = 'application/json'
data = JSON.stringify(body)
headers['Content-Type'] = 'application/json';
data = JSON.stringify(body);
}
this.poolee.request(
{
@ -55,64 +55,64 @@ Pool.prototype.request = function (method, url, params, query, body, headers = {
data
},
handleResponse
)
return d.promise
);
return d.promise;
function handleResponse (err, res, body) {
var parsedBody = safeParse(body)
var parsedBody = safeParse(body);
if (err) {
return d.reject(err)
return d.reject(err);
}
if (res.statusCode < 200 || res.statusCode >= 300) {
var error = new Error()
var error = new Error();
if (! parsedBody) {
error.message = body
error.message = body;
} else {
Object.assign(error, parsedBody)
Object.assign(error, parsedBody);
}
error.statusCode = res.statusCode
return d.reject(error)
error.statusCode = res.statusCode;
return d.reject(error);
}
if (! body) {
return d.resolve()
return d.resolve();
}
if (! parsedBody) {
return d.reject(new Error('Invalid JSON'))
return d.reject(new Error('Invalid JSON'));
}
d.resolve(parsedBody)
d.resolve(parsedBody);
}
}
};
Pool.prototype.post = function (path, params, body, {query = {}, headers = {}} = {}) {
return this.request('POST', path, params, query, body, headers)
}
return this.request('POST', path, params, query, body, headers);
};
Pool.prototype.put = function (path, params, body, {query = {}, headers = {}} = {}) {
return this.request('PUT', path, params, query, body, headers)
}
return this.request('PUT', path, params, query, body, headers);
};
Pool.prototype.get = function (path, params, {query = {}, headers = {}} = {}) {
return this.request('GET', path, params, query, null, headers)
}
return this.request('GET', path, params, query, null, headers);
};
Pool.prototype.del = function (path, params, body, {query = {}, headers = {}} = {}) {
return this.request('DELETE', path, params, query, body, headers)
}
return this.request('DELETE', path, params, query, body, headers);
};
Pool.prototype.head = function (path, params, {query = {}, headers = {}} = {}) {
return this.request('HEAD', path, params, query, null, headers)
}
return this.request('HEAD', path, params, query, null, headers);
};
Pool.prototype.close = function () {
/*/
This is a hack to coax the server to close its existing connections
/*/
var socketCount = this.poolee.options.maxSockets || 20
var socketCount = this.poolee.options.maxSockets || 20;
function noop() {}
for (var i = 0; i < socketCount; i++) {
this.poolee.request(
@ -124,15 +124,15 @@ Pool.prototype.close = function () {
}
},
noop
)
);
}
}
};
module.exports = Pool
module.exports = Pool;
function safeParse (json) {
try {
return JSON.parse(json)
return JSON.parse(json);
}
catch (e) {
}

Просмотреть файл

@ -2,33 +2,33 @@
* 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'
'use strict';
module.exports = function (log) {
return function start(messageQueue, push, db) {
function handleProfileUpdated(message) {
const uid = message && message.uid
const uid = message && message.uid;
log.info('handleProfileUpdated', { uid, action: 'notify' })
log.info('handleProfileUpdated', { uid, action: 'notify' });
return db.devices(uid)
.then(devices => push.notifyProfileUpdated(uid, devices))
.catch(err => log.error('handleProfileUpdated', { uid, action: 'error', err, stack: err && err.stack }))
.then(() => {
log.info('handleProfileUpdated', { uid, action: 'delete' })
log.info('handleProfileUpdated', { uid, action: 'delete' });
// We always delete the message, we are not really mission critical
message.del()
})
message.del();
});
}
messageQueue.on('data', handleProfileUpdated)
messageQueue.start()
messageQueue.on('data', handleProfileUpdated);
messageQueue.start();
return {
messageQueue: messageQueue,
handleProfileUpdated: handleProfileUpdated
}
}
}
};
};
};

Просмотреть файл

@ -2,7 +2,7 @@
* 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'
'use strict';
// for easy promise lib switching
module.exports = require('bluebird')
module.exports = require('bluebird');

Просмотреть файл

@ -2,20 +2,20 @@
* 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'
'use strict';
var crypto = require('crypto')
var base64url = require('base64url')
var webpush = require('web-push')
var P = require('./promise')
var crypto = require('crypto');
var base64url = require('base64url');
var webpush = require('web-push');
var P = require('./promise');
var ERR_NO_PUSH_CALLBACK = 'No Push Callback'
var ERR_DATA_BUT_NO_KEYS = 'Data payload present but missing key(s)'
var ERR_TOO_MANY_DEVICES = 'Too many devices connected to account'
var ERR_NO_PUSH_CALLBACK = 'No Push Callback';
var ERR_DATA_BUT_NO_KEYS = 'Data payload present but missing key(s)';
var ERR_TOO_MANY_DEVICES = 'Too many devices connected to account';
var LOG_OP_PUSH_TO_DEVICES = 'push.sendPush'
var LOG_OP_PUSH_TO_DEVICES = 'push.sendPush';
var PUSH_PAYLOAD_SCHEMA_VERSION = 1
var PUSH_PAYLOAD_SCHEMA_VERSION = 1;
var PUSH_COMMANDS = {
DEVICE_CONNECTED: 'fxaccounts:device_connected',
DEVICE_DISCONNECTED: 'fxaccounts:device_disconnected',
@ -24,26 +24,26 @@ var PUSH_COMMANDS = {
PASSWORD_RESET: 'fxaccounts:password_reset',
ACCOUNT_DESTROYED: 'fxaccounts:account_destroyed',
COMMAND_RECEIVED: 'fxaccounts:command_received'
}
};
const TTL_DEVICE_DISCONNECTED = 5 * 3600 // 5 hours
const TTL_PASSWORD_CHANGED = 6 * 3600 // 6 hours
const TTL_PASSWORD_RESET = TTL_PASSWORD_CHANGED
const TTL_ACCOUNT_DESTROYED = TTL_DEVICE_DISCONNECTED
const TTL_COMMAND_RECEIVED = TTL_PASSWORD_CHANGED
const TTL_DEVICE_DISCONNECTED = 5 * 3600; // 5 hours
const TTL_PASSWORD_CHANGED = 6 * 3600; // 6 hours
const TTL_PASSWORD_RESET = TTL_PASSWORD_CHANGED;
const TTL_ACCOUNT_DESTROYED = TTL_DEVICE_DISCONNECTED;
const TTL_COMMAND_RECEIVED = TTL_PASSWORD_CHANGED;
// An arbitrary, but very generous, limit on the number of active devices.
// Currently only for metrics purposes, not enforced.
var MAX_ACTIVE_DEVICES = 200
var MAX_ACTIVE_DEVICES = 200;
const pushReasonsToEvents = (() => {
const reasons = ['accountVerify', 'accountConfirm', 'passwordReset',
'passwordChange', 'deviceConnected', 'deviceDisconnected',
'profileUpdated', 'devicesNotify', 'accountDestroyed',
'commandReceived']
const events = {}
'commandReceived'];
const events = {};
for (const reason of reasons) {
const id = reason.replace(/[A-Z]/, c => `_${c.toLowerCase()}`) // snake-cased.
const id = reason.replace(/[A-Z]/, c => `_${c.toLowerCase()}`); // snake-cased.
events[reason] = {
send: `push.${id}.send`,
success: `push.${id}.success`,
@ -51,10 +51,10 @@ const pushReasonsToEvents = (() => {
failed: `push.${id}.failed`,
noCallback: `push.${id}.no_push_callback`,
noKeys: `push.${id}.data_but_no_keys`
};
}
}
return events
})()
return events;
})();
/**
* A device object returned by the db,
@ -63,14 +63,14 @@ const pushReasonsToEvents = (() => {
*/
module.exports = function (log, db, config) {
var vapid
var vapid;
if (config.vapidKeysFile) {
var vapidKeys = require(config.vapidKeysFile)
var vapidKeys = require(config.vapidKeysFile);
vapid = {
privateKey: vapidKeys.privateKey,
publicKey: vapidKeys.publicKey,
subject: config.publicUrl
}
};
}
/**
@ -85,7 +85,7 @@ module.exports = function (log, db, config) {
uid: uid,
deviceId: deviceId,
err: err
})
});
}
/**
@ -97,7 +97,7 @@ module.exports = function (log, db, config) {
if (name) {
log.info(LOG_OP_PUSH_TO_DEVICES, {
name: name
})
});
}
}
@ -113,43 +113,43 @@ module.exports = function (log, db, config) {
* The list of devices to which to send the push.
*/
function filterSupportedDevices(payload, devices) {
const command = (payload && payload.command) || null
let canSendToIOSVersion/* ({Number} version) => bool */
const command = (payload && payload.command) || null;
let canSendToIOSVersion;/* ({Number} version) => bool */
switch (command) {
case 'fxaccounts:command_received':
canSendToIOSVersion = () => true
break
canSendToIOSVersion = () => true;
break;
case 'sync:collection_changed':
canSendToIOSVersion = () => payload.data.reason !== 'firstsync'
break
canSendToIOSVersion = () => payload.data.reason !== 'firstsync';
break;
case null: // In the null case this is an account verification push message
canSendToIOSVersion = (deviceVersion, deviceBrowser) => {
return deviceVersion >= 10.0 && deviceBrowser === 'Firefox Beta'
}
break
return deviceVersion >= 10.0 && deviceBrowser === 'Firefox Beta';
};
break;
case 'fxaccounts:device_connected':
case 'fxaccounts:device_disconnected':
canSendToIOSVersion = deviceVersion => deviceVersion >= 10.0
break
canSendToIOSVersion = deviceVersion => deviceVersion >= 10.0;
break;
default:
canSendToIOSVersion = () => false
canSendToIOSVersion = () => false;
}
return devices.filter(function(device) {
const deviceOS = device.uaOS && device.uaOS.toLowerCase()
const deviceOS = device.uaOS && device.uaOS.toLowerCase();
if (deviceOS === 'ios') {
const deviceVersion = device.uaBrowserVersion ? parseFloat(device.uaBrowserVersion) : 0
const deviceBrowserName = device.uaBrowser
const deviceVersion = device.uaBrowserVersion ? parseFloat(device.uaBrowserVersion) : 0;
const deviceBrowserName = device.uaBrowser;
if (! canSendToIOSVersion(deviceVersion, deviceBrowserName)) {
log.info('push.filteredUnsupportedDevice', {
command: command,
uaOS: device.uaOS,
uaBrowserVersion: device.uaBrowserVersion
})
return false
});
return false;
}
}
return true
})
return true;
});
}
/**
@ -164,29 +164,29 @@ module.exports = function (log, db, config) {
* The public key as a b64url string.
*/
var dummySigner = crypto.createSign('RSA-SHA256')
var dummyKey = Buffer.alloc(0)
var dummyCurve = crypto.createECDH('prime256v1')
dummyCurve.generateKeys()
var dummySigner = crypto.createSign('RSA-SHA256');
var dummyKey = Buffer.alloc(0);
var dummyCurve = crypto.createECDH('prime256v1');
dummyCurve.generateKeys();
function isValidPublicKey(publicKey) {
// Try to use the key in an ECDH agreement.
// If the key is invalid then this will throw an error.
try {
dummyCurve.computeSecret(base64url.toBuffer(publicKey))
return true
dummyCurve.computeSecret(base64url.toBuffer(publicKey));
return true;
} catch (err) {
log.info('push.isValidPublicKey', {
name: 'Bad public key detected'
})
});
// However! The above call might have left some junk
// sitting around on the openssl error stack.
// Clear it by deliberately triggering a signing error
// before anything yields the event loop.
try {
dummySigner.sign(dummyKey)
dummySigner.sign(dummyKey);
} catch (e) {}
return false
return false;
}
}
@ -207,7 +207,7 @@ module.exports = function (log, db, config) {
*/
notifyCommandReceived(uid, device, command, sender, index, url, ttl) {
if (typeof ttl === 'undefined') {
ttl = TTL_COMMAND_RECEIVED
ttl = TTL_COMMAND_RECEIVED;
}
const options = {
data: {
@ -221,8 +221,8 @@ module.exports = function (log, db, config) {
}
},
TTL: ttl
}
return this.sendPush(uid, [device], 'commandReceived', options)
};
return this.sendPush(uid, [device], 'commandReceived', options);
},
/**
@ -242,7 +242,7 @@ module.exports = function (log, db, config) {
deviceName
}
}
})
});
},
/**
@ -263,7 +263,7 @@ module.exports = function (log, db, config) {
}
},
TTL: TTL_DEVICE_DISCONNECTED
})
});
},
/**
@ -279,7 +279,7 @@ module.exports = function (log, db, config) {
version: PUSH_PAYLOAD_SCHEMA_VERSION,
command: PUSH_COMMANDS.PROFILE_UPDATED
}
})
});
},
/**
@ -296,7 +296,7 @@ module.exports = function (log, db, config) {
command: PUSH_COMMANDS.PASSWORD_CHANGED
},
TTL: TTL_PASSWORD_CHANGED
})
});
},
/**
@ -313,7 +313,7 @@ module.exports = function (log, db, config) {
command: PUSH_COMMANDS.PASSWORD_RESET
},
TTL: TTL_PASSWORD_RESET
})
});
},
/**
@ -325,7 +325,7 @@ module.exports = function (log, db, config) {
* @promise
*/
notifyAccountUpdated (uid, devices, reason) {
return this.sendPush(uid, devices, reason)
return this.sendPush(uid, devices, reason);
},
/**
@ -345,7 +345,7 @@ module.exports = function (log, db, config) {
}
},
TTL: TTL_ACCOUNT_DESTROYED
})
});
},
/**
@ -360,59 +360,59 @@ module.exports = function (log, db, config) {
* @promise
*/
sendPush (uid, devices, reason, options = {}) {
devices = filterSupportedDevices(options.data, devices)
var events = pushReasonsToEvents[reason]
devices = filterSupportedDevices(options.data, devices);
var events = pushReasonsToEvents[reason];
if (! events) {
return P.reject('Unknown push reason: ' + reason)
return P.reject('Unknown push reason: ' + reason);
}
// There's no spec-compliant way to error out as a result of having
// too many devices to notify. For now, just log metrics about it.
if (devices.length > MAX_ACTIVE_DEVICES) {
reportPushError(new Error(ERR_TOO_MANY_DEVICES), uid, null)
reportPushError(new Error(ERR_TOO_MANY_DEVICES), uid, null);
}
return P.each(devices, function(device) {
var deviceId = device.id
var deviceId = device.id;
log.trace(LOG_OP_PUSH_TO_DEVICES, {
uid: uid,
deviceId: deviceId,
pushCallback: device.pushCallback
})
});
if (device.pushCallback && ! device.pushEndpointExpired) {
// send the push notification
incrementPushAction(events.send)
var pushSubscription = { endpoint: device.pushCallback }
var pushPayload = null
var pushOptions = { 'TTL': options.TTL || '0' }
incrementPushAction(events.send);
var pushSubscription = { endpoint: device.pushCallback };
var pushPayload = null;
var pushOptions = { 'TTL': options.TTL || '0' };
if (options.data) {
if (! device.pushPublicKey || ! device.pushAuthKey) {
reportPushError(new Error(ERR_DATA_BUT_NO_KEYS), uid, deviceId)
incrementPushAction(events.noKeys)
return
reportPushError(new Error(ERR_DATA_BUT_NO_KEYS), uid, deviceId);
incrementPushAction(events.noKeys);
return;
}
pushSubscription.keys = {
p256dh: device.pushPublicKey,
auth: device.pushAuthKey
}
pushPayload = Buffer.from(JSON.stringify(options.data))
};
pushPayload = Buffer.from(JSON.stringify(options.data));
}
if (vapid) {
pushOptions.vapidDetails = vapid
pushOptions.vapidDetails = vapid;
}
return webpush.sendNotification(pushSubscription, pushPayload, pushOptions)
.then(
function () {
incrementPushAction(events.success)
incrementPushAction(events.success);
},
function (err) {
// If we've stored an invalid key in the db for some reason, then we
// might get an encryption failure here. Check the key, which also
// happens to work around bugginess in node's handling of said failures.
var keyWasInvalid = false
var keyWasInvalid = false;
if (! err.statusCode && device.pushPublicKey) {
if (! isValidPublicKey(device.pushPublicKey)) {
keyWasInvalid = true
keyWasInvalid = true;
}
}
// 404 or 410 error from the push servers means
@ -421,25 +421,25 @@ module.exports = function (log, db, config) {
if (err.statusCode === 404 || err.statusCode === 410 || keyWasInvalid) {
// set the push endpoint expired flag
// Warning: this method is called without any session tokens or auth validation.
device.pushEndpointExpired = true
device.pushEndpointExpired = true;
return db.updateDevice(uid, device).catch(function (err) {
reportPushError(err, uid, deviceId)
reportPushError(err, uid, deviceId);
}).then(function() {
incrementPushAction(events.resetSettings)
})
incrementPushAction(events.resetSettings);
});
} else {
reportPushError(err, uid, deviceId)
incrementPushAction(events.failed)
reportPushError(err, uid, deviceId);
incrementPushAction(events.failed);
}
}
)
);
} else {
// keep track if there are any devices with no push urls.
reportPushError(new Error(ERR_NO_PUSH_CALLBACK), uid, deviceId)
incrementPushAction(events.noCallback)
reportPushError(new Error(ERR_NO_PUSH_CALLBACK), uid, deviceId);
incrementPushAction(events.noCallback);
}
})
});
}
}
}
};
};

Просмотреть файл

@ -14,14 +14,14 @@
* oauth-authenticated service, once we get more experience with using it.
*/
'use strict'
'use strict';
const isA = require('joi')
const error = require('./error')
const createBackendServiceAPI = require('./backendService')
const validators = require('./routes/validators')
const isA = require('joi');
const error = require('./error');
const createBackendServiceAPI = require('./backendService');
const validators = require('./routes/validators');
const base64url = require('base64url')
const base64url = require('base64url');
const PUSHBOX_RETRIEVE_SCHEMA = isA.object({
last: isA.boolean().optional(),
@ -32,24 +32,24 @@ const PUSHBOX_RETRIEVE_SCHEMA = isA.object({
})).optional(),
status: isA.number().required(),
error: isA.string().optional()
}).and('last', 'messages').or('index', 'error')
}).and('last', 'messages').or('index', 'error');
const PUSHBOX_STORE_SCHEMA = isA.object({
index: isA.number().optional(),
error: isA.string().optional(),
status: isA.number().required()
}).or('index', 'error')
}).or('index', 'error');
// Pushbox stores strings, so these are a little pair
// of helper functions to allow us to store arbitrary
// JSON-serializable objects.
function encodeForStorage(data) {
return base64url.encode(JSON.stringify(data))
return base64url.encode(JSON.stringify(data));
}
function decodeFromStorage(data) {
return JSON.parse(base64url.decode(data))
return JSON.parse(base64url.decode(data));
}
@ -57,12 +57,12 @@ module.exports = function (log, config) {
if (! config.pushbox.enabled) {
return {
retrieve() {
return Promise.reject(error.featureNotEnabled())
return Promise.reject(error.featureNotEnabled());
},
store() {
return Promise.reject(error.featureNotEnabled())
}
return Promise.reject(error.featureNotEnabled());
}
};
}
const PushboxAPI = createBackendServiceAPI(log, config, 'pushbox', {
@ -99,15 +99,15 @@ module.exports = function (log, config) {
}
},
})
});
const api = new PushboxAPI(config.pushbox.url, {
headers: {Authorization: `FxA-Server-Key ${config.pushbox.key}`},
timeout: 15000
})
});
// pushbox expects this in seconds, not millis.
const maxTTL = Math.round(config.pushbox.maxTTL / 1000)
const maxTTL = Math.round(config.pushbox.maxTTL / 1000);
return {
/**
@ -125,15 +125,15 @@ module.exports = function (log, config) {
async retrieve (uid, deviceId, limit, index) {
const query = {
limit: limit.toString()
}
};
if (index) {
query.index = index.toString()
query.index = index.toString();
}
const body = await api.retrieve(uid, deviceId, query)
log.info('pushbox.retrieve.response', { body: body })
const body = await api.retrieve(uid, deviceId, query);
log.info('pushbox.retrieve.response', { body: body });
if (body.error) {
log.error('pushbox.retrieve', { status: body.status, error: body.error })
throw error.backendServiceFailure()
log.error('pushbox.retrieve', { status: body.status, error: body.error });
throw error.backendServiceFailure();
}
return {
last: body.last,
@ -142,9 +142,9 @@ module.exports = function (log, config) {
return {
index: msg.index,
data: decodeFromStorage(msg.data)
}
};
})
}
};
},
/**
@ -160,17 +160,17 @@ module.exports = function (log, config) {
*/
async store (uid, deviceId, data, ttl) {
if (typeof ttl === 'undefined' || ttl > maxTTL) {
ttl = maxTTL
ttl = maxTTL;
}
const body = await api.store(uid, deviceId, {data: encodeForStorage(data), ttl})
log.info('pushbox.store.response', { body: body })
const body = await api.store(uid, deviceId, {data: encodeForStorage(data), ttl});
log.info('pushbox.store.response', { body: body });
if (body.error) {
log.error('pushbox.store', { status: body.status, error: body.error })
throw error.backendServiceFailure()
log.error('pushbox.store', { status: body.status, error: body.error });
throw error.backendServiceFailure();
}
return body
return body;
}
}
}
};
};
module.exports.RETRIEVE_SCHEMA = PUSHBOX_RETRIEVE_SCHEMA
module.exports.RETRIEVE_SCHEMA = PUSHBOX_RETRIEVE_SCHEMA;

Просмотреть файл

@ -2,39 +2,39 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
'use strict'
'use strict';
const error = require('./error')
const error = require('./error');
module.exports = (config, log) => {
const redis = require('fxa-shared/redis')(config, log)
const redis = require('fxa-shared/redis')(config, log);
if (! redis) {
return
return;
}
return Object.entries(redis).reduce((object, [ key, value ]) => {
if (typeof value === 'function') {
object[key] = async (...args) => {
try {
return await value(...args)
return await value(...args);
} catch (err) {
if (err.message === 'redis.watch.conflict') {
// This error is nothing to worry about, just a sign that our
// protection against concurrent updates is working correctly.
// fxa-shared is responsible for logging.
throw error.redisConflict()
throw error.redisConflict();
}
// If you see this line in a stack trace in Sentry
// it means something unexpected has really occurred.
// fxa-shared is responsible for logging.
throw error.unexpectedError()
}
throw error.unexpectedError();
}
};
} else {
object[key] = value
object[key] = value;
}
return object
}, {})
}
return object;
}, {});
};

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -2,26 +2,26 @@
* 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'
'use strict';
const path = require('path')
const cp = require('child_process')
const error = require('../error')
const path = require('path');
const cp = require('child_process');
const error = require('../error');
const version = require('../../package.json').version
var commitHash
var sourceRepo
const version = require('../../package.json').version;
var commitHash;
var sourceRepo;
const UNKNOWN = 'unknown'
const UNKNOWN = 'unknown';
// Production and stage provide './config/version.json'. Try to load this at
// startup; punt on failure. For dev environments, we'll get this from `git`
// for dev environments.
try {
var versionJson = path.join(__dirname, '..', '..', 'config', 'version.json')
var info = require(versionJson)
commitHash = info.version.hash
sourceRepo = info.version.source
var versionJson = path.join(__dirname, '..', '..', 'config', 'version.json');
var info = require(versionJson);
commitHash = info.version.hash;
sourceRepo = info.version.source;
} catch (e) {
/* ignore */
}
@ -29,22 +29,22 @@ try {
module.exports = (log, db) => {
async function versionHandler(request, h) {
log.begin('Defaults.root', request)
log.begin('Defaults.root', request);
function getVersion() {
return new Promise(function (resolve, reject) {
// ignore errors and default to 'unknown' if not found
var gitDir = path.resolve(__dirname, '..', '..', '.git')
var gitDir = path.resolve(__dirname, '..', '..', '.git');
cp.exec('git rev-parse HEAD', { cwd: gitDir }, function(err, stdout1) {
var configPath = path.join(gitDir, 'config')
var cmd = 'git config --get remote.origin.url'
var configPath = path.join(gitDir, 'config');
var cmd = 'git config --get remote.origin.url';
cp.exec(cmd, { env: { GIT_CONFIG: configPath, PATH: process.env.PATH } }, function(err, stdout2) {
commitHash = (stdout1 && stdout1.trim()) || UNKNOWN
sourceRepo = (stdout2 && stdout2.trim()) || UNKNOWN
resolve()
})
})
commitHash = (stdout1 && stdout1.trim()) || UNKNOWN;
sourceRepo = (stdout2 && stdout2.trim()) || UNKNOWN;
resolve();
});
});
});
}
@ -53,15 +53,15 @@ module.exports = (log, db) => {
version: version,
commit: commitHash,
source: sourceRepo
}).spaces(2).suffix('\n')
}).spaces(2).suffix('\n');
}
// if we already have the commitHash, send the reply and return
if (commitHash) {
return getResp()
return getResp();
}
await getVersion()
await getVersion();
return getResp();
}
@ -81,25 +81,25 @@ module.exports = (log, db) => {
method: 'GET',
path: '/__heartbeat__',
handler: async function heartbeat(request) {
log.begin('Defaults.heartbeat', request)
log.begin('Defaults.heartbeat', request);
return db.ping()
.then(
function () {
return {}
return {};
},
function (err) {
log.error('heartbeat', { err: err })
throw error.serviceUnavailable()
log.error('heartbeat', { err: err });
throw error.serviceUnavailable();
}
)
);
}
},
{
method: 'GET',
path: '/__lbheartbeat__',
handler: async function heartbeat(request) {
log.begin('Defaults.lbheartbeat', request)
return {}
log.begin('Defaults.lbheartbeat', request);
return {};
}
},
{
@ -112,11 +112,11 @@ module.exports = (log, db) => {
}
},
handler: async function v0(request) {
log.begin('Defaults.v0', request)
throw error.gone()
log.begin('Defaults.v0', request);
throw error.gone();
}
}
]
];
return routes
}
return routes;
};

Просмотреть файл

@ -2,61 +2,61 @@
* 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'
'use strict';
const { URL } = require('url')
const Ajv = require('ajv')
const ajv = new Ajv()
const error = require('../error')
const fs = require('fs')
const i18n = require('i18n-abide')
const isA = require('joi')
const P = require('../promise')
const path = require('path')
const validators = require('./validators')
const { URL } = require('url');
const Ajv = require('ajv');
const ajv = new Ajv();
const error = require('../error');
const fs = require('fs');
const i18n = require('i18n-abide');
const isA = require('joi');
const P = require('../promise');
const path = require('path');
const validators = require('./validators');
const HEX_STRING = validators.HEX_STRING
const DEVICES_SCHEMA = require('../devices').schema
const PUSH_PAYLOADS_SCHEMA_PATH = path.resolve(__dirname, '../../docs/pushpayloads.schema.json')
const HEX_STRING = validators.HEX_STRING;
const DEVICES_SCHEMA = require('../devices').schema;
const PUSH_PAYLOADS_SCHEMA_PATH = path.resolve(__dirname, '../../docs/pushpayloads.schema.json');
// Assign a default TTL for well-known commands if a request didn't specify it.
const DEFAULT_COMMAND_TTL = new Map([
['https://identity.mozilla.com/cmd/open-uri', 30 * 24 * 3600], // 30 days
])
]);
module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) => {
// Loads and compiles a json validator for the payloads received
// in /account/devices/notify
const validatePushSchema = JSON.parse(fs.readFileSync(PUSH_PAYLOADS_SCHEMA_PATH))
const validatePushPayloadAjv = ajv.compile(validatePushSchema)
const { supportedLanguages, defaultLanguage } = config.i18n
const validatePushSchema = JSON.parse(fs.readFileSync(PUSH_PAYLOADS_SCHEMA_PATH));
const validatePushPayloadAjv = ajv.compile(validatePushSchema);
const { supportedLanguages, defaultLanguage } = config.i18n;
const localizeTimestamp = require('fxa-shared').l10n.localizeTimestamp({
supportedLanguages,
defaultLanguage
})
const earliestSaneTimestamp = config.lastAccessTimeUpdates.earliestSaneTimestamp
});
const earliestSaneTimestamp = config.lastAccessTimeUpdates.earliestSaneTimestamp;
function validatePushPayload(payload, endpoint) {
if (endpoint === 'accountVerify') {
if (isEmpty(payload)) {
return true
return true;
}
return false
return false;
}
return validatePushPayloadAjv(payload)
return validatePushPayloadAjv(payload);
}
function isEmpty(payload) {
return payload && Object.keys(payload).length === 0
return payload && Object.keys(payload).length === 0;
}
function marshallLastAccessTime (lastAccessTime, request) {
const languages = request.app.acceptLanguage
const languages = request.app.acceptLanguage;
const result = {
lastAccessTime,
lastAccessTimeFormatted: localizeTimestamp.format(lastAccessTime, languages),
}
};
if (lastAccessTime < earliestSaneTimestamp) {
// Values older than earliestSaneTimestamp are probably wrong.
@ -64,24 +64,24 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
// an approximate string like "last sync over 2 months ago".
// And do it using additional properties so we don't affect
// older content servers that are unfamiliar with the change.
result.approximateLastAccessTime = earliestSaneTimestamp
result.approximateLastAccessTimeFormatted = localizeTimestamp.format(earliestSaneTimestamp, languages)
result.approximateLastAccessTime = earliestSaneTimestamp;
result.approximateLastAccessTimeFormatted = localizeTimestamp.format(earliestSaneTimestamp, languages);
}
return result
return result;
}
function marshallLocation (location, request) {
let language
let language;
if (! location) {
// Shortcut the error logging if location isn't set
return {}
return {};
}
try {
const languages = i18n.parseAcceptLanguage(request.app.acceptLanguage)
language = i18n.bestLanguage(languages, supportedLanguages, defaultLanguage)
const languages = i18n.parseAcceptLanguage(request.app.acceptLanguage);
language = i18n.bestLanguage(languages, supportedLanguages, defaultLanguage);
if (language[0] === 'e' && language[1] === 'n') {
// For English, return all of the location components
@ -90,25 +90,25 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
country: location.country,
state: location.state,
stateCode: location.stateCode
}
};
}
// For other languages, only return what we can translate
const territories = require(`cldr-localenames-full/main/${language}/territories.json`)
const territories = require(`cldr-localenames-full/main/${language}/territories.json`);
return {
country: territories.main[language].localeDisplayNames.territories[location.countryCode]
}
};
} catch (err) {
log.warn('devices.marshallLocation.warning', {
err: err.message,
languages: request.app.acceptLanguage,
language,
location
})
});
}
// If something failed, don't return location
return {}
return {};
}
// Creates a "full" device response, provided a credentials object and an optional
@ -128,7 +128,7 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
name: (device && device.name) || credentials.deviceName || devices.synthesizeName(credentials),
type: (device && device.type) || credentials.deviceType || 'desktop',
availableCommands: (device && device.availableCommands) || credentials.deviceAvailableCommands || {},
}
};
}
return [
@ -173,51 +173,51 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
}
},
handler: async function (request) {
log.begin('Account.device', request)
log.begin('Account.device', request);
const payload = request.payload
const credentials = request.auth.credentials
const payload = request.payload;
const credentials = request.auth.credentials;
// Remove obsolete field, so we don't try to echo it back to the client.
delete payload.capabilities
delete payload.capabilities;
// Some additional, slightly tricky validation to detect bad public keys.
if (payload.pushPublicKey && ! push.isValidPublicKey(payload.pushPublicKey)) {
throw error.invalidRequestParameter('invalid pushPublicKey')
throw error.invalidRequestParameter('invalid pushPublicKey');
}
if (payload.id) {
// Don't write out the update if nothing has actually changed.
if (devices.isSpuriousUpdate(payload, credentials)) {
return buildDeviceResponse(credentials)
return buildDeviceResponse(credentials);
}
// We also reserve the right to disable updates until
// we're confident clients are behaving correctly.
if (config.deviceUpdatesEnabled === false) {
throw error.featureNotEnabled()
throw error.featureNotEnabled();
}
} else if (credentials.deviceId) {
// Keep the old id, which is probably from a synthesized device record
payload.id = credentials.deviceId
payload.id = credentials.deviceId;
}
const pushEndpointOk = ! payload.id || // New device.
(payload.id && payload.pushCallback &&
payload.pushCallback !== credentials.deviceCallbackURL) // Updating the pushCallback
payload.pushCallback !== credentials.deviceCallbackURL); // Updating the pushCallback
if (pushEndpointOk) {
payload.pushEndpointExpired = false
payload.pushEndpointExpired = false;
}
// We're doing a gradual rollout of the 'device commands' feature
// in support of pushbox, so accept an 'availableCommands' field
// if pushbox is enabled.
if (payload.availableCommands && ! config.pushbox.enabled) {
payload.availableCommands = {}
payload.availableCommands = {};
}
const device = await devices.upsert(request, credentials, payload)
return buildDeviceResponse(credentials, device)
const device = await devices.upsert(request, credentials, payload);
return buildDeviceResponse(credentials, device);
}
},
{
@ -252,19 +252,19 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
}
},
handler: async function (request) {
log.begin('Account.deviceCommands', request)
log.begin('Account.deviceCommands', request);
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
const deviceId = sessionToken.deviceId
const query = request.query || {}
const {index, limit} = query
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
const deviceId = sessionToken.deviceId;
const query = request.query || {};
const {index, limit} = query;
return pushbox.retrieve(uid, deviceId, limit, index)
.then(resp => {
log.info('commands.fetch', { resp: resp })
return resp
})
log.info('commands.fetch', { resp: resp });
return resp;
});
}
},
{
@ -290,19 +290,19 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
}
},
handler: async function (request) {
log.begin('Account.invokeDeviceCommand', request)
log.begin('Account.invokeDeviceCommand', request);
const {target, command, payload} = request.payload
let {ttl} = request.payload
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
const sender = sessionToken.deviceId
const {target, command, payload} = request.payload;
let {ttl} = request.payload;
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
const sender = sessionToken.deviceId;
return customs.checkAuthenticated(request, uid, 'invokeDeviceCommand')
.then(() => db.device(uid, target))
.then(device => {
if (! device.availableCommands.hasOwnProperty(command)) {
throw error.unavailableDeviceCommand()
throw error.unavailableDeviceCommand();
}
// 0 is perfectly acceptable TTL, hence the strict equality check.
if (ttl === undefined && DEFAULT_COMMAND_TTL.has(command)) {
@ -312,16 +312,16 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
command,
payload,
sender,
}
};
return pushbox.store(uid, device.id, data, ttl)
.then(({index}) => {
const url = new URL('v1/account/device/commands', config.publicUrl)
url.searchParams.set('index', index)
url.searchParams.set('limit', 1)
return push.notifyCommandReceived(uid, device, command, sender, index, url.href, ttl)
const url = new URL('v1/account/device/commands', config.publicUrl);
url.searchParams.set('index', index);
url.searchParams.set('limit', 1);
return push.notifyCommandReceived(uid, device, command, sender, index, url.href, ttl);
});
})
})
.then(() => { return {} })
.then(() => { return {}; });
}
},
{
@ -356,54 +356,54 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
}
},
handler: async function (request) {
log.begin('Account.devicesNotify', request)
log.begin('Account.devicesNotify', request);
// We reserve the right to disable notifications until
// we're confident clients are behaving correctly.
if (config.deviceNotificationsEnabled === false) {
throw error.featureNotEnabled()
throw error.featureNotEnabled();
}
const body = request.payload
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
const payload = body.payload
const endpointAction = body._endpointAction || 'devicesNotify'
const body = request.payload;
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
const payload = body.payload;
const endpointAction = body._endpointAction || 'devicesNotify';
if (! validatePushPayload(payload, endpointAction)) {
throw error.invalidRequestParameter('invalid payload')
throw error.invalidRequestParameter('invalid payload');
}
const pushOptions = {
data: payload
}
};
if (body.TTL) {
pushOptions.TTL = body.TTL
pushOptions.TTL = body.TTL;
}
return customs.checkAuthenticated(request, uid, endpointAction)
.then(() => request.app.devices)
.then(devices => {
if (body.to !== 'all') {
const include = new Set(body.to)
devices = devices.filter(device => include.has(device.id))
const include = new Set(body.to);
devices = devices.filter(device => include.has(device.id));
if (devices.length === 0) {
log.error('Account.devicesNotify', {
uid: uid,
error: 'devices empty'
})
return
});
return;
}
} else if (body.excluded) {
const exclude = new Set(body.excluded)
devices = devices.filter(device => ! exclude.has(device.id))
const exclude = new Set(body.excluded);
devices = devices.filter(device => ! exclude.has(device.id));
}
return push.sendPush(uid, devices, endpointAction, pushOptions)
.catch(catchPushError)
.catch(catchPushError);
})
.then(() => {
// Emit a metrics event for when a user sends tabs between devices.
@ -416,20 +416,20 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
payload.data.collections.length === 1 &&
payload.data.collections[0] === 'clients'
) {
let deviceId = undefined
let deviceId = undefined;
if (sessionToken.deviceId) {
deviceId = sessionToken.deviceId
deviceId = sessionToken.deviceId;
}
return request.emitMetricsEvent('sync.sentTabToDevice', {
device_id: deviceId,
service: 'sync',
uid: uid
})
});
}
})
.then(() => { return {} })
.then(() => { return {}; });
function catchPushError (err) {
// push may fail due to not found devices or a bad push action
@ -437,7 +437,7 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
log.error('Account.devicesNotify', {
uid: uid,
error: err
})
});
}
}
},
@ -471,9 +471,9 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
}
},
handler: async function (request) {
log.begin('Account.devices', request)
log.begin('Account.devices', request);
const sessionToken = request.auth.credentials
const sessionToken = request.auth.credentials;
return request.app.devices
.then(deviceArray => {
@ -489,10 +489,10 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
pushAuthKey: device.pushAuthKey,
pushEndpointExpired: device.pushEndpointExpired,
availableCommands: device.availableCommands
}, marshallLastAccessTime(device.lastAccessTime, request))
})
}, marshallLastAccessTime(device.lastAccessTime, request));
});
}
)
);
}
},
{
@ -532,30 +532,30 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
}
},
handler: async function (request) {
log.begin('Account.sessions', request)
log.begin('Account.sessions', request);
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
return db.sessions(uid)
.then(sessions => {
return sessions.map(session => {
const deviceId = session.deviceId
const isDevice = !! deviceId
const deviceId = session.deviceId;
const isDevice = !! deviceId;
let deviceName = session.deviceName
let deviceName = session.deviceName;
if (! deviceName) {
deviceName = devices.synthesizeName(session)
deviceName = devices.synthesizeName(session);
}
let userAgent
let userAgent;
if (! session.uaBrowser) {
userAgent = ''
userAgent = '';
} else if (! session.uaBrowserVersion) {
userAgent = session.uaBrowser
userAgent = session.uaBrowser;
} else {
const { uaBrowser: browser, uaBrowserVersion: version } = session
userAgent = `${browser} ${version.split('.')[0]}`
const { uaBrowser: browser, uaBrowserVersion: version } = session;
userAgent = `${browser} ${version.split('.')[0]}`;
}
return Object.assign({
@ -578,10 +578,10 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
),
os: session.uaOS,
userAgent
}, marshallLastAccessTime(session.lastAccessTime, request))
})
}, marshallLastAccessTime(session.lastAccessTime, request));
});
}
)
);
}
},
{
@ -604,32 +604,32 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
}
},
handler: async function (request) {
log.begin('Account.deviceDestroy', request)
log.begin('Account.deviceDestroy', request);
const credentials = request.auth.credentials
const uid = credentials.uid
const id = request.payload.id
let devices
const credentials = request.auth.credentials;
const uid = credentials.uid;
const id = request.payload.id;
let devices;
// We want to include the disconnected device in the list
// of devices to notify, so list them before disconnecting.
return request.app.devices
.then(res => {
devices = res
return db.deleteDevice(uid, id)
devices = res;
return db.deleteDevice(uid, id);
})
.then(() => {
const deviceToDelete = devices.find(d => d.id === id)
const deviceToDelete = devices.find(d => d.id === id);
if (deviceToDelete && deviceToDelete.refreshTokenId) {
// attempt to clean up the refreshToken in the OAuth DB
return oauthdb.revokeRefreshTokenById(deviceToDelete.refreshTokenId).catch((err) => {
log.error('deviceDestroy.revokeRefreshTokenById.error', {err: err.message})
})
log.error('deviceDestroy.revokeRefreshTokenById.error', {err: err.message});
});
}
})
.then(() => {
push.notifyDeviceDisconnected(uid, devices, id)
.catch(() => {})
.catch(() => {});
return P.all([
request.emitMetricsEvent('device.deleted', {
uid: uid,
@ -640,10 +640,10 @@ module.exports = (log, db, config, customs, push, pushbox, devices, oauthdb) =>
id: id,
timestamp: Date.now()
})
])
]);
})
.then(() => { return {} })
.then(() => { return {}; });
}
}
]
}
];
};

Просмотреть файл

@ -2,17 +2,17 @@
* 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'
'use strict';
const butil = require('../crypto/butil')
const emailUtils = require('./utils/email')
const error = require('../error')
const isA = require('joi')
const P = require('../promise')
const random = require('../crypto/random')
const validators = require('./validators')
const butil = require('../crypto/butil');
const emailUtils = require('./utils/email');
const error = require('../error');
const isA = require('joi');
const P = require('../promise');
const random = require('../crypto/random');
const validators = require('./validators');
const HEX_STRING = validators.HEX_STRING
const HEX_STRING = validators.HEX_STRING;
module.exports = (log, db, mailer, config, customs, push) => {
return [
@ -40,22 +40,22 @@ module.exports = (log, db, mailer, config, customs, push) => {
}
},
handler: async function (request) {
log.begin('Account.RecoveryEmailStatus', request)
log.begin('Account.RecoveryEmailStatus', request);
const sessionToken = request.auth.credentials
const sessionToken = request.auth.credentials;
if (request.query && request.query.reason === 'push') {
// log to the push namespace that account was verified via push
log.info('push.pushToDevices', {
name: 'recovery_email_reason.push'
})
});
}
return cleanUpIfAccountInvalid()
.then(createResponse)
.then(createResponse);
function cleanUpIfAccountInvalid () {
const now = new Date().getTime()
const staleTime = now - config.emailStatusPollingTimeout
const now = new Date().getTime();
const staleTime = now - config.emailStatusPollingTimeout;
if (sessionToken.createdAt < staleTime) {
log.info('recovery_email.status.stale', {
@ -65,7 +65,7 @@ module.exports = (log, db, mailer, config, customs, push) => {
emailVerified: sessionToken.emailVerified,
tokenVerified: sessionToken.tokenVerified,
browser: `${sessionToken.uaBrowser} ${sessionToken.uaBrowserVersion}`
})
});
}
if (! sessionToken.emailVerified) {
// Some historical bugs mean we've allowed creation
@ -75,20 +75,20 @@ module.exports = (log, db, mailer, config, customs, push) => {
if (! validators.isValidEmailAddress(sessionToken.email)) {
return db.deleteAccount(sessionToken)
.then(() => {
log.info('accountDeleted.invalidEmailAddress', { ...sessionToken })
log.info('accountDeleted.invalidEmailAddress', { ...sessionToken });
// Act as though we deleted the account asynchronously
// and caused the sessionToken to become invalid.
throw error.invalidToken('This account was invalid and has been deleted')
})
throw error.invalidToken('This account was invalid and has been deleted');
});
}
}
return P.resolve()
return P.resolve();
}
function createResponse () {
const sessionVerified = sessionToken.tokenVerified
const emailVerified = !! sessionToken.emailVerified
const sessionVerified = sessionToken.tokenVerified;
const emailVerified = !! sessionToken.emailVerified;
// For backwards-compatibility reasons, the reported verification status
// depends on whether the sessionToken was created with keys=true and
@ -97,9 +97,9 @@ module.exports = (log, db, mailer, config, customs, push) => {
// has been verified. Otherwise, desktop clients will attempt to use
// an unverified session to connect to sync, and produce a very confusing
// user experience.
let isVerified = emailVerified
let isVerified = emailVerified;
if (sessionToken.mustVerify) {
isVerified = isVerified && sessionVerified
isVerified = isVerified && sessionVerified;
}
return {
@ -107,7 +107,7 @@ module.exports = (log, db, mailer, config, customs, push) => {
verified: isVerified,
sessionVerified: sessionVerified,
emailVerified: emailVerified
}
};
}
}
},
@ -133,38 +133,38 @@ module.exports = (log, db, mailer, config, customs, push) => {
}
},
handler: async function (request) {
log.begin('Account.RecoveryEmailResend', request)
log.begin('Account.RecoveryEmailResend', request);
const email = request.payload.email
const sessionToken = request.auth.credentials
const service = request.payload.service || request.query.service
const type = request.payload.type || request.query.type
const ip = request.app.clientAddress
const geoData = request.app.geo
const email = request.payload.email;
const sessionToken = request.auth.credentials;
const service = request.payload.service || request.query.service;
const type = request.payload.type || request.query.type;
const ip = request.app.clientAddress;
const geoData = request.app.geo;
// This endpoint can resend multiple types of codes, set these values once it
// is known what is being verified.
let code
let verifyFunction
let event
let emails = []
let sendEmail = true
let code;
let verifyFunction;
let event;
let emails = [];
let sendEmail = true;
// Return immediately if this session or token is already verified. Only exception
// is if the email param has been specified, which means that this is
// a request to verify a secondary email.
if (sessionToken.emailVerified && sessionToken.tokenVerified && ! email) {
return {}
return {};
}
const { flowId, flowBeginTime } = await request.app.metricsContext
const { flowId, flowBeginTime } = await request.app.metricsContext;
return customs.check(request, sessionToken.email, 'recoveryEmailResendCode')
.then(setVerifyCode)
.then(setVerifyFunction)
.then(() => {
if (! sendEmail) {
return
return;
}
const mailerOpts = {
@ -186,12 +186,12 @@ module.exports = (log, db, mailer, config, customs, push) => {
uaOSVersion: sessionToken.uaOSVersion,
uaDeviceType: sessionToken.uaDeviceType,
uid: sessionToken.uid
}
};
return verifyFunction(emails, sessionToken, mailerOpts)
.then(() => request.emitMetricsEvent(`email.${event}.resent`))
.then(() => request.emitMetricsEvent(`email.${event}.resent`));
})
.then(() => { return {} })
.then(() => { return {}; });
function setVerifyCode () {
return db.accountEmails(sessionToken.uid)
@ -199,65 +199,65 @@ module.exports = (log, db, mailer, config, customs, push) => {
if (email) {
// If an email address is specified in payload, this is a request to verify
// a secondary email. This should return the corresponding email code for verification.
let emailVerified = false
let emailVerified = false;
emailData.some((userEmail) => {
if (userEmail.normalizedEmail === email.toLowerCase()) {
code = userEmail.emailCode
emailVerified = userEmail.isVerified
emails = [userEmail]
return true
code = userEmail.emailCode;
emailVerified = userEmail.isVerified;
emails = [userEmail];
return true;
}
})
});
// This user is attempting to verify a secondary email that doesn't belong to the account.
if (emails.length === 0) {
throw error.cannotResendEmailCodeToUnownedEmail()
throw error.cannotResendEmailCodeToUnownedEmail();
}
// Don't resend code for already verified emails
if (emailVerified) {
return {}
return {};
}
} else if (sessionToken.tokenVerificationId) {
emails = emailData
code = sessionToken.tokenVerificationId
emails = emailData;
code = sessionToken.tokenVerificationId;
// Check to see if this account has a verified TOTP token. If so, then it should
// not be allowed to bypass TOTP requirement by sending a sign-in confirmation email.
return db.totpToken(sessionToken.uid)
.then((result) => {
if (result && result.verified && result.enabled) {
sendEmail = false
return
sendEmail = false;
return;
}
code = sessionToken.tokenVerificationId
code = sessionToken.tokenVerificationId;
}, (err) => {
if (err.errno === error.ERRNO.TOTP_TOKEN_NOT_FOUND) {
code = sessionToken.tokenVerificationId
return
code = sessionToken.tokenVerificationId;
return;
}
throw err
})
throw err;
});
} else {
code = sessionToken.emailCode
code = sessionToken.emailCode;
}
})
});
}
function setVerifyFunction () {
if (type && type === 'upgradeSession') {
verifyFunction = mailer.sendVerifyPrimaryEmail
event = 'verification_email_primary'
verifyFunction = mailer.sendVerifyPrimaryEmail;
event = 'verification_email_primary';
} else if (email) {
verifyFunction = mailer.sendVerifySecondaryEmail
event = 'verification_email'
verifyFunction = mailer.sendVerifySecondaryEmail;
event = 'verification_email';
} else if (! sessionToken.emailVerified) {
verifyFunction = mailer.sendVerifyCode
event = 'verification'
verifyFunction = mailer.sendVerifyCode;
event = 'verification';
} else {
verifyFunction = mailer.sendVerifyLoginEmail
event = 'confirmation'
verifyFunction = mailer.sendVerifyLoginEmail;
event = 'confirmation';
}
}
}
@ -279,15 +279,15 @@ module.exports = (log, db, mailer, config, customs, push) => {
}
},
handler: async function (request) {
log.begin('Account.RecoveryEmailVerify', request)
log.begin('Account.RecoveryEmailVerify', request);
const { code, marketingOptIn, service, type, uid } = request.payload
const { code, marketingOptIn, service, type, uid } = request.payload;
// verify_code because we don't know what type this is yet, but
// we want to record right away before anything could fail, so
// we can see in a flow that a user tried to verify, even if it
// failed right away.
request.emitMetricsEvent('email.verify_code.clicked')
request.emitMetricsEvent('email.verify_code.clicked');
/**
* Below is a summary of the verify_code flow. This flow is used to verify emails, sign-in and
@ -305,60 +305,60 @@ module.exports = (log, db, mailer, config, customs, push) => {
// This endpoint is not authenticated, so we need to look up
// the target email address before we can check it with customs.
return customs.check(request, account.email, 'recoveryEmailVerifyCode')
.then(() => { return account })
.then(() => { return account; });
})
.then((account) => {
// Check if param `type` is specified and equal to `secondary`
// If so, verify the secondary email and respond
if (type && type === 'secondary') {
let matchedEmail
let matchedEmail;
return db.accountEmails(uid)
.then((emails) => {
const isEmailVerification = emails.some((email) => {
if (email.emailCode && (code === email.emailCode)) {
matchedEmail = email
log.info('account.verifyEmail.secondary.started', { uid, code })
return true
matchedEmail = email;
log.info('account.verifyEmail.secondary.started', { uid, code });
return true;
}
})
});
// Attempt to verify email token not associated with account
if (! isEmailVerification) {
throw error.invalidVerificationCode()
throw error.invalidVerificationCode();
}
// User is attempting to verify a secondary email that has already been verified.
// Silently succeed and don't send post verification email.
if (matchedEmail.isVerified) {
log.info('account.verifyEmail.secondary.already-verified', { uid, code })
return P.resolve()
log.info('account.verifyEmail.secondary.already-verified', { uid, code });
return P.resolve();
}
return db.verifyEmail(account, code)
.then(() => {
log.info('account.verifyEmail.secondary.confirmed', { uid, code })
log.info('account.verifyEmail.secondary.confirmed', { uid, code });
return mailer.sendPostVerifySecondaryEmail([], account, {
acceptLanguage: request.app.acceptLanguage,
secondaryEmail: matchedEmail.email,
service,
uid
})
})
})
});
});
});
}
const isAccountVerification = butil.buffersAreEqual(code, account.emailCode)
let device
const isAccountVerification = butil.buffersAreEqual(code, account.emailCode);
let device;
return db.deviceFromTokenVerificationId(uid, code)
.then(
associatedDevice => {
device = associatedDevice
device = associatedDevice;
},
err => {
if (err.errno !== error.ERRNO.DEVICE_UNKNOWN) {
log.error('Account.RecoveryEmailVerify', { err, uid, code })
log.error('Account.RecoveryEmailVerify', { err, uid, code });
}
}
)
@ -374,35 +374,35 @@ module.exports = (log, db, mailer, config, customs, push) => {
*
* 3) Verify account email if not already verified.
*/
return db.verifyTokens(code, account)
return db.verifyTokens(code, account);
})
.then(() => {
if (! isAccountVerification) {
// Don't log sign-in confirmation success for the account verification case
log.info('account.signin.confirm.success', { uid, code })
log.info('account.signin.confirm.success', { uid, code });
request.emitMetricsEvent('account.confirmed', { uid })
request.emitMetricsEvent('account.confirmed', { uid });
request.app.devices.then(devices =>
push.notifyAccountUpdated(uid, devices, 'accountConfirm')
)
);
}
})
.catch(err => {
if (err.errno === error.ERRNO.INVALID_VERIFICATION_CODE && isAccountVerification) {
// The code is just for the account, not for any sessions
return
return;
}
log.error('account.signin.confirm.invalid', { err, uid, code })
throw err
log.error('account.signin.confirm.invalid', { err, uid, code });
throw err;
})
.then(() => {
if (device) {
request.app.devices.then(devices => {
const otherDevices = devices.filter(d => d.id !== device.id)
return push.notifyDeviceConnected(uid, otherDevices, device.name)
})
const otherDevices = devices.filter(d => d.id !== device.id);
return push.notifyDeviceConnected(uid, otherDevices, device.name);
});
}
})
.then(() => {
@ -410,7 +410,7 @@ module.exports = (log, db, mailer, config, customs, push) => {
// for sign-in confirmation or they may have been clicking a
// stale link. Silently succeed.
if (account.emailVerified) {
return
return;
}
// Any matching code verifies the account
@ -430,13 +430,13 @@ module.exports = (log, db, mailer, config, customs, push) => {
marketingOptIn: marketingOptIn || false,
uid
})
])
]);
})
.then(() => {
// send a push notification to all devices that the account changed
request.app.devices.then(devices =>
push.notifyAccountUpdated(uid, devices, 'accountVerify')
)
);
})
.then(() => {
// Our post-verification email is very specific to sync,
@ -446,12 +446,12 @@ module.exports = (log, db, mailer, config, customs, push) => {
acceptLanguage: request.app.acceptLanguage,
service,
uid
})
});
}
});
});
})
})
})
.then(() => { return {} })
.then(() => { return {}; });
}
},
@ -472,22 +472,22 @@ module.exports = (log, db, mailer, config, customs, push) => {
}
},
handler: async function (request) {
log.begin('Account.RecoveryEmailEmails', request)
log.begin('Account.RecoveryEmailEmails', request);
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
return db.account(uid)
.then((account) => {
return createResponse(account.emails)
})
return createResponse(account.emails);
});
function createResponse (emails) {
return emails.map((email) => ({
email: email.email,
isPrimary: !! email.isPrimary,
verified: !! email.isVerified
}))
}));
}
}
},
@ -506,85 +506,85 @@ module.exports = (log, db, mailer, config, customs, push) => {
response: {}
},
handler: async function (request) {
log.begin('Account.RecoveryEmailCreate', request)
log.begin('Account.RecoveryEmailCreate', request);
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
const primaryEmail = sessionToken.email
const ip = request.app.clientAddress
const email = request.payload.email
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
const primaryEmail = sessionToken.email;
const ip = request.app.clientAddress;
const email = request.payload.email;
const emailData = {
email: email,
normalizedEmail: email.toLowerCase(),
isVerified: false,
isPrimary: false,
uid: uid
}
};
return customs.check(request, primaryEmail, 'createEmail')
.then(() => {
if (! sessionToken.emailVerified) {
throw error.unverifiedAccount()
throw error.unverifiedAccount();
}
if (sessionToken.tokenVerificationId) {
throw error.unverifiedSession()
throw error.unverifiedSession();
}
if (sessionToken.email.toLowerCase() === email.toLowerCase()) {
throw error.yourPrimaryEmailExists()
throw error.yourPrimaryEmailExists();
}
})
.then(deleteAccountIfUnverified)
.then(generateRandomValues)
.then(createEmail)
.then(sendEmailVerification)
.then(() => { return {} })
.then(() => { return {}; });
function deleteAccountIfUnverified() {
return db.getSecondaryEmail(email)
.then((secondaryEmailRecord) => {
if (secondaryEmailRecord.isPrimary) {
if (secondaryEmailRecord.isVerified) {
throw error.verifiedPrimaryEmailAlreadyExists()
throw error.verifiedPrimaryEmailAlreadyExists();
}
const msSinceCreated = Date.now() - secondaryEmailRecord.createdAt
const minUnverifiedAccountTime = config.secondaryEmail.minUnverifiedAccountTime
const msSinceCreated = Date.now() - secondaryEmailRecord.createdAt;
const minUnverifiedAccountTime = config.secondaryEmail.minUnverifiedAccountTime;
if (msSinceCreated >= minUnverifiedAccountTime) {
return db.deleteAccount(secondaryEmailRecord)
.then(() => log.info('accountDeleted.unverifiedSecondaryEmail', { ...secondaryEmailRecord }))
.then(() => log.info('accountDeleted.unverifiedSecondaryEmail', { ...secondaryEmailRecord }));
} else {
throw error.unverifiedPrimaryEmailNewlyCreated()
throw error.unverifiedPrimaryEmailNewlyCreated();
}
}
// Only delete secondary email if it is unverified and does not belong
// to the current user.
if (! secondaryEmailRecord.isVerified && ! butil.buffersAreEqual(secondaryEmailRecord.uid, uid)) {
return db.deleteEmail(secondaryEmailRecord.uid, secondaryEmailRecord.email)
return db.deleteEmail(secondaryEmailRecord.uid, secondaryEmailRecord.email);
}
})
.catch((err) => {
if (err.errno !== error.ERRNO.SECONDARY_EMAIL_UNKNOWN) {
throw err
throw err;
}
})
});
}
function generateRandomValues () {
return random.hex(16)
.then(hex => {
emailData.emailCode = hex
})
emailData.emailCode = hex;
});
}
function createEmail () {
return db.createEmail(uid, emailData)
return db.createEmail(uid, emailData);
}
function sendEmailVerification() {
const geoData = request.app.geo
const geoData = request.app.geo;
return mailer.sendVerifySecondaryEmail([emailData], sessionToken, {
code: emailData.emailCode,
deviceId: sessionToken.deviceId,
@ -601,12 +601,12 @@ module.exports = (log, db, mailer, config, customs, push) => {
uid
})
.catch((err) => {
log.error('mailer.sendVerifySecondaryEmail', { err: err})
log.error('mailer.sendVerifySecondaryEmail', { err: err});
return db.deleteEmail(emailData.uid, emailData.normalizedEmail)
.then(() => {
throw emailUtils.sendError(err, true)
})
})
throw emailUtils.sendError(err, true);
});
});
}
}
},
@ -625,23 +625,23 @@ module.exports = (log, db, mailer, config, customs, push) => {
response: {}
},
handler: async function (request) {
log.begin('Account.RecoveryEmailDestroy', request)
log.begin('Account.RecoveryEmailDestroy', request);
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
const primaryEmail = sessionToken.email
const email = request.payload.email
let account
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
const primaryEmail = sessionToken.email;
const email = request.payload.email;
let account;
return customs.check(request, primaryEmail, 'deleteEmail')
.then(() => {
return db.account(uid)
return db.account(uid);
})
.then((result) => {
account = result
account = result;
if (sessionToken.tokenVerificationId) {
throw error.unverifiedSession()
throw error.unverifiedSession();
}
})
.then(deleteEmail)
@ -649,35 +649,35 @@ module.exports = (log, db, mailer, config, customs, push) => {
.then(() => {
// Find the email object that corresponds to the email being deleted
const emailIsVerified = account.emails.find((item) => {
return item.normalizedEmail === email.toLowerCase() && item.isVerified
})
return item.normalizedEmail === email.toLowerCase() && item.isVerified;
});
// Don't bother sending a notification if removing an email that was never verified
if (! emailIsVerified) {
return P.resolve()
return P.resolve();
}
// Notify only primary email and all *other* verified secondary emails about the
// deletion.
const emails = account.emails.filter((item) => {
if (item.normalizedEmail !== email.toLowerCase()) {
return item
return item;
}
})
});
return mailer.sendPostRemoveSecondaryEmail(emails, account, {
deviceId: sessionToken.deviceId,
secondaryEmail: email,
uid
});
})
})
.then(() => { return {} })
.then(() => { return {}; });
function deleteEmail () {
return db.deleteEmail(uid, email.toLowerCase())
return db.deleteEmail(uid, email.toLowerCase());
}
function resetAccountTokens () {
return db.resetAccountTokens(uid)
return db.resetAccountTokens(uid);
}
}
},
@ -696,58 +696,58 @@ module.exports = (log, db, mailer, config, customs, push) => {
response: {}
},
handler: async function (request) {
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
const primaryEmail = sessionToken.email
const email = request.payload.email
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
const primaryEmail = sessionToken.email;
const email = request.payload.email;
log.begin('Account.RecoveryEmailSetPrimary', request)
log.begin('Account.RecoveryEmailSetPrimary', request);
return customs.check(request, primaryEmail, 'setPrimaryEmail')
.then(() => {
if (sessionToken.tokenVerificationId) {
throw error.unverifiedSession()
throw error.unverifiedSession();
}
})
.then(setPrimaryEmail)
.then(() => {
return {}
})
return {};
});
function setPrimaryEmail() {
return db.getSecondaryEmail(email)
.then((email) => {
if (email.uid !== uid) {
throw error.cannotChangeEmailToUnownedEmail()
throw error.cannotChangeEmailToUnownedEmail();
}
if (! email.isVerified) {
throw error.cannotChangeEmailToUnverifiedEmail()
throw error.cannotChangeEmailToUnverifiedEmail();
}
if (email.isPrimary) {
return
return;
}
return db.setPrimaryEmail(uid, email.normalizedEmail)
return db.setPrimaryEmail(uid, email.normalizedEmail);
})
.then(() => {
request.app.devices.then(devices => push.notifyProfileUpdated(uid, devices))
request.app.devices.then(devices => push.notifyProfileUpdated(uid, devices));
log.notifyAttachedServices('primaryEmailChanged', request, {
uid,
email: email
})
});
return db.account(uid)
return db.account(uid);
})
.then((account) => {
return mailer.sendPostChangePrimaryEmail(account.emails, account, {
acceptLanguage: request.app.acceptLanguage,
uid
})
})
});
});
}
}
}
]
}
];
};

Просмотреть файл

@ -2,21 +2,21 @@
* 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'
'use strict';
var jwtool = require('fxa-jwtool')
var jwtool = require('fxa-jwtool');
function b64toDec(str) {
var n = new jwtool.BN(Buffer.from(str, 'base64'))
return n.toString(10)
var n = new jwtool.BN(Buffer.from(str, 'base64'));
return n.toString(10);
}
function toDec(str) {
return /^[0-9]+$/.test(str) ? str : b64toDec(str)
return /^[0-9]+$/.test(str) ? str : b64toDec(str);
}
function browseridFormat(keys) {
var primary = keys[0]
var primary = keys[0];
return {
'public-key': {
kid: primary.jwk.kid,
@ -28,14 +28,14 @@ function browseridFormat(keys) {
authentication: '/.well-known/browserid/nonexistent.html',
provisioning: '/.well-known/browserid/nonexistent.html',
keys: keys
}
};
}
module.exports = function (log, serverPublicKeys) {
var keys = [ serverPublicKeys.primary ]
if (serverPublicKeys.secondary) { keys.push(serverPublicKeys.secondary) }
var keys = [ serverPublicKeys.primary ];
if (serverPublicKeys.secondary) { keys.push(serverPublicKeys.secondary); }
var browserid = browseridFormat(keys)
var browserid = browseridFormat(keys);
var routes = [
{
@ -48,8 +48,8 @@ module.exports = function (log, serverPublicKeys) {
}
},
handler: async function (request) {
log.begin('browserid', request)
return browserid
log.begin('browserid', request);
return browserid;
}
},
{
@ -59,10 +59,10 @@ module.exports = function (log, serverPublicKeys) {
// FOR DEV PURPOSES ONLY
return {
keys: keys
};
}
}
}
]
];
return routes
}
return routes;
};

Просмотреть файл

@ -2,9 +2,9 @@
* 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'
'use strict';
const url = require('url')
const url = require('url');
module.exports = function (
log,
@ -19,13 +19,13 @@ module.exports = function (
customs
) {
// Various extra helpers.
const push = require('../push')(log, db, config)
const pushbox = require('../pushbox')(log, config)
const devicesImpl = require('../devices')(log, db, push)
const signinUtils = require('./utils/signin')(log, config, customs, db, mailer)
const push = require('../push')(log, db, config);
const pushbox = require('../pushbox')(log, config);
const devicesImpl = require('../devices')(log, db, push);
const signinUtils = require('./utils/signin')(log, config, customs, db, mailer);
// The routing modules themselves.
const defaults = require('./defaults')(log, db)
const idp = require('./idp')(log, serverPublicKeys)
const defaults = require('./defaults')(log, db);
const idp = require('./idp')(log, serverPublicKeys);
const account = require('./account')(
log,
db,
@ -35,10 +35,10 @@ module.exports = function (
customs,
signinUtils,
push
)
const oauth = require('./oauth')(log, config, oauthdb)
const devicesSessions = require('./devices-and-sessions')(log, db, config, customs, push, pushbox, devicesImpl, oauthdb)
const emails = require('./emails')(log, db, mailer, config, customs, push)
);
const oauth = require('./oauth')(log, config, oauthdb);
const devicesSessions = require('./devices-and-sessions')(log, db, config, customs, push, pushbox, devicesImpl, oauthdb);
const emails = require('./emails')(log, db, mailer, config, customs, push);
const password = require('./password')(
log,
db,
@ -50,24 +50,24 @@ module.exports = function (
signinUtils,
push,
config
)
const tokenCodes = require('./token-codes')(log, db, config, customs)
const session = require('./session')(log, db, Password, config, signinUtils)
const sign = require('./sign')(log, signer, db, config.domain, devicesImpl)
const signinCodes = require('./signin-codes')(log, db, customs)
const smsRoute = require('./sms')(log, db, config, customs, smsImpl)
const unblockCodes = require('./unblock-codes')(log, db, mailer, config.signinUnblock, customs)
const totp = require('./totp')(log, db, mailer, customs, config.totp)
const recoveryCodes = require('./recovery-codes')(log, db, config.totp, customs, mailer)
const recoveryKey = require('./recovery-key')(log, db, Password, config.verifierVersion, customs, mailer)
);
const tokenCodes = require('./token-codes')(log, db, config, customs);
const session = require('./session')(log, db, Password, config, signinUtils);
const sign = require('./sign')(log, signer, db, config.domain, devicesImpl);
const signinCodes = require('./signin-codes')(log, db, customs);
const smsRoute = require('./sms')(log, db, config, customs, smsImpl);
const unblockCodes = require('./unblock-codes')(log, db, mailer, config.signinUnblock, customs);
const totp = require('./totp')(log, db, mailer, customs, config.totp);
const recoveryCodes = require('./recovery-codes')(log, db, config.totp, customs, mailer);
const recoveryKey = require('./recovery-key')(log, db, Password, config.verifierVersion, customs, mailer);
const util = require('./util')(
log,
config,
config.smtp.redirectDomain
)
);
let basePath = url.parse(config.publicUrl).path
if (basePath === '/') { basePath = '' }
let basePath = url.parse(config.publicUrl).path;
if (basePath === '/') { basePath = ''; }
const v1Routes = [].concat(
account,
@ -85,24 +85,24 @@ module.exports = function (
unblockCodes,
util,
recoveryKey
)
v1Routes.forEach(r => { r.path = basePath + '/v1' + r.path })
defaults.forEach(r => { r.path = basePath + r.path })
const allRoutes = defaults.concat(idp, v1Routes)
);
v1Routes.forEach(r => { r.path = basePath + '/v1' + r.path; });
defaults.forEach(r => { r.path = basePath + r.path; });
const allRoutes = defaults.concat(idp, v1Routes);
allRoutes.forEach(r => {
// Default auth.payload to 'optional' for all authenticated routes.
// We'll validate the payload hash if the client provides it,
// but allow them to skip it if they can't or don't want to.
const auth = r.options && r.options.auth
const auth = r.options && r.options.auth;
if (auth && ! auth.hasOwnProperty('payload')) {
auth.payload = 'optional'
auth.payload = 'optional';
}
// Remove custom `apidoc` key which we use for generating docs,
// but which Hapi doesn't like if it's there at runtime.
delete r.apidoc
})
delete r.apidoc;
});
return allRoutes
}
return allRoutes;
};

Просмотреть файл

@ -2,7 +2,7 @@
* 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'
'use strict';
/* Routes for managing OAuth authorization grants.
*
@ -15,7 +15,7 @@
*
*/
const Joi = require('joi')
const Joi = require('joi');
module.exports = (log, config, oauthdb) => {
const routes = [
@ -31,7 +31,7 @@ module.exports = (log, config, oauthdb) => {
}
},
handler: async function (request) {
return oauthdb.getClientInfo(request.params.client_id)
return oauthdb.getClientInfo(request.params.client_id);
}
},
{
@ -51,10 +51,10 @@ module.exports = (log, config, oauthdb) => {
}
},
handler: async function (request) {
const sessionToken = request.auth.credentials
return oauthdb.getScopedKeyData(sessionToken, request.payload)
const sessionToken = request.auth.credentials;
return oauthdb.getScopedKeyData(sessionToken, request.payload);
}
},
]
return routes
}
];
return routes;
};

Просмотреть файл

@ -2,19 +2,19 @@
* 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'
'use strict';
const validators = require('./validators')
const HEX_STRING = validators.HEX_STRING
const validators = require('./validators');
const HEX_STRING = validators.HEX_STRING;
const butil = require('../crypto/butil')
const error = require('../error')
const isA = require('joi')
const P = require('../promise')
const random = require('../crypto/random')
const requestHelper = require('../routes/utils/request_helper')
const butil = require('../crypto/butil');
const error = require('../error');
const isA = require('joi');
const P = require('../promise');
const random = require('../crypto/random');
const requestHelper = require('../routes/utils/request_helper');
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema;
module.exports = function (
log,
@ -29,12 +29,12 @@ module.exports = function (
config
) {
const totpUtils = require('../../lib/routes/utils/totp')(log, config, db)
const totpUtils = require('../../lib/routes/utils/totp')(log, config, db);
function failVerifyAttempt(passwordForgotToken) {
return (passwordForgotToken.failAttempt()) ?
db.deletePasswordForgotToken(passwordForgotToken) :
db.updatePasswordForgotToken(passwordForgotToken)
db.updatePasswordForgotToken(passwordForgotToken);
}
var routes = [
@ -50,9 +50,9 @@ module.exports = function (
}
},
handler: async function (request) {
log.begin('Password.changeStart', request)
var form = request.payload
var oldAuthPW = form.oldAuthPW
log.begin('Password.changeStart', request);
var form = request.payload;
var oldAuthPW = form.oldAuthPW;
return customs.check(
request,
@ -61,19 +61,19 @@ module.exports = function (
.then(db.accountRecord.bind(db, form.email))
.then(
function (emailRecord) {
const password = new Password(oldAuthPW, emailRecord.authSalt, emailRecord.verifierVersion)
const password = new Password(oldAuthPW, emailRecord.authSalt, emailRecord.verifierVersion);
return signinUtils.checkPassword(emailRecord, password, request.app.clientAddress)
.then(
function (match) {
if (! match) {
throw error.incorrectPassword(emailRecord.email, form.email)
throw error.incorrectPassword(emailRecord.email, form.email);
}
var password = new Password(
oldAuthPW,
emailRecord.authSalt,
emailRecord.verifierVersion
)
return password.unwrap(emailRecord.wrapWrapKb)
);
return password.unwrap(emailRecord.wrapWrapKb);
}
)
.then(
@ -96,22 +96,22 @@ module.exports = function (
return {
keyFetchToken: keyFetchToken,
passwordChangeToken: passwordChangeToken
};
}
);
}
)
);
}
)
}
)
);
},
function (err) {
if (err.errno === error.ERRNO.ACCOUNT_UNKNOWN) {
customs.flag(request.app.clientAddress, {
email: form.email,
errno: err.errno
})
});
}
throw err
throw err;
}
)
.then(
@ -120,10 +120,10 @@ module.exports = function (
keyFetchToken: tokens.keyFetchToken.data,
passwordChangeToken: tokens.passwordChangeToken.data,
verified: tokens.keyFetchToken.emailVerified
}
};
}
)
);
}
},
{
@ -145,15 +145,15 @@ module.exports = function (
}
},
handler: async function (request) {
log.begin('Password.changeFinish', request)
var passwordChangeToken = request.auth.credentials
var authPW = request.payload.authPW
var wrapKb = request.payload.wrapKb
var sessionTokenId = request.payload.sessionToken
var wantsKeys = requestHelper.wantsKeys(request)
const ip = request.app.clientAddress
log.begin('Password.changeFinish', request);
var passwordChangeToken = request.auth.credentials;
var authPW = request.payload.authPW;
var wrapKb = request.payload.wrapKb;
var sessionTokenId = request.payload.sessionToken;
var wantsKeys = requestHelper.wantsKeys(request);
const ip = request.app.clientAddress;
var account, verifyHash, sessionToken, keyFetchToken, verifiedStatus,
devicesToNotify, originatingDeviceId, hasTotp = false
devicesToNotify, originatingDeviceId, hasTotp = false;
return checkTotpToken()
.then(getSessionVerificationStatus)
@ -162,21 +162,21 @@ module.exports = function (
.then(notifyAccount)
.then(createSessionToken)
.then(createKeyFetchToken)
.then(createResponse)
.then(createResponse);
function checkTotpToken() {
return totpUtils.hasTotpToken(passwordChangeToken)
.then((result) => {
hasTotp = result
hasTotp = result;
// Currently, users that have a TOTP token must specify a sessionTokenId to complete the
// password change process. While the `sessionTokenId` is optional, we require it
// in the case of TOTP because we want to check that session has been verified
// by TOTP.
if (result && ! sessionTokenId) {
throw error.unverifiedSession()
throw error.unverifiedSession();
}
})
});
}
function getSessionVerificationStatus() {
@ -184,20 +184,20 @@ module.exports = function (
return db.sessionToken(sessionTokenId)
.then(
function (tokenData) {
verifiedStatus = tokenData.tokenVerified
verifiedStatus = tokenData.tokenVerified;
if (tokenData.deviceId) {
originatingDeviceId = tokenData.deviceId
originatingDeviceId = tokenData.deviceId;
}
if (hasTotp && tokenData.authenticatorAssuranceLevel <= 1) {
throw error.unverifiedSession()
throw error.unverifiedSession();
}
}
)
);
} else {
// Don't create a verified session unless they already had one.
verifiedStatus = false
return P.resolve()
verifiedStatus = false;
return P.resolve();
}
}
@ -205,33 +205,33 @@ module.exports = function (
// We fetch the devices to notify before changePassword() because
// db.resetAccount() deletes all the devices saved in the account.
return request.app.devices.then(devices => {
devicesToNotify = devices
devicesToNotify = devices;
// If the originating sessionToken belongs to a device,
// do not send the notification to that device. It will
// get informed about the change via WebChannel message.
if (originatingDeviceId) {
devicesToNotify = devicesToNotify.filter(d => (d.id !== originatingDeviceId))
devicesToNotify = devicesToNotify.filter(d => (d.id !== originatingDeviceId));
}
})
});
}
function changePassword() {
let authSalt, password
let authSalt, password;
return random.hex(32)
.then(hex => {
authSalt = hex
password = new Password(authPW, authSalt, verifierVersion)
return db.deletePasswordChangeToken(passwordChangeToken)
authSalt = hex;
password = new Password(authPW, authSalt, verifierVersion);
return db.deletePasswordChangeToken(passwordChangeToken);
})
.then(
function () {
return password.verifyHash()
return password.verifyHash();
}
)
.then(
function (hash) {
verifyHash = hash
return password.wrap(wrapKb)
verifyHash = hash;
return password.wrap(wrapKb);
}
)
.then(
@ -245,7 +245,7 @@ module.exports = function (
wrapWrapKb: wrapWrapKb,
verifierVersion: password.version
}
)
);
}
)
.then(
@ -255,42 +255,42 @@ module.exports = function (
})
.then(
function () {
return result
return result;
}
)
);
}
)
);
}
function notifyAccount() {
if (devicesToNotify) {
// Notify the devices that the account has changed.
push.notifyPasswordChanged(passwordChangeToken.uid, devicesToNotify)
push.notifyPasswordChanged(passwordChangeToken.uid, devicesToNotify);
}
return db.account(passwordChangeToken.uid)
.then(
function (accountData) {
account = accountData
account = accountData;
log.notifyAttachedServices('passwordChange', request, {
uid: passwordChangeToken.uid,
iss: config.domain,
generation: account.verifierSetAt
})
return db.accountEmails(passwordChangeToken.uid)
});
return db.accountEmails(passwordChangeToken.uid);
}
)
.then(
function (emails) {
const geoData = request.app.geo
const geoData = request.app.geo;
const {
browser: uaBrowser,
browserVersion: uaBrowserVersion,
os: uaOS,
osVersion: uaOSVersion,
deviceType: uaDeviceType
} = request.app.ua
} = request.app.ua;
return mailer.sendPasswordChangedNotification(emails, account, {
acceptLanguage: request.app.acceptLanguage,
@ -309,17 +309,17 @@ module.exports = function (
// and pretend everything worked.
log.trace('Password.changeFinish.sendPasswordChangedNotification.error', {
error: e
})
})
});
});
}
)
);
}
function createSessionToken() {
return P.resolve()
.then(() => {
if (! verifiedStatus) {
return random.hex(16)
return random.hex(16);
}
})
.then(maybeToken => {
@ -330,7 +330,7 @@ module.exports = function (
osVersion: uaOSVersion,
deviceType: uaDeviceType,
formFactor: uaFormFactor
} = request.app.ua
} = request.app.ua;
// Create a sessionToken with the verification status of the current session
const sessionTokenOptions = {
@ -347,15 +347,15 @@ module.exports = function (
uaOSVersion,
uaDeviceType,
uaFormFactor
}
};
return db.createSessionToken(sessionTokenOptions)
return db.createSessionToken(sessionTokenOptions);
})
.then(
function (result) {
sessionToken = result
sessionToken = result;
}
)
);
}
function createKeyFetchToken() {
@ -370,9 +370,9 @@ module.exports = function (
})
.then(
function (result) {
keyFetchToken = result
keyFetchToken = result;
}
)
);
}
}
@ -380,7 +380,7 @@ module.exports = function (
// If no sessionToken, this could be a legacy client
// attempting to change password, return legacy response.
if (! sessionTokenId) {
return {}
return {};
}
var response = {
@ -388,13 +388,13 @@ module.exports = function (
sessionToken: sessionToken.data,
verified: sessionToken.emailVerified && sessionToken.tokenVerified,
authAt: sessionToken.lastAuthAt()
}
};
if (wantsKeys) {
response.keyFetchToken = keyFetchToken.data
response.keyFetchToken = keyFetchToken.data;
}
return response
return response;
}
}
},
@ -425,24 +425,24 @@ module.exports = function (
}
},
handler: async function (request) {
log.begin('Password.forgotSend', request)
var email = request.payload.email
var service = request.payload.service || request.query.service
const ip = request.app.clientAddress
log.begin('Password.forgotSend', request);
var email = request.payload.email;
var service = request.payload.service || request.query.service;
const ip = request.app.clientAddress;
request.validateMetricsContext()
request.validateMetricsContext();
let flowCompleteSignal
let flowCompleteSignal;
if (requestHelper.wantsKeys(request)) {
flowCompleteSignal = 'account.signed'
flowCompleteSignal = 'account.signed';
} else {
flowCompleteSignal = 'account.reset'
flowCompleteSignal = 'account.reset';
}
request.setMetricsFlowCompleteSignal(flowCompleteSignal)
request.setMetricsFlowCompleteSignal(flowCompleteSignal);
const { deviceId, flowId, flowBeginTime } = await request.app.metricsContext
const { deviceId, flowId, flowBeginTime } = await request.app.metricsContext;
let passwordForgotToken
let passwordForgotToken;
return P.all([
request.emitMetricsEvent('password.forgot.send_code.start'),
@ -451,29 +451,29 @@ module.exports = function (
.then(db.accountRecord.bind(db, email))
.then(accountRecord => {
if (accountRecord.primaryEmail.normalizedEmail !== email.toLowerCase()) {
throw error.cannotResetPasswordWithSecondaryEmail()
throw error.cannotResetPasswordWithSecondaryEmail();
}
// The token constructor sets createdAt from its argument.
// Clobber the timestamp to prevent prematurely expired tokens.
accountRecord.createdAt = undefined
return db.createPasswordForgotToken(accountRecord)
accountRecord.createdAt = undefined;
return db.createPasswordForgotToken(accountRecord);
})
.then(result => {
passwordForgotToken = result
passwordForgotToken = result;
return P.all([
request.stashMetricsContext(passwordForgotToken),
db.accountEmails(passwordForgotToken.uid)
])
]);
})
.then(([_, emails]) => {
const geoData = request.app.geo
const geoData = request.app.geo;
const {
browser: uaBrowser,
browserVersion: uaBrowserVersion,
os: uaOS,
osVersion: uaOSVersion,
deviceType: uaDeviceType
} = request.app.ua
} = request.app.ua;
return mailer.sendRecoveryCode(emails, passwordForgotToken, {
token: passwordForgotToken,
@ -494,7 +494,7 @@ module.exports = function (
uaOSVersion,
uaDeviceType,
uid: passwordForgotToken.uid
})
});
})
.then(() => request.emitMetricsEvent('password.forgot.send_code.completed'))
.then(() => ({
@ -502,7 +502,7 @@ module.exports = function (
ttl: passwordForgotToken.ttl(),
codeLength: passwordForgotToken.passCode.length,
tries: passwordForgotToken.tries
}))
}));
}
},
{
@ -533,12 +533,12 @@ module.exports = function (
}
},
handler: async function (request) {
log.begin('Password.forgotResend', request)
var passwordForgotToken = request.auth.credentials
var service = request.payload.service || request.query.service
const ip = request.app.clientAddress
log.begin('Password.forgotResend', request);
var passwordForgotToken = request.auth.credentials;
var service = request.payload.service || request.query.service;
const ip = request.app.clientAddress;
const { deviceId, flowId, flowBeginTime } = await request.app.metricsContext
const { deviceId, flowId, flowBeginTime } = await request.app.metricsContext;
return P.all([
request.emitMetricsEvent('password.forgot.resend_code.start'),
@ -548,14 +548,14 @@ module.exports = function (
function () {
return db.accountEmails(passwordForgotToken.uid)
.then(emails => {
const geoData = request.app.geo
const geoData = request.app.geo;
const {
browser: uaBrowser,
browserVersion: uaBrowserVersion,
os: uaOS,
osVersion: uaOSVersion,
deviceType: uaDeviceType
} = request.app.ua
} = request.app.ua;
return mailer.sendRecoveryCode(emails, passwordForgotToken, {
code: passwordForgotToken.passCode,
@ -576,13 +576,13 @@ module.exports = function (
uaOSVersion,
uaDeviceType,
uid: passwordForgotToken.uid
})
})
});
});
}
)
.then(
function(){
return request.emitMetricsEvent('password.forgot.resend_code.completed')
return request.emitMetricsEvent('password.forgot.resend_code.completed');
}
)
.then(
@ -592,9 +592,9 @@ module.exports = function (
ttl: passwordForgotToken.ttl(),
codeLength: passwordForgotToken.passCode.length,
tries: passwordForgotToken.tries
};
}
}
)
);
}
},
{
@ -617,14 +617,14 @@ module.exports = function (
}
},
handler: async function (request) {
log.begin('Password.forgotVerify', request)
var passwordForgotToken = request.auth.credentials
var code = request.payload.code
const accountResetWithRecoveryKey = request.payload.accountResetWithRecoveryKey
log.begin('Password.forgotVerify', request);
var passwordForgotToken = request.auth.credentials;
var code = request.payload.code;
const accountResetWithRecoveryKey = request.payload.accountResetWithRecoveryKey;
const { deviceId, flowId, flowBeginTime } = await request.app.metricsContext
const { deviceId, flowId, flowBeginTime } = await request.app.metricsContext;
let accountResetToken
let accountResetToken;
return P.all([
request.emitMetricsEvent('password.forgot.verify_code.start'),
@ -632,7 +632,7 @@ module.exports = function (
])
.then(() => {
if (butil.buffersAreEqual(passwordForgotToken.passCode, code) && passwordForgotToken.ttl() > 0) {
return db.forgotPasswordVerified(passwordForgotToken)
return db.forgotPasswordVerified(passwordForgotToken);
}
return failVerifyAttempt(passwordForgotToken)
@ -640,22 +640,22 @@ module.exports = function (
throw error.invalidVerificationCode({
tries: passwordForgotToken.tries,
ttl: passwordForgotToken.ttl()
})
})
});
});
})
.then(result => {
accountResetToken = result
accountResetToken = result;
return P.all([
request.propagateMetricsContext(passwordForgotToken, accountResetToken),
db.accountEmails(passwordForgotToken.uid)
])
]);
})
.then(([_, emails]) => {
if (accountResetWithRecoveryKey) {
// To prevent multiple password change emails being sent to a user,
// we check for a flag to see if this is a reset using an account recovery key.
// If it is, then the notification email will be sent in `/account/reset`
return P.resolve()
return P.resolve();
}
return mailer.sendPasswordResetNotification(
@ -669,12 +669,12 @@ module.exports = function (
flowBeginTime,
uid: passwordForgotToken.uid
}
)
);
})
.then(() => request.emitMetricsEvent('password.forgot.verify_code.completed'))
.then(() => ({
accountResetToken: accountResetToken.data
}))
}));
}
},
{
@ -692,16 +692,16 @@ module.exports = function (
}
},
handler: async function (request) {
log.begin('Password.forgotStatus', request)
var passwordForgotToken = request.auth.credentials
log.begin('Password.forgotStatus', request);
var passwordForgotToken = request.auth.credentials;
return {
tries: passwordForgotToken.tries,
ttl: passwordForgotToken.ttl()
}
};
}
}
]
];
return routes
}
return routes;
};

Просмотреть файл

@ -2,16 +2,16 @@
* 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'
'use strict';
const errors = require('../error')
const isA = require('joi')
const BASE_36 = require('./validators').BASE_36
const RECOVERY_CODE_SANE_MAX_LENGTH = 20
const errors = require('../error');
const isA = require('joi');
const BASE_36 = require('./validators').BASE_36;
const RECOVERY_CODE_SANE_MAX_LENGTH = 20;
module.exports = (log, db, config, customs, mailer) => {
const codeConfig = config.recoveryCodes
const RECOVERY_CODE_COUNT = codeConfig && codeConfig.count || 8
const codeConfig = config.recoveryCodes;
const RECOVERY_CODE_COUNT = codeConfig && codeConfig.count || 8;
return [
{
@ -28,13 +28,13 @@ module.exports = (log, db, config, customs, mailer) => {
}
},
handler: async function (request) {
log.begin('replaceRecoveryCodes', request)
log.begin('replaceRecoveryCodes', request);
const uid = request.auth.credentials.uid
const sessionToken = request.auth.credentials
const geoData = request.app.geo
const ip = request.app.clientAddress
let codes
const uid = request.auth.credentials.uid;
const sessionToken = request.auth.credentials;
const geoData = request.app.geo;
const ip = request.app.clientAddress;
let codes;
return replaceRecoveryCodes()
.then(sendEmailNotification)
@ -42,20 +42,20 @@ module.exports = (log, db, config, customs, mailer) => {
.then(() => {
return {
recoveryCodes: codes
}
})
};
});
function replaceRecoveryCodes() {
// Since TOTP and recovery codes go hand in hand, you should only be
// able to replace recovery codes in a TOTP verified session.
if (! sessionToken.authenticatorAssuranceLevel || sessionToken.authenticatorAssuranceLevel <= 1) {
throw errors.unverifiedSession()
throw errors.unverifiedSession();
}
return db.replaceRecoveryCodes(uid, RECOVERY_CODE_COUNT)
.then((result) => {
codes = result
})
codes = result;
});
}
function sendEmailNotification() {
@ -72,17 +72,17 @@ module.exports = (log, db, config, customs, mailer) => {
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid
})
})
});
});
}
function emitMetrics() {
log.info('account.recoveryCode.replaced', {
uid: uid
})
});
return request.emitMetricsEvent('recoveryCode.replaced', {uid: uid})
.then(() => ({}))
.then(() => ({}));
}
}
},
@ -105,14 +105,14 @@ module.exports = (log, db, config, customs, mailer) => {
}
},
handler: async function (request) {
log.begin('session.verify.recoveryCode', request)
log.begin('session.verify.recoveryCode', request);
const code = request.payload.code
const uid = request.auth.credentials.uid
const sessionToken = request.auth.credentials
const geoData = request.app.geo
const ip = request.app.clientAddress
let remainingRecoveryCodes
const code = request.payload.code;
const uid = request.auth.credentials.uid;
const sessionToken = request.auth.credentials;
const geoData = request.app.geo;
const ip = request.app.clientAddress;
let remainingRecoveryCodes;
return customs.check(request, sessionToken.email, 'verifyRecoveryCode')
.then(consumeRecoveryCode)
@ -122,31 +122,31 @@ module.exports = (log, db, config, customs, mailer) => {
.then(() => {
return {
remaining: remainingRecoveryCodes
}
})
};
});
function consumeRecoveryCode() {
return db.consumeRecoveryCode(uid, code)
.then((result) => {
remainingRecoveryCodes = result.remaining
remainingRecoveryCodes = result.remaining;
if (remainingRecoveryCodes === 0) {
log.info('account.recoveryCode.consumedAllCodes', {
uid
})
});
}
})
});
}
function verifySession() {
if (sessionToken.tokenVerificationId) {
return db.verifyTokensWithMethod(sessionToken.id, 'recovery-code')
return db.verifyTokensWithMethod(sessionToken.id, 'recovery-code');
}
}
function sendEmailNotification() {
return db.account(sessionToken.uid)
.then((account) => {
const defers = []
const defers = [];
const sendConsumeEmail = mailer.sendPostConsumeRecoveryCodeNotification(account.emails, account, {
acceptLanguage: request.app.acceptLanguage,
@ -159,34 +159,34 @@ module.exports = (log, db, config, customs, mailer) => {
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid
})
defers.push(sendConsumeEmail)
});
defers.push(sendConsumeEmail);
if (remainingRecoveryCodes <= codeConfig.notifyLowCount) {
log.info('account.recoveryCode.notifyLowCount', {
uid,
remaining: remainingRecoveryCodes
})
});
const sendLowCodesEmail = mailer.sendLowRecoveryCodeNotification(account.emails, account, {
acceptLanguage: request.app.acceptLanguage,
uid: sessionToken.uid
})
defers.push(sendLowCodesEmail)
});
defers.push(sendLowCodesEmail);
}
return Promise.all(defers)
})
return Promise.all(defers);
});
}
function emitMetrics() {
log.info('account.recoveryCode.verified', {
uid: uid
})
});
return request.emitMetricsEvent('recoveryCode.verified', {uid: uid})
.then(() => ({}))
.then(() => ({}));
}
}
}
]
}
];
};

Просмотреть файл

@ -2,11 +2,11 @@
* 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'
'use strict';
const errors = require('../error')
const validators = require('./validators')
const isA = require('joi')
const errors = require('../error');
const validators = require('./validators');
const isA = require('joi');
module.exports = (log, db, Password, verifierVersion, customs, mailer) => {
return [
@ -25,40 +25,40 @@ module.exports = (log, db, Password, verifierVersion, customs, mailer) => {
}
},
handler: async function (request) {
log.begin('createRecoveryKey', request)
log.begin('createRecoveryKey', request);
const uid = request.auth.credentials.uid
const sessionToken = request.auth.credentials
const {recoveryKeyId, recoveryData} = request.payload
const uid = request.auth.credentials.uid;
const sessionToken = request.auth.credentials;
const {recoveryKeyId, recoveryData} = request.payload;
return createRecoveryKey()
.then(emitMetrics)
.then(sendNotificationEmails)
.then(() => {
return {}
})
return {};
});
function createRecoveryKey() {
if (sessionToken.tokenVerificationId) {
throw errors.unverifiedSession()
throw errors.unverifiedSession();
}
return db.createRecoveryKey(uid, recoveryKeyId, recoveryData)
return db.createRecoveryKey(uid, recoveryKeyId, recoveryData);
}
function emitMetrics() {
log.info('account.recoveryKey.created', {
uid
})
});
return request.emitMetricsEvent('recoveryKey.created', {uid})
return request.emitMetricsEvent('recoveryKey.created', {uid});
}
function sendNotificationEmails() {
return db.account(uid)
.then((account) => {
const geoData = request.app.geo
const ip = request.app.clientAddress
const geoData = request.app.geo;
const ip = request.app.clientAddress;
const emailOptions = {
acceptLanguage: request.app.acceptLanguage,
ip: ip,
@ -70,10 +70,10 @@ module.exports = (log, db, Password, verifierVersion, customs, mailer) => {
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid
}
};
return mailer.sendPostAddAccountRecoveryNotification(account.emails, account, emailOptions)
})
return mailer.sendPostAddAccountRecoveryNotification(account.emails, account, emailOptions);
});
}
}
},
@ -91,21 +91,21 @@ module.exports = (log, db, Password, verifierVersion, customs, mailer) => {
}
},
handler: async function (request) {
log.begin('getRecoveryKey', request)
log.begin('getRecoveryKey', request);
const uid = request.auth.credentials.uid
const recoveryKeyId = request.params.recoveryKeyId
let recoveryData
const uid = request.auth.credentials.uid;
const recoveryKeyId = request.params.recoveryKeyId;
let recoveryData;
return customs.checkAuthenticated(request, uid, 'getRecoveryKey')
.then(getRecoveryKey)
.then(() => {
return {recoveryData}
})
return {recoveryData};
});
function getRecoveryKey() {
return db.getRecoveryKey(uid, recoveryKeyId)
.then((res) => recoveryData = res.recoveryData)
.then((res) => recoveryData = res.recoveryData);
}
}
},
@ -129,13 +129,13 @@ module.exports = (log, db, Password, verifierVersion, customs, mailer) => {
}
},
handler(request) {
log.begin('recoveryKeyExists', request)
log.begin('recoveryKeyExists', request);
const email = request.payload.email
let uid
const email = request.payload.email;
let uid;
if (request.auth.credentials) {
uid = request.auth.credentials.uid
uid = request.auth.credentials.uid;
}
return Promise.resolve()
@ -146,18 +146,18 @@ module.exports = (log, db, Password, verifierVersion, customs, mailer) => {
// password reset page and allows us to redirect the user to either
// the regular password reset or account recovery password reset.
if (! email) {
throw errors.missingRequestParameter('email')
throw errors.missingRequestParameter('email');
}
return customs.check(request, email, 'recoveryKeyExists')
.then(() => db.accountRecord(email))
.then((result) => uid = result.uid)
.then((result) => uid = result.uid);
}
// When checking from `/settings` a sessionToken is required and the
// request is not rate limited.
})
.then(() => db.recoveryKeyExists(uid))
.then(() => db.recoveryKeyExists(uid));
}
},
{
@ -169,28 +169,28 @@ module.exports = (log, db, Password, verifierVersion, customs, mailer) => {
}
},
handler(request) {
log.begin('recoveryKeyDelete', request)
log.begin('recoveryKeyDelete', request);
const sessionToken = request.auth.credentials
const sessionToken = request.auth.credentials;
return Promise.resolve()
.then(deleteRecoveryKey)
.then(sendNotificationEmail)
.then(() => {
return {}
})
return {};
});
function deleteRecoveryKey() {
if (sessionToken.tokenVerificationId) {
throw errors.unverifiedSession()
throw errors.unverifiedSession();
}
return db.deleteRecoveryKey(sessionToken.uid)
return db.deleteRecoveryKey(sessionToken.uid);
}
function sendNotificationEmail() {
const geoData = request.app.geo
const ip = request.app.clientAddress
const geoData = request.app.geo;
const ip = request.app.clientAddress;
const emailOptions = {
acceptLanguage: request.app.acceptLanguage,
ip: ip,
@ -202,12 +202,12 @@ module.exports = (log, db, Password, verifierVersion, customs, mailer) => {
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid
}
};
return db.account(sessionToken.uid)
.then((account) => mailer.sendPostRemoveAccountRecoveryNotification(account.emails, account, emailOptions))
.then((account) => mailer.sendPostRemoveAccountRecoveryNotification(account.emails, account, emailOptions));
}
}
}
]
}
];
};

Просмотреть файл

@ -2,20 +2,20 @@
* 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'
'use strict';
const error = require('../error')
const isA = require('joi')
const requestHelper = require('../routes/utils/request_helper')
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema
const P = require('../promise')
const random = require('../crypto/random')
const error = require('../error');
const isA = require('joi');
const requestHelper = require('../routes/utils/request_helper');
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema;
const P = require('../promise');
const random = require('../crypto/random');
const validators = require('./validators')
const HEX_STRING = validators.HEX_STRING
const validators = require('./validators');
const HEX_STRING = validators.HEX_STRING;
module.exports = function (log, db, Password, config, signinUtils) {
const totpUtils = require('../../lib/routes/utils/totp')(log, config, db)
const totpUtils = require('../../lib/routes/utils/totp')(log, config, db);
const routes = [
{
@ -32,14 +32,14 @@ module.exports = function (log, db, Password, config, signinUtils) {
}
},
handler: async function (request) {
log.begin('Session.destroy', request)
var sessionToken = request.auth.credentials
var uid = request.auth.credentials.uid
log.begin('Session.destroy', request);
var sessionToken = request.auth.credentials;
var uid = request.auth.credentials.uid;
return P.resolve()
.then(() => {
if (request.payload && request.payload.customSessionToken) {
const customSessionToken = request.payload.customSessionToken
const customSessionToken = request.payload.customSessionToken;
return db.sessionToken(customSessionToken)
.then(function (tokenData) {
@ -48,21 +48,21 @@ module.exports = function (log, db, Password, config, signinUtils) {
sessionToken = {
id: customSessionToken,
uid: uid,
}
};
return sessionToken
return sessionToken;
} else {
throw error.invalidToken('Invalid session token')
throw error.invalidToken('Invalid session token');
}
})
});
} else {
return sessionToken
return sessionToken;
}
})
.then((sessionToken) => {
return db.deleteSessionToken(sessionToken)
return db.deleteSessionToken(sessionToken);
})
.then(() => { return {} })
.then(() => { return {}; });
}
},
{
@ -113,17 +113,17 @@ module.exports = function (log, db, Password, config, signinUtils) {
}
},
handler: async function (request) {
log.begin('Session.reauth', request)
log.begin('Session.reauth', request);
const sessionToken = request.auth.credentials
const email = request.payload.email
const authPW = request.payload.authPW
const originalLoginEmail = request.payload.originalLoginEmail
let verificationMethod = request.payload.verificationMethod
const sessionToken = request.auth.credentials;
const email = request.payload.email;
const authPW = request.payload.authPW;
const originalLoginEmail = request.payload.originalLoginEmail;
let verificationMethod = request.payload.verificationMethod;
let accountRecord, password, keyFetchToken
let accountRecord, password, keyFetchToken;
request.validateMetricsContext()
request.validateMetricsContext();
return checkCustomsAndLoadAccount()
.then(checkEmailAndPassword)
@ -131,7 +131,7 @@ module.exports = function (log, db, Password, config, signinUtils) {
.then(updateSessionToken)
.then(sendSigninNotifications)
.then(createKeyFetchToken)
.then(createResponse)
.then(createResponse);
function checkTotpToken() {
// Check to see if the user has a TOTP token and it is verified and
@ -141,18 +141,18 @@ module.exports = function (log, db, Password, config, signinUtils) {
.then((result) => {
if (result) {
// User has enabled TOTP, no way around it, they must verify TOTP token
verificationMethod = 'totp-2fa'
verificationMethod = 'totp-2fa';
} else if (! result && verificationMethod === 'totp-2fa') {
// Error if requesting TOTP verification with TOTP not setup
throw error.totpRequired()
throw error.totpRequired();
}
})
});
}
function checkCustomsAndLoadAccount() {
return signinUtils.checkCustomsAndLoadAccount(request, email).then(res => {
accountRecord = res.accountRecord
})
accountRecord = res.accountRecord;
});
}
function checkEmailAndPassword() {
@ -162,18 +162,18 @@ module.exports = function (log, db, Password, config, signinUtils) {
authPW,
accountRecord.authSalt,
accountRecord.verifierVersion
)
return signinUtils.checkPassword(accountRecord, password, request.app.clientAddress)
);
return signinUtils.checkPassword(accountRecord, password, request.app.clientAddress);
})
.then(match => {
if (! match) {
throw error.incorrectPassword(accountRecord.email, email)
throw error.incorrectPassword(accountRecord.email, email);
}
})
});
}
function updateSessionToken() {
sessionToken.authAt = sessionToken.lastAccessTime = Date.now()
sessionToken.authAt = sessionToken.lastAccessTime = Date.now();
sessionToken.setUserAgentInfo({
uaBrowser: request.app.ua.browser,
uaBrowserVersion: request.app.ua.browserVersion,
@ -181,23 +181,23 @@ module.exports = function (log, db, Password, config, signinUtils) {
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uaFormFactor: request.app.ua.formFactor
})
});
if (! sessionToken.mustVerify && (requestHelper.wantsKeys(request) || verificationMethod)) {
sessionToken.mustVerify = true
sessionToken.mustVerify = true;
}
return db.updateSessionToken(sessionToken)
return db.updateSessionToken(sessionToken);
}
function sendSigninNotifications() {
return signinUtils.sendSigninNotifications(request, accountRecord, sessionToken, verificationMethod)
return signinUtils.sendSigninNotifications(request, accountRecord, sessionToken, verificationMethod);
}
function createKeyFetchToken() {
if (requestHelper.wantsKeys(request)) {
return signinUtils.createKeyFetchToken(request, accountRecord, password, sessionToken)
.then(result => {
keyFetchToken = result
})
keyFetchToken = result;
});
}
}
@ -205,15 +205,15 @@ module.exports = function (log, db, Password, config, signinUtils) {
var response = {
uid: sessionToken.uid,
authAt: sessionToken.lastAuthAt()
}
};
if (keyFetchToken) {
response.keyFetchToken = keyFetchToken.data
response.keyFetchToken = keyFetchToken.data;
}
Object.assign(response, signinUtils.getSessionVerificationStatus(sessionToken, verificationMethod))
Object.assign(response, signinUtils.getSessionVerificationStatus(sessionToken, verificationMethod));
return response
return response;
}
}
},
@ -232,12 +232,12 @@ module.exports = function (log, db, Password, config, signinUtils) {
}
},
handler: async function (request) {
log.begin('Session.status', request)
const sessionToken = request.auth.credentials
log.begin('Session.status', request);
const sessionToken = request.auth.credentials;
return {
state: sessionToken.state,
uid: sessionToken.uid
}
};
}
},
{
@ -254,30 +254,30 @@ module.exports = function (log, db, Password, config, signinUtils) {
}
},
handler: async function (request) {
log.begin('Session.duplicate', request)
const origSessionToken = request.auth.credentials
log.begin('Session.duplicate', request);
const origSessionToken = request.auth.credentials;
return P.resolve()
.then(duplicateVerificationState)
.then(createSessionToken)
.then(formatResponse)
.then(formatResponse);
function duplicateVerificationState() {
// Copy verification state of the token, but generate
// independent verification codes.
const newVerificationState = {}
const newVerificationState = {};
if (origSessionToken.tokenVerificationId) {
newVerificationState.tokenVerificationId = random.hex(origSessionToken.tokenVerificationId.length / 2)
newVerificationState.tokenVerificationId = random.hex(origSessionToken.tokenVerificationId.length / 2);
}
if (origSessionToken.tokenVerificationCode) {
// Using expiresAt=0 here prevents the new token from being verified via email code.
// That's OK, because we don't send them a new email with the new verification code
// unless they explicitly ask us to resend it, and resend only handles email links
// rather than email codes.
newVerificationState.tokenVerificationCode = random.hex(origSessionToken.tokenVerificationCode.length / 2)
newVerificationState.tokenVerificationCodeExpiresAt = 0
newVerificationState.tokenVerificationCode = random.hex(origSessionToken.tokenVerificationCode.length / 2);
newVerificationState.tokenVerificationCodeExpiresAt = 0;
}
return P.props(newVerificationState)
return P.props(newVerificationState);
}
function createSessionToken(newVerificationState) {
@ -289,15 +289,15 @@ module.exports = function (log, db, Password, config, signinUtils) {
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uaFormFactor: request.app.ua.formFactor
}
};
// Copy all other details from the original sessionToken.
// We have to lie a little here and copy the creation time
// of the original sessionToken. If we set createdAt to the
// current time, we would falsely report the new session's
// `lastAuthAt` value as the current timestamp.
const sessionTokenOptions = Object.assign({}, origSessionToken, newUAInfo, newVerificationState)
return db.createSessionToken(sessionTokenOptions)
const sessionTokenOptions = Object.assign({}, origSessionToken, newUAInfo, newVerificationState);
return db.createSessionToken(sessionTokenOptions);
}
function formatResponse(newSessionToken) {
@ -305,24 +305,24 @@ module.exports = function (log, db, Password, config, signinUtils) {
uid: newSessionToken.uid,
sessionToken: newSessionToken.data,
authAt: newSessionToken.lastAuthAt()
}
};
if (! newSessionToken.emailVerified) {
response.verified = false
response.verificationMethod = 'email'
response.verificationReason = 'signup'
response.verified = false;
response.verificationMethod = 'email';
response.verificationReason = 'signup';
} else if (! newSessionToken.tokenVerified) {
response.verified = false
response.verificationMethod = 'email'
response.verificationReason = 'login'
response.verified = false;
response.verificationMethod = 'email';
response.verificationReason = 'login';
} else {
response.verified = true
response.verified = true;
}
return response
return response;
}
}
} ]
} ];
return routes
}
return routes;
};

Просмотреть файл

@ -2,16 +2,16 @@
* 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'
'use strict';
const error = require('../error')
const isA = require('joi')
const P = require('../promise')
const validators = require('./validators')
const error = require('../error');
const isA = require('joi');
const P = require('../promise');
const validators = require('./validators');
module.exports = (log, signer, db, domain, devices) => {
const HOUR = 1000 * 60 * 60
const HOUR = 1000 * 60 * 60;
var routes = [
{
@ -42,12 +42,12 @@ module.exports = (log, signer, db, domain, devices) => {
}
},
handler: async function certificateSign(request) {
log.begin('Sign.cert', request)
var sessionToken = request.auth.credentials
var publicKey = request.payload.publicKey
var duration = request.payload.duration
var service = request.query.service
var deviceId, uid, certResult
log.begin('Sign.cert', request);
var sessionToken = request.auth.credentials;
var publicKey = request.payload.publicKey;
var duration = request.payload.duration;
var service = request.query.service;
var deviceId, uid, certResult;
if (request.headers['user-agent']) {
const {
browser: uaBrowser,
@ -56,7 +56,7 @@ module.exports = (log, signer, db, domain, devices) => {
osVersion: uaOSVersion,
deviceType: uaDeviceType,
formFactor: uaFormFactor
} = request.app.ua
} = request.app.ua;
sessionToken.setUserAgentInfo({
uaBrowser,
uaBrowserVersion,
@ -65,26 +65,26 @@ module.exports = (log, signer, db, domain, devices) => {
uaDeviceType,
uaFormFactor,
lastAccessTime: Date.now()
})
});
// No need to wait for a response, update in the background.
db.touchSessionToken(sessionToken, request.app.geo)
db.touchSessionToken(sessionToken, request.app.geo);
} else {
log.warn('signer.updateSessionToken', { message: 'no user agent string, session token not updated'
})
});
}
if (! sessionToken.emailVerified) {
throw error.unverifiedAccount()
throw error.unverifiedAccount();
}
if (sessionToken.mustVerify && ! sessionToken.tokenVerified) {
throw error.unverifiedSession()
throw error.unverifiedSession();
}
return P.resolve()
.then(
function () {
if (sessionToken.deviceId) {
deviceId = sessionToken.deviceId
deviceId = sessionToken.deviceId;
} else if (! service || service === 'sync') {
// Synthesize a device record for Sync sessions that don't already have one.
// Include the UA info so that we can synthesize a device name
@ -94,19 +94,19 @@ module.exports = (log, signer, db, domain, devices) => {
uaBrowserVersion: sessionToken.uaBrowserVersion,
uaOS: sessionToken.uaOS,
uaOSVersion: sessionToken.uaOSVersion
}
};
return devices.upsert(request, sessionToken, deviceInfo)
.then(result => {
deviceId = result.id
deviceId = result.id;
})
.catch(err => {
// There's a small chance that a device registration was performed
// concurrently. If so, just use that device id.
if (err.errno !== error.ERRNO.DEVICE_CONFLICT) {
throw err
throw err;
}
deviceId = err.output.payload.deviceId
})
deviceId = err.output.payload.deviceId;
});
}
}
)
@ -114,24 +114,24 @@ module.exports = (log, signer, db, domain, devices) => {
function () {
if (publicKey.algorithm === 'RS') {
if (! publicKey.n) {
throw error.missingRequestParameter('n')
throw error.missingRequestParameter('n');
}
if (! publicKey.e) {
throw error.missingRequestParameter('e')
throw error.missingRequestParameter('e');
}
}
else { // DS
if (! publicKey.y) {
throw error.missingRequestParameter('y')
throw error.missingRequestParameter('y');
}
if (! publicKey.p) {
throw error.missingRequestParameter('p')
throw error.missingRequestParameter('p');
}
if (! publicKey.q) {
throw error.missingRequestParameter('q')
throw error.missingRequestParameter('q');
}
if (! publicKey.g) {
throw error.missingRequestParameter('g')
throw error.missingRequestParameter('g');
}
}
@ -140,8 +140,8 @@ module.exports = (log, signer, db, domain, devices) => {
// Log details to sanity-check locale backfilling.
log.info('signer.updateLocale', {
locale: request.app.acceptLanguage
})
db.updateLocale(sessionToken.uid, request.app.acceptLanguage)
});
db.updateLocale(sessionToken.uid, request.app.acceptLanguage);
// meh on the result
} else {
// We're seeing a surprising number of accounts that don't get
@ -150,10 +150,10 @@ module.exports = (log, signer, db, domain, devices) => {
email: sessionToken.email,
locale: request.app.acceptLanguage,
agent: request.headers['user-agent']
})
});
}
}
uid = sessionToken.uid
uid = sessionToken.uid;
return signer.sign(
{
@ -170,22 +170,22 @@ module.exports = (log, signer, db, domain, devices) => {
authenticatorAssuranceLevel: sessionToken.authenticatorAssuranceLevel,
profileChangedAt: sessionToken.profileChangedAt
}
)
);
}
)
.then(
function(result) {
certResult = result
certResult = result;
return request.emitMetricsEvent('account.signed', {
uid: uid,
device_id: deviceId
})
});
}
)
.then(() => { return certResult })
.then(() => { return certResult; });
}
}
]
];
return routes
}
return routes;
};

Просмотреть файл

@ -2,12 +2,12 @@
* 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'
'use strict';
const isA = require('joi')
const validators = require('./validators')
const isA = require('joi');
const validators = require('./validators');
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').requiredSchema
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').requiredSchema;
module.exports = (log, db, customs) => {
return [
@ -28,22 +28,22 @@ module.exports = (log, db, customs) => {
}
},
handler: async function (request) {
log.begin('signinCodes.consume', request)
request.validateMetricsContext()
log.begin('signinCodes.consume', request);
request.validateMetricsContext();
return customs.checkIpOnly(request, 'consumeSigninCode')
.then(hexSigninCode)
.then(consumeSigninCode)
.then(consumeSigninCode);
function hexSigninCode () {
let base64 = request.payload.code.replace(/-/g, '+').replace(/_/g, '/')
let base64 = request.payload.code.replace(/-/g, '+').replace(/_/g, '/');
const padCount = base64.length % 4
const padCount = base64.length % 4;
for (let i = 0; i < padCount; ++i) {
base64 += '='
base64 += '=';
}
return Buffer.from(base64, 'base64').toString('hex')
return Buffer.from(base64, 'base64').toString('hex');
}
function consumeSigninCode (code) {
@ -52,14 +52,14 @@ module.exports = (log, db, customs) => {
return request.emitMetricsEvent('signinCode.consumed')
.then(() => {
if (result.flowId) {
return request.emitMetricsEvent(`flow.continued.${result.flowId}`)
return request.emitMetricsEvent(`flow.continued.${result.flowId}`);
}
})
.then(() => ({ email: result.email }))
})
.then(() => ({ email: result.email }));
});
}
}
}
]
}
];
};

Просмотреть файл

@ -2,26 +2,26 @@
* 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'
'use strict';
const error = require('../error')
const isA = require('joi')
const PhoneNumberUtil = require('google-libphonenumber').PhoneNumberUtil
const validators = require('./validators')
const error = require('../error');
const isA = require('joi');
const PhoneNumberUtil = require('google-libphonenumber').PhoneNumberUtil;
const validators = require('./validators');
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema
const FEATURES_SCHEMA = require('../features').schema
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema;
const FEATURES_SCHEMA = require('../features').schema;
const TEMPLATE_NAMES = new Map([
[ 1, 'installFirefox' ]
])
]);
module.exports = (log, db, config, customs, sms) => {
if (! config.sms.enabled) {
return []
return [];
}
const REGIONS = new Set(config.sms.countryCodes)
const IS_STATUS_GEO_ENABLED = config.sms.isStatusGeoEnabled
const REGIONS = new Set(config.sms.countryCodes);
const IS_STATUS_GEO_ENABLED = config.sms.isStatusGeoEnabled;
return [
{
@ -41,15 +41,15 @@ module.exports = (log, db, config, customs, sms) => {
}
},
handler: async function (request) {
log.begin('sms.send', request)
request.validateMetricsContext()
log.begin('sms.send', request);
request.validateMetricsContext();
const sessionToken = request.auth.credentials
const phoneNumber = request.payload.phoneNumber
const templateName = TEMPLATE_NAMES.get(request.payload.messageId)
const acceptLanguage = request.app.acceptLanguage
const sessionToken = request.auth.credentials;
const phoneNumber = request.payload.phoneNumber;
const templateName = TEMPLATE_NAMES.get(request.payload.messageId);
const acceptLanguage = request.app.acceptLanguage;
let phoneNumberUtil, parsedPhoneNumber
let phoneNumberUtil, parsedPhoneNumber;
return customs.check(request, sessionToken.email, 'connectDeviceSms')
.then(parsePhoneNumber)
@ -58,51 +58,51 @@ module.exports = (log, db, config, customs, sms) => {
.then(createSigninCode)
.then(sendMessage)
.then(logSuccess)
.then(createResponse)
.then(createResponse);
function parsePhoneNumber () {
try {
phoneNumberUtil = PhoneNumberUtil.getInstance()
parsedPhoneNumber = phoneNumberUtil.parse(phoneNumber)
phoneNumberUtil = PhoneNumberUtil.getInstance();
parsedPhoneNumber = phoneNumberUtil.parse(phoneNumber);
} catch (err) {
throw error.invalidPhoneNumber()
throw error.invalidPhoneNumber();
}
}
function validatePhoneNumber () {
if (! phoneNumberUtil.isValidNumber(parsedPhoneNumber)) {
throw error.invalidPhoneNumber()
throw error.invalidPhoneNumber();
}
}
function validateRegion () {
const region = phoneNumberUtil.getRegionCodeForNumber(parsedPhoneNumber)
request.emitMetricsEvent(`sms.region.${region}`)
const region = phoneNumberUtil.getRegionCodeForNumber(parsedPhoneNumber);
request.emitMetricsEvent(`sms.region.${region}`);
if (! REGIONS.has(region)) {
throw error.invalidRegion(region)
throw error.invalidRegion(region);
}
}
function createSigninCode () {
if (request.app.features.has('signinCodes')) {
return request.gatherMetricsContext({})
.then(metricsContext => db.createSigninCode(sessionToken.uid, metricsContext.flow_id))
.then(metricsContext => db.createSigninCode(sessionToken.uid, metricsContext.flow_id));
}
}
function sendMessage (signinCode) {
return sms.send(phoneNumber, templateName, acceptLanguage, signinCode)
return sms.send(phoneNumber, templateName, acceptLanguage, signinCode);
}
function logSuccess () {
return request.emitMetricsEvent(`sms.${templateName}.sent`)
return request.emitMetricsEvent(`sms.${templateName}.sent`);
}
function createResponse () {
return {
formattedPhoneNumber: phoneNumberUtil.format(parsedPhoneNumber, 'international')
}
};
}
}
},
@ -120,40 +120,40 @@ module.exports = (log, db, config, customs, sms) => {
}
},
handler: async function (request) {
log.begin('sms.status', request)
log.begin('sms.status', request);
let country
let country;
return createResponse(getLocation())
return createResponse(getLocation());
function getLocation () {
country = request.query.country
country = request.query.country;
if (! country) {
if (! IS_STATUS_GEO_ENABLED) {
log.warn('sms.getGeoData', { warning: 'skipping geolocation step' })
return true
log.warn('sms.getGeoData', { warning: 'skipping geolocation step' });
return true;
}
const location = request.app.geo.location
const location = request.app.geo.location;
if (location && location.countryCode) {
country = location.countryCode
country = location.countryCode;
}
}
if (country) {
return REGIONS.has(country)
return REGIONS.has(country);
}
log.error('sms.getGeoData', { err: 'missing location data' })
return false
log.error('sms.getGeoData', { err: 'missing location data' });
return false;
}
function createResponse (isLocationOk) {
return { ok: isLocationOk && sms.isBudgetOk(), country }
return { ok: isLocationOk && sms.isBudgetOk(), country };
}
}
}
]
}
];
};

Просмотреть файл

@ -2,18 +2,18 @@
* 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'
'use strict';
const errors = require('../error')
const isA = require('joi')
const validators = require('./validators')
const HEX_STRING = validators.HEX_STRING
const DIGITS = validators.DIGITS
const P = require('../promise')
const errors = require('../error');
const isA = require('joi');
const validators = require('./validators');
const HEX_STRING = validators.HEX_STRING;
const DIGITS = validators.DIGITS;
const P = require('../promise');
module.exports = (log, db, config, customs) => {
const tokenCodeConfig = config.signinConfirmation.tokenVerificationCode
const TOKEN_CODE_LENGTH = tokenCodeConfig && tokenCodeConfig.codeLength || 6
const tokenCodeConfig = config.signinConfirmation.tokenVerificationCode;
const TOKEN_CODE_LENGTH = tokenCodeConfig && tokenCodeConfig.codeLength || 6;
return [
{
@ -31,23 +31,23 @@ module.exports = (log, db, config, customs) => {
}
},
handler: async function (request) {
log.begin('session.verify.token', request)
log.begin('session.verify.token', request);
const code = request.payload.code.toUpperCase()
const uid = request.auth.credentials.uid
const email = request.auth.credentials.email
const code = request.payload.code.toUpperCase();
const uid = request.auth.credentials.uid;
const email = request.auth.credentials.email;
return customs.check(request, email, 'verifyTokenCode')
.then(checkOptionalUidParam)
.then(verifyCode)
.then(emitMetrics)
.then(() => { return {} })
.then(() => { return {}; });
function checkOptionalUidParam() {
// For b/w compat we accept `uid` in the request body,
// but it must match the uid of the sessionToken.
if (request.payload.uid && request.payload.uid !== uid) {
throw errors.invalidRequestParameter('uid')
throw errors.invalidRequestParameter('uid');
}
}
@ -58,22 +58,22 @@ module.exports = (log, db, config, customs) => {
log.error('account.token.code.expired', {
uid: uid,
err: err
})
});
}
throw err
})
throw err;
});
}
function emitMetrics() {
log.info('account.token.code.verified', {
uid: uid
})
});
return P.all([request.emitMetricsEvent('tokenCodes.verified', {uid: uid}), request.emitMetricsEvent('account.confirmed', {uid: uid})])
.then(() => ({}))
.then(() => ({}));
}
}
}
]
}
];
};

Просмотреть файл

@ -2,36 +2,36 @@
* 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'
'use strict';
const errors = require('../error')
const validators = require('./validators')
const isA = require('joi')
const P = require('../promise')
const otplib = require('otplib')
const qrcode = require('qrcode')
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema
const errors = require('../error');
const validators = require('./validators');
const isA = require('joi');
const P = require('../promise');
const otplib = require('otplib');
const qrcode = require('qrcode');
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema;
module.exports = (log, db, mailer, customs, config) => {
const totpUtils = require('../../lib/routes/utils/totp')(log, config, db)
const totpUtils = require('../../lib/routes/utils/totp')(log, config, db);
// Default options for TOTP
otplib.authenticator.options = {
encoding: 'hex',
step: config.step,
window: config.window
}
};
// Currently, QR codes are rendered with the highest possible
// error correction, which should in theory allow clients to
// scan the image better.
// Ref: https://github.com/soldair/node-qrcode#error-correction-level
const qrCodeOptions = {errorCorrectionLevel: 'H'}
const qrCodeOptions = {errorCorrectionLevel: 'H'};
const RECOVERY_CODE_COUNT = config.recoveryCodes && config.recoveryCodes.count || 8
const RECOVERY_CODE_COUNT = config.recoveryCodes && config.recoveryCodes.count || 8;
P.promisify(qrcode.toDataURL)
P.promisify(qrcode.toDataURL);
return [
{
@ -54,49 +54,49 @@ module.exports = (log, db, mailer, customs, config) => {
}
},
handler: async function (request) {
log.begin('totp.create', request)
log.begin('totp.create', request);
let response
let secret
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
const authenticator = new otplib.authenticator.Authenticator()
authenticator.options = otplib.authenticator.options
let response;
let secret;
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
const authenticator = new otplib.authenticator.Authenticator();
authenticator.options = otplib.authenticator.options;
return customs.check(request, sessionToken.email, 'totpCreate')
.then(() => {
secret = authenticator.generateSecret()
return createTotpToken()
secret = authenticator.generateSecret();
return createTotpToken();
})
.then(emitMetrics)
.then(createResponse)
.then(() => response)
.then(() => response);
function createTotpToken() {
if (sessionToken.tokenVerificationId) {
throw errors.unverifiedSession()
throw errors.unverifiedSession();
}
return db.createTotpToken(uid, secret, 0)
return db.createTotpToken(uid, secret, 0);
}
function createResponse() {
const otpauth = authenticator.keyuri(sessionToken.email, config.serviceName, secret)
const otpauth = authenticator.keyuri(sessionToken.email, config.serviceName, secret);
return qrcode.toDataURL(otpauth, qrCodeOptions)
.then((qrCodeUrl) => {
response = {
qrCodeUrl,
secret
}
})
};
});
}
function emitMetrics() {
log.info('totpToken.created', {
uid: uid
})
return request.emitMetricsEvent('totpToken.created', {uid: uid})
});
return request.emitMetricsEvent('totpToken.created', {uid: uid});
}
}
},
@ -110,47 +110,47 @@ module.exports = (log, db, mailer, customs, config) => {
response: {}
},
handler: async function (request) {
log.begin('totp.destroy', request)
log.begin('totp.destroy', request);
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
let hasEnabledToken = false
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
let hasEnabledToken = false;
return customs.check(request, sessionToken.email, 'totpDestroy')
.then(checkTotpToken)
.then(deleteTotpToken)
.then(sendEmailNotification)
.then(() => { return {} })
.then(() => { return {}; });
function checkTotpToken() {
// If a TOTP token is not verified, we should be able to safely delete regardless of session
// verification state.
return totpUtils.hasTotpToken({uid})
.then((result) => hasEnabledToken = result)
.then((result) => hasEnabledToken = result);
}
function deleteTotpToken() {
if (hasEnabledToken && (sessionToken.tokenVerificationId || sessionToken.authenticatorAssuranceLevel <= 1)) {
throw errors.unverifiedSession()
throw errors.unverifiedSession();
}
return db.deleteTotpToken(uid)
.then(() => {
return log.notifyAttachedServices('profileDataChanged', request, {
uid: sessionToken.uid
})
})
});
});
}
function sendEmailNotification() {
if (! hasEnabledToken) {
return
return;
}
return db.account(sessionToken.uid)
.then((account) => {
const geoData = request.app.geo
const ip = request.app.clientAddress
const geoData = request.app.geo;
const ip = request.app.clientAddress;
const emailOptions = {
acceptLanguage: request.app.acceptLanguage,
ip: ip,
@ -162,10 +162,10 @@ module.exports = (log, db, mailer, customs, config) => {
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid
}
};
mailer.sendPostRemoveTwoStepAuthNotification(account.emails, account, emailOptions)
})
mailer.sendPostRemoveTwoStepAuthNotification(account.emails, account, emailOptions);
});
}
}
},
@ -183,22 +183,22 @@ module.exports = (log, db, mailer, customs, config) => {
}
},
handler: async function (request) {
log.begin('totp.exists', request)
log.begin('totp.exists', request);
const sessionToken = request.auth.credentials
let exists = false
const sessionToken = request.auth.credentials;
let exists = false;
return getTotpToken()
.then(() => { return {exists} })
.then(() => { return {exists}; });
function getTotpToken() {
return P.resolve()
.then(() => {
if (sessionToken.tokenVerificationId) {
throw errors.unverifiedSession()
throw errors.unverifiedSession();
}
return db.totpToken(sessionToken.uid)
return db.totpToken(sessionToken.uid);
})
.then((token) => {
@ -208,18 +208,18 @@ module.exports = (log, db, mailer, customs, config) => {
if (! token.verified) {
return db.deleteTotpToken(sessionToken.uid)
.then(() => {
exists = false
})
exists = false;
});
} else {
exists = true
exists = true;
}
}, (err) => {
if (err.errno === errors.ERRNO.TOTP_TOKEN_NOT_FOUND) {
exists = false
return
exists = false;
return;
}
throw err
})
throw err;
});
}
}
},
@ -244,13 +244,13 @@ module.exports = (log, db, mailer, customs, config) => {
}
},
handler: async function (request) {
log.begin('session.verify.totp', request)
log.begin('session.verify.totp', request);
const code = request.payload.code
const sessionToken = request.auth.credentials
const uid = sessionToken.uid
const email = sessionToken.email
let sharedSecret, isValidCode, tokenVerified, recoveryCodes
const code = request.payload.code;
const sessionToken = request.auth.credentials;
const uid = sessionToken.uid;
const email = sessionToken.email;
let sharedSecret, isValidCode, tokenVerified, recoveryCodes;
return customs.check(request, email, 'verifyTotpCode')
.then(getTotpToken)
@ -263,27 +263,27 @@ module.exports = (log, db, mailer, customs, config) => {
.then(() => {
const response = {
success: isValidCode
}
};
if (recoveryCodes) {
response.recoveryCodes = recoveryCodes
response.recoveryCodes = recoveryCodes;
}
return response
})
return response;
});
function getTotpToken() {
return db.totpToken(sessionToken.uid)
.then((token) => {
sharedSecret = token.sharedSecret
tokenVerified = token.verified
})
sharedSecret = token.sharedSecret;
tokenVerified = token.verified;
});
}
function verifyTotpCode() {
const authenticator = new otplib.authenticator.Authenticator()
authenticator.options = Object.assign({}, otplib.authenticator.options, {secret: sharedSecret})
isValidCode = authenticator.check(code, sharedSecret)
const authenticator = new otplib.authenticator.Authenticator();
authenticator.options = Object.assign({}, otplib.authenticator.options, {secret: sharedSecret});
isValidCode = authenticator.check(code, sharedSecret);
}
// Once a valid TOTP code has been detected, the token becomes verified
@ -296,8 +296,8 @@ module.exports = (log, db, mailer, customs, config) => {
}).then(() => {
return log.notifyAttachedServices('profileDataChanged', request, {
uid: sessionToken.uid
})
})
});
});
}
}
@ -305,14 +305,14 @@ module.exports = (log, db, mailer, customs, config) => {
function replaceRecoveryCodes() {
if (isValidCode && ! tokenVerified) {
return db.replaceRecoveryCodes(uid, RECOVERY_CODE_COUNT)
.then((result) => recoveryCodes = result)
.then((result) => recoveryCodes = result);
}
}
// If a valid code was sent, this verifies the session using the `totp-2fa` method.
function verifySession() {
if (isValidCode && sessionToken.authenticatorAssuranceLevel <= 1) {
return db.verifyTokensWithMethod(sessionToken.id, 'totp-2fa')
return db.verifyTokensWithMethod(sessionToken.id, 'totp-2fa');
}
}
@ -320,22 +320,22 @@ module.exports = (log, db, mailer, customs, config) => {
if (isValidCode) {
log.info('totp.verified', {
uid: uid
})
request.emitMetricsEvent('totpToken.verified', {uid: uid})
});
request.emitMetricsEvent('totpToken.verified', {uid: uid});
} else {
log.info('totp.unverified', {
uid: uid
})
request.emitMetricsEvent('totpToken.unverified', {uid: uid})
});
request.emitMetricsEvent('totpToken.unverified', {uid: uid});
}
}
function sendEmailNotification() {
return db.account(sessionToken.uid)
.then((account) => {
const geoData = request.app.geo
const ip = request.app.clientAddress
const service = request.payload.service || request.query.service
const geoData = request.app.geo;
const ip = request.app.clientAddress;
const service = request.payload.service || request.query.service;
const emailOptions = {
acceptLanguage: request.app.acceptLanguage,
ip: ip,
@ -348,13 +348,13 @@ module.exports = (log, db, mailer, customs, config) => {
uaOSVersion: request.app.ua.osVersion,
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid
}
};
// Check to see if this token was just verified, if it is, then this means
// the user has enabled two step authentication, otherwise send new device
// login email.
if (isValidCode && ! tokenVerified) {
return mailer.sendPostAddTwoStepAuthNotification(account.emails, account, emailOptions)
return mailer.sendPostAddTwoStepAuthNotification(account.emails, account, emailOptions);
}
// All accounts that have a TOTP token, force the session to be verified, therefore
@ -362,12 +362,12 @@ module.exports = (log, db, mailer, customs, config) => {
// login email. Instead, lets perform a basic check that the service is `sync`, otherwise
// don't send.
if (isValidCode && service === 'sync') {
return mailer.sendNewDeviceLoginNotification(account.emails, account, emailOptions)
return mailer.sendNewDeviceLoginNotification(account.emails, account, emailOptions);
}
})
});
}
}
}
]
}
];
};

Просмотреть файл

@ -2,16 +2,16 @@
* 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'
'use strict';
const isA = require('joi')
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema
const validators = require('./validators')
const isA = require('joi');
const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema;
const validators = require('./validators');
const { HEX_STRING, BASE_36 } = validators
const { HEX_STRING, BASE_36 } = validators;
module.exports = (log, db, mailer, config, customs) => {
const unblockCodeLen = config && config.codeLength || 0
const unblockCodeLen = config && config.codeLength || 0;
return [
{
@ -26,45 +26,45 @@ module.exports = (log, db, mailer, config, customs) => {
}
},
handler: async function (request) {
log.begin('Account.SendUnblockCode', request)
log.begin('Account.SendUnblockCode', request);
const email = request.payload.email
let emailRecord
const email = request.payload.email;
let emailRecord;
request.validateMetricsContext()
request.validateMetricsContext();
const { flowId, flowBeginTime } = await request.app.metricsContext
const { flowId, flowBeginTime } = await request.app.metricsContext;
return customs.check(request, email, 'sendUnblockCode')
.then(lookupAccount)
.then(createUnblockCode)
.then(mailUnblockCode)
.then(() => request.emitMetricsEvent('account.login.sentUnblockCode'))
.then(() => { return {} })
.then(() => { return {}; });
function lookupAccount () {
return db.accountRecord(email)
.then(record => {
emailRecord = record
return record.uid
})
emailRecord = record;
return record.uid;
});
}
function createUnblockCode (uid) {
return db.createUnblockCode(uid)
return db.createUnblockCode(uid);
}
function mailUnblockCode (code) {
return db.accountEmails(emailRecord.uid)
.then(emails => {
const geoData = request.app.geo
const geoData = request.app.geo;
const {
browser: uaBrowser,
browserVersion: uaBrowserVersion,
os: uaOS,
osVersion: uaOSVersion,
deviceType: uaDeviceType
} = request.app.ua
} = request.app.ua;
return mailer.sendUnblockCode(emails, emailRecord, {
acceptLanguage: request.app.acceptLanguage,
@ -80,8 +80,8 @@ module.exports = (log, db, mailer, config, customs) => {
uaOSVersion,
uaDeviceType,
uid: emailRecord.uid
})
})
});
});
}
}
},
@ -97,17 +97,17 @@ module.exports = (log, db, mailer, config, customs) => {
}
},
handler: async function (request) {
log.begin('Account.RejectUnblockCode', request)
log.begin('Account.RejectUnblockCode', request);
const uid = request.payload.uid
const code = request.payload.unblockCode.toUpperCase()
const uid = request.payload.uid;
const code = request.payload.unblockCode.toUpperCase();
return db.consumeUnblockCode(uid, code)
.then(() => {
log.info('account.login.rejectedUnblockCode', { uid, unblockCode: code })
return {}
})
log.info('account.login.rejectedUnblockCode', { uid, unblockCode: code });
return {};
});
}
}
]
}
];
};

Просмотреть файл

@ -2,12 +2,12 @@
* 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'
'use strict';
const isA = require('joi')
const random = require('../crypto/random')
const validators = require('./validators')
const HEX_STRING = validators.HEX_STRING
const isA = require('joi');
const random = require('../crypto/random');
const validators = require('./validators');
const HEX_STRING = validators.HEX_STRING;
module.exports = (log, config, redirectDomain) => {
return [
@ -17,9 +17,9 @@ module.exports = (log, config, redirectDomain) => {
handler: async function getRandomBytes(request) {
return random(32)
.then(
bytes => { return { data: bytes.toString('hex') }},
err => { throw err }
)
bytes => { return { data: bytes.toString('hex') };},
err => { throw err; }
);
}
},
{
@ -36,7 +36,7 @@ module.exports = (log, config, redirectDomain) => {
}
},
handler: async function (request, h) {
return h.redirect(config.contentServer.url + request.raw.req.url)
return h.redirect(config.contentServer.url + request.raw.req.url);
}
},
{
@ -54,8 +54,8 @@ module.exports = (log, config, redirectDomain) => {
}
},
handler: async function (request, h) {
return h.redirect(config.contentServer.url + request.raw.req.url)
return h.redirect(config.contentServer.url + request.raw.req.url);
}
}
]
}
];
};

Просмотреть файл

@ -2,22 +2,22 @@
* 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'
'use strict';
const error = require('../../error')
const error = require('../../error');
const BOUNCE_ERRORS = new Set([
error.ERRNO.BOUNCE_COMPLAINT,
error.ERRNO.BOUNCE_HARD,
error.ERRNO.BOUNCE_SOFT
])
]);
module.exports = {
sendError (err, isNewAddress) {
if (err && BOUNCE_ERRORS.has(err.errno)) {
return err
return err;
}
return error.cannotSendEmail(isNewAddress)
return error.cannotSendEmail(isNewAddress);
}
}
};

Просмотреть файл

@ -2,7 +2,7 @@
* 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'
'use strict';
/**
* Returns `true` if request has a keys=true query param.
@ -11,9 +11,9 @@
* @returns {boolean}
*/
function wantsKeys (request) {
return !! (request.query && request.query.keys)
return !! (request.query && request.query.keys);
}
module.exports = {
wantsKeys: wantsKeys
}
};

Просмотреть файл

@ -2,25 +2,25 @@
* 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'
'use strict';
const emailUtils = require('./email')
const isA = require('joi')
const validators = require('../validators')
const P = require('../../promise')
const butil = require('../../crypto/butil')
const error = require('../../error')
const emailUtils = require('./email');
const isA = require('joi');
const validators = require('../validators');
const P = require('../../promise');
const butil = require('../../crypto/butil');
const error = require('../../error');
const BASE_36 = validators.BASE_36
const BASE_36 = validators.BASE_36;
// An arbitrary, but very generous, limit on the number of active sessions.
// Currently only for metrics purposes, not enforced.
const MAX_ACTIVE_SESSIONS = 200
const MAX_ACTIVE_SESSIONS = 200;
module.exports = (log, config, customs, db, mailer) => {
const unblockCodeLifetime = config.signinUnblock && config.signinUnblock.codeLifetime || 0
const unblockCodeLen = config.signinUnblock && config.signinUnblock.codeLength || 8
const unblockCodeLifetime = config.signinUnblock && config.signinUnblock.codeLifetime || 0;
const unblockCodeLen = config.signinUnblock && config.signinUnblock.codeLength || 8;
return {
@ -40,22 +40,22 @@ module.exports = (log, config, customs, db, mailer) => {
email: accountRecord.email,
errno: error.ERRNO.ACCOUNT_RESET
}).then(() => {
throw error.mustResetAccount(accountRecord.email)
})
throw error.mustResetAccount(accountRecord.email);
});
}
return password.verifyHash()
.then(verifyHash => {
return db.checkPassword(accountRecord.uid, verifyHash)
return db.checkPassword(accountRecord.uid, verifyHash);
})
.then(match => {
if (match) {
return match
return match;
}
return customs.flag(clientAddress, {
email: accountRecord.email,
errno: error.ERRNO.INCORRECT_PASSWORD
}).then(() => match)
})
}).then(() => match);
});
},
/**
@ -67,13 +67,13 @@ module.exports = (log, config, customs, db, mailer) => {
// that the user typed into the login form. This might differ from the address
// used for calculating the password hash, which is provided in `email` param.
if (! originalLoginEmail) {
originalLoginEmail = email
originalLoginEmail = email;
}
// Logging in with a secondary email address is not currently supported.
if (originalLoginEmail.toLowerCase() !== accountRecord.primaryEmail.normalizedEmail) {
throw error.cannotLoginWithSecondaryEmail()
throw error.cannotLoginWithSecondaryEmail();
}
return P.resolve(true)
return P.resolve(true);
},
/**
@ -90,81 +90,81 @@ module.exports = (log, config, customs, db, mailer) => {
* }
*/
checkCustomsAndLoadAccount(request, email) {
let accountRecord, originalError
let didSigninUnblock = false
let accountRecord, originalError;
let didSigninUnblock = false;
return P.resolve().then(() => {
// For testing purposes, some email addresses are forced
// to go through signin unblock on every login attempt.
const forced = config.signinUnblock && config.signinUnblock.forcedEmailAddresses
const forced = config.signinUnblock && config.signinUnblock.forcedEmailAddresses;
if (forced && forced.test(email)) {
return P.reject(error.requestBlocked(true))
return P.reject(error.requestBlocked(true));
}
return customs.check(request, email, 'accountLogin')
return customs.check(request, email, 'accountLogin');
}).catch((e) => {
originalError = e
originalError = e;
// Non-customs-related errors get thrown straight back to the caller.
if (e.errno !== error.ERRNO.REQUEST_BLOCKED && e.errno !== error.ERRNO.THROTTLED) {
throw e
throw e;
}
return request.emitMetricsEvent('account.login.blocked').then(() => {
// If this customs error cannot be bypassed with email confirmation,
// throw it straight back to the caller.
var verificationMethod = e.output.payload.verificationMethod
var verificationMethod = e.output.payload.verificationMethod;
if (verificationMethod !== 'email-captcha' || ! request.payload.unblockCode) {
throw e
throw e;
}
// Check for a valid unblockCode, to allow the request to proceed.
// This requires that we load the accountRecord to learn the uid.
const unblockCode = request.payload.unblockCode.toUpperCase()
const unblockCode = request.payload.unblockCode.toUpperCase();
return db.accountRecord(email).then(result => {
accountRecord = result
accountRecord = result;
return db.consumeUnblockCode(accountRecord.uid, unblockCode).then(code => {
if (Date.now() - code.createdAt > unblockCodeLifetime) {
log.info('Account.login.unblockCode.expired', {
uid: accountRecord.uid
})
throw error.invalidUnblockCode()
});
throw error.invalidUnblockCode();
}
}).then(() => {
didSigninUnblock = true
return request.emitMetricsEvent('account.login.confirmedUnblockCode')
didSigninUnblock = true;
return request.emitMetricsEvent('account.login.confirmedUnblockCode');
}).catch((e) => {
if (e.errno !== error.ERRNO.INVALID_UNBLOCK_CODE) {
throw e
throw e;
}
return request.emitMetricsEvent('account.login.invalidUnblockCode').then(() => {
throw e
})
})
})
})
throw e;
});
});
});
});
}).then(() => {
// If we didn't load it above while checking unblock codes,
// it's now safe to load the account record from the db.
if (! accountRecord) {
return db.accountRecord(email).then(result => {
accountRecord = result
})
accountRecord = result;
});
}
}).then(() => {
return { accountRecord, didSigninUnblock }
return { accountRecord, didSigninUnblock };
}).catch((e) => {
// Some errors need to be flagged with customs.
if (e.errno === error.ERRNO.INVALID_UNBLOCK_CODE || e.errno === error.ERRNO.ACCOUNT_UNKNOWN) {
customs.flag(request.app.clientAddress, {
email: email,
errno: e.errno
})
});
}
// For any error other than INVALID_UNBLOCK_CODE, hide it behind the original customs error.
// This prevents us from accidentally leaking additional info to a caller that's been
// blocked, including e.g. whether or not the target account exists.
if (originalError && e.errno !== error.ERRNO.INVALID_UNBLOCK_CODE) {
throw originalError
throw originalError;
}
throw e
})
throw e;
});
},
/**
@ -173,38 +173,38 @@ module.exports = (log, config, customs, db, mailer) => {
* notifying attached services.
*/
async sendSigninNotifications (request, accountRecord, sessionToken, verificationMethod) {
const service = request.payload.service || request.query.service
const redirectTo = request.payload.redirectTo
const resume = request.payload.resume
const ip = request.app.clientAddress
const isUnverifiedAccount = ! accountRecord.primaryEmail.isVerified
const service = request.payload.service || request.query.service;
const redirectTo = request.payload.redirectTo;
const resume = request.payload.resume;
const ip = request.app.clientAddress;
const isUnverifiedAccount = ! accountRecord.primaryEmail.isVerified;
let sessions
let sessions;
const { deviceId, flowId, flowBeginTime } = await request.app.metricsContext
const { deviceId, flowId, flowBeginTime } = await request.app.metricsContext;
const mustVerifySession = sessionToken.mustVerify && ! sessionToken.tokenVerified
const mustVerifySession = sessionToken.mustVerify && ! sessionToken.tokenVerified;
// The final event to complete the login flow depends on the details
// of the flow being undertaken, so prepare accordingly.
let flowCompleteSignal
let flowCompleteSignal;
if (service === 'sync') {
// Sync signins are only complete when the browser actually syncs.
flowCompleteSignal = 'account.signed'
flowCompleteSignal = 'account.signed';
} else if (mustVerifySession) {
// Sessions that require verification are only complete once confirmed.
flowCompleteSignal = 'account.confirmed'
flowCompleteSignal = 'account.confirmed';
} else {
// Otherwise, the login itself is the end of the flow.
flowCompleteSignal = 'account.login'
flowCompleteSignal = 'account.login';
}
request.setMetricsFlowCompleteSignal(flowCompleteSignal, 'login')
request.setMetricsFlowCompleteSignal(flowCompleteSignal, 'login');
return stashMetricsContext()
.then(checkNumberOfActiveSessions)
.then(emitLoginEvent)
.then(sendEmail)
.then(recordSecurityEvent)
.then(recordSecurityEvent);
function stashMetricsContext() {
return request.stashMetricsContext(sessionToken)
@ -215,15 +215,15 @@ module.exports = (log, config, customs, db, mailer) => {
return request.stashMetricsContext({
uid: accountRecord.uid,
id: sessionToken.tokenVerificationId
})
});
}
})
});
}
function checkNumberOfActiveSessions () {
return db.sessions(accountRecord.uid)
.then(s => {
sessions = s
sessions = s;
if (sessions.length > MAX_ACTIVE_SESSIONS) {
// There's no spec-compliant way to error out
// as a result of having too many active sessions.
@ -232,15 +232,15 @@ module.exports = (log, config, customs, db, mailer) => {
uid: accountRecord.uid,
userAgent: request.headers['user-agent'],
numSessions: sessions.length
})
});
}
})
});
}
async function emitLoginEvent () {
await request.emitMetricsEvent('account.login', {
uid: accountRecord.uid
})
});
if (request.payload.reason === 'signin') {
await log.notifyAttachedServices('login', request, {
@ -249,18 +249,18 @@ module.exports = (log, config, customs, db, mailer) => {
service,
uid: accountRecord.uid,
userAgent: request.headers['user-agent']
})
});
}
}
function sendEmail() {
// For unverified accounts, we always re-send the account verification email.
if (isUnverifiedAccount) {
return sendVerifyAccountEmail()
return sendVerifyAccountEmail();
}
// If the session needs to be verified, send the sign-in confirmation email.
if (mustVerifySession) {
return sendVerifySessionEmail()
return sendVerifySessionEmail();
}
// Otherwise, no email is necessary.
}
@ -268,7 +268,7 @@ module.exports = (log, config, customs, db, mailer) => {
function sendVerifyAccountEmail() {
// If the session doesn't require verification,
// fall back to the account-level email code for the link.
const emailCode = sessionToken.tokenVerificationId || accountRecord.primaryEmail.emailCode
const emailCode = sessionToken.tokenVerificationId || accountRecord.primaryEmail.emailCode;
return mailer.sendVerifyCode([], accountRecord, {
code: emailCode,
@ -288,7 +288,7 @@ module.exports = (log, config, customs, db, mailer) => {
uaDeviceType: request.app.ua.deviceType,
uid: sessionToken.uid
})
.then(() => request.emitMetricsEvent('email.verification.sent'))
.then(() => request.emitMetricsEvent('email.verification.sent'));
}
function sendVerifySessionEmail() {
@ -297,21 +297,21 @@ module.exports = (log, config, customs, db, mailer) => {
switch (verificationMethod) {
case 'email':
// Sends an email containing a link to verify login
return sendVerifyLoginEmail()
return sendVerifyLoginEmail();
case 'email-2fa':
// Sends an email containing a code that can verify a login
return sendVerifyLoginCodeEmail()
return sendVerifyLoginCodeEmail();
case 'email-captcha':
// `email-captcha` is a custom verification method used only for
// unblock codes. We do not need to send a verification email
// in this case.
break
break;
case 'totp-2fa':
// This verification method requires a user to use a third-party
// application.
break
break;
default:
return sendVerifyLoginEmail()
return sendVerifyLoginEmail();
}
}
@ -319,9 +319,9 @@ module.exports = (log, config, customs, db, mailer) => {
log.info('account.signin.confirm.start', {
uid: accountRecord.uid,
tokenVerificationId: sessionToken.tokenVerificationId
})
});
const geoData = request.app.geo
const geoData = request.app.geo;
return mailer.sendVerifyLoginEmail(
accountRecord.emails,
accountRecord,
@ -347,18 +347,18 @@ module.exports = (log, config, customs, db, mailer) => {
)
.then(() => request.emitMetricsEvent('email.confirmation.sent'))
.catch(err => {
log.error('mailer.confirmation.error', { err })
log.error('mailer.confirmation.error', { err });
throw emailUtils.sendError(err, isUnverifiedAccount)
})
throw emailUtils.sendError(err, isUnverifiedAccount);
});
}
function sendVerifyLoginCodeEmail() {
log.info('account.token.code.start', {
uid: accountRecord.uid
})
});
const geoData = request.app.geo
const geoData = request.app.geo;
return mailer.sendVerifyLoginCodeEmail(
accountRecord.emails,
accountRecord,
@ -382,7 +382,7 @@ module.exports = (log, config, customs, db, mailer) => {
uid: sessionToken.uid
}
)
.then(() => request.emitMetricsEvent('email.tokencode.sent'))
.then(() => request.emitMetricsEvent('email.tokencode.sent'));
}
function recordSecurityEvent() {
@ -391,7 +391,7 @@ module.exports = (log, config, customs, db, mailer) => {
uid: accountRecord.uid,
ipAddr: ip,
tokenId: sessionToken.id
})
});
}
},
@ -404,12 +404,12 @@ module.exports = (log, config, customs, db, mailer) => {
wrapKb: wrapKb,
emailVerified: accountRecord.primaryEmail.isVerified,
tokenVerificationId: sessionToken.tokenVerificationId
})
});
})
.then(keyFetchToken => {
return request.stashMetricsContext(keyFetchToken)
.then(() => { return keyFetchToken } )
})
.then(() => { return keyFetchToken; } );
});
},
getSessionVerificationStatus(sessionToken, verificationMethod) {
@ -418,7 +418,7 @@ module.exports = (log, config, customs, db, mailer) => {
verified: false,
verificationMethod: 'email',
verificationReason: 'signup'
}
};
}
if (sessionToken.mustVerify && ! sessionToken.tokenVerified) {
return {
@ -426,10 +426,10 @@ module.exports = (log, config, customs, db, mailer) => {
// Override the verification method if it was explicitly specified in the request.
verificationMethod: verificationMethod || 'email',
verificationReason: 'login'
};
}
}
return { verified: true }
return { verified: true };
},
}
}
};
};

Просмотреть файл

@ -2,9 +2,9 @@
* 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'
'use strict';
const errors = require('../../error')
const errors = require('../../error');
module.exports = (log, config, db) => {
@ -17,18 +17,18 @@ module.exports = (log, config, db) => {
* @returns boolean
*/
hasTotpToken(account) {
const {uid} = account
const {uid} = account;
return db.totpToken(uid)
.then((result) => {
if (result && result.verified && result.enabled) {
return true
return true;
}
}, (err) => {
if (err.errno === errors.ERRNO.TOTP_TOKEN_NOT_FOUND) {
return false
return false;
}
throw err
})
throw err;
});
}
}
}
};
};

Просмотреть файл

@ -2,29 +2,29 @@
* 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'
'use strict';
const { URL } = require('url')
const punycode = require('punycode.js')
const isA = require('joi')
const { URL } = require('url');
const punycode = require('punycode.js');
const isA = require('joi');
// Match any non-empty hex-encoded string.
const HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/
module.exports.HEX_STRING = HEX_STRING
const HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/;
module.exports.HEX_STRING = HEX_STRING;
module.exports.BASE_36 = /^[a-zA-Z0-9]*$/
module.exports.BASE_36 = /^[a-zA-Z0-9]*$/;
// RFC 4648, section 5
module.exports.URL_SAFE_BASE_64 = /^[A-Za-z0-9_-]+$/
module.exports.URL_SAFE_BASE_64 = /^[A-Za-z0-9_-]+$/;
// Crude phone number validation. The handler code does it more thoroughly.
exports.E164_NUMBER = /^\+[1-9]\d{1,14}$/
exports.E164_NUMBER = /^\+[1-9]\d{1,14}$/;
exports.DIGITS = /^[0-9]+$/
exports.DIGITS = /^[0-9]+$/;
exports.DEVICE_COMMAND_NAME = /^[a-zA-Z0-9._\/\-:]{1,100}$/
exports.DEVICE_COMMAND_NAME = /^[a-zA-Z0-9._\/\-:]{1,100}$/;
exports.IP_ADDRESS = isA.string().ip()
exports.IP_ADDRESS = isA.string().ip();
// Match display-safe unicode characters.
// We're pretty liberal with what's allowed in a unicode string,
@ -40,16 +40,16 @@ exports.IP_ADDRESS = isA.string().ip()
//
// We might tweak this list in future.
const DISPLAY_SAFE_UNICODE = /^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uD800-\uDFFF\uE000-\uF8FF\uFFF9-\uFFFF])*$/
module.exports.DISPLAY_SAFE_UNICODE = DISPLAY_SAFE_UNICODE
const DISPLAY_SAFE_UNICODE = /^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uD800-\uDFFF\uE000-\uF8FF\uFFF9-\uFFFF])*$/;
module.exports.DISPLAY_SAFE_UNICODE = DISPLAY_SAFE_UNICODE;
// Similar display-safe match but includes non-BMP characters
const DISPLAY_SAFE_UNICODE_WITH_NON_BMP = /^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uE000-\uF8FF\uFFF9-\uFFFF])*$/
module.exports.DISPLAY_SAFE_UNICODE_WITH_NON_BMP = DISPLAY_SAFE_UNICODE_WITH_NON_BMP
const DISPLAY_SAFE_UNICODE_WITH_NON_BMP = /^(?:[^\u0000-\u001F\u007F\u0080-\u009F\u2028-\u2029\uE000-\uF8FF\uFFF9-\uFFFF])*$/;
module.exports.DISPLAY_SAFE_UNICODE_WITH_NON_BMP = DISPLAY_SAFE_UNICODE_WITH_NON_BMP;
// Bearer auth header regex
const BEARER_AUTH_REGEX = /^Bearer\s+([a-z0-9+\/]+)$/i
module.exports.BEARER_AUTH_REGEX = BEARER_AUTH_REGEX
const BEARER_AUTH_REGEX = /^Bearer\s+([a-z0-9+\/]+)$/i;
module.exports.BEARER_AUTH_REGEX = BEARER_AUTH_REGEX;
// Joi validator to match any valid email address.
// This is different to Joi's builtin email validator, and
@ -60,29 +60,29 @@ module.exports.BEARER_AUTH_REGEX = BEARER_AUTH_REGEX
// see examples here: https://github.com/hapijs/joi/blob/master/lib/string.js
module.exports.email = function() {
var email = isA.string().max(255).regex(DISPLAY_SAFE_UNICODE)
var email = isA.string().max(255).regex(DISPLAY_SAFE_UNICODE);
// Imma add a custom test to this Joi object using internal
// properties because I can't find a nice API to do that.
email._tests.push({ func: function(value, state, options) {
if (value !== undefined && value !== null) {
if (module.exports.isValidEmailAddress(value)) {
return value
return value;
}
}
return email.createError('string.base', { value }, state, options)
return email.createError('string.base', { value }, state, options);
}})
}});
return email
}
return email;
};
module.exports.service = isA.string().max(16).regex(/^[a-zA-Z0-9\-]*$/)
module.exports.hexString = isA.string().regex(HEX_STRING)
module.exports.clientId = module.exports.hexString.length(16)
module.exports.accessToken = module.exports.hexString.length(64)
module.exports.refreshToken = module.exports.hexString.length(64)
module.exports.scope = isA.string().max(256).regex(/^[a-zA-Z0-9 _\/.:-]+$/)
module.exports.service = isA.string().max(16).regex(/^[a-zA-Z0-9\-]*$/);
module.exports.hexString = isA.string().regex(HEX_STRING);
module.exports.clientId = module.exports.hexString.length(16);
module.exports.accessToken = module.exports.hexString.length(64);
module.exports.refreshToken = module.exports.hexString.length(64);
module.exports.scope = isA.string().max(256).regex(/^[a-zA-Z0-9 _\/.:-]+$/);
module.exports.assertion = isA.string().min(50).max(10240).regex(/^[a-zA-Z0-9_\-\.~=]+$/);
module.exports.jwe = isA.string().max(1024)
// JWE token format: 'protectedheader.encryptedkey.iv.cyphertext.authenticationtag'
@ -98,72 +98,72 @@ module.exports.jwe = isA.string().max(1024)
//
// https://github.com/mozilla/fxa-email-service/blob/6fc6c31043598b246102cd1fdd27fc325f4514fb/src/validate/mod.rs#L28-L30
const EMAIL_USER = /^[A-Z0-9.!#$%&'*+\/=?^_`{|}~-]{1,64}$/i
const EMAIL_DOMAIN = /^[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?(?:\.[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?)+$/i
const EMAIL_USER = /^[A-Z0-9.!#$%&'*+\/=?^_`{|}~-]{1,64}$/i;
const EMAIL_DOMAIN = /^[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?(?:\.[A-Z0-9](?:[A-Z0-9-]{0,253}[A-Z0-9])?)+$/i;
module.exports.isValidEmailAddress = function(value) {
if (! value) {
return false
return false;
}
const parts = value.split('@')
const parts = value.split('@');
if (parts.length !== 2 || parts[1].length > 255) {
return false
return false;
}
if (! EMAIL_USER.test(punycode.toASCII(parts[0]))) {
return false
return false;
}
if (! EMAIL_DOMAIN.test(punycode.toASCII(parts[1]))) {
return false
return false;
}
return true
}
return true;
};
module.exports.redirectTo = function redirectTo(base) {
const validator = isA.string().max(512)
let hostnameRegex = null
const validator = isA.string().max(512);
let hostnameRegex = null;
if (base) {
hostnameRegex = new RegExp('(?:\\.|^)' + base.replace('.', '\\.') + '$')
hostnameRegex = new RegExp('(?:\\.|^)' + base.replace('.', '\\.') + '$');
}
validator._tests.push(
{
func: (value, state, options) => {
if (value !== undefined && value !== null) {
if (isValidUrl(value, hostnameRegex)) {
return value
return value;
}
}
return validator.createError('string.base', { value }, state, options)
return validator.createError('string.base', { value }, state, options);
}
}
)
return validator
}
);
return validator;
};
module.exports.url = function url(options) {
const validator = isA.string().uri(options)
const validator = isA.string().uri(options);
validator._tests.push(
{
func: (value, state, options) => {
if (value !== undefined && value !== null) {
if (isValidUrl(value)) {
return value
return value;
}
}
return validator.createError('string.base', { value }, state, options)
return validator.createError('string.base', { value }, state, options);
}
}
)
return validator
}
);
return validator;
};
module.exports.pushCallbackUrl = function pushUrl(options) {
const validator = isA.string().uri(options)
const validator = isA.string().uri(options);
validator._tests.push(
{
func: (value, state, options) => {
@ -172,33 +172,33 @@ module.exports.pushCallbackUrl = function pushUrl(options) {
// Fx Desktop registers https push urls with a :443 which causes `isValidUrl`
// to fail because the :443 is expected to have been normalized away.
if (/^https:\/\/[a-zA-Z0-9._-]+(:443)($|\/)/.test(value)) {
normalizedValue = value.replace(':443', '')
normalizedValue = value.replace(':443', '');
}
if (isValidUrl(normalizedValue)) {
return value
return value;
}
}
return validator.createError('string.base', { value }, state, options)
return validator.createError('string.base', { value }, state, options);
}
}
)
return validator
}
);
return validator;
};
function isValidUrl(url, hostnameRegex) {
let parsed
let parsed;
try {
parsed = new URL(url)
parsed = new URL(url);
} catch (err) {
return false
return false;
}
if (hostnameRegex && ! hostnameRegex.test(parsed.hostname)) {
return false
return false;
}
if (! /^https?:$/.test(parsed.protocol)) {
return false
return false;
}
// Reject anything that won't round-trip unambiguously
// through a parse. This puts the onus on the requestor
@ -207,15 +207,15 @@ function isValidUrl(url, hostnameRegex) {
// slash if there's no path component, which is why we also
// compare to `origin` below.
if (parsed.href !== url && parsed.origin !== url) {
return false
return false;
}
return parsed.href
return parsed.href;
}
module.exports.verificationMethod = isA.string().valid(['email', 'email-2fa', 'email-captcha', 'totp-2fa'])
module.exports.verificationMethod = isA.string().valid(['email', 'email-2fa', 'email-captcha', 'totp-2fa']);
module.exports.authPW = isA.string().length(64).regex(HEX_STRING).required()
module.exports.wrapKb = isA.string().length(64).regex(HEX_STRING)
module.exports.authPW = isA.string().length(64).regex(HEX_STRING).required();
module.exports.wrapKb = isA.string().length(64).regex(HEX_STRING);
module.exports.recoveryKeyId = isA.string().regex(HEX_STRING).max(32)
module.exports.recoveryData = isA.string().regex(/[a-zA-Z0-9.]/).max(1024).required()
module.exports.recoveryKeyId = isA.string().regex(HEX_STRING).max(32);
module.exports.recoveryData = isA.string().regex(/[a-zA-Z0-9.]/).max(1024).required();

Просмотреть файл

@ -25,68 +25,68 @@
// url.render({}) // throws error.internalValidationError()
// url.render({ uid: 'foo', id: 'bar' }) // throws error.internalValidationError()
'use strict'
'use strict';
const error = require('./error')
const impl = require('safe-url-assembler')()
const error = require('./error');
const impl = require('safe-url-assembler')();
const SAFE_URL_COMPONENT = /^[\w.]+$/
const SAFE_URL_COMPONENT = /^[\w.]+$/;
module.exports = log => class SafeUrl {
constructor (path, caller) {
const expectedKeys = path.split('/')
.filter(part => part.indexOf(':') === 0)
.map(part => part.substr(1))
.map(part => part.substr(1));
this._expectedKeys = {
array: expectedKeys,
set: new Set(expectedKeys)
}
this._template = impl.template(path)
this._caller = caller
};
this._template = impl.template(path);
this._caller = caller;
}
params () {
return this._expectedKeys.array.slice(0)
return this._expectedKeys.array.slice(0);
}
render (params = {}, query = {}) {
const paramsKeys = Object.keys(params)
const { array: expected, set: expectedSet } = this._expectedKeys
const paramsKeys = Object.keys(params);
const { array: expected, set: expectedSet } = this._expectedKeys;
if (paramsKeys.length !== expected.length) {
this._fail('safeUrl.params.mismatch', { keys: paramsKeys, expected })
this._fail('safeUrl.params.mismatch', { keys: paramsKeys, expected });
}
paramsKeys.forEach(key => {
if (! expectedSet.has(key)) {
this._fail('safeUrl.params.unexpected', { key, expected })
this._fail('safeUrl.params.unexpected', { key, expected });
}
const value = params[key]
this._checkSafe('paramVal', key, value)
})
const value = params[key];
this._checkSafe('paramVal', key, value);
});
Object.keys(query).forEach(key => {
const value = query[key]
this._checkSafe('queryKey', key, key)
this._checkSafe('queryVal', key, value)
})
const value = query[key];
this._checkSafe('queryKey', key, key);
this._checkSafe('queryVal', key, value);
});
return this._template.param(params).query(query).toString()
return this._template.param(params).query(query).toString();
}
_checkSafe(location, key, value) {
if (! value || typeof value !== 'string') {
this._fail('safeUrl.bad', { location, key, value })
this._fail('safeUrl.bad', { location, key, value });
}
if (! SAFE_URL_COMPONENT.test(value)) {
this._fail('safeUrl.unsafe', { location, key, value })
this._fail('safeUrl.unsafe', { location, key, value });
}
}
_fail (op, data) {
log.error(op, Object.assign({ caller: this._caller }, data))
throw error.internalValidationError(op, data)
log.error(op, Object.assign({ caller: this._caller }, data));
throw error.internalValidationError(op, data);
}
}
};

Просмотреть файл

@ -2,71 +2,71 @@
* 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'
'use strict';
const AppError = require('./error')
const joi = require('joi')
const validators = require('./routes/validators')
const { BEARER_AUTH_REGEX } = require('./routes/validators')
const ScopeSet = require('fxa-shared').oauth.scopes
const AppError = require('./error');
const joi = require('joi');
const validators = require('./routes/validators');
const { BEARER_AUTH_REGEX } = require('./routes/validators');
const ScopeSet = require('fxa-shared').oauth.scopes;
// the refresh token scheme is currently used by things connected to sync,
// and we're at a transitionary stage of its evolution into something more generic,
// so we limit to the scope below as a safety mechanism
const ALLOWED_REFRESH_TOKEN_SCHEME_SCOPES = ScopeSet.fromArray(['https://identity.mozilla.com/apps/oldsync'])
const ALLOWED_REFRESH_TOKEN_SCHEME_SCOPES = ScopeSet.fromArray(['https://identity.mozilla.com/apps/oldsync']);
module.exports = function schemeRefreshTokenScheme(db, oauthdb) {
return function schemeRefreshToken(server, options) {
return {
async authenticate (request, h) {
const bearerMatch = BEARER_AUTH_REGEX.exec(request.headers.authorization)
const bearerMatchErr = new AppError.invalidRequestParameter('authorization')
const refreshToken = bearerMatch && bearerMatch[1]
const bearerMatch = BEARER_AUTH_REGEX.exec(request.headers.authorization);
const bearerMatchErr = new AppError.invalidRequestParameter('authorization');
const refreshToken = bearerMatch && bearerMatch[1];
if (refreshToken) {
joi.attempt(bearerMatch[1], validators.refreshToken, bearerMatchErr)
joi.attempt(bearerMatch[1], validators.refreshToken, bearerMatchErr);
} else {
throw bearerMatchErr
throw bearerMatchErr;
}
const refreshTokenInfo = await oauthdb.checkRefreshToken(refreshToken)
const refreshTokenInfo = await oauthdb.checkRefreshToken(refreshToken);
if (! refreshTokenInfo || ! refreshTokenInfo.active) {
return h.unauthenticated()
return h.unauthenticated();
}
const credentials = {
uid: refreshTokenInfo.sub,
tokenVerified: true,
refreshTokenId: refreshTokenInfo.jti
}
};
const scopeSet = ScopeSet.fromString(refreshTokenInfo.scope)
const scopeSet = ScopeSet.fromString(refreshTokenInfo.scope);
if (! scopeSet.intersects(ALLOWED_REFRESH_TOKEN_SCHEME_SCOPES)) {
// unauthenticated if refreshToken is missing the required scope
return h.unauthenticated()
return h.unauthenticated();
}
credentials.client = await oauthdb.getClientInfo(refreshTokenInfo.client_id)
const devices = await db.devices(refreshTokenInfo.sub)
credentials.client = await oauthdb.getClientInfo(refreshTokenInfo.client_id);
const devices = await db.devices(refreshTokenInfo.sub);
// use the hashed refreshToken id to find devices
const device = devices.filter(device => device.refreshTokenId === credentials.refreshTokenId)[0]
const device = devices.filter(device => device.refreshTokenId === credentials.refreshTokenId)[0];
if (device) {
credentials.deviceId = device.id
credentials.deviceName = device.name
credentials.deviceType = device.type
credentials.deviceCreatedAt = device.createdAt
credentials.deviceCallbackURL = device.callbackURL
credentials.deviceCallbackPublicKey = device.callbackPublicKey
credentials.deviceCallbackAuthKey = device.callbackAuthKey
credentials.deviceCallbackIsExpired = device.callbackIsExpired
credentials.deviceAvailableCommands = device.availableCommands
credentials.deviceId = device.id;
credentials.deviceName = device.name;
credentials.deviceType = device.type;
credentials.deviceCreatedAt = device.createdAt;
credentials.deviceCallbackURL = device.callbackURL;
credentials.deviceCallbackPublicKey = device.callbackPublicKey;
credentials.deviceCallbackAuthKey = device.callbackAuthKey;
credentials.deviceCallbackIsExpired = device.callbackIsExpired;
credentials.deviceAvailableCommands = device.availableCommands;
}
return h.authenticated({
credentials: credentials
})
});
}
}
}
}
};
};
};

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -2,26 +2,26 @@
* 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'
'use strict';
const request = require('request')
const error = require('../error')
const request = require('request');
const error = require('../error');
const ERRNO = {
// From fxa-email-service, src/app_errors/mod.rs
COMPLAINT: 106,
SOFT_BOUNCE: 107,
HARD_BOUNCE: 108
}
};
module.exports = (config) => {
function sendMail(emailConfig, cb) {
// Email service requires that all headers are strings.
const headers = {}
const headers = {};
for (const header in emailConfig.headers) {
// Check to make sure header is not null. Issue #2771
if (emailConfig.headers[header]) {
headers[header] = emailConfig.headers[header].toString()
headers[header] = emailConfig.headers[header].toString();
}
}
const options = {
@ -38,42 +38,42 @@ module.exports = (config) => {
html: emailConfig.html
}
}
}
};
if (emailConfig.provider) {
options.body.provider = emailConfig.provider
options.body.provider = emailConfig.provider;
}
request(options, function(err, res, body) {
if (! err && res.statusCode >= 400) {
err = marshallError(res.statusCode, body)
err = marshallError(res.statusCode, body);
}
cb(err, {
messageId: body && body.messageId,
message: body && body.message
})
})
});
});
}
function marshallError (status, body) {
if (status === 429) {
// Error structure is changing in mozilla/fxa-email-service#198,
// temporarily handle both formats
return marshallBounceError(body.errno, body.bouncedAt || body.time)
return marshallBounceError(body.errno, body.bouncedAt || body.time);
}
return error.unexpectedError()
return error.unexpectedError();
}
function marshallBounceError (errno, bouncedAt) {
switch (errno) {
case ERRNO.COMPLAINT:
return error.emailComplaint(bouncedAt)
return error.emailComplaint(bouncedAt);
case ERRNO.SOFT_BOUNCE:
return error.emailBouncedSoft(bouncedAt)
return error.emailBouncedSoft(bouncedAt);
case ERRNO.HARD_BOUNCE:
default:
return error.emailBouncedHard(bouncedAt)
return error.emailBouncedHard(bouncedAt);
}
}
@ -84,5 +84,5 @@ module.exports = (config) => {
return {
sendMail,
close
}
}
};
};

Просмотреть файл

@ -2,29 +2,29 @@
* 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'
'use strict';
const createMailer = require('./email')
const createSms = require('./sms')
const P = require('../promise')
const createMailer = require('./email');
const createSms = require('./sms');
const P = require('../promise');
module.exports = (log, config, error, bounces, translator, oauthdb, sender) => {
const defaultLanguage = config.i18n.defaultLanguage
const defaultLanguage = config.i18n.defaultLanguage;
function createSenders() {
const Mailer = createMailer(log, config, oauthdb)
const Mailer = createMailer(log, config, oauthdb);
return require('./templates').init()
.then(function (templates) {
return {
email: new Mailer(translator, templates, config.smtp, sender),
sms: createSms(log, translator, templates, config)
}
})
};
});
}
return createSenders()
.then(function (senders) {
const ungatedMailer = senders.email
const ungatedMailer = senders.email;
function getSafeMailer(email) {
return bounces.check(email)
@ -32,41 +32,41 @@ module.exports = (log, config, error, bounces, translator, oauthdb, sender) => {
.catch(function (e) {
const info = {
errno: e.errno
}
const bouncedAt = e.output && e.output.payload && e.output.payload.bouncedAt
};
const bouncedAt = e.output && e.output.payload && e.output.payload.bouncedAt;
if (bouncedAt) {
info.bouncedAt = bouncedAt
info.bouncedAt = bouncedAt;
}
log.info('mailer.blocked', info)
throw e
})
log.info('mailer.blocked', info);
throw e;
});
}
function getSafeMailerWithEmails(emails) {
let ungatedPrimaryEmail
const ungatedCcEmails = []
const gatedEmailErrors = []
let ungatedPrimaryEmail;
const ungatedCcEmails = [];
const gatedEmailErrors = [];
return P.filter(emails, (email) => {
// We will only send to primary, or verified secondary.
return email.isPrimary || email.isVerified
return email.isPrimary || email.isVerified;
}).then((emails) => {
if (emails.length === 0) {
// No emails we can even attempt to send to? Should never happen!
throw new Error('Empty list of sendable email addresses')
throw new Error('Empty list of sendable email addresses');
}
return emails
return emails;
}).each((email) => {
// We only send to addresses that are not gated, to protect our sender score.
return getSafeMailer(email.email).then(() => {
if (email.isPrimary) {
ungatedPrimaryEmail = email.email
ungatedPrimaryEmail = email.email;
} else {
ungatedCcEmails.push(email.email)
ungatedCcEmails.push(email.email);
}
}, (err) => {
gatedEmailErrors.push(err)
})
gatedEmailErrors.push(err);
});
}).then(() => {
if (! ungatedPrimaryEmail) {
// This user is having a bad time, their primary email is bouncing.
@ -74,63 +74,63 @@ module.exports = (log, config, error, bounces, translator, oauthdb, sender) => {
if (ungatedCcEmails.length === 0) {
// Nope. Block the send, using first error reported.
// Since we always check at least one email, there will be at least one error here.
throw gatedEmailErrors[0]
throw gatedEmailErrors[0];
}
ungatedPrimaryEmail = ungatedCcEmails.shift()
ungatedPrimaryEmail = ungatedCcEmails.shift();
}
return {
ungatedMailer: ungatedMailer,
ungatedPrimaryEmail: ungatedPrimaryEmail,
ungatedCcEmails: ungatedCcEmails
}
})
};
});
}
senders.email = {
sendVerifyCode: function (emails, account, opts) {
const primaryEmail = account.email
const primaryEmail = account.email;
return getSafeMailer(primaryEmail)
.then(function (mailer) {
return mailer.verifyEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
email: primaryEmail,
uid: account.uid
}))
})
}));
});
},
sendVerifyLoginEmail: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.verifyLoginEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail,
uid: account.uid
}))
})
}));
});
},
sendVerifyLoginCodeEmail: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.verifyLoginCodeEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail,
uid: account.uid
}))
})
}));
});
},
sendVerifyPrimaryEmail: function (emails, account, opts) {
const primaryEmail = account.email
const primaryEmail = account.email;
return getSafeMailer(primaryEmail)
.then(function (mailer) {
@ -138,12 +138,12 @@ module.exports = (log, config, error, bounces, translator, oauthdb, sender) => {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
email: primaryEmail,
uid: account.uid
}))
})
}));
});
},
sendVerifySecondaryEmail: function (emails, account, opts) {
const primaryEmail = account.email
const verifyEmailAddress = emails[0].email
const primaryEmail = account.email;
const verifyEmailAddress = emails[0].email;
return getSafeMailer(primaryEmail)
.then(function (mailer) {
@ -152,15 +152,15 @@ module.exports = (log, config, error, bounces, translator, oauthdb, sender) => {
email: verifyEmailAddress,
primaryEmail,
uid: account.uid
}))
})
}));
});
},
sendRecoveryCode: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.recoveryEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
@ -168,236 +168,236 @@ module.exports = (log, config, error, bounces, translator, oauthdb, sender) => {
email: primaryEmail,
emailToHashWith: account.email,
token: opts.token.data
}))
})
}));
});
},
sendPasswordChangedNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.passwordChangedEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendPasswordResetNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.passwordResetEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendNewDeviceLoginNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.newDeviceLoginEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendPostVerifyEmail: function (emails, account, opts) {
const primaryEmail = account.email
const primaryEmail = account.email;
return getSafeMailer(primaryEmail)
.then(function (mailer) {
return mailer.postVerifyEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
email: primaryEmail
}))
})
}));
});
},
sendPostRemoveSecondaryEmail: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.postRemoveSecondaryEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendPostVerifySecondaryEmail: function (emails, account, opts) {
const primaryEmail = account.primaryEmail.email
const primaryEmail = account.primaryEmail.email;
return getSafeMailer(primaryEmail)
.then(function (mailer) {
return mailer.postVerifySecondaryEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
email: primaryEmail
}))
})
}));
});
},
sendPostChangePrimaryEmail: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.postChangePrimaryEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendPostNewRecoveryCodesNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.postNewRecoveryCodesEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendPostConsumeRecoveryCodeNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.postConsumeRecoveryCodeEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendLowRecoveryCodeNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.lowRecoveryCodesEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendUnblockCode: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.unblockCodeEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail,
uid: account.uid
}))
})
}));
});
},
sendPostAddTwoStepAuthNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.postAddTwoStepAuthenticationEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendPostRemoveTwoStepAuthNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.postRemoveTwoStepAuthenticationEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendPostAddAccountRecoveryNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.postAddAccountRecoveryEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendPostRemoveAccountRecoveryNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.postRemoveAccountRecoveryEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
sendPasswordResetAccountRecoveryNotification: function (emails, account, opts) {
return getSafeMailerWithEmails(emails)
.then(function (result) {
const mailer = result.ungatedMailer
const primaryEmail = result.ungatedPrimaryEmail
const ccEmails = result.ungatedCcEmails
const mailer = result.ungatedMailer;
const primaryEmail = result.ungatedPrimaryEmail;
const ccEmails = result.ungatedCcEmails;
return mailer.passwordResetAccountRecoveryEmail(Object.assign({}, opts, {
acceptLanguage: opts.acceptLanguage || defaultLanguage,
ccEmails,
email: primaryEmail
}))
})
}));
});
},
translator: function () {
return ungatedMailer.translator.apply(ungatedMailer, arguments)
return ungatedMailer.translator.apply(ungatedMailer, arguments);
},
stop: function () {
return ungatedMailer.stop()
return ungatedMailer.stop();
},
_ungatedMailer: ungatedMailer
}
return senders
})
}
};
return senders;
});
};

Просмотреть файл

@ -2,7 +2,7 @@
* 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'
'use strict';
/**
* This module converts old logging messages to mozlog
@ -13,22 +13,22 @@
module.exports = function (log) {
return {
trace: function (data) {
log.debug(data.op, data)
log.debug(data.op, data);
},
error: function (data) {
log.error(data.op, data)
log.error(data.op, data);
},
fatal: function (data) {
log.critical(data.op, data)
log.critical(data.op, data);
},
warn: function (data) {
log.warn(data.op, data)
log.warn(data.op, data);
},
info: function (data) {
log.info(data.op, data)
log.info(data.op, data);
},
amplitudeEvent: function (data) {
log.info(data.op, data)
log.info(data.op, data);
}
}
}
};
};

Просмотреть файл

@ -2,12 +2,12 @@
* 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'
'use strict';
var mozlog = require('mozlog')
var mozlog = require('mozlog');
var logConfig = require('../../config').get('log')
var logConfig = require('../../config').get('log');
mozlog.config(logConfig)
mozlog.config(logConfig);
module.exports = mozlog
module.exports = mozlog;

Просмотреть файл

@ -2,22 +2,22 @@
* 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'
'use strict';
const Keyv = require('keyv')
const Keyv = require('keyv');
module.exports = (log, config, oauthdb) => {
const OAUTH_CLIENT_INFO_CACHE_TTL = config.oauth.clientInfoCacheTTL
const OAUTH_CLIENT_INFO_CACHE_NAMESPACE = 'oauthClientInfo'
const OAUTH_CLIENT_INFO_CACHE_TTL = config.oauth.clientInfoCacheTTL;
const OAUTH_CLIENT_INFO_CACHE_NAMESPACE = 'oauthClientInfo';
const FIREFOX_CLIENT = {
name: 'Firefox'
}
};
const clientCache = new Keyv({
ttl: OAUTH_CLIENT_INFO_CACHE_TTL,
namespace: OAUTH_CLIENT_INFO_CACHE_NAMESPACE
})
});
/**
* Fetches OAuth client info from the OAuth server.
@ -26,42 +26,42 @@ module.exports = (log, config, oauthdb) => {
* @returns {Promise<any>}
*/
async function fetch(clientId) {
log.trace('fetch.start')
log.trace('fetch.start');
if (! clientId || clientId === 'sync') {
log.trace('fetch.sync')
return FIREFOX_CLIENT
log.trace('fetch.sync');
return FIREFOX_CLIENT;
}
const cachedRecord = await clientCache.get(clientId)
const cachedRecord = await clientCache.get(clientId);
if (cachedRecord) {
// used the cachedRecord if it exists
log.trace('fetch.usedCache')
return cachedRecord
log.trace('fetch.usedCache');
return cachedRecord;
}
let clientInfo
let clientInfo;
try {
clientInfo = await oauthdb.getClientInfo(clientId)
clientInfo = await oauthdb.getClientInfo(clientId);
} catch (err) {
// fallback to the Firefox client if request fails
if (! err.statusCode) {
log.fatal('fetch.failed', { err: err })
log.fatal('fetch.failed', { err: err });
} else {
log.warn('fetch.failedForClient', { clientId })
log.warn('fetch.failedForClient', { clientId });
}
return FIREFOX_CLIENT
return FIREFOX_CLIENT;
}
log.trace('fetch.usedServer', { body: clientInfo })
log.trace('fetch.usedServer', { body: clientInfo });
// We deliberately don't wait for this to resolve, since the
// client doesn't need to wait for us to write to the cache.
clientCache.set(clientId, clientInfo)
return clientInfo
clientCache.set(clientId, clientInfo);
return clientInfo;
}
return {
fetch: fetch,
__clientCache: clientCache
}
}
};
};

Просмотреть файл

@ -2,49 +2,49 @@
* 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'
'use strict';
const Cloudwatch = require('aws-sdk/clients/cloudwatch')
const error = require('../error')
const MockSns = require('../../test/mock-sns')
const P = require('bluebird')
const Sns = require('aws-sdk/clients/sns')
const time = require('../time')
const Cloudwatch = require('aws-sdk/clients/cloudwatch');
const error = require('../error');
const MockSns = require('../../test/mock-sns');
const P = require('bluebird');
const Sns = require('aws-sdk/clients/sns');
const time = require('../time');
const SECONDS_PER_MINUTE = 60
const MILLISECONDS_PER_MINUTE = SECONDS_PER_MINUTE * 1000
const MILLISECONDS_PER_HOUR = MILLISECONDS_PER_MINUTE * 60
const PERIOD_IN_MINUTES = 5
const SECONDS_PER_MINUTE = 60;
const MILLISECONDS_PER_MINUTE = SECONDS_PER_MINUTE * 1000;
const MILLISECONDS_PER_HOUR = MILLISECONDS_PER_MINUTE * 60;
const PERIOD_IN_MINUTES = 5;
class MockCloudwatch {
getMetricStatistics () {
return {
promise: () => P.resolve({ Datapoints: [ { Maximum: 0 } ] })
}
};
}
}
module.exports = (log, translator, templates, config) => {
const cloudwatch = initService(config, Cloudwatch, MockCloudwatch)
const sns = initService(config, Sns, MockSns)
const cloudwatch = initService(config, Cloudwatch, MockCloudwatch);
const sns = initService(config, Sns, MockSns);
const { minimumCreditThresholdUSD: CREDIT_THRESHOLD } = config.sms
const { minimumCreditThresholdUSD: CREDIT_THRESHOLD } = config.sms;
let isBudgetOk = true
let isBudgetOk = true;
if (config.sms.enableBudgetChecks) {
setImmediate(pollCurrentSpend)
setImmediate(pollCurrentSpend);
}
return {
isBudgetOk: () => isBudgetOk,
send (phoneNumber, templateName, acceptLanguage, signinCode) {
log.trace('sms.send', { templateName, acceptLanguage })
log.trace('sms.send', { templateName, acceptLanguage });
return P.resolve()
.then(() => {
const message = getMessage(templateName, acceptLanguage, signinCode)
const message = getMessage(templateName, acceptLanguage, signinCode);
const params = {
Message: message.trim(),
MessageAttributes: {
@ -65,7 +65,7 @@ module.exports = (log, translator, templates, config) => {
}
},
PhoneNumber: phoneNumber
}
};
return sns.publish(params).promise()
.then(result => {
@ -73,30 +73,30 @@ module.exports = (log, translator, templates, config) => {
templateName,
acceptLanguage,
messageId: result.MessageId
})
});
})
.catch(sendError => {
const { message, code, statusCode } = sendError
log.error('sms.send.error', { message, code, statusCode })
const { message, code, statusCode } = sendError;
log.error('sms.send.error', { message, code, statusCode });
throw error.messageRejected(message, code)
})
})
}
throw error.messageRejected(message, code);
});
});
}
};
function pollCurrentSpend () {
let limit
let limit;
sns.getSMSAttributes({ attributes: [ 'MonthlySpendLimit' ] }).promise()
.then(result => {
limit = parseFloat(result.attributes.MonthlySpendLimit)
limit = parseFloat(result.attributes.MonthlySpendLimit);
if (isNaN(limit)) {
throw new Error(`Invalid getSMSAttributes result "${result.attributes.MonthlySpendLimit}"`)
throw new Error(`Invalid getSMSAttributes result "${result.attributes.MonthlySpendLimit}"`);
}
const endTime = new Date()
const startTime = new Date(endTime.getTime() - PERIOD_IN_MINUTES * MILLISECONDS_PER_MINUTE)
const endTime = new Date();
const startTime = new Date(endTime.getTime() - PERIOD_IN_MINUTES * MILLISECONDS_PER_MINUTE);
return cloudwatch.getMetricStatistics({
Namespace: 'AWS/SNS',
MetricName: 'SMSMonthToDateSpentUSD',
@ -104,63 +104,63 @@ module.exports = (log, translator, templates, config) => {
EndTime: time.startOfMinute(endTime),
Period: PERIOD_IN_MINUTES * SECONDS_PER_MINUTE,
Statistics: [ 'Maximum' ]
}).promise()
}).promise();
})
.then(result => {
let current
let current;
try {
current = parseFloat(result.Datapoints[0].Maximum)
current = parseFloat(result.Datapoints[0].Maximum);
} catch (err) {
err.result = JSON.stringify(result)
throw err
err.result = JSON.stringify(result);
throw err;
}
if (isNaN(current)) {
throw new Error(`Invalid getMetricStatistics result "${result.Datapoints[0].Maximum}"`)
throw new Error(`Invalid getMetricStatistics result "${result.Datapoints[0].Maximum}"`);
}
isBudgetOk = current <= limit - CREDIT_THRESHOLD
log.info('sms.budget.ok', { isBudgetOk, current, limit, threshold: CREDIT_THRESHOLD })
isBudgetOk = current <= limit - CREDIT_THRESHOLD;
log.info('sms.budget.ok', { isBudgetOk, current, limit, threshold: CREDIT_THRESHOLD });
})
.catch(err => {
log.error('sms.budget.error', { err: err.message, result: err.result })
log.error('sms.budget.error', { err: err.message, result: err.result });
// If we failed to query the data, assume current spend is fine
isBudgetOk = true
isBudgetOk = true;
})
.then(() => setTimeout(pollCurrentSpend, MILLISECONDS_PER_HOUR))
.then(() => setTimeout(pollCurrentSpend, MILLISECONDS_PER_HOUR));
}
function getMessage (templateName, acceptLanguage, signinCode) {
const template = templates[`sms.${templateName}`]
const template = templates[`sms.${templateName}`];
if (! template) {
log.error('sms.getMessage.error', { templateName })
throw error.invalidMessageId()
log.error('sms.getMessage.error', { templateName });
throw error.invalidMessageId();
}
let link
let link;
if (signinCode) {
link = `${config.sms.installFirefoxWithSigninCodeBaseUri}/${urlSafeBase64(signinCode)}`
link = `${config.sms.installFirefoxWithSigninCodeBaseUri}/${urlSafeBase64(signinCode)}`;
} else {
link = config.sms[`${templateName}Link`]
link = config.sms[`${templateName}Link`];
}
return template({ link, translator: translator.getTranslator(acceptLanguage) }).text
return template({ link, translator: translator.getTranslator(acceptLanguage) }).text;
}
}
};
function initService (config, Class, MockClass) {
const options = {
region: config.sms.apiRegion
}
};
if (config.sms.useMock) {
return new MockClass(options, config)
return new MockClass(options, config);
}
return new Class(options)
return new Class(options);
}
function urlSafeBase64 (hex) {
@ -168,5 +168,5 @@ function urlSafeBase64 (hex) {
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
.replace(/=/g, '');
}

Просмотреть файл

@ -2,33 +2,33 @@
* 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'
'use strict';
var path = require('path')
var P = require('bluebird')
var handlebars = require('handlebars')
var readFile = P.promisify(require('fs').readFile)
var path = require('path');
var P = require('bluebird');
var handlebars = require('handlebars');
var readFile = P.promisify(require('fs').readFile);
handlebars.registerHelper(
't',
function (string) {
if (this.translator) {
return this.translator.format(this.translator.gettext(string), this)
return this.translator.format(this.translator.gettext(string), this);
}
return string
return string;
}
)
);
function generateTemplateName (str) {
if (/^sms\.[A-Za-z]+/.test(str)) {
return str
return str;
}
return str.replace(/_(.)/g,
function(match, c) {
return c.toUpperCase()
return c.toUpperCase();
}
) + 'Email'
) + 'Email';
}
function loadTemplates(name) {
@ -40,19 +40,19 @@ function loadTemplates(name) {
)
.spread(
function (text, html) {
var renderText = handlebars.compile(text)
var renderHtml = handlebars.compile(html)
var renderText = handlebars.compile(text);
var renderHtml = handlebars.compile(html);
return {
name: generateTemplateName(name),
fn: function (values) {
return {
text: renderText(values),
html: renderHtml(values)
};
}
};
}
}
}
)
);
}
module.exports = {
@ -96,11 +96,11 @@ module.exports = {
// }
return templates.reduce(
function (result, template) {
result[template.name] = template.fn
return result
result[template.name] = template.fn;
return result;
},
{}
)
);
}
)
}
};

Просмотреть файл

@ -2,22 +2,22 @@
* 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'
'use strict';
var path = require('path')
var i18n = require('i18n-abide')
var Jed = require('jed')
var P = require('bluebird')
var po2json = require('po2json')
var poParseFile = P.promisify(po2json.parseFile)
var path = require('path');
var i18n = require('i18n-abide');
var Jed = require('jed');
var P = require('bluebird');
var po2json = require('po2json');
var poParseFile = P.promisify(po2json.parseFile);
Jed.prototype.format = i18n.format
Jed.prototype.format = i18n.format;
var parseCache = {}
var parseCache = {};
function parseLocale(locale) {
if (parseCache[locale]) {
return P.resolve(parseCache[locale])
return P.resolve(parseCache[locale]);
}
return poParseFile(
@ -32,9 +32,9 @@ function parseLocale(locale) {
format: 'jed'
}
).then(function (parsed) {
parseCache[locale] = parsed
return parsed
})
parseCache[locale] = parsed;
return parsed;
});
}
module.exports = function (locales, defaultLanguage) {
@ -43,38 +43,38 @@ module.exports = function (locales, defaultLanguage) {
)
.then(
function (translations) {
var languageTranslations = {}
var supportedLanguages = []
var languageTranslations = {};
var supportedLanguages = [];
for (var i = 0; i < translations.length; i++) {
var t = translations[i]
const localeMessageData = t.locale_data.messages['']
var t = translations[i];
const localeMessageData = t.locale_data.messages[''];
if (localeMessageData.lang === 'ar') {
// NOTE: there seems to be some incompatibility with Jed and Arabic plural forms from Pontoon
// We disable plural forms manually below, we don't use them anyway. Issue #2714
localeMessageData.plural_forms = null
localeMessageData.plural_forms = null;
}
var language = i18n.normalizeLanguage(i18n.languageFrom(localeMessageData.lang))
supportedLanguages.push(language)
var translator = new Jed(t)
translator.language = language
languageTranslations[language] = translator
var language = i18n.normalizeLanguage(i18n.languageFrom(localeMessageData.lang));
supportedLanguages.push(language);
var translator = new Jed(t);
translator.language = language;
languageTranslations[language] = translator;
}
return {
getTranslator: function (acceptLanguage) {
return languageTranslations[getLocale(acceptLanguage)]
return languageTranslations[getLocale(acceptLanguage)];
},
getLocale: getLocale
}
};
function getLocale (acceptLanguage) {
var languages = i18n.parseAcceptLanguage(acceptLanguage)
var bestLanguage = i18n.bestLanguage(languages, supportedLanguages, defaultLanguage)
return i18n.normalizeLanguage(bestLanguage)
var languages = i18n.parseAcceptLanguage(acceptLanguage);
var bestLanguage = i18n.bestLanguage(languages, supportedLanguages, defaultLanguage);
return i18n.normalizeLanguage(bestLanguage);
}
}
)
}
);
};

Просмотреть файл

@ -2,33 +2,33 @@
* 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'
'use strict';
const fs = require('fs')
const Hapi = require('hapi')
const joi = require('joi')
const Raven = require('raven')
const path = require('path')
const url = require('url')
const userAgent = require('./userAgent')
const schemeRefreshToken = require('./scheme-refresh-token')
const fs = require('fs');
const Hapi = require('hapi');
const joi = require('joi');
const Raven = require('raven');
const path = require('path');
const url = require('url');
const userAgent = require('./userAgent');
const schemeRefreshToken = require('./scheme-refresh-token');
const { HEX_STRING, IP_ADDRESS } = require('./routes/validators')
const { HEX_STRING, IP_ADDRESS } = require('./routes/validators');
function trimLocale(header) {
if (! header) {
return header
return header;
}
if (header.length < 256) {
return header.trim()
return header.trim();
}
var parts = header.split(',')
var str = parts[0]
if (str.length >= 255) { return null }
var parts = header.split(',');
var str = parts[0];
if (str.length >= 255) { return null; }
for (var i = 1; i < parts.length && str.length + parts[i].length < 255; i++) {
str += ',' + parts[i]
str += ',' + parts[i];
}
return str.trim()
return str.trim();
}
function logEndpointErrors(response, log) {
@ -39,25 +39,25 @@ function logEndpointErrors(response, log) {
var endpointLog = {
message: response.message,
reason: response.reason
}
};
if (response.attempt && response.attempt.method) {
// log the DB attempt to understand the action
endpointLog.method = response.attempt.method
endpointLog.method = response.attempt.method;
}
log.error('server.EndpointError', endpointLog)
log.error('server.EndpointError', endpointLog);
}
}
function configureSentry(server, config) {
const sentryDsn = config.sentryDsn
const sentryDsn = config.sentryDsn;
if (sentryDsn) {
Raven.config(sentryDsn, {})
Raven.config(sentryDsn, {});
server.events.on({ name: 'request', channels: 'error' }, function (request, event) {
const err = event && event.error || null
let exception = ''
const err = event && event.error || null;
let exception = '';
if (err && err.stack) {
try {
exception = err.stack.split('\n')[0]
exception = err.stack.split('\n')[0];
} catch (e) {
// ignore bad stack frames
}
@ -67,24 +67,24 @@ function configureSentry(server, config) {
extra: {
exception: exception
}
})
})
});
});
}
}
async function create (log, error, config, routes, db, oauthdb, translator) {
const getGeoData = require('./geodb')(log)
const metricsContext = require('./metrics/context')(log, config)
const metricsEvents = require('./metrics/events')(log, config)
const getGeoData = require('./geodb')(log);
const metricsContext = require('./metrics/context')(log, config);
const metricsEvents = require('./metrics/events')(log, config);
// Hawk needs to calculate request signatures based on public URL,
// not the local URL to which it is bound.
var publicURL = url.parse(config.publicUrl)
var publicURL = url.parse(config.publicUrl);
var defaultPorts = {
'http:': 80,
'https:': 443
}
};
var hawkOptions = {
host: publicURL.hostname,
port: publicURL.port ? publicURL.port : defaultPorts[publicURL.protocol],
@ -98,35 +98,35 @@ async function create (log, error, config, routes, db, oauthdb, translator) {
// Since we've disabled timestamp checks, there's not much point
// keeping a nonce cache. Instead we use this as an opportunity
// to report on the clock skew values seen in the wild.
var skew = (Date.now() / 1000) - (+ts)
log.trace('server.nonceFunc', { skew: skew })
}
var skew = (Date.now() / 1000) - (+ts);
log.trace('server.nonceFunc', { skew: skew });
}
};
function makeCredentialFn(dbGetFn) {
return function (id) {
log.trace('DB.getToken', { id: id })
log.trace('DB.getToken', { id: id });
if (! HEX_STRING.test(id)) {
return null
return null;
}
return dbGetFn(id)
.then(token => {
if (token.expired(Date.now())) {
const err = error.invalidToken('The authentication token has expired')
const err = error.invalidToken('The authentication token has expired');
if (token.constructor.tokenTypeID === 'sessionToken') {
return db.pruneSessionTokens(token.uid, [ token ])
.catch(() => {})
.then(() => { throw err })
.then(() => { throw err; });
}
return null
return null;
}
return token
})
return token;
});
}
};
}
var serverOptions = {
@ -169,61 +169,61 @@ async function create (log, error, config, routes, db, oauthdb, translator) {
sampleInterval: 1000,
maxEventLoopDelay: config.maxEventLoopDelay
},
}
};
if (config.useHttps) {
serverOptions.tls = {
key: fs.readFileSync(config.keyPath),
cert: fs.readFileSync(config.certPath)
}
};
}
var server = new Hapi.Server(serverOptions)
var server = new Hapi.Server(serverOptions);
server.ext('onRequest', (request, h) => {
log.begin('server.onRequest', request)
return h.continue
})
log.begin('server.onRequest', request);
return h.continue;
});
server.ext('onPreAuth', (request, h) => {
defineLazyGetter(request.app, 'remoteAddressChain', () => {
const xff = (request.headers['x-forwarded-for'] || '').split(/\s*,\s*/)
const xff = (request.headers['x-forwarded-for'] || '').split(/\s*,\s*/);
xff.push(request.info.remoteAddress)
xff.push(request.info.remoteAddress);
return xff.map(address => address.trim())
.filter(address => ! joi.validate(address, IP_ADDRESS.required()).error)
})
.filter(address => ! joi.validate(address, IP_ADDRESS.required()).error);
});
defineLazyGetter(request.app, 'clientAddress', () => {
const remoteAddressChain = request.app.remoteAddressChain
let clientAddressIndex = remoteAddressChain.length - (config.clientAddressDepth || 1)
const remoteAddressChain = request.app.remoteAddressChain;
let clientAddressIndex = remoteAddressChain.length - (config.clientAddressDepth || 1);
if (clientAddressIndex < 0) {
clientAddressIndex = 0
clientAddressIndex = 0;
}
return remoteAddressChain[clientAddressIndex]
})
return remoteAddressChain[clientAddressIndex];
});
defineLazyGetter(request.app, 'acceptLanguage', () => trimLocale(request.headers['accept-language']))
defineLazyGetter(request.app, 'locale', () => translator.getLocale(request.app.acceptLanguage))
defineLazyGetter(request.app, 'acceptLanguage', () => trimLocale(request.headers['accept-language']));
defineLazyGetter(request.app, 'locale', () => translator.getLocale(request.app.acceptLanguage));
defineLazyGetter(request.app, 'ua', () => userAgent(request.headers['user-agent']))
defineLazyGetter(request.app, 'geo', () => getGeoData(request.app.clientAddress))
defineLazyGetter(request.app, 'metricsContext', () => metricsContext.get(request))
defineLazyGetter(request.app, 'ua', () => userAgent(request.headers['user-agent']));
defineLazyGetter(request.app, 'geo', () => getGeoData(request.app.clientAddress));
defineLazyGetter(request.app, 'metricsContext', () => metricsContext.get(request));
defineLazyGetter(request.app, 'devices', () => {
let uid
let uid;
if (request.auth && request.auth.credentials) {
uid = request.auth.credentials.uid
uid = request.auth.credentials.uid;
} else if (request.payload && request.payload.uid) {
uid = request.payload.uid
uid = request.payload.uid;
}
return db.devices(uid)
})
return db.devices(uid);
});
if (request.headers.authorization) {
// Log some helpful details for debugging authentication problems.
@ -232,55 +232,55 @@ async function create (log, error, config, routes, db, oauthdb, translator) {
path: request.path,
auth: request.headers.authorization,
type: request.headers['content-type'] || ''
})
});
}
return h.continue
})
return h.continue;
});
server.ext('onPreHandler', (request, h) => {
const features = request.payload && request.payload.features
request.app.features = new Set(Array.isArray(features) ? features : [])
const features = request.payload && request.payload.features;
request.app.features = new Set(Array.isArray(features) ? features : []);
return h.continue
})
return h.continue;
});
server.ext('onPreResponse', (request) => {
let response = request.response
let response = request.response;
if (response.isBoom) {
logEndpointErrors(response, log)
response = error.translate(request, response)
logEndpointErrors(response, log);
response = error.translate(request, response);
if (config.env !== 'prod') {
response.backtrace(request.app.traced)
response.backtrace(request.app.traced);
}
}
response.header('Timestamp', '' + Math.floor(Date.now() / 1000))
log.summary(request, response)
return response
})
response.header('Timestamp', '' + Math.floor(Date.now() / 1000));
log.summary(request, response);
return response;
});
// configure Sentry
configureSentry(server, config)
configureSentry(server, config);
server.decorate('request', 'stashMetricsContext', metricsContext.stash)
server.decorate('request', 'gatherMetricsContext', metricsContext.gather)
server.decorate('request', 'propagateMetricsContext', metricsContext.propagate)
server.decorate('request', 'clearMetricsContext', metricsContext.clear)
server.decorate('request', 'validateMetricsContext', metricsContext.validate)
server.decorate('request', 'setMetricsFlowCompleteSignal', metricsContext.setFlowCompleteSignal)
server.decorate('request', 'stashMetricsContext', metricsContext.stash);
server.decorate('request', 'gatherMetricsContext', metricsContext.gather);
server.decorate('request', 'propagateMetricsContext', metricsContext.propagate);
server.decorate('request', 'clearMetricsContext', metricsContext.clear);
server.decorate('request', 'validateMetricsContext', metricsContext.validate);
server.decorate('request', 'setMetricsFlowCompleteSignal', metricsContext.setFlowCompleteSignal);
server.decorate('request', 'emitMetricsEvent', metricsEvents.emit)
server.decorate('request', 'emitRouteFlowEvent', metricsEvents.emitRouteFlowEvent)
server.decorate('request', 'emitMetricsEvent', metricsEvents.emit);
server.decorate('request', 'emitRouteFlowEvent', metricsEvents.emitRouteFlowEvent);
server.stat = function() {
return {
stat: 'mem',
rss: server.load.rss,
heapUsed: server.load.heapUsed
}
}
};
};
await server.register(require('hapi-auth-hawk'))
await server.register(require('hapi-auth-hawk'));
server.auth.strategy(
'sessionToken',
@ -289,7 +289,7 @@ async function create (log, error, config, routes, db, oauthdb, translator) {
getCredentialsFunc: makeCredentialFn(db.sessionToken.bind(db)),
hawk: hawkOptions
}
)
);
server.auth.strategy(
'keyFetchToken',
'hawk',
@ -297,7 +297,7 @@ async function create (log, error, config, routes, db, oauthdb, translator) {
getCredentialsFunc: makeCredentialFn(db.keyFetchToken.bind(db)),
hawk: hawkOptions
}
)
);
server.auth.strategy(
// This strategy fetches the keyFetchToken with its
// verification state. It doesn't check that state.
@ -307,7 +307,7 @@ async function create (log, error, config, routes, db, oauthdb, translator) {
getCredentialsFunc: makeCredentialFn(db.keyFetchTokenWithVerificationStatus.bind(db)),
hawk: hawkOptions
}
)
);
server.auth.strategy(
'accountResetToken',
'hawk',
@ -315,7 +315,7 @@ async function create (log, error, config, routes, db, oauthdb, translator) {
getCredentialsFunc: makeCredentialFn(db.accountResetToken.bind(db)),
hawk: hawkOptions
}
)
);
server.auth.strategy(
'passwordForgotToken',
'hawk',
@ -323,7 +323,7 @@ async function create (log, error, config, routes, db, oauthdb, translator) {
getCredentialsFunc: makeCredentialFn(db.passwordForgotToken.bind(db)),
hawk: hawkOptions
}
)
);
server.auth.strategy(
'passwordChangeToken',
'hawk',
@ -331,33 +331,33 @@ async function create (log, error, config, routes, db, oauthdb, translator) {
getCredentialsFunc: makeCredentialFn(db.passwordChangeToken.bind(db)),
hawk: hawkOptions
}
)
await server.register(require('hapi-fxa-oauth'))
);
await server.register(require('hapi-fxa-oauth'));
server.auth.strategy('oauthToken', 'fxa-oauth', config.oauth)
server.auth.strategy('oauthToken', 'fxa-oauth', config.oauth);
server.auth.scheme('fxa-oauth-refreshToken', schemeRefreshToken(db, oauthdb))
server.auth.scheme('fxa-oauth-refreshToken', schemeRefreshToken(db, oauthdb));
server.auth.strategy('refreshToken', 'fxa-oauth-refreshToken')
server.auth.strategy('refreshToken', 'fxa-oauth-refreshToken');
// routes should be registered after all auth strategies have initialized:
// ref: http://hapijs.com/tutorials/auth
server.route(routes)
return server
server.route(routes);
return server;
}
function defineLazyGetter (object, key, getter) {
let value
let value;
Object.defineProperty(object, key, {
get () {
if (! value) {
value = getter()
value = getter();
}
return value
return value;
}
})
});
}
module.exports = {
@ -366,4 +366,4 @@ module.exports = {
_configureSentry: configureSentry,
_logEndpointErrors: logEndpointErrors,
_trimLocale: trimLocale
}
};

Просмотреть файл

@ -2,17 +2,17 @@
* 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'
'use strict';
var jwtool = require('fxa-jwtool')
var jwtool = require('fxa-jwtool');
module.exports = function (secretKeyFile, domain) {
var key = jwtool.JWK.fromFile(secretKeyFile, {iss: domain })
var key = jwtool.JWK.fromFile(secretKeyFile, {iss: domain });
return {
sign: function (data) {
var now = Date.now()
var now = Date.now();
return key.sign(
{
'public-key': data.publicKey,
@ -33,9 +33,9 @@ module.exports = function (secretKeyFile, domain) {
)
.then(
function (cert) {
return { cert: cert }
return { cert: cert };
}
)
);
}
}
}
};
};

Просмотреть файл

@ -2,29 +2,29 @@
* 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'
'use strict';
var AWS = require('aws-sdk')
var inherits = require('util').inherits
var EventEmitter = require('events').EventEmitter
var AWS = require('aws-sdk');
var inherits = require('util').inherits;
var EventEmitter = require('events').EventEmitter;
module.exports = function (log) {
function SQSReceiver(region, urls) {
this.sqs = new AWS.SQS({ region : region })
this.queueUrls = urls || []
EventEmitter.call(this)
this.sqs = new AWS.SQS({ region : region });
this.queueUrls = urls || [];
EventEmitter.call(this);
}
inherits(SQSReceiver, EventEmitter)
inherits(SQSReceiver, EventEmitter);
function checkDeleteError(err) {
if (err) {
log.error('deleteMessage', { err: err })
log.error('deleteMessage', { err: err });
}
}
SQSReceiver.prototype.fetch = function (url) {
var errTimer = null
var errTimer = null;
this.sqs.receiveMessage(
{
QueueUrl: url,
@ -34,13 +34,13 @@ module.exports = function (log) {
},
function (err, data) {
if (err) {
log.error('fetch', { url: url, err: err })
log.error('fetch', { url: url, err: err });
if (! errTimer) {
// unacceptable! this aws lib will call the callback
// more than once with different errors. ಠ_ಠ
errTimer = setTimeout(this.fetch.bind(this, url), 2000)
errTimer = setTimeout(this.fetch.bind(this, url), 2000);
}
return
return;
}
function deleteMessage(message) {
this.sqs.deleteMessage(
@ -49,33 +49,33 @@ module.exports = function (log) {
ReceiptHandle: message.ReceiptHandle
},
checkDeleteError
)
);
}
data.Messages = data.Messages || []
data.Messages = data.Messages || [];
for (var i = 0; i < data.Messages.length; i++) {
var msg = data.Messages[i]
var deleteFromQueue = deleteMessage.bind(this, msg)
var msg = data.Messages[i];
var deleteFromQueue = deleteMessage.bind(this, msg);
try {
var body = JSON.parse(msg.Body)
var message = JSON.parse(body.Message)
message.del = deleteFromQueue
this.emit('data', message)
var body = JSON.parse(msg.Body);
var message = JSON.parse(body.Message);
message.del = deleteFromQueue;
this.emit('data', message);
}
catch (e) {
log.error('fetch', { url: url, err: e })
deleteFromQueue()
log.error('fetch', { url: url, err: e });
deleteFromQueue();
}
}
this.fetch(url)
this.fetch(url);
}.bind(this)
)
}
);
};
SQSReceiver.prototype.start = function () {
for (var i = 0; i < this.queueUrls.length; i++) {
this.fetch(this.queueUrls[i])
}
this.fetch(this.queueUrls[i]);
}
};
return SQSReceiver
}
return SQSReceiver;
};

Просмотреть файл

@ -2,24 +2,24 @@
* 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'
'use strict';
module.exports = {
startOfMinute (date) {
const year = date.getUTCFullYear()
const month = date.getUTCMonth() + 1
const day = date.getUTCDate()
const hour = date.getUTCHours()
const minute = date.getUTCMinutes()
return `${year}-${pad(month)}-${pad(day)}T${pad(hour)}:${pad(minute)}:00Z`
const year = date.getUTCFullYear();
const month = date.getUTCMonth() + 1;
const day = date.getUTCDate();
const hour = date.getUTCHours();
const minute = date.getUTCMinutes();
return `${year}-${pad(month)}-${pad(day)}T${pad(hour)}:${pad(minute)}:00Z`;
}
}
};
function pad (number) {
if (number < 10) {
return `0${number}`
return `0${number}`;
}
return number
return number;
}

Просмотреть файл

@ -2,30 +2,30 @@
* 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'
'use strict';
const inherits = require('util').inherits
const inherits = require('util').inherits;
module.exports = function (log, Token, lifetime) {
function AccountResetToken(keys, details) {
details.lifetime = lifetime
Token.call(this, keys, details)
details.lifetime = lifetime;
Token.call(this, keys, details);
}
inherits(AccountResetToken, Token)
inherits(AccountResetToken, Token);
AccountResetToken.tokenTypeID = 'accountResetToken'
AccountResetToken.tokenTypeID = 'accountResetToken';
AccountResetToken.create = function (details) {
log.trace('AccountResetToken.create', { uid: details && details.uid })
return Token.createNewToken(AccountResetToken, details || {})
}
log.trace('AccountResetToken.create', { uid: details && details.uid });
return Token.createNewToken(AccountResetToken, details || {});
};
AccountResetToken.fromHex = function (string, details) {
log.trace('AccountResetToken.fromHex')
details = details || {}
return Token.createTokenFromHexData(AccountResetToken, string, details)
}
log.trace('AccountResetToken.fromHex');
details = details || {};
return Token.createTokenFromHexData(AccountResetToken, string, details);
};
return AccountResetToken
}
return AccountResetToken;
};

Просмотреть файл

@ -2,7 +2,7 @@
* 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'
'use strict';
/* Utility functions for working with encrypted data bundles.
@ -25,52 +25,52 @@
*
*/
const butil = require('../crypto/butil')
const crypto = require('crypto')
const error = require('../error')
const hkdf = require('../crypto/hkdf')
const butil = require('../crypto/butil');
const crypto = require('crypto');
const error = require('../error');
const hkdf = require('../crypto/hkdf');
const HASH_ALGORITHM = 'sha256'
const HASH_ALGORITHM = 'sha256';
module.exports = {
// Encrypt the given buffer into a hex ciphertext string.
//
bundle(key, keyInfo, payload) {
key = Buffer.from(key, 'hex')
payload = Buffer.from(payload, 'hex')
key = Buffer.from(key, 'hex');
payload = Buffer.from(payload, 'hex');
return deriveBundleKeys(key, keyInfo, payload.length)
.then(
function (keys) {
var ciphertext = butil.xorBuffers(payload, keys.xorKey)
var hmac = crypto.createHmac(HASH_ALGORITHM, keys.hmacKey)
hmac.update(ciphertext)
var mac = hmac.digest()
return Buffer.concat([ciphertext, mac]).toString('hex')
var ciphertext = butil.xorBuffers(payload, keys.xorKey);
var hmac = crypto.createHmac(HASH_ALGORITHM, keys.hmacKey);
hmac.update(ciphertext);
var mac = hmac.digest();
return Buffer.concat([ciphertext, mac]).toString('hex');
}
)
);
},
// Decrypt the given hex string into a buffer of plaintext data.
//
unbundle(key, keyInfo, payload) {
key = Buffer.from(key, 'hex')
payload = Buffer.from(payload, 'hex')
var ciphertext = payload.slice(0, -32)
var expectedHmac = payload.slice(-32)
key = Buffer.from(key, 'hex');
payload = Buffer.from(payload, 'hex');
var ciphertext = payload.slice(0, -32);
var expectedHmac = payload.slice(-32);
return deriveBundleKeys(key, keyInfo, ciphertext.length)
.then(
function (keys) {
var hmac = crypto.createHmac(HASH_ALGORITHM, keys.hmacKey)
hmac.update(ciphertext)
var mac = hmac.digest()
var hmac = crypto.createHmac(HASH_ALGORITHM, keys.hmacKey);
hmac.update(ciphertext);
var mac = hmac.digest();
if (! butil.buffersAreEqual(mac, expectedHmac)) {
throw error.invalidSignature()
throw error.invalidSignature();
}
return butil.xorBuffers(ciphertext, keys.xorKey).toString('hex')
return butil.xorBuffers(ciphertext, keys.xorKey).toString('hex');
}
)
);
}
}
};
// Derive the HMAC and XOR keys required to encrypt a given size of payload.
@ -82,8 +82,8 @@ function deriveBundleKeys(key, keyInfo, payloadSize) {
return {
hmacKey: keyMaterial.slice(0, 32),
xorKey: keyMaterial.slice(32)
};
}
}
)
);
}

Просмотреть файл

@ -2,46 +2,46 @@
* 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'
'use strict';
const error = require('../error')
const error = require('../error');
module.exports = (log, config) => {
config = config || {}
config = config || {};
const lifetimes = config.tokenLifetimes = config.tokenLifetimes || {
accountResetToken: 1000 * 60 * 15,
passwordChangeToken: 1000 * 60 * 15,
passwordForgotToken: 1000 * 60 * 15
}
const Bundle = require('./bundle')
const Token = require('./token')(log, config)
};
const Bundle = require('./bundle');
const Token = require('./token')(log, config);
const KeyFetchToken = require('./key_fetch_token')(log, Token)
const KeyFetchToken = require('./key_fetch_token')(log, Token);
const AccountResetToken = require('./account_reset_token')(
log,
Token,
lifetimes.accountResetToken
)
const SessionToken = require('./session_token')(log, Token, config)
);
const SessionToken = require('./session_token')(log, Token, config);
const PasswordForgotToken = require('./password_forgot_token')(
log,
Token,
lifetimes.passwordForgotToken
)
);
const PasswordChangeToken = require('./password_change_token')(
log,
Token,
lifetimes.passwordChangeToken
)
);
Token.error = error
Token.Bundle = Bundle
Token.AccountResetToken = AccountResetToken
Token.KeyFetchToken = KeyFetchToken
Token.SessionToken = SessionToken
Token.PasswordForgotToken = PasswordForgotToken
Token.PasswordChangeToken = PasswordChangeToken
Token.error = error;
Token.Bundle = Bundle;
Token.AccountResetToken = AccountResetToken;
Token.KeyFetchToken = KeyFetchToken;
Token.SessionToken = SessionToken;
Token.PasswordForgotToken = PasswordForgotToken;
Token.PasswordChangeToken = PasswordChangeToken;
return Token
}
return Token;
};

Просмотреть файл

@ -2,71 +2,71 @@
* 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'
'use strict';
const inherits = require('util').inherits
const P = require('../promise')
const inherits = require('util').inherits;
const P = require('../promise');
module.exports = function (log, Token) {
function KeyFetchToken(keys, details) {
Token.call(this, keys, details)
this.keyBundle = details.keyBundle
this.emailVerified = !! details.emailVerified
Token.call(this, keys, details);
this.keyBundle = details.keyBundle;
this.emailVerified = !! details.emailVerified;
// Tokens are considered verified if no tokenVerificationId exists
this.tokenVerificationId = details.tokenVerificationId || null
this.tokenVerified = this.tokenVerificationId ? false : true
this.tokenVerificationId = details.tokenVerificationId || null;
this.tokenVerified = this.tokenVerificationId ? false : true;
}
inherits(KeyFetchToken, Token)
inherits(KeyFetchToken, Token);
KeyFetchToken.tokenTypeID = 'keyFetchToken'
KeyFetchToken.tokenTypeID = 'keyFetchToken';
KeyFetchToken.create = function (details) {
log.trace('KeyFetchToken.create', { uid: details && details.uid })
log.trace('KeyFetchToken.create', { uid: details && details.uid });
return Token.createNewToken(KeyFetchToken, details || {})
.then(
function (token) {
return token.bundleKeys(details.kA, details.wrapKb)
.then(
function (keyBundle) {
token.keyBundle = keyBundle
return token
token.keyBundle = keyBundle;
return token;
}
)
}
)
);
}
);
};
KeyFetchToken.fromId = function (id, details) {
log.trace('KeyFetchToken.fromId')
return P.resolve(new KeyFetchToken({ id, authKey: details.authKey }, details))
}
log.trace('KeyFetchToken.fromId');
return P.resolve(new KeyFetchToken({ id, authKey: details.authKey }, details));
};
KeyFetchToken.fromHex = function (string, details) {
log.trace('KeyFetchToken.fromHex')
return Token.createTokenFromHexData(KeyFetchToken, string, details || {})
}
log.trace('KeyFetchToken.fromHex');
return Token.createTokenFromHexData(KeyFetchToken, string, details || {});
};
KeyFetchToken.prototype.bundleKeys = function (kA, wrapKb) {
log.trace('keyFetchToken.bundleKeys', { id: this.id })
kA = Buffer.from(kA, 'hex')
wrapKb = Buffer.from(wrapKb, 'hex')
return this.bundle('account/keys', Buffer.concat([kA, wrapKb]))
}
log.trace('keyFetchToken.bundleKeys', { id: this.id });
kA = Buffer.from(kA, 'hex');
wrapKb = Buffer.from(wrapKb, 'hex');
return this.bundle('account/keys', Buffer.concat([kA, wrapKb]));
};
KeyFetchToken.prototype.unbundleKeys = function (bundle) {
log.trace('keyFetchToken.unbundleKeys', { id: this.id })
log.trace('keyFetchToken.unbundleKeys', { id: this.id });
return this.unbundle('account/keys', bundle)
.then(
function (plaintext) {
return {
kA: plaintext.slice(0, 64), // strings, not buffers
wrapKb: plaintext.slice(64, 128)
};
}
}
)
}
);
};
return KeyFetchToken
}
return KeyFetchToken;
};

Просмотреть файл

@ -2,29 +2,29 @@
* 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'
'use strict';
const inherits = require('util').inherits
const inherits = require('util').inherits;
module.exports = function (log, Token, lifetime) {
function PasswordChangeToken(keys, details) {
details.lifetime = lifetime
Token.call(this, keys, details)
details.lifetime = lifetime;
Token.call(this, keys, details);
}
inherits(PasswordChangeToken, Token)
inherits(PasswordChangeToken, Token);
PasswordChangeToken.tokenTypeID = 'passwordChangeToken'
PasswordChangeToken.tokenTypeID = 'passwordChangeToken';
PasswordChangeToken.create = function (details) {
log.trace('PasswordChangeToken.create', { uid: details && details.uid })
return Token.createNewToken(PasswordChangeToken, details || {})
}
log.trace('PasswordChangeToken.create', { uid: details && details.uid });
return Token.createNewToken(PasswordChangeToken, details || {});
};
PasswordChangeToken.fromHex = function (string, details) {
log.trace('PasswordChangeToken.fromHex')
return Token.createTokenFromHexData(PasswordChangeToken, string, details || {})
}
log.trace('PasswordChangeToken.fromHex');
return Token.createTokenFromHexData(PasswordChangeToken, string, details || {});
};
return PasswordChangeToken
}
return PasswordChangeToken;
};

Просмотреть файл

@ -2,48 +2,48 @@
* 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'
'use strict';
const inherits = require('util').inherits
const random = require('../crypto/random')
const inherits = require('util').inherits;
const random = require('../crypto/random');
module.exports = (log, Token, lifetime) => {
function PasswordForgotToken(keys, details) {
details.lifetime = lifetime
Token.call(this, keys, details)
this.email = details.email || null
this.passCode = details.passCode || null
this.tries = details.tries || null
details.lifetime = lifetime;
Token.call(this, keys, details);
this.email = details.email || null;
this.passCode = details.passCode || null;
this.tries = details.tries || null;
}
inherits(PasswordForgotToken, Token)
inherits(PasswordForgotToken, Token);
PasswordForgotToken.tokenTypeID = 'passwordForgotToken'
PasswordForgotToken.tokenTypeID = 'passwordForgotToken';
PasswordForgotToken.create = function (details) {
details = details || {}
details = details || {};
log.trace('PasswordForgotToken.create', {
uid: details.uid,
email: details.email
})
});
return random.hex(16)
.then(bytes => {
details.passCode = bytes
details.tries = 3
return Token.createNewToken(PasswordForgotToken, details)
})
}
details.passCode = bytes;
details.tries = 3;
return Token.createNewToken(PasswordForgotToken, details);
});
};
PasswordForgotToken.fromHex = function (string, details) {
log.trace('PasswordForgotToken.fromHex')
details = details || {}
return Token.createTokenFromHexData(PasswordForgotToken, string, details)
}
log.trace('PasswordForgotToken.fromHex');
details = details || {};
return Token.createTokenFromHexData(PasswordForgotToken, string, details);
};
PasswordForgotToken.prototype.failAttempt = function () {
this.tries --
return this.tries < 1
}
this.tries --;
return this.tries < 1;
};
return PasswordForgotToken
}
return PasswordForgotToken;
};

Просмотреть файл

@ -2,12 +2,12 @@
* 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'
'use strict';
const authMethods = require('../authMethods')
const authMethods = require('../authMethods');
module.exports = (log, Token, config) => {
const MAX_AGE_WITHOUT_DEVICE = config.tokenLifetimes.sessionTokenWithoutDevice
const MAX_AGE_WITHOUT_DEVICE = config.tokenLifetimes.sessionTokenWithoutDevice;
// Convert verificationMethod to a more readable format. Maps to
// https://github.com/mozilla/fxa-auth-db-mysql/blob/master/lib/db/util.js#L34
@ -18,105 +18,105 @@ module.exports = (log, Token, config) => {
[2, 'totp-2fa'],
[3, 'recovery-code']
]
)
);
class SessionToken extends Token {
constructor(keys, details) {
super(keys, details)
super(keys, details);
if (MAX_AGE_WITHOUT_DEVICE && ! details.deviceId) {
this.lifetime = MAX_AGE_WITHOUT_DEVICE
this.lifetime = MAX_AGE_WITHOUT_DEVICE;
}
this.setUserAgentInfo(details)
this.setDeviceInfo(details)
this.email = details.email || null
this.emailCode = details.emailCode || null
this.emailVerified = !! details.emailVerified
this.verifierSetAt = details.verifierSetAt
this.profileChangedAt = details.profileChangedAt
this.authAt = details.authAt || 0
this.locale = details.locale || null
this.mustVerify = !! details.mustVerify || false
this.setUserAgentInfo(details);
this.setDeviceInfo(details);
this.email = details.email || null;
this.emailCode = details.emailCode || null;
this.emailVerified = !! details.emailVerified;
this.verifierSetAt = details.verifierSetAt;
this.profileChangedAt = details.profileChangedAt;
this.authAt = details.authAt || 0;
this.locale = details.locale || null;
this.mustVerify = !! details.mustVerify || false;
// Tokens are considered verified if no tokenVerificationId exists
this.tokenVerificationId = details.tokenVerificationId || null
this.tokenVerified = this.tokenVerificationId ? false : true
this.tokenVerificationId = details.tokenVerificationId || null;
this.tokenVerified = this.tokenVerificationId ? false : true;
this.tokenVerificationCode = details.tokenVerificationCode || null
this.tokenVerificationCodeExpiresAt = details.tokenVerificationCodeExpiresAt || null
this.tokenVerificationCode = details.tokenVerificationCode || null;
this.tokenVerificationCodeExpiresAt = details.tokenVerificationCodeExpiresAt || null;
this.verificationMethod = details.verificationMethod || null
this.verificationMethodValue = VERIFICATION_METHODS.get(this.verificationMethod)
this.verifiedAt = details.verifiedAt || null
this.verificationMethod = details.verificationMethod || null;
this.verificationMethodValue = VERIFICATION_METHODS.get(this.verificationMethod);
this.verifiedAt = details.verifiedAt || null;
}
static create(details) {
details = details || {}
log.trace('SessionToken.create', { uid: details.uid })
return Token.createNewToken(SessionToken, details)
details = details || {};
log.trace('SessionToken.create', { uid: details.uid });
return Token.createNewToken(SessionToken, details);
}
static fromHex(string, details) {
log.trace('SessionToken.fromHex')
return Token.createTokenFromHexData(SessionToken, string, details || {})
log.trace('SessionToken.fromHex');
return Token.createTokenFromHexData(SessionToken, string, details || {});
}
lastAuthAt() {
return Math.floor((this.authAt || this.createdAt) / 1000)
return Math.floor((this.authAt || this.createdAt) / 1000);
}
get state() {
if (this.tokenVerified) {
return 'verified'
return 'verified';
} else {
return 'unverified'
return 'unverified';
}
}
get authenticationMethods() {
const amrValues = new Set()
const amrValues = new Set();
// All sessionTokens require password authentication.
amrValues.add('pwd')
amrValues.add('pwd');
// Verified sessionTokens imply some additional authentication method(s).
if (this.verificationMethodValue) {
amrValues.add(authMethods.verificationMethodToAMR(this.verificationMethodValue))
amrValues.add(authMethods.verificationMethodToAMR(this.verificationMethodValue));
} else if (this.tokenVerified) {
amrValues.add('email')
amrValues.add('email');
}
return amrValues
return amrValues;
}
get authenticatorAssuranceLevel() {
return authMethods.maximumAssuranceLevel(this.authenticationMethods)
return authMethods.maximumAssuranceLevel(this.authenticationMethods);
}
setUserAgentInfo(data) {
this.uaBrowser = data.uaBrowser
this.uaBrowserVersion = data.uaBrowserVersion
this.uaOS = data.uaOS
this.uaOSVersion = data.uaOSVersion
this.uaDeviceType = data.uaDeviceType
this.uaFormFactor = data.uaFormFactor
this.uaBrowser = data.uaBrowser;
this.uaBrowserVersion = data.uaBrowserVersion;
this.uaOS = data.uaOS;
this.uaOSVersion = data.uaOSVersion;
this.uaDeviceType = data.uaDeviceType;
this.uaFormFactor = data.uaFormFactor;
if (data.lastAccessTime) {
this.lastAccessTime = data.lastAccessTime
this.lastAccessTime = data.lastAccessTime;
}
}
setDeviceInfo(data) {
this.deviceId = data.deviceId
this.deviceName = data.deviceName
this.deviceType = data.deviceType
this.deviceCreatedAt = data.deviceCreatedAt
this.callbackURL = data.callbackURL
this.callbackPublicKey = data.callbackPublicKey
this.callbackAuthKey = data.callbackAuthKey
this.callbackIsExpired = data.callbackIsExpired
this.deviceId = data.deviceId;
this.deviceName = data.deviceName;
this.deviceType = data.deviceType;
this.deviceCreatedAt = data.deviceCreatedAt;
this.callbackURL = data.callbackURL;
this.callbackPublicKey = data.callbackPublicKey;
this.callbackAuthKey = data.callbackAuthKey;
this.callbackIsExpired = data.callbackIsExpired;
}
}
SessionToken.tokenTypeID = 'sessionToken'
return SessionToken
}
SessionToken.tokenTypeID = 'sessionToken';
return SessionToken;
};

Просмотреть файл

@ -2,7 +2,7 @@
* 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'
'use strict';
/* Base class for handling various types of token.
*
@ -25,11 +25,11 @@
*
*/
const Bundle = require('./bundle')
const hkdf = require('../crypto/hkdf')
const random = require('../crypto/random')
const Bundle = require('./bundle');
const hkdf = require('../crypto/hkdf');
const random = require('../crypto/random');
const KEYS = ['data', 'id', 'authKey', 'bundleKey']
const KEYS = ['data', 'id', 'authKey', 'bundleKey'];
module.exports = (log, config) => {
@ -40,11 +40,11 @@ module.exports = (log, config) => {
//
function Token(keys, details) {
KEYS.forEach(name => {
this[name] = keys[name] && keys[name].toString('hex')
})
this.uid = details.uid || null
this.lifetime = details.lifetime || Infinity
this.createdAt = details.createdAt || 0
this[name] = keys[name] && keys[name].toString('hex');
});
this.uid = details.uid || null;
this.lifetime = details.lifetime || Infinity;
this.createdAt = details.createdAt || 0;
}
// Create a new token of the given type.
@ -52,22 +52,22 @@ module.exports = (log, config) => {
//
Token.createNewToken = function(TokenType, details) {
// Avoid modifying the argument.
details = Object.assign({}, details)
details.createdAt = Date.now()
details = Object.assign({}, details);
details.createdAt = Date.now();
return random(32)
.then(bytes => Token.deriveTokenKeys(TokenType, bytes))
.then(keys => new TokenType(keys, details))
}
.then(keys => new TokenType(keys, details));
};
// Re-create an existing token of the given type.
// This uses known seed data to derive the keys.
//
Token.createTokenFromHexData = function(TokenType, hexData, details) {
var data = Buffer.from(hexData, 'hex')
var data = Buffer.from(hexData, 'hex');
return Token.deriveTokenKeys(TokenType, data)
.then(keys => new TokenType(keys, details || {}))
}
.then(keys => new TokenType(keys, details || {}));
};
// Derive id, authKey and bundleKey from token seed data.
@ -81,49 +81,49 @@ module.exports = (log, config) => {
id: keyMaterial.slice(0, 32),
authKey: keyMaterial.slice(32, 64),
bundleKey: keyMaterial.slice(64, 96)
};
}
}
)
}
);
};
// Convenience method to bundle a payload using token bundleKey.
//
Token.prototype.bundle = function(keyInfo, payload) {
log.trace('Token.bundle')
return Bundle.bundle(this.bundleKey, keyInfo, payload)
}
log.trace('Token.bundle');
return Bundle.bundle(this.bundleKey, keyInfo, payload);
};
// Convenience method to unbundle a payload using token bundleKey.
//
Token.prototype.unbundle = function(keyInfo, payload) {
log.trace('Token.unbundle')
return Bundle.unbundle(this.bundleKey, keyInfo, payload)
}
log.trace('Token.unbundle');
return Bundle.unbundle(this.bundleKey, keyInfo, payload);
};
Token.prototype.ttl = function (asOf) {
asOf = asOf || Date.now()
var ttl = (this.lifetime - (asOf - this.createdAt)) / 1000
return Math.max(Math.ceil(ttl), 0)
}
asOf = asOf || Date.now();
var ttl = (this.lifetime - (asOf - this.createdAt)) / 1000;
return Math.max(Math.ceil(ttl), 0);
};
Token.prototype.expired = function (asOf) {
return this.ttl(asOf) === 0
}
return this.ttl(asOf) === 0;
};
// Properties defined for HAWK
Object.defineProperties(
Token.prototype,
{
key: {
get: function () { return Buffer.from(this.authKey, 'hex') }
get: function () { return Buffer.from(this.authKey, 'hex'); }
},
algorithm: {
get: function () { return 'sha256' }
get: function () { return 'sha256'; }
}
}
)
);
return Token
}
return Token;
};

Просмотреть файл

@ -2,10 +2,10 @@
* 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'
'use strict';
const ua = require('node-uap')
const safe = require('./safe')
const ua = require('node-uap');
const safe = require('./safe');
const MOBILE_OS_FAMILIES = new Set([
'Android',
@ -25,7 +25,7 @@ const MOBILE_OS_FAMILIES = new Set([
'Windows CE',
'Windows Mobile',
'Windows Phone'
])
]);
// $1 = 'Firefox' indicates Firefox Sync, 'Mobile' indicates Sync mobile library
// $2 = OS
@ -33,10 +33,10 @@ const MOBILE_OS_FAMILIES = new Set([
// $4 = form factor
// $5 = OS version
// $6 = application name
const SYNC_USER_AGENT = /^(Firefox|Mobile)-(\w+)-(?:FxA(?:ccounts)?|Sync)\/([^\sb]*)(?:b\S+)? ?(?:\(([\w\s]+); [\w\s]+ ([^\s()]+)\))?(?: \((.+)\))?$/
const SYNC_USER_AGENT = /^(Firefox|Mobile)-(\w+)-(?:FxA(?:ccounts)?|Sync)\/([^\sb]*)(?:b\S+)? ?(?:\(([\w\s]+); [\w\s]+ ([^\s()]+)\))?(?: \((.+)\))?$/;
module.exports = function (userAgentString) {
const matches = SYNC_USER_AGENT.exec(userAgentString)
const matches = SYNC_USER_AGENT.exec(userAgentString);
if (matches && matches.length > 2) {
// Always parse known Sync user-agents ourselves,
// because node-uap makes a pig's ear of it.
@ -47,10 +47,10 @@ module.exports = function (userAgentString) {
osVersion: safe.version(matches[5]),
deviceType: marshallDeviceType(matches[4]),
formFactor: safe.name(matches[4])
}
};
}
const userAgentData = ua.parse(userAgentString)
const userAgentData = ua.parse(userAgentString);
return {
browser: safe.name(getFamily(userAgentData.ua)),
browserVersion: safe.version(userAgentData.ua.toVersionString()),
@ -58,62 +58,62 @@ module.exports = function (userAgentString) {
osVersion: safe.version(userAgentData.os.toVersionString()),
deviceType: getDeviceType(userAgentData) || null,
formFactor: safe.name(getFormFactor(userAgentData))
}
}
};
};
function getFamily (data) {
if (data.family && data.family !== 'Other') {
return data.family
return data.family;
}
}
function getDeviceType (data) {
if (getFamily(data.device) || isMobileOS(data.os)) {
if (isTablet(data)) {
return 'tablet'
return 'tablet';
} else {
return 'mobile'
return 'mobile';
}
}
}
function isMobileOS (os) {
return MOBILE_OS_FAMILIES.has(os.family)
return MOBILE_OS_FAMILIES.has(os.family);
}
function isTablet(data) {
return isIpad(data) || isAndroidTablet(data) || isKindle(data) || isGenericTablet(data)
return isIpad(data) || isAndroidTablet(data) || isKindle(data) || isGenericTablet(data);
}
function isIpad (data) {
return /iPad/.test(data.device.family)
return /iPad/.test(data.device.family);
}
function isAndroidTablet (data) {
return data.os.family === 'Android' &&
data.userAgent.indexOf('Mobile') === -1 &&
data.userAgent.indexOf('AndroidSync') === -1
data.userAgent.indexOf('AndroidSync') === -1;
}
function isKindle (data) {
return /Kindle/.test(data.device.family)
return /Kindle/.test(data.device.family);
}
function isGenericTablet (data) {
return data.device.brand === 'Generic' && data.device.model === 'Tablet'
return data.device.brand === 'Generic' && data.device.model === 'Tablet';
}
function getFormFactor (data) {
if (data.device.brand !== 'Generic') {
return getFamily(data.device)
return getFamily(data.device);
}
}
function marshallDeviceType (formFactor) {
if (/iPad/.test(formFactor) || /tablet/i.test(formFactor)) {
return 'tablet'
return 'tablet';
}
return 'mobile'
return 'mobile';
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше