refactor(fxa-auth-server): Added semicolons(semi rule)
This commit is contained in:
Родитель
8a6490e22c
Коммит
1b910f0af9
|
@ -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:
|
||||
|
|
14
Gruntfile.js
14
Gruntfile.js
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
44
lib/cache.js
44
lib/cache.js
|
@ -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
1088
lib/db.js
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
110
lib/devices.js
110
lib/devices.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
|
||||
}
|
||||
};
|
||||
|
|
374
lib/error.js
374
lib/error.js
|
@ -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;
|
||||
|
||||
|
|
42
lib/geodb.js
42
lib/geodb.js
|
@ -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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
148
lib/log.js
148
lib/log.js
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
92
lib/pool.js
92
lib/pool.js
|
@ -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');
|
||||
|
|
194
lib/push.js
194
lib/push.js
|
@ -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;
|
||||
|
|
24
lib/redis.js
24
lib/redis.js
|
@ -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);
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
224
lib/server.js
224
lib/server.js
|
@ -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 };
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
60
lib/sqs.js
60
lib/sqs.js
|
@ -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;
|
||||
};
|
||||
|
|
20
lib/time.js
20
lib/time.js
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче