├── .dockerignore ├── .nsprc ├── test ├── load │ ├── config │ │ ├── test.ini │ │ ├── bench.ini │ │ └── megabench.ini │ ├── README.txt │ └── Makefile ├── config │ └── mock_oauth.json ├── e2e │ ├── README.txt │ └── push_tests.js ├── .env.dev ├── signer-stub.js ├── key_server_stub.js ├── routes_helpers.js ├── oauth_helper.js ├── remote │ ├── token_expiry_tests.js │ ├── sign_key_tests.js │ ├── concurrent_tests.js │ ├── account_destroy_tests.js │ ├── account_unlock_tests.js │ ├── email_validity_tests.js │ ├── base_path_tests.js │ ├── session_destroy_tests.js │ ├── recovery_email_verify_tests.js │ ├── account_locale_tests.js │ ├── verification_reminder_db_tests.js │ ├── account_preverified_token_tests.js │ ├── flow_tests.js │ ├── account_signin_verification_enable_tests.js │ └── verifier_upgrade_tests.js ├── local │ ├── geodb.js │ ├── account_reset_token_tests.js │ ├── pbkdf2_tests.js │ ├── scrypt_tests.js │ ├── hkdf_tests.js │ ├── forgot_password_token_tests.js │ ├── password_tests.js │ ├── token_tests.js │ ├── mailer_locales_tests.js │ ├── butil_tests.js │ ├── error_tests.js │ └── verification_reminder_tests.js ├── ptaptest.js ├── push_helper.js ├── mailbox.js ├── bench │ ├── bot.js │ └── index.js ├── mail_helper.js └── test_server.js ├── .eslintrc ├── scripts ├── .eslintrc ├── start-server.sh ├── test-local.sh ├── start-local.sh ├── reset-send-all-batches.sh ├── start-local-mysql.sh ├── tap-coverage.js ├── test-remote-quick.js ├── start-travis-auth-db-mysql.sh ├── rpm-version.js ├── reset-send-batch.sh ├── bulk-mailer │ └── nodemailer-mock.js ├── gen_keys.js ├── check-i18n.js ├── e2e-email │ └── localeQuirks.js ├── must-reset.js └── reset-send-create-batches.js ├── .gitignore ├── lib ├── promise.js ├── routes │ ├── static │ │ ├── provision.html │ │ └── sign_in.html │ ├── session.js │ ├── utils │ │ ├── password_check.js │ │ └── request_helper.js │ ├── util.js │ ├── idp.js │ ├── index.js │ └── defaults.js ├── newrelic.js ├── crypto │ ├── hkdf.js │ ├── pbkdf2.js │ ├── butil.js │ ├── scrypt.js │ └── password.js ├── tokens │ ├── password_change_token.js │ ├── account_reset_token.js │ ├── password_forgot_token.js │ ├── index.js │ ├── key_fetch_token.js │ └── bundle.js ├── signer.js ├── preverifier.js ├── geodb.js ├── devices.js ├── sqs.js ├── verification-reminders.js ├── pool.js ├── bounces.js ├── userAgent.js ├── mailer.js └── metrics │ └── statsd.js ├── Gruntfile.js ├── config └── supportedLanguages.js ├── docs ├── self-host.docker ├── schema.md └── pushpayloads.schema.json ├── grunttasks ├── nsp.js ├── copyright.js ├── eslint.js ├── changelog.js ├── bump.js └── version.js ├── .env.dev ├── AUTHORS ├── bin ├── email_bouncer.js └── notifier.js ├── .travis.yml ├── Vagrantfile ├── package.json ├── CONTRIBUTING.md └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | -------------------------------------------------------------------------------- /.nsprc: -------------------------------------------------------------------------------- 1 | { 2 | "exceptions": [ 3 | ] 4 | } 5 | -------------------------------------------------------------------------------- /test/load/config/test.ini: -------------------------------------------------------------------------------- 1 | [loads] 2 | hits = 1 3 | users = 1 4 | -------------------------------------------------------------------------------- /test/load/config/bench.ini: -------------------------------------------------------------------------------- 1 | [loads] 2 | users = 20 3 | duration = 300 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: fxa/server 2 | 3 | rules: 4 | handle-callback-err: 0 5 | -------------------------------------------------------------------------------- /scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/config/mock_oauth.json: -------------------------------------------------------------------------------- 1 | { 2 | "oauth": { 3 | "url": "http://localhost:9010" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /scripts/start-server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | node ./bin/key_server.js | node ./bin/notifier.js >/dev/null 3 | exit $PIPESTATUS[0] 4 | -------------------------------------------------------------------------------- /test/e2e/README.txt: -------------------------------------------------------------------------------- 1 | The tests in this directory make requests to external servers. 2 | You need to be connected to the internet to run these tests. 3 | -------------------------------------------------------------------------------- /test/.env.dev: -------------------------------------------------------------------------------- 1 | CONTENT_SERVER_URL=http://127.0.0.1:3030 2 | SMTP_HOST=127.0.0.1 3 | SMTP_PORT=9999 4 | SMTP_SECURE=false 5 | LOG_LEVEL=info 6 | CUSTOMS_SERVER_URL=none 7 | SNS_TOPIC_ARN=disabled 8 | -------------------------------------------------------------------------------- /test/load/config/megabench.ini: -------------------------------------------------------------------------------- 1 | [loads] 2 | users = 20 3 | duration = 1800 4 | include_file = ./loadtests.py 5 | python_dep = hawkauthlib 6 | agents = 5 7 | detach = true 8 | observer = irc 9 | ssh = ubuntu@loads.services.mozilla.com 10 | -------------------------------------------------------------------------------- /test/signer-stub.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | require('../bin/signer.js') 6 | -------------------------------------------------------------------------------- /test/key_server_stub.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | require('../bin/key_server.js') 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage.html 2 | /node_modules 3 | /sandbox 4 | /config/public-key.json 5 | /config/secret-key.json 6 | *.swp 7 | server.log* 8 | secret* 9 | *.gpg 10 | /test/load/bin 11 | /test/load/lib* 12 | /test/load/include 13 | *~ 14 | *.pyc 15 | /.vagrant 16 | .nyc_output 17 | -------------------------------------------------------------------------------- /scripts/test-local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | glob=$* 6 | if [ "$glob" == "" ]; then 7 | glob="test/local test/remote" 8 | fi 9 | 10 | ./scripts/gen_keys.js 11 | ./scripts/check-i18n.js 12 | ./scripts/tap-coverage.js $glob 2>/dev/null 13 | grunt eslint copyright 14 | -------------------------------------------------------------------------------- /lib/promise.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // for easy promise lib switching 6 | module.exports = require('bluebird') 7 | -------------------------------------------------------------------------------- /scripts/start-local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | node ./scripts/gen_keys.js 6 | node ./test/mail_helper.js & 7 | MH=$! 8 | node ./node_modules/fxa-auth-db-mysql/bin/mem.js & 9 | DB=$! 10 | 11 | node ./bin/key_server.js | node ./bin/notifier.js >/dev/null 12 | 13 | kill $MH 14 | kill $DB 15 | -------------------------------------------------------------------------------- /scripts/reset-send-all-batches.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | echo "batch directory must be specified" 5 | exit 1 6 | fi 7 | 8 | DIRNAME=${1} 9 | 10 | for BATCH in $(find ${DIRNAME} -name "*.json" -type f); do 11 | echo ${BATCH} $PWD 12 | ./reset-send-batch.sh ${BATCH} 13 | rc=$?; if [[ $rc != 0 ]]; then exit $rc; fi 14 | done 15 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | require('load-grunt-tasks')(grunt) 7 | 8 | grunt.loadTasks('grunttasks') 9 | 10 | grunt.registerTask('default', ['eslint', 'copyright']) 11 | } 12 | -------------------------------------------------------------------------------- /config/supportedLanguages.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // The list below should be kept in sync with: 6 | // https://raw.githubusercontent.com/mozilla/fxa-content-server/master/server/config/production-locales.json 7 | 8 | module.exports = require('fxa-shared').l10n.supportedLanguages 9 | -------------------------------------------------------------------------------- /docs/self-host.docker: -------------------------------------------------------------------------------- 1 | FROM vladikoff/fxa-slim-image:1.0.0 2 | 3 | RUN adduser --disabled-password --gecos '' fxa && adduser fxa sudo && echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers 4 | 5 | COPY . /home/fxa/fxa-auth-server 6 | WORKDIR /home/fxa/fxa-auth-server 7 | RUN chown -R fxa . 8 | USER fxa 9 | 10 | RUN npm install --production \ 11 | && npm cache clear 12 | 13 | CMD node ./bin/key_server.js | node ./bin/notifier.js 14 | 15 | # Expose ports 16 | EXPOSE 9000 17 | -------------------------------------------------------------------------------- /test/routes_helpers.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | exports.getRoute = function (routes, path) { 6 | var route = null 7 | 8 | routes.some(function (r) { 9 | if (r.path === path) { 10 | route = r 11 | return true 12 | } 13 | }) 14 | 15 | return route 16 | } 17 | -------------------------------------------------------------------------------- /grunttasks/nsp.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict' 7 | 8 | grunt.config('nsp', { 9 | output: 'summary', 10 | package: grunt.file.readJSON('package.json'), 11 | shrinkwrap: grunt.file.readJSON('npm-shrinkwrap.json') 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /scripts/start-local-mysql.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | node ./scripts/gen_keys.js 6 | node ./test/mail_helper.js & 7 | MH=$! 8 | ls ./node_modules/fxa-auth-db-mysql/node_modules/mysql-patcher || npm i ./node_modules/fxa-auth-db-mysql 9 | node ./node_modules/fxa-auth-db-mysql/bin/db_patcher.js 10 | node ./node_modules/fxa-auth-db-mysql/bin/server.js & 11 | DB=$! 12 | 13 | node ./bin/key_server.js | node ./bin/notifier.js >/dev/null 14 | 15 | kill $MH 16 | kill $DB 17 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | CONTENT_SERVER_URL=http://127.0.0.1:3030 2 | CUSTOMS_SERVER_URL=none 3 | LOCKOUT_ENABLED=true 4 | LOG_FORMAT=pretty 5 | LOG_LEVEL=info 6 | RESEND_BLACKOUT_PERIOD=0 7 | SIGNIN_CONFIRMATION_ENABLED=true 8 | SIGNIN_CONFIRMATION_RATE=1 9 | SMTP_HOST=127.0.0.1 10 | SMTP_PORT=9999 11 | SMTP_SECURE=false 12 | SNS_TOPIC_ARN=disabled 13 | STATSD_SAMPLE_RATE=1 14 | TRUSTED_JKUS=http://127.0.0.1:8080/.well-known/public-keys,http://127.0.0.1:10139/.well-known/public-keys 15 | VERIFICATION_REMINDER_RATE=1 16 | VERIFIER_VERSION=0 17 | -------------------------------------------------------------------------------- /grunttasks/copyright.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict' 7 | 8 | grunt.config('copyright', { 9 | app: { 10 | options: { 11 | pattern: 'This Source Code Form is subject to the terms of the Mozilla' 12 | }, 13 | src: [ 14 | '<%= eslint.files %>' 15 | ] 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /grunttasks/eslint.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (grunt) { 6 | 'use strict' 7 | 8 | grunt.config('eslint', { 9 | options: { 10 | eslintrc: '.eslintrc' 11 | }, 12 | files: [ 13 | '{,bin/,config/,grunttasks/,lib/**/,scripts/**/,test/**/}*.js' 14 | ] 15 | }) 16 | grunt.registerTask('quicklint', 'lint the modified files', 'newer:eslint') 17 | } 18 | -------------------------------------------------------------------------------- /lib/routes/static/provision.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

This is the Firefox Accounts server. You probably don't want to login to this like a normal BrowserID IdP. See https://github.com/mozilla/fxa-auth-server/blob/master/docs/overview.md for more details.

10 | 11 | 12 | -------------------------------------------------------------------------------- /grunttasks/changelog.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var fxaChangelog = require('fxa-conventional-changelog')() 6 | 7 | module.exports = function (grunt) { 8 | grunt.config('conventionalChangelog', { 9 | options: { 10 | changelogOpts: {}, 11 | parserOpts: fxaChangelog.parserOpts, 12 | writerOpts: fxaChangelog.writerOpts 13 | }, 14 | release: { 15 | src: 'CHANGELOG.md' 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /lib/newrelic.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // To be enabled via the environment of stage or prod. NEW_RELIC_HIGH_SECURITY 6 | // and NEW_RELIC_LOG should be set in addition to NEW_RELIC_APP_NAME and 7 | // NEW_RELIC_LICENSE_KEY. 8 | 9 | function maybeRequireNewRelic() { 10 | var env = process.env 11 | 12 | if (env.NEW_RELIC_APP_NAME && env.NEW_RELIC_LICENSE_KEY) { 13 | return require('newrelic') 14 | } 15 | 16 | return null 17 | } 18 | 19 | module.exports = maybeRequireNewRelic 20 | 21 | -------------------------------------------------------------------------------- /lib/routes/static/sign_in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 |

This is the Firefox Accounts server. You probably don't want to login to this like a normal BrowserID IdP. See https://github.com/mozilla/fxa-auth-server/blob/master/docs/overview.md for more details.

16 | 17 | 18 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Akshay Katyal 2 | Andrew Chilton 3 | Brian Warner 4 | Chris Karlof 5 | Danny Coates 6 | Eric Le Lay 7 | Francois Marier 8 | James Bonacci 9 | Jed Parsons 10 | John Morrison 11 | Lloyd Hilaiel 12 | Peter deHaan 13 | Rishi Baldawa 14 | Rob Miller 15 | Robert Kowalski 16 | Ryan Kelly 17 | Shane Tomlinson 18 | Vlad Filippov 19 | Zach Carter 20 | -------------------------------------------------------------------------------- /scripts/tap-coverage.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | var path = require('path'), 8 | spawn = require('child_process').spawn 9 | 10 | var COVERAGE_ARGS = ['--coverage', '--cov'] 11 | if (process.env.NO_COVERAGE) { 12 | COVERAGE_ARGS = [] 13 | } 14 | 15 | var p = spawn(path.join(path.dirname(__dirname), 'node_modules', '.bin', 'tap'), 16 | process.argv.slice(2).concat(COVERAGE_ARGS), { stdio: 'inherit', env: process.env }) 17 | 18 | // exit this process with the same exit code as the test process 19 | p.on('close', function (code) { 20 | process.exit(code) 21 | }) 22 | -------------------------------------------------------------------------------- /scripts/test-remote-quick.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | var path = require('path') 8 | var spawn = require('child_process').spawn 9 | var config = require('../config').getProperties() 10 | var TestServer = require('../test/test_server') 11 | 12 | TestServer.start(config, false) 13 | .then( 14 | function (server) { 15 | var tap = spawn( 16 | path.join(path.dirname(__dirname), 'node_modules/.bin/tap'), 17 | ['test/remote'], 18 | { stdio: 'inherit' } 19 | ) 20 | 21 | tap.on('close', function(code) { 22 | server.stop() 23 | }) 24 | } 25 | ) 26 | 27 | -------------------------------------------------------------------------------- /scripts/start-travis-auth-db-mysql.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -evuo pipefail 4 | 5 | node ./scripts/gen_keys.js 6 | 7 | # Force install of mysql-patcher 8 | (cd node_modules/fxa-auth-db-mysql && npm install &>/var/tmp/db-mysql.out) 9 | 10 | mysql -u root -e 'DROP DATABASE IF EXISTS fxa' 11 | node ./node_modules/fxa-auth-db-mysql/bin/db_patcher.js 12 | 13 | # Start backgrounded fxa-auth-db-mysql server 14 | nohup node ./node_modules/fxa-auth-db-mysql/bin/server.js &>>/var/tmp/db-mysql.out & 15 | 16 | # Give auth-db-mysql a moment to start up 17 | sleep 5 18 | 19 | # If either the curl fails to get a response, or the grep fails to match, this 20 | # script will exit non-zero and fail the test run. 21 | authdb_version=$(curl -s http://127.0.0.1:8000/__version__) 22 | echo $authdb_version | grep '"implementation":"MySql"' 23 | -------------------------------------------------------------------------------- /test/oauth_helper.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var config = require('../config').getProperties() 6 | 7 | var url = require('url') 8 | var hapi = require('hapi') 9 | 10 | var api = new hapi.Server() 11 | api.connection({ 12 | host: url.parse(config.oauth.url).hostname, 13 | port: parseInt(url.parse(config.oauth.url).port) 14 | }) 15 | 16 | api.route( 17 | [ 18 | { 19 | method: 'POST', 20 | path: '/v1/verify', 21 | handler: function (request, reply) { 22 | var data = JSON.parse(Buffer(request.payload.token, 'hex')) 23 | return reply(data).code(data.code || 200) 24 | } 25 | } 26 | ] 27 | ) 28 | 29 | api.start() 30 | -------------------------------------------------------------------------------- /lib/crypto/hkdf.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var HKDF = require('hkdf') 6 | var P = require('../promise') 7 | 8 | const NAMESPACE = 'identity.mozilla.com/picl/v1/' 9 | 10 | function KWE(name, email) { 11 | return Buffer(NAMESPACE + name + ':' + email) 12 | } 13 | 14 | function KW(name) { 15 | return Buffer(NAMESPACE + name) 16 | } 17 | 18 | function hkdf(km, info, salt, len) { 19 | var d = P.defer() 20 | var df = new HKDF('sha256', salt, km) 21 | df.derive( 22 | KW(info), 23 | len, 24 | function(key) { 25 | d.resolve(key) 26 | } 27 | ) 28 | return d.promise 29 | } 30 | 31 | hkdf.KW = KW 32 | hkdf.KWE = KWE 33 | 34 | module.exports = hkdf 35 | -------------------------------------------------------------------------------- /test/load/README.txt: -------------------------------------------------------------------------------- 1 | This directory contains some very simple loadtests, written using 2 | the "loads" framework: 3 | 4 | https://github.com/mozilla/loads 5 | 6 | 7 | To run them, you will need the following dependencies: 8 | 9 | * Python development files (e.g. python-dev or python-devel package) 10 | * Virtualenv (e.g. python-virtualenv package) 11 | * ZeroMQ development files (e.g. libzmq-dev package) 12 | * (for megabench) ssh access to the mozilla loads cluster 13 | 14 | Then do the following: 15 | 16 | $> make build # installs local environment with all dependencies 17 | $> make test # runs a single test, to check that everything's working 18 | $> make bench # runs a longer, higher-concurrency test. 19 | $> make megabench # runs a really-long, really-high-concurrency test 20 | # using https://loads.services.mozilla.com 21 | 22 | -------------------------------------------------------------------------------- /grunttasks/bump.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // takes care of bumping the version number in package.json 6 | 7 | module.exports = function (grunt) { 8 | 'use strict' 9 | 10 | grunt.config('bump', { 11 | options: { 12 | files: ['package.json', 'npm-shrinkwrap.json'], 13 | bumpVersion: true, 14 | commit: true, 15 | commitMessage: 'Release v%VERSION%', 16 | commitFiles: ['package.json', 'npm-shrinkwrap.json', 'CHANGELOG.md'], 17 | createTag: true, 18 | tagName: 'v%VERSION%', 19 | tagMessage: 'Version %VERSION%', 20 | push: false, 21 | pushTo: 'origin', 22 | gitDescribeOptions: '--tags --always --abrev=1 --dirty=-d' 23 | } 24 | }) 25 | } 26 | 27 | -------------------------------------------------------------------------------- /lib/crypto/pbkdf2.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var sjcl = require('sjcl') 6 | var P = require('../promise') 7 | 8 | /** pbkdf2 string creator 9 | * 10 | * @param {Buffer} input The password hex buffer. 11 | * @param {Buffer} salt The salt string buffer. 12 | * @return {Buffer} the derived key hex buffer. 13 | */ 14 | function derive(input, salt, iterations, len) { 15 | var password = sjcl.codec.hex.toBits(input.toString('hex')) 16 | var saltBits = sjcl.codec.hex.toBits(salt.toString('hex')) 17 | var result = sjcl.misc.pbkdf2(password, saltBits, iterations, len * 8, sjcl.misc.hmac) 18 | 19 | return P.resolve(Buffer(sjcl.codec.hex.fromBits(result), 'hex')) 20 | } 21 | 22 | module.exports.derive = derive 23 | -------------------------------------------------------------------------------- /lib/tokens/password_change_token.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (log, inherits, Token, lifetime) { 6 | 7 | function PasswordChangeToken(keys, details) { 8 | details.lifetime = lifetime 9 | Token.call(this, keys, details) 10 | } 11 | inherits(PasswordChangeToken, Token) 12 | 13 | PasswordChangeToken.tokenTypeID = 'passwordChangeToken' 14 | 15 | PasswordChangeToken.create = function (details) { 16 | log.trace({ op: 'PasswordChangeToken.create', uid: details && details.uid }) 17 | return Token.createNewToken(PasswordChangeToken, details || {}) 18 | } 19 | 20 | PasswordChangeToken.fromHex = function (string, details) { 21 | log.trace({ op: 'PasswordChangeToken.fromHex' }) 22 | return Token.createTokenFromHexData(PasswordChangeToken, string, details || {}) 23 | } 24 | 25 | return PasswordChangeToken 26 | } 27 | -------------------------------------------------------------------------------- /lib/tokens/account_reset_token.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (log, inherits, Token, crypto, lifetime) { 6 | 7 | function AccountResetToken(keys, details) { 8 | details.lifetime = lifetime 9 | Token.call(this, keys, details) 10 | } 11 | inherits(AccountResetToken, Token) 12 | 13 | AccountResetToken.tokenTypeID = 'accountResetToken' 14 | 15 | AccountResetToken.create = function (details) { 16 | log.trace({ op: 'AccountResetToken.create', uid: details && details.uid }) 17 | return Token.createNewToken(AccountResetToken, details || {}) 18 | } 19 | 20 | AccountResetToken.fromHex = function (string, details) { 21 | log.trace({ op: 'AccountResetToken.fromHex' }) 22 | details = details || {} 23 | return Token.createTokenFromHexData(AccountResetToken, string, details) 24 | } 25 | 26 | return AccountResetToken 27 | } 28 | -------------------------------------------------------------------------------- /grunttasks/version.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // 6 | // A task to stamp a new version. 7 | // 8 | // Before running this task you should update CHANGELOG with the 9 | // changes for this release. Protip: you only need to make changes 10 | // to CHANGELOG; this task will add and commit for you. 11 | // 12 | // * version is updated in package.json 13 | // * git tag with version name is created. 14 | // * git commit with updated package.json created. 15 | // 16 | // NOTE: This task will not push this commit for you. 17 | // 18 | 19 | module.exports = function (grunt) { 20 | 'use strict' 21 | 22 | grunt.registerTask('version', [ 23 | 'bump-only:minor', 24 | 'conventionalChangelog:release', 25 | 'bump-commit' 26 | ]) 27 | 28 | grunt.registerTask('version:patch', [ 29 | 'bump-only:patch', 30 | 'conventionalChangelog:release', 31 | 'bump-commit' 32 | ]) 33 | } 34 | -------------------------------------------------------------------------------- /bin/email_bouncer.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var config = require('../config').getProperties() 6 | var log = require('../lib/log')(config.log.level, 'fxa-email-bouncer') 7 | var error = require('../lib/error') 8 | var Token = require('../lib/tokens')(log, config.tokenLifetimes) 9 | var SQSReceiver = require('../lib/sqs')(log) 10 | var bounces = require('../lib/bounces')(log, error) 11 | 12 | var DB = require('../lib/db')( 13 | config.db.backend, 14 | log, 15 | error, 16 | Token.SessionToken, 17 | Token.KeyFetchToken, 18 | Token.AccountResetToken, 19 | Token.PasswordForgotToken, 20 | Token.PasswordChangeToken 21 | ) 22 | 23 | var bounceQueue = new SQSReceiver(config.bounces.region, [ 24 | config.bounces.bounceQueueUrl, 25 | config.bounces.complaintQueueUrl 26 | ]) 27 | 28 | DB.connect(config[config.db.backend]) 29 | .done( 30 | function (db) { 31 | bounces(bounceQueue, db) 32 | } 33 | ) 34 | -------------------------------------------------------------------------------- /lib/signer.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var jwtool = require('fxa-jwtool') 6 | 7 | module.exports = function (secretKeyFile, domain) { 8 | 9 | var key = jwtool.JWK.fromFile(secretKeyFile, {iss: domain }) 10 | 11 | return { 12 | sign: function (data) { 13 | var now = Date.now() 14 | return key.sign( 15 | { 16 | 'public-key': data.publicKey, 17 | principal: { 18 | email: data.email 19 | }, 20 | iat: now - (10 * 1000), 21 | exp: now + data.duration, 22 | 'fxa-generation': data.generation, 23 | 'fxa-lastAuthAt': data.lastAuthAt, 24 | 'fxa-verifiedEmail': data.verifiedEmail, 25 | 'fxa-deviceId': data.deviceId, 26 | 'fxa-tokenVerified': data.tokenVerified 27 | } 28 | ) 29 | .then( 30 | function (cert) { 31 | return { cert: cert } 32 | } 33 | ) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | # workaround for obsolete `temp` module 0.6 4 | env: 5 | global: 6 | - TMPDIR=/tmp 7 | 8 | node_js: 9 | - "4.5" 10 | 11 | dist: trusty 12 | sudo: required 13 | 14 | addons: 15 | apt: 16 | sources: 17 | - ubuntu-toolchain-r-test 18 | packages: 19 | - g++-4.8 20 | - mysql-server-5.6 21 | - mysql-client-core-5.6 22 | - mysql-client-5.6 23 | 24 | env: 25 | global: 26 | - NODE_ENV=test 27 | matrix: 28 | - DB=memory 29 | - DB=mysql 30 | 31 | notifications: 32 | email: 33 | - zcarter@mozilla.com 34 | - dcoates@mozilla.com 35 | - jbonacci@mozilla.com 36 | - rfkelly@mozilla.com 37 | - jrgm@mozilla.com 38 | irc: 39 | channels: 40 | - "irc.mozilla.org#fxa-bots" 41 | use_notice: false 42 | skip_join: false 43 | 44 | before_install: 45 | - npm config set spin false 46 | 47 | install: 48 | # use c++-11 with node4, default compiler on downlevel versions 49 | - if [ $TRAVIS_NODE_VERSION == "4" ]; then CXX=g++-4.8 npm install; else npm install; fi 50 | 51 | script: 52 | - if [ $DB == "mysql" ]; then ./scripts/start-travis-auth-db-mysql.sh; fi 53 | - COVERALLS_REPO_TOKEN=vKN3jjhAOwxkv9HG0VBX4EYIlWLPwiJ9d npm test 54 | - npm run test-e2e 55 | - grunt nsp 56 | -------------------------------------------------------------------------------- /lib/routes/session.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (log, isA, error, db) { 6 | 7 | var routes = [ 8 | { 9 | method: 'POST', 10 | path: '/session/destroy', 11 | config: { 12 | auth: { 13 | strategy: 'sessionToken' 14 | } 15 | }, 16 | handler: function (request, reply) { 17 | log.begin('Session.destroy', request) 18 | var sessionToken = request.auth.credentials 19 | db.deleteSessionToken(sessionToken) 20 | .then( 21 | function () { 22 | reply({}) 23 | }, 24 | reply 25 | ) 26 | } 27 | }, 28 | { 29 | method: 'GET', 30 | path: '/session/status', 31 | config: { 32 | auth: { 33 | strategy: 'sessionToken' 34 | } 35 | }, 36 | handler: function (request, reply) { 37 | log.begin('Session.status', request) 38 | var sessionToken = request.auth.credentials 39 | reply({ uid: sessionToken.uid.toString('hex') }) 40 | } 41 | } 42 | ] 43 | 44 | return routes 45 | } 46 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | VAGRANTFILE_API_VERSION = "2" 5 | 6 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 7 | config.vm.box = "precise64" 8 | config.vm.box_url = "http://files.vagrantup.com/precise64.box" 9 | config.vm.synced_folder ".", 10 | "/vagrant", 11 | type: "rsync", 12 | rsync__exclude: ["sandbox", "node_modules"] 13 | #config.vm.network :forwarded_port, guest: 9000, host: 9000, auto_correct: true 14 | config.ssh.forward_agent = true 15 | config.vm.provider "virtualbox" do |v| 16 | v.memory = 2048 17 | v.customize ["modifyvm", :id, "--cpus", "2"] 18 | end 19 | config.vm.provider "vmware_fusion" do |v| 20 | v.vmx["memsize"] = "2048" 21 | v.vmx["numvcpus"] = "2" 22 | end 23 | script = 24 | "wget -q http://nodejs.org/dist/v0.10.26/node-v0.10.26-linux-x64.tar.gz;" \ 25 | "tar --strip-components 1 -C /usr/local -xzf node-v0.10.26-linux-x64.tar.gz;" \ 26 | "apt-get -qq update;" \ 27 | "export DEBIAN_FRONTEND=noninteractive;" \ 28 | "apt-get -qq install curl mysql-server-5.5 libgmp-dev git build-essential python-dev python-pip libevent-dev tmux htop;" \ 29 | "pip install virtualenv;" 30 | config.vm.provision "shell", inline: script 31 | end 32 | -------------------------------------------------------------------------------- /test/remote/token_expiry_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var TestServer = require('../test_server') 7 | var Client = require('../client') 8 | 9 | process.env.PASSWORD_CHANGE_TOKEN_TTL = '1' 10 | var config = require('../../config').getProperties() 11 | 12 | function fail() { throw new Error() } 13 | 14 | TestServer.start(config) 15 | .then(function main(server) { 16 | 17 | test( 18 | 'token expiry', 19 | function (t) { 20 | // FYI config.tokenLifetimes.passwordChangeToken = 1 21 | var email = Math.random() + '@example.com' 22 | var password = 'ok' 23 | return Client.create(config.publicUrl, email, password, { preVerified: true }) 24 | .then( 25 | function (c) { 26 | return c.changePassword('hello') 27 | } 28 | ) 29 | .then( 30 | fail, 31 | function (err) { 32 | t.equal(err.errno, 110, 'invalid token') 33 | } 34 | ) 35 | } 36 | ) 37 | 38 | test( 39 | 'teardown', 40 | function (t) { 41 | server.stop() 42 | t.end() 43 | } 44 | ) 45 | }) 46 | -------------------------------------------------------------------------------- /test/remote/sign_key_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var TestServer = require('../test_server') 7 | var P = require('../../lib/promise') 8 | var request = require('request') 9 | var path = require('path') 10 | 11 | process.env.OLD_PUBLIC_KEY_FILE = path.resolve(__dirname, '../../config/public-key.json') 12 | var config = require('../../config').getProperties() 13 | 14 | TestServer.start(config) 15 | .then(function main(server) { 16 | 17 | test( 18 | '.well-known/browserid has keys', 19 | function (t) { 20 | var d = P.defer() 21 | request('http://127.0.0.1:9000/.well-known/browserid', 22 | function (err, res, body) { 23 | if (err) { d.reject(err) } 24 | t.equal(res.statusCode, 200) 25 | var json = JSON.parse(body) 26 | t.equal(json.authentication, '/.well-known/browserid/sign_in.html') 27 | t.equal(json.keys.length, 2) 28 | d.resolve(json) 29 | } 30 | ) 31 | return d.promise 32 | } 33 | ) 34 | 35 | test( 36 | 'teardown', 37 | function (t) { 38 | server.stop() 39 | t.end() 40 | } 41 | ) 42 | }) 43 | -------------------------------------------------------------------------------- /scripts/rpm-version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | var cp = require('child_process') 7 | var util = require('util') 8 | 9 | // Generate legacy-format output that looks something like: 10 | // 11 | // { 12 | // "version": { 13 | // "hash": "88f6f24e53da56faa933c357bffb61cbeaec7ff3", 14 | // "subject": "Merge pull request #2939 from vladikoff/sentry-patch3", 15 | // "committer date": "1439472293", 16 | // "source": "git://github.com/mozilla/fxa-content-server.git" 17 | // } 18 | // } 19 | // 20 | // This content is placed in the stage/prod rpm at `./config/version.json`. 21 | // Ignore errors and always produce a (possibly empty struct) output. 22 | 23 | var args = '{"hash":"%H","subject":"%s","committer date":"%ct"}' 24 | var cmd = util.format('git --no-pager log --format=format:\'%s\' -1', args) 25 | cp.exec(cmd, function (err, stdout) { 26 | var info = { 27 | version: JSON.parse(stdout || '{}') 28 | } 29 | 30 | var cmd = 'git config --get remote.origin.url' 31 | cp.exec(cmd, function (err, stdout) { 32 | info.version.source = (stdout && stdout.trim()) || '' 33 | console.log(JSON.stringify(info, null, 2)) 34 | }) 35 | }) 36 | 37 | -------------------------------------------------------------------------------- /scripts/reset-send-batch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | echo "batch file must be specified" 5 | exit 1 6 | fi 7 | 8 | BATCH=${1} 9 | BATCH_DIRNAME=`dirname ${BATCH}` 10 | BASENAME=`basename ${BATCH}` 11 | 12 | ERRORS_DIRNAME=${BATCH_DIRNAME}-errors 13 | ERRORS_OUTPUT=${ERRORS_DIRNAME}/${BASENAME} 14 | 15 | if [ ! -d ${ERRORS_DIRNAME} ]; then 16 | echo "creating ${ERRORS_DIRNAME}" 17 | mkdir -p ${ERRORS_DIRNAME} 18 | fi 19 | 20 | UNSENT_DIRNAME=${BATCH_DIRNAME}-unsent 21 | UNSENT_OUTPUT=${UNSENT_DIRNAME}/${BASENAME} 22 | 23 | if [ ! -d ${UNSENT_DIRNAME} ]; then 24 | echo "creating ${UNSENT_DIRNAME}" 25 | mkdir -p ${UNSENT_DIRNAME} 26 | fi 27 | 28 | echo ${BATCH} 29 | 30 | node must-reset.js -i ${BATCH} 31 | rc=$?; if [[ $rc != 0 ]]; then exit $rc; fi 32 | 33 | ### TODO If we need more delay in between the batches the bulk-mailer itself sends, add a `-d to the below line. 34 | ### If instead of sending 10 emails at a time, the mailer should send 5 (or some other number), -b 35 | ### TODO Finally, when sending for real for real, the --send option needs to be added. 36 | node bulk-mailer.js -i ${BATCH} -t password_reset_required -e ${ERRORS_OUTPUT} -u ${UNSENT_OUTPUT} --real 37 | rc=$?; if [[ $rc != 0 ]]; then exit $rc; fi 38 | 39 | 40 | # once the batch is completed, remove the batch file to 41 | # reduce the possibility of resending emails to the same people 42 | rm ${BATCH} 43 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | 2 | ## SessionTokens 3 | 4 | * id 5 | * uid 6 | * email 7 | * emailCode 8 | * verified 9 | 10 | ## KeyFetchTokens 11 | 12 | * id 13 | * uid 14 | * kA 15 | * wrapKb 16 | * verified 17 | 18 | ## AccountResetTokens 19 | 20 | * id 21 | * uid 22 | 23 | ## AuthTokens 24 | 25 | * id 26 | * uid 27 | * (for sessionToken) 28 | * email 29 | * emailCode 30 | * (for keyFetchToken) 31 | * kA 32 | * wrapKb 33 | * (for both) 34 | * verified 35 | 36 | ## SrpTokens 37 | 38 | * id 39 | * uid 40 | * N 41 | * g 42 | * s 43 | * v 44 | * b 45 | * B 46 | * passwordStretching 47 | * (for authToken) 48 | * email 49 | * emailCode 50 | * kA 51 | * wrapKb 52 | * verified 53 | 54 | ## PasswordForgotTokens 55 | 56 | * id 57 | * uid 58 | * email 59 | * passCode 60 | * ttl 61 | * codeLength 62 | * tries 63 | 64 | ## Emails 65 | 66 | * email 67 | * uid 68 | * srp 69 | * passwordStretching 70 | * (for srpToken) 71 | * emailCode 72 | * kA 73 | * wrapKb 74 | * verified 75 | 76 | ## Accounts 77 | 78 | * uid 79 | * email 80 | * emailCode 81 | * sessionTokens 82 | * keyFetchTokens 83 | * srpTokens 84 | * authTokens 85 | * accountResetToken 86 | * passwordForgotToken 87 | 88 | ## Devices 89 | 90 | * uid 91 | * id 92 | * sessionTokenId 93 | * createdAt 94 | * name 95 | * type 96 | * pushCallback 97 | * pushPublicKey 98 | * pushAuthKey 99 | 100 | -------------------------------------------------------------------------------- /lib/preverifier.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var P = require('./promise') 6 | var JWTool = require('fxa-jwtool') 7 | 8 | module.exports = function (error, config) { 9 | 10 | var jwtool = new JWTool(config.trustedJKUs) 11 | 12 | function nowSeconds() { 13 | return Math.floor(Date.now() / 1000) 14 | } 15 | 16 | function jwtError(email, payload) { 17 | if (payload.exp < nowSeconds()) { 18 | return { exp: payload.exp } 19 | } 20 | if (payload.aud !== config.domain) { 21 | return { aud: payload.aud } 22 | } 23 | if (!payload.sub || payload.sub !== email) { 24 | return { sub: payload.sub } 25 | } 26 | return false 27 | } 28 | 29 | function isValidToken(email, token) { 30 | return jwtool.verify(token) 31 | .then( 32 | function (payload) { 33 | var invalid = jwtError(email, payload) 34 | if (invalid) { 35 | throw error.invalidVerificationCode(invalid) 36 | } 37 | return true 38 | }, 39 | function (err) { 40 | throw error.invalidVerificationCode({ internal: err.message }) 41 | } 42 | ) 43 | } 44 | 45 | return function isPreVerified(email, token) { 46 | if (!token) { return P.resolve(false) } 47 | return isValidToken(email, token) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/tokens/password_forgot_token.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (log, inherits, Token, crypto, lifetime) { 6 | 7 | function PasswordForgotToken(keys, details) { 8 | details.lifetime = lifetime 9 | Token.call(this, keys, details) 10 | this.email = details.email || null 11 | this.passCode = details.passCode || null 12 | this.tries = details.tries || null 13 | } 14 | inherits(PasswordForgotToken, Token) 15 | 16 | PasswordForgotToken.tokenTypeID = 'passwordForgotToken' 17 | 18 | PasswordForgotToken.create = function (details) { 19 | details = details || {} 20 | log.trace({ 21 | op: 'PasswordForgotToken.create', 22 | uid: details.uid, 23 | email: details.email 24 | }) 25 | details.passCode = crypto.randomBytes(16) 26 | details.tries = 3 27 | return Token.createNewToken(PasswordForgotToken, details) 28 | } 29 | 30 | PasswordForgotToken.fromHex = function (string, details) { 31 | log.trace({ op: 'PasswordForgotToken.fromHex' }) 32 | details = details || {} 33 | return Token.createTokenFromHexData(PasswordForgotToken, string, details) 34 | } 35 | 36 | PasswordForgotToken.prototype.failAttempt = function () { 37 | this.tries-- 38 | return this.tries < 1 39 | } 40 | 41 | return PasswordForgotToken 42 | } 43 | -------------------------------------------------------------------------------- /scripts/bulk-mailer/nodemailer-mock.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var fs = require('fs') 6 | var path = require('path') 7 | 8 | module.exports = function (config) { 9 | var messageId = 0 10 | 11 | if (config.outputDir) { 12 | ensureOutputDirExists(config.outputDir) 13 | } 14 | 15 | return { 16 | sendMail: function (emailConfig, callback) { 17 | if (config.outputDir) { 18 | 19 | var outputPath = path.join(config.outputDir, emailConfig.to) 20 | 21 | var textPath = outputPath + '.txt' 22 | fs.writeFileSync(textPath, emailConfig.text) 23 | 24 | var htmlPath = outputPath + '.html' 25 | fs.writeFileSync(htmlPath, emailConfig.html) 26 | } 27 | 28 | if (Math.random() > config.failureRate) { 29 | messageId++ 30 | callback(null, { 31 | message: 'good', 32 | messageId: messageId 33 | }) 34 | } else { 35 | callback(new Error('uh oh')) 36 | } 37 | }, 38 | 39 | close: function () {} 40 | } 41 | } 42 | 43 | function ensureOutputDirExists(outputDir) { 44 | var dirStats 45 | try { 46 | dirStats = fs.statSync(outputDir) 47 | } catch (e) { 48 | fs.mkdirSync(outputDir) 49 | return 50 | } 51 | 52 | if (! dirStats.isDirectory()) { 53 | console.error(outputDir + ' is not a directory') 54 | process.exit(1) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/load/Makefile: -------------------------------------------------------------------------------- 1 | SERVER_URL = https://api-accounts.stage.mozaws.net/v1 2 | 3 | # Hackety-hack around OSX system python bustage. 4 | # The need for this should go away with a future osx/xcode update. 5 | ARCHFLAGS = -Wno-error=unused-command-line-argument-hard-error-in-future 6 | INSTALL = ARCHFLAGS=$(ARCHFLAGS) ./bin/pip install 7 | 8 | .PHONY: build test bench 9 | 10 | # Build virtualenv, to ensure we have all the dependencies. 11 | build: 12 | virtualenv --no-site-packages . 13 | $(INSTALL) gevent 14 | $(INSTALL) pexpect 15 | $(INSTALL) PyFxA[openssl] 16 | $(INSTALL) backports.functools_lru_cache 17 | $(INSTALL) https://github.com/mozilla-services/loads/archive/master.zip 18 | rm -rf ./local # ubuntu, why you create this useless folder? 19 | 20 | # Clean all the things installed by `make build`. 21 | clean: 22 | rm -rf ./include ./bin ./lib ./lib64 *.pyc 23 | 24 | # Run a single test from the local machine, for sanity-checking. 25 | test: 26 | ./bin/loads-runner --config=./config/test.ini --server-url=$(SERVER_URL) loadtests.LoadTest.test_auth_server 27 | 28 | # Run a fuller bench suite from the local machine. 29 | bench: 30 | ./bin/loads-runner --config=./config/bench.ini --server-url=$(SERVER_URL) loadtests.LoadTest.test_auth_server 31 | 32 | # Run a full bench, by submitting to broker in AWS. 33 | megabench: 34 | ./bin/loads-runner --config=./config/megabench.ini --user-id=$(USER) --server-url=$(SERVER_URL) loadtests.LoadTest.test_auth_server 35 | 36 | # Purge any currently-running loadtest runs. 37 | purge: 38 | ./bin/loads-runner --config=./config/megabench.ini --purge-broker 39 | -------------------------------------------------------------------------------- /test/local/geodb.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var tap = require('tap') 6 | var proxyquire = require('proxyquire') 7 | var test = tap.test 8 | var mockLog = require('../mocks').mockLog 9 | 10 | test( 11 | 'returns location data when enabled', 12 | function (t) { 13 | var moduleMocks = { 14 | '../config': { 15 | 'get': function (item) { 16 | if (item === 'geodb') { 17 | return { 18 | enabled: true 19 | } 20 | } 21 | } 22 | } 23 | } 24 | var thisMockLog = mockLog({}) 25 | 26 | var getGeoData = proxyquire('../../lib/geodb', moduleMocks)(thisMockLog) 27 | getGeoData('63.245.221.32') // MTV 28 | .then(function (geoData) { 29 | t.equal(geoData.location.city, 'Mountain View') 30 | t.equal(geoData.location.country, 'United States') 31 | t.equal(geoData.timeZone, 'America/Los_Angeles') 32 | t.end() 33 | }) 34 | } 35 | ) 36 | 37 | test( 38 | 'returns empty object data when disabled', 39 | function (t) { 40 | var moduleMocks = { 41 | '../config': { 42 | 'get': function (item) { 43 | if (item === 'geodb') { 44 | return { 45 | enabled: false 46 | } 47 | } 48 | } 49 | } 50 | } 51 | var thisMockLog = mockLog({}) 52 | 53 | var getGeoData = proxyquire('../../lib/geodb', moduleMocks)(thisMockLog) 54 | getGeoData('8.8.8.8') 55 | .then(function (geoData) { 56 | t.deepEqual(geoData, {}) 57 | t.end() 58 | }) 59 | } 60 | ) 61 | -------------------------------------------------------------------------------- /test/remote/concurrent_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('tap').test 6 | var TestServer = require('../test_server') 7 | var Client = require('../client') 8 | var P = require('../../lib/promise') 9 | 10 | var config = require('../../config').getProperties() 11 | 12 | process.env.VERIFIER_VERSION = '1' 13 | 14 | TestServer.start(config) 15 | .then(function main(server) { 16 | 17 | test( 18 | 'concurrent create requests', 19 | function (t) { 20 | var email = server.uniqueEmail() 21 | var password = 'abcdef' 22 | // Two shall enter, only one shall survive! 23 | var r1 = Client.create(config.publicUrl, email, password, server.mailbox) 24 | var r2 = Client.create(config.publicUrl, email, password, server.mailbox) 25 | return P.all( 26 | [r1, r2] 27 | ) 28 | .then( 29 | t.fail.bind(t, 'created both accounts'), 30 | function (err) { 31 | t.equal(err.errno, 101, 'account exists') 32 | // Note that P.all fails fast when one of the requests fails, 33 | // but we have to wait for *both* to complete before tearing 34 | // down the test infrastructure. Bleh. 35 | if (!r1.isRejected()) { 36 | return r1 37 | } else { 38 | return r2 39 | } 40 | } 41 | ).then( 42 | function () { 43 | return server.mailbox.waitForEmail(email) 44 | } 45 | ) 46 | } 47 | ) 48 | 49 | test( 50 | 'teardown', 51 | function (t) { 52 | server.stop() 53 | t.end() 54 | } 55 | ) 56 | }) 57 | -------------------------------------------------------------------------------- /scripts/gen_keys.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | /* scripts/gen_keys.js creates public and private keys suitable for 8 | key signing Persona Primary IdP's. 9 | 10 | Usage: 11 | scripts/gen_keys.js 12 | 13 | Will create these files 14 | 15 | ./config/public-key.json 16 | ./config/secret-key.json 17 | 18 | If these files already exist, this script will show an error message 19 | and exit. You must remove both keys if you want to generate a new 20 | keypair. 21 | */ 22 | 23 | const fs = require('fs') 24 | const cp = require('child_process') 25 | const assert = require('assert') 26 | const config = require('../config') 27 | 28 | const pubKeyFile = config.get('publicKeyFile') 29 | const secretKeyFile = config.get('secretKeyFile') 30 | 31 | try { 32 | var keysExist = fs.existsSync(pubKeyFile) && fs.existsSync(secretKeyFile) 33 | assert(!keysExist, 'keys already exists') 34 | } catch(e) { 35 | process.exit() 36 | } 37 | 38 | console.error('Generating keypair') 39 | 40 | cp.exec( 41 | 'openssl genrsa 2048 | ../node_modules/fxa-jwtool/node_modules/pem-jwk/bin/pem-jwk.js', 42 | { 43 | cwd: __dirname 44 | }, 45 | function (err, stdout, stderr) { 46 | var secret = stdout 47 | fs.writeFileSync(secretKeyFile, secret) 48 | console.error('Secret Key saved:', secretKeyFile) 49 | var s = JSON.parse(secret) 50 | var pub = { 51 | kid: 'dev-1', 52 | kty: 'RSA', 53 | n: s.n, 54 | e: s.e 55 | } 56 | fs.writeFileSync(pubKeyFile, JSON.stringify(pub)) 57 | console.error('Public Key saved:', pubKeyFile) 58 | } 59 | ) 60 | 61 | -------------------------------------------------------------------------------- /lib/routes/utils/password_check.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * Check if the password a user entered matches the one on 7 | * file for the account. If it does not, flag the account with 8 | * customs. Higher levels will take care of 9 | * returning an error to the user. 10 | */ 11 | 12 | var butil = require('../../crypto/butil') 13 | var error = require('../../error') 14 | 15 | module.exports = function (log, config, Password, customs, db) { 16 | return function (emailRecord, authPW, clientAddress) { 17 | if (butil.buffersAreEqual(emailRecord.authSalt, butil.ONES)) { 18 | return customs.flag(clientAddress, { 19 | email: emailRecord.email, 20 | errno: error.ERRNO.ACCOUNT_RESET 21 | }) 22 | .then( 23 | function () { 24 | throw error.mustResetAccount(emailRecord.email) 25 | } 26 | ) 27 | } 28 | var password = new Password( 29 | authPW, 30 | emailRecord.authSalt, 31 | emailRecord.verifierVersion 32 | ) 33 | return password.verifyHash() 34 | .then( 35 | function (verifyHash) { 36 | return db.checkPassword(emailRecord.uid, verifyHash) 37 | } 38 | ) 39 | .then( 40 | function (match) { 41 | if (match) { 42 | return match 43 | } 44 | 45 | return customs.flag(clientAddress, { 46 | email: emailRecord.email, 47 | errno: error.ERRNO.INCORRECT_PASSWORD 48 | }) 49 | .then( 50 | function () { 51 | return match 52 | } 53 | ) 54 | } 55 | ) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/ptaptest.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /* 6 | * A promise-ified version of tap.test. 7 | * 8 | * This module provides a 'test' function that operates just like tap.test, but 9 | * will properly close a promise if the test returns one. This makes it easier 10 | * to ensure that any unhandled errors cause the test to fail. Use like so: 11 | * 12 | * var test = require('./ptap') 13 | * 14 | * test( 15 | * 'an example test', 16 | * function (t) { 17 | * return someAPI.thingThatReturnsPromise() 18 | * .then(function(result) { 19 | * t.assertEqual(result, 42) 20 | * }) 21 | * } 22 | * ) 23 | * 24 | * Because the test function returns a promise, we get the following for free: 25 | * 26 | * * wait for the promise to resolve, and call t.end() when it does 27 | * * check for unhandled errors and fail the test if they occur 28 | * 29 | */ 30 | 31 | /* eslint-disable no-console */ 32 | var tap = require('tap') 33 | 34 | module.exports = function(name, testfunc, parentTest) { 35 | var t = parentTest || tap 36 | if (!testfunc) { 37 | return t.test(name) 38 | } 39 | var wrappedtestfunc = function(t) { 40 | var res = testfunc(t) 41 | if (typeof res !== 'undefined') { 42 | if (typeof res.done === 'function') { 43 | res.done( 44 | function() { 45 | t.end() 46 | }, 47 | function(err) { 48 | console.error(err.stack) 49 | t.fail(err.message || err.error || err) 50 | t.end() 51 | } 52 | ) 53 | } 54 | } 55 | } 56 | return t.test(name, wrappedtestfunc) 57 | } 58 | -------------------------------------------------------------------------------- /lib/tokens/index.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var crypto = require('crypto') 6 | var inherits = require('util').inherits 7 | 8 | var P = require('../promise') 9 | var hkdf = require('../crypto/hkdf') 10 | var butil = require('../crypto/butil') 11 | 12 | var error = require('../error') 13 | 14 | module.exports = function (log, lifetimes) { 15 | lifetimes = lifetimes || { 16 | accountResetToken: 1000 * 60 * 15, 17 | passwordChangeToken: 1000 * 60 * 15, 18 | passwordForgotToken: 1000 * 60 * 15 19 | } 20 | var Bundle = require('./bundle')(crypto, P, hkdf, butil, error) 21 | var Token = require('./token')(log, crypto, P, hkdf, Bundle, error) 22 | 23 | var KeyFetchToken = require('./key_fetch_token')(log, inherits, Token, P, error) 24 | var AccountResetToken = require('./account_reset_token')( 25 | log, 26 | inherits, 27 | Token, 28 | crypto, 29 | lifetimes.accountResetToken 30 | ) 31 | var SessionToken = require('./session_token')(log, inherits, Token) 32 | var PasswordForgotToken = require('./password_forgot_token')( 33 | log, 34 | inherits, 35 | Token, 36 | crypto, 37 | lifetimes.passwordForgotToken 38 | ) 39 | 40 | var PasswordChangeToken = require('./password_change_token')( 41 | log, 42 | inherits, 43 | Token, 44 | lifetimes.passwordChangeToken 45 | ) 46 | 47 | Token.error = error 48 | Token.Bundle = Bundle 49 | Token.AccountResetToken = AccountResetToken 50 | Token.KeyFetchToken = KeyFetchToken 51 | Token.SessionToken = SessionToken 52 | Token.PasswordForgotToken = PasswordForgotToken 53 | Token.PasswordChangeToken = PasswordChangeToken 54 | 55 | return Token 56 | } 57 | -------------------------------------------------------------------------------- /bin/notifier.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var split = require('binary-split') 6 | var through = require('through') 7 | var AWS = require('aws-sdk') 8 | var log = require('../lib/log')('info') 9 | var snsTopicArn = '' 10 | var sns = { 11 | publish: function (msg, cb) { 12 | cb('event before config') 13 | } 14 | } 15 | 16 | function init(config) { 17 | snsTopicArn = config.snsTopicArn 18 | if (snsTopicArn === 'disabled') { 19 | sns = { publish: function (msg, cb) { cb() }} 20 | return 21 | } 22 | // Pull the region info out of the topic arn. 23 | // For some reason we need to pass this in explicitly. 24 | // Format is "arn:aws:sns::" 25 | var region = config.snsTopicArn.split(':')[3] 26 | // This will pull in default credentials, region data etc 27 | // from the metadata service available to the instance. 28 | // It's magic, and it's awesome. 29 | sns = new AWS.SNS({ region: region }) 30 | } 31 | 32 | function handleEvent(json) { 33 | if (json.event === 'config') { 34 | init(json.data) 35 | } 36 | else { 37 | var msg = json.data 38 | msg.event = json.event 39 | sns.publish( 40 | { 41 | TopicArn: snsTopicArn, 42 | Message: JSON.stringify(msg) 43 | }, 44 | function (err) { 45 | if (err) { 46 | log.error({ op: 'Notifier.publish', err: err }) 47 | } 48 | } 49 | ) 50 | } 51 | } 52 | 53 | process.stdin.pipe(split()) 54 | .pipe( 55 | through( 56 | function (line) { 57 | // pass it on down the line 58 | process.stdout.write(line + '\n') 59 | try { 60 | this.emit('data', JSON.parse(line)) 61 | } 62 | catch (e) {} 63 | } 64 | ) 65 | ) 66 | .on('data', handleEvent) 67 | -------------------------------------------------------------------------------- /lib/routes/util.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var validators = require('./validators') 6 | var HEX_STRING = validators.HEX_STRING 7 | 8 | module.exports = function (log, crypto, isA, config, redirectDomain) { 9 | 10 | var routes = [ 11 | { 12 | method: 'POST', 13 | path: '/get_random_bytes', 14 | handler: function getRandomBytes(request, reply) { 15 | reply({ data: crypto.randomBytes(32).toString('hex') }) 16 | } 17 | }, 18 | { 19 | method: 'GET', 20 | path: '/verify_email', 21 | config: { 22 | validate: { 23 | query: { 24 | code: isA.string().max(32).regex(HEX_STRING).required(), 25 | uid: isA.string().max(32).regex(HEX_STRING).required(), 26 | service: isA.string().max(16).alphanum().optional(), 27 | redirectTo: validators.redirectTo(redirectDomain).optional() 28 | } 29 | } 30 | }, 31 | handler: function (request, reply) { 32 | return reply().redirect(config.contentServer.url + request.raw.req.url) 33 | } 34 | }, 35 | { 36 | method: 'GET', 37 | path: '/complete_reset_password', 38 | config: { 39 | validate: { 40 | query: { 41 | email: validators.email().required(), 42 | code: isA.string().max(32).regex(HEX_STRING).required(), 43 | token: isA.string().max(64).regex(HEX_STRING).required(), 44 | service: isA.string().max(16).alphanum().optional(), 45 | redirectTo: validators.redirectTo(redirectDomain).optional() 46 | } 47 | } 48 | }, 49 | handler: function (request, reply) { 50 | return reply().redirect(config.contentServer.url + request.raw.req.url) 51 | } 52 | } 53 | ] 54 | 55 | return routes 56 | } 57 | -------------------------------------------------------------------------------- /test/local/account_reset_token_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var log = { trace: function() {} } 7 | 8 | var tokens = require('../../lib/tokens')(log) 9 | var AccountResetToken = tokens.AccountResetToken 10 | 11 | var ACCOUNT = { 12 | uid: 'xxx' 13 | } 14 | 15 | 16 | test( 17 | 're-creation from tokenData works', 18 | function (t) { 19 | var token = null 20 | return AccountResetToken.create(ACCOUNT) 21 | .then( 22 | function (x) { 23 | token = x 24 | } 25 | ) 26 | .then( 27 | function () { 28 | return AccountResetToken.fromHex(token.data, ACCOUNT) 29 | } 30 | ) 31 | .then( 32 | function (token2) { 33 | t.deepEqual(token.data, token2.data) 34 | t.deepEqual(token.id, token2.id) 35 | t.deepEqual(token.authKey, token2.authKey) 36 | t.deepEqual(token.bundleKey, token2.bundleKey) 37 | t.deepEqual(token.uid, token2.uid) 38 | } 39 | ) 40 | } 41 | ) 42 | 43 | test( 44 | 'accountResetToken key derivations are test-vector compliant', 45 | function (t) { 46 | var token = null 47 | var tokenData = 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf' 48 | return AccountResetToken.fromHex(tokenData, ACCOUNT) 49 | .then( 50 | function (x) { 51 | token = x 52 | t.equal(token.data.toString('hex'), tokenData) 53 | t.equal(token.id.toString('hex'), '46ec557e56e531a058620e9344ca9c75afac0d0bcbdd6f8c3c2f36055d9540cf') 54 | t.equal(token.authKey.toString('hex'), '716ebc28f5122ef48670a48209190a1605263c3188dfe45256265929d1c45e48') 55 | t.equal(token.bundleKey.toString('hex'), 'aa5906d2318c6e54ecebfa52f10df4c036165c230cc78ee859f546c66ea3c126') 56 | } 57 | ) 58 | } 59 | ) 60 | -------------------------------------------------------------------------------- /test/local/pbkdf2_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var pbkdf2 = require('../../lib/crypto/pbkdf2') 7 | var ITERATIONS = 20000 8 | var LENGTH = 32 9 | 10 | test( 11 | 'pbkdf2 derive', 12 | function (t) { 13 | var salt = Buffer('identity.mozilla.com/picl/v1/first-PBKDF:andré@example.org') 14 | var password = Buffer('pässwörd') 15 | return pbkdf2.derive(password, salt, ITERATIONS, LENGTH) 16 | .then( 17 | function (K1) { 18 | t.equal(K1.toString('hex'), 'f84913e3d8e6d624689d0a3e9678ac8dcc79d2c2f3d9641488cd9d6ef6cd83dd') 19 | } 20 | ) 21 | } 22 | ) 23 | 24 | test( 25 | 'pbkdf2 derive long input', 26 | function (t) { 27 | var email = Buffer('ijqmkkafer3xsj5rzoq+msnxsacvkmqxabtsvxvj@some-test-domain-with-a-long-name-example.org') 28 | var password = Buffer('mSnxsacVkMQxAbtSVxVjCCoWArNUsFhiJqmkkafER3XSJ5rzoQ') 29 | var salt = Buffer('identity.mozilla.com/picl/v1/first-PBKDF:' + email) 30 | return pbkdf2.derive(password, salt, ITERATIONS, LENGTH) 31 | .then( 32 | function (K1) { 33 | t.equal(K1.toString('hex'), '5f99c22dfac713b6d73094604a05082e6d345f8a00d4947e57105733f51216eb') 34 | } 35 | ) 36 | } 37 | ) 38 | 39 | test( 40 | 'pbkdf2 derive bit array', 41 | function (t) { 42 | var salt = Buffer('identity.mozilla.com/picl/v1/second-PBKDF:andré@example.org') 43 | var K2 = '5b82f146a64126923e4167a0350bb181feba61f63cb1714012b19cb0be0119c5' 44 | var passwordString = 'pässwörd' 45 | var password = Buffer.concat([ 46 | Buffer(K2, 'hex'), 47 | Buffer(passwordString) 48 | ]) 49 | 50 | return pbkdf2.derive(password, salt, ITERATIONS, LENGTH) 51 | .then( 52 | function (K1) { 53 | t.equal(K1.toString('hex'), 'c16d46c31bee242cb31f916e9e38d60b76431d3f5304549cc75ae4bc20c7108c') 54 | } 55 | ) 56 | } 57 | ) 58 | -------------------------------------------------------------------------------- /lib/crypto/butil.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var HEX = /^(?:[a-fA-F0-9]{2})+$/ 6 | 7 | module.exports.ONES = Buffer('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 'hex') 8 | 9 | module.exports.buffersAreEqual = function buffersAreEqual(buffer1, buffer2) { 10 | var mismatch = buffer1.length - buffer2.length 11 | if (mismatch) { 12 | return false 13 | } 14 | for (var i = 0; i < buffer1.length; i++) { 15 | mismatch |= buffer1[i] ^ buffer2[i] 16 | } 17 | return mismatch === 0 18 | } 19 | 20 | module.exports.xorBuffers = function xorBuffers(buffer1, buffer2) { 21 | if (buffer1.length !== buffer2.length) { 22 | throw new Error( 23 | 'XOR buffers must be same length (%d != %d)', 24 | buffer1.length, 25 | buffer2.length 26 | ) 27 | } 28 | var result = Buffer(buffer1.length) 29 | for (var i = 0; i < buffer1.length; i++) { 30 | result[i] = buffer1[i] ^ buffer2[i] 31 | } 32 | return result 33 | } 34 | 35 | module.exports.unbuffer = function unbuffer(object, inplace) { 36 | var keys = Object.keys(object) 37 | var copy = inplace ? object : {} 38 | for (var i = 0; i < keys.length; i++) { 39 | var x = object[keys[i]] 40 | copy[keys[i]] = Buffer.isBuffer(x) ? x.toString('hex') : x 41 | } 42 | return copy 43 | } 44 | 45 | module.exports.bufferize = function bufferize(object, options) { 46 | var keys = Object.keys(object) 47 | options = options || {} 48 | var copy = options.inplace ? object : {} 49 | var ignore = options.ignore || [] 50 | for (var i = 0; i < keys.length; i++) { 51 | var key = keys[i] 52 | var value = object[key] 53 | if ( 54 | ignore.indexOf(key) === -1 && 55 | typeof value === 'string' && 56 | HEX.test(value) 57 | ) { 58 | copy[key] = Buffer(value, 'hex') 59 | } else { 60 | copy[key] = value 61 | } 62 | } 63 | return copy 64 | } 65 | -------------------------------------------------------------------------------- /test/local/scrypt_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var promise = require('../../lib/promise') 7 | var config = { scrypt: { maxPending: 5 } } 8 | var log = { 9 | buffer: [], 10 | warn: function(obj){ log.buffer.push(obj) }, 11 | } 12 | 13 | var scrypt = require('../../lib/crypto/scrypt')(log, config) 14 | 15 | test( 16 | 'scrypt basic', 17 | function (t) { 18 | var K1 = Buffer('f84913e3d8e6d624689d0a3e9678ac8dcc79d2c2f3d9641488cd9d6ef6cd83dd', 'hex') 19 | var salt = Buffer('identity.mozilla.com/picl/v1/scrypt') 20 | 21 | return scrypt.hash(K1, salt, 65536, 8, 1, 32) 22 | .then( 23 | function (K2) { 24 | t.equal(K2, '5b82f146a64126923e4167a0350bb181feba61f63cb1714012b19cb0be0119c5') 25 | } 26 | ) 27 | } 28 | ) 29 | 30 | test( 31 | 'scrypt enforces maximum number of pending requests', 32 | function (t) { 33 | var K1 = Buffer('f84913e3d8e6d624689d0a3e9678ac8dcc79d2c2f3d9641488cd9d6ef6cd83dd', 'hex') 34 | var salt = Buffer('identity.mozilla.com/picl/v1/scrypt') 35 | // Check the we're using the lower maxPending setting from config. 36 | t.equal(scrypt.maxPending, 5, 'maxPending is correctly set from config') 37 | // Send many concurrent requests. 38 | // Not yielding the event loop ensures they will pile up quickly. 39 | var promises = [] 40 | for (var i = 0; i < 10; i++) { 41 | promises.push(scrypt.hash(K1, salt, 65536, 8, 1, 32)) 42 | } 43 | return promise.all(promises).then( 44 | function () { 45 | t.fail('too many pending scrypt hashes were allowed') 46 | }, 47 | function (err) { 48 | t.equal(err.message, 'too many pending scrypt hashes') 49 | t.equal(scrypt.numPendingHWM, 6, 'HWM should be maxPending+1') 50 | t.equal(log.buffer[0].op, 'scrypt.maxPendingExceeded') 51 | } 52 | ) 53 | } 54 | ) 55 | -------------------------------------------------------------------------------- /lib/crypto/scrypt.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var P = require('../promise') 6 | var scrypt_hash = require('scrypt-hash') 7 | 8 | // The maximum numer of hash operations allowed concurrently. 9 | // This can be customized by setting the `maxPending` attribute on the 10 | // exported object, or by setting the `scrypt.maxPending` config option. 11 | const DEFAULT_MAX_PENDING = 100 12 | 13 | module.exports = function(log, config) { 14 | 15 | var scrypt = { 16 | hash: hash, 17 | // The current number of hash operations in progress. 18 | numPending: 0, 19 | // The high-water-mark on number of hash operations in progress. 20 | numPendingHWM: 0, 21 | // The maximum number of hash operations that may be in progress. 22 | maxPending: DEFAULT_MAX_PENDING 23 | } 24 | if (config.scrypt && config.scrypt.hasOwnProperty('maxPending')) { 25 | scrypt.maxPending = config.scrypt.maxPending 26 | } 27 | 28 | /** hash - Creates an scrypt hash asynchronously 29 | * 30 | * @param {Buffer} input The input for scrypt 31 | * @param {Buffer} salt The salt for the hash 32 | * @returns {Object} d.promise Deferred promise 33 | */ 34 | function hash(input, salt, N, r, p, len) { 35 | var d = P.defer() 36 | if (scrypt.maxPending > 0 && scrypt.numPending > scrypt.maxPending) { 37 | log.warn({ op: 'scrypt.maxPendingExceeded' }) 38 | d.reject(new Error('too many pending scrypt hashes')) 39 | } else { 40 | scrypt.numPending += 1 41 | if (scrypt.numPending > scrypt.numPendingHWM) { 42 | scrypt.numPendingHWM = scrypt.numPending 43 | } 44 | scrypt_hash(input, salt, N, r, p, len, 45 | function (err, hash) { 46 | scrypt.numPending -= 1 47 | return err ? d.reject(err) : d.resolve(hash.toString('hex')) 48 | } 49 | ) 50 | } 51 | return d.promise 52 | } 53 | 54 | return scrypt 55 | } 56 | -------------------------------------------------------------------------------- /test/e2e/push_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var tap = require('tap') 6 | var proxyquire = require('proxyquire') 7 | 8 | var test = tap.test 9 | var P = require('../../lib/promise') 10 | var mockLog = require('../mocks').mockLog 11 | var mockUid = new Buffer('foo') 12 | 13 | var PushManager = require('../push_helper').PushManager 14 | 15 | var pushManager = new PushManager({ 16 | server: 'wss://push.services.mozilla.com/', 17 | channelId: '9500b5e6-9954-40d5-8ac1-3920832e781e' 18 | }) 19 | 20 | test( 21 | 'pushToAllDevices sends notifications using a real push server', 22 | function (t) { 23 | t.assert(true, 'Test Skipped. See issue #1368.') 24 | return t.end() 25 | 26 | pushManager.getSubscription().then(function (subscription) { // eslint-disable-line no-unreachable 27 | var mockDbResult = { 28 | devices: function (/* uid */) { 29 | return P.resolve([ 30 | { 31 | 'id': '0f7aa00356e5416e82b3bef7bc409eef', 32 | 'isCurrentDevice': true, 33 | 'lastAccessTime': 1449235471335, 34 | 'name': 'My Phone', 35 | 'type': 'mobile', 36 | 'pushCallback': subscription.endpoint, 37 | 'pushPublicKey': 'BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc', 38 | 'pushAuthKey': 'GSsIiaD2Mr83iPqwFNK4rw' 39 | } 40 | ]) 41 | } 42 | } 43 | 44 | var thisMockLog = mockLog({ 45 | info: function (log) { 46 | if (log.name === 'push.account_verify.success') { 47 | t.end() 48 | } 49 | } 50 | }) 51 | 52 | var push = proxyquire('../../lib/push', {})(thisMockLog, mockDbResult) 53 | var options = { 54 | data: new Buffer('foodata') 55 | } 56 | push.pushToAllDevices(mockUid, 'accountVerify', options) 57 | 58 | }) 59 | } 60 | ) 61 | -------------------------------------------------------------------------------- /test/local/hkdf_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var hkdf = require('../../lib/crypto/hkdf') 7 | 8 | test( 9 | 'hkdf basic', 10 | function (t) { 11 | var stretchedPw = 'c16d46c31bee242cb31f916e9e38d60b76431d3f5304549cc75ae4bc20c7108c' 12 | stretchedPw = new Buffer (stretchedPw, 'hex') 13 | var info = 'mainKDF' 14 | var salt = new Buffer ('00f000000000000000000000000000000000000000000000000000000000034d', 'hex') 15 | var lengthHkdf = 2 * 32 16 | 17 | return hkdf(stretchedPw, info, salt, lengthHkdf) 18 | .then( 19 | function (hkdfResult) { 20 | var hkdfStr = hkdfResult.toString('hex') 21 | 22 | t.equal(hkdfStr.substring(0, 64), '00f9b71800ab5337d51177d8fbc682a3653fa6dae5b87628eeec43a18af59a9d') 23 | t.equal(hkdfStr.substring(64, 128), '6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a') 24 | return hkdfResult 25 | } 26 | ) 27 | } 28 | ) 29 | 30 | test( 31 | 'hkdf basic with salt', 32 | function (t) { 33 | var stretchedPw = 'c16d46c31bee242cb31f916e9e38d60b76431d3f5304549cc75ae4bc20c7108c' 34 | stretchedPw = new Buffer (stretchedPw, 'hex') 35 | var info = 'mainKDF' 36 | var salt = new Buffer ('00f000000000000000000000000000000000000000000000000000000000034d', 'hex') 37 | var lengthHkdf = 2 * 32 38 | 39 | return hkdf(stretchedPw, info, salt, lengthHkdf) 40 | .then( 41 | function (hkdfResult) { 42 | var hkdfStr = hkdfResult.toString('hex') 43 | 44 | t.equal(hkdfStr.substring(0, 64), '00f9b71800ab5337d51177d8fbc682a3653fa6dae5b87628eeec43a18af59a9d') 45 | t.equal(hkdfStr.substring(64, 128), '6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a') 46 | t.equal(salt.toString('hex'), '00f000000000000000000000000000000000000000000000000000000000034d') 47 | return hkdfResult 48 | } 49 | ) 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /lib/routes/idp.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var jwtool = require('fxa-jwtool') 6 | 7 | function b64toDec(str) { 8 | var n = new jwtool.BN(Buffer(str, 'base64')) 9 | return n.toString(10) 10 | } 11 | 12 | function toDec(str) { 13 | return /^[0-9]+$/.test(str) ? str : b64toDec(str) 14 | } 15 | 16 | function browseridFormat(keys) { 17 | var primary = keys[0] 18 | return { 19 | 'public-key': { 20 | algorithm: primary.jwk.algorithm, 21 | n: toDec(primary.jwk.n), 22 | e: toDec(primary.jwk.e) 23 | }, 24 | authentication: '/.well-known/browserid/sign_in.html', 25 | provisioning: '/.well-known/browserid/provision.html', 26 | keys: keys 27 | } 28 | } 29 | 30 | module.exports = function (log, serverPublicKeys) { 31 | var keys = [ serverPublicKeys.primary ] 32 | if (serverPublicKeys.secondary) { keys.push(serverPublicKeys.secondary) } 33 | 34 | var browserid = browseridFormat(keys) 35 | 36 | var routes = [ 37 | { 38 | method: 'GET', 39 | path: '/.well-known/browserid', 40 | config: { 41 | cache: { 42 | privacy: 'public', 43 | expiresIn: 10000 44 | } 45 | }, 46 | handler: function (request, reply) { 47 | log.begin('browserid', request) 48 | reply(browserid) 49 | } 50 | }, 51 | { 52 | method: 'GET', 53 | path: '/.well-known/public-keys', 54 | handler: function (request, reply) { 55 | // FOR DEV PURPOSES ONLY 56 | reply( 57 | { 58 | keys: keys 59 | } 60 | ) 61 | } 62 | }, 63 | { 64 | method: 'GET', 65 | path: '/.well-known/browserid/sign_in.html', 66 | handler: { 67 | file: './lib/routes/static/sign_in.html' 68 | } 69 | }, 70 | { 71 | method: 'GET', 72 | path: '/.well-known/browserid/provision.html', 73 | handler: { 74 | file: './lib/routes/static/provision.html' 75 | } 76 | } 77 | ] 78 | 79 | return routes 80 | } 81 | -------------------------------------------------------------------------------- /test/remote/account_destroy_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var TestServer = require('../test_server') 7 | var Client = require('../client') 8 | 9 | var config = require('../../config').getProperties() 10 | 11 | TestServer.start(config) 12 | .then(function main(server) { 13 | 14 | test( 15 | 'account destroy', 16 | function (t) { 17 | var email = server.uniqueEmail() 18 | var password = 'allyourbasearebelongtous' 19 | var client = null 20 | return Client.createAndVerify(config.publicUrl, email, password, server.mailbox) 21 | .then( 22 | function (x) { 23 | client = x 24 | return client.sessionStatus() 25 | } 26 | ) 27 | .then( 28 | function (status) { 29 | return client.destroyAccount() 30 | } 31 | ) 32 | .then( 33 | function () { 34 | return client.keys() 35 | } 36 | ) 37 | .then( 38 | function (keys) { 39 | t.fail('account not destroyed') 40 | }, 41 | function (err) { 42 | t.equal(err.message, 'Unknown account', 'account destroyed') 43 | } 44 | ) 45 | } 46 | ) 47 | 48 | test( 49 | 'invalid authPW on account destroy', 50 | function (t) { 51 | var email = server.uniqueEmail() 52 | var password = 'ok' 53 | return Client.createAndVerify(config.publicUrl, email, password, server.mailbox) 54 | .then( 55 | function (c) { 56 | c.authPW = Buffer('0000000000000000000000000000000000000000000000000000000000000000', 'hex') 57 | return c.destroyAccount() 58 | } 59 | ) 60 | .then( 61 | t.fail, 62 | function (err) { 63 | t.equal(err.errno, 103) 64 | } 65 | ) 66 | } 67 | ) 68 | 69 | test( 70 | 'teardown', 71 | function (t) { 72 | server.stop() 73 | t.end() 74 | } 75 | ) 76 | }) 77 | -------------------------------------------------------------------------------- /lib/geodb.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var config = require('../config').get('geodb') 6 | var geodb = require('fxa-geodb')(config) 7 | var P = require('./promise') 8 | var ACCURACY_MAX_KM = 200 9 | var ACCURACY_MIN_KM = 25 10 | 11 | /** 12 | * Thin wrapper around geodb, to help log the accuracy 13 | * and catch errors. On success, returns an object with 14 | * `location` data. On failure, returns an empty object 15 | **/ 16 | module.exports = function (log) { 17 | 18 | log.info({ op: 'geodb.start', enabled: config.enabled, dbPath: config.dbPath }) 19 | 20 | return function (ip) { 21 | // this is a kill-switch and can be used to not return location data 22 | if (config.enabled === false) { 23 | // if kill-switch is set, return a promise that resolves 24 | // with an empty object 25 | return new P.resolve({}) 26 | } 27 | return geodb(ip) 28 | .then(function (location) { 29 | var logEventPrefix = 'fxa.location.accuracy.' 30 | var logEvent = 'no_accuracy_data' 31 | var accuracy = location.accuracy 32 | 33 | if (accuracy) { 34 | if (accuracy > ACCURACY_MAX_KM) { 35 | logEvent = 'unknown' 36 | } else if (accuracy > ACCURACY_MIN_KM && accuracy <= ACCURACY_MAX_KM) { 37 | logEvent = 'uncertain' 38 | } else if (accuracy <= ACCURACY_MIN_KM) { 39 | logEvent = 'confident' 40 | } 41 | } 42 | 43 | log.info({op: 'geodb.accuracy', 'accuracy': accuracy}) 44 | log.info({op: 'geodb.accuracy_confidence', 'accuracy_confidence': logEventPrefix + logEvent}) 45 | return { 46 | location: { 47 | city: location.city, 48 | country: location.country 49 | }, 50 | timeZone: location.timeZone 51 | } 52 | }).catch(function (err) { 53 | log.error({ op: 'geodb.1', err: err.message}) 54 | // return an empty object, so that we can still send out 55 | // emails without the location data 56 | return {} 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/push_helper.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var WebSocket = require('ws') 6 | var P = require('../lib/promise') 7 | 8 | /** 9 | * PushManager, helps create subscriptions against a push server 10 | * 11 | * Built based on https://github.com/websockets/wscat 12 | * Built based on Service Worker Push APIs 13 | * 14 | * @param options 15 | * Push Manager options 16 | * @param options.server 17 | * Push server setting. i.e wss://push.services.mozilla.com/ 18 | * @param options.channelId 19 | * Push channel id, uuid4 format. 20 | * @constructor 21 | */ 22 | function PushManager(options) { 23 | if (!options || !options.server) { 24 | throw new Error('Server is required') 25 | } 26 | 27 | this.server = options.server 28 | this.channelId = options.channelId 29 | } 30 | 31 | /** 32 | * Gets a subscription from the push server 33 | * Returns a promise which resolves to a subscription object. 34 | * 35 | * Based on https://developer.mozilla.org/en-US/docs/Web/API/PushManager/getSubscription 36 | * @returns {Promise} 37 | */ 38 | PushManager.prototype.getSubscription = function getSubscription() { 39 | var self = this 40 | var d = P.defer() 41 | var ws = new WebSocket(this.server) 42 | 43 | ws.on('open', function open() { 44 | var helloMessage = { 45 | messageType: 'hello', 46 | use_webpush: true 47 | } 48 | 49 | var registerMessage = { 50 | messageType: 'register', 51 | channelID: self.channelId 52 | } 53 | 54 | ws.send(JSON.stringify(helloMessage), { mask: true }) 55 | ws.send(JSON.stringify(registerMessage), { mask: true }) 56 | }).on('error', function error(code, description) { 57 | ws.close() 58 | throw new Error(code + description) 59 | }).on('message', function message(data, flags) { 60 | data = JSON.parse(data) 61 | if (data && data.messageType === 'register') { 62 | ws.close() 63 | return d.resolve({ 64 | endpoint: data.pushEndpoint 65 | }) 66 | } 67 | }) 68 | 69 | return d.promise 70 | } 71 | 72 | module.exports.PushManager = PushManager 73 | -------------------------------------------------------------------------------- /scripts/check-i18n.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | /* 8 | Check that we are still using the same list of supported locales 9 | that fxa-content-server supports. 10 | */ 11 | 12 | const CONTENT_SERVER_CONFIG = 'https://raw.githubusercontent.com/mozilla/fxa-content-server/master/server/config/production-locales.json' 13 | 14 | const assert = require('assert') 15 | const request = require('request') 16 | const config = require('../config') 17 | 18 | function diffLists(first, second) { 19 | var langs = {} 20 | 21 | first.forEach(function(lang) { 22 | langs[lang] = 1 23 | }) 24 | 25 | second.forEach(function(lang) { 26 | if (langs[lang]) { 27 | langs[lang] += 1 28 | } else { 29 | langs[lang] = 1 30 | } 31 | }) 32 | 33 | return Object.keys(langs).filter(function(key) { 34 | return langs[key] !== 2 35 | }) 36 | } 37 | 38 | function main() { 39 | var options = { 40 | url: CONTENT_SERVER_CONFIG, 41 | timeout: 3000, // don't block 42 | json: true 43 | } 44 | 45 | request.get(options, function(err, res, body) { 46 | // Don't get worried about a transient problem with github. 47 | if (err || res.statusCode !== 200) { 48 | console.log('Could not fetch content server config:', err || res.statusCode) 49 | console.log('Better luck next time.') 50 | return 51 | } 52 | 53 | var actual = config.get('i18n').supportedLanguages 54 | var expect = body.i18n.supportedLanguages 55 | 56 | try { 57 | assert.deepEqual(actual, expect) 58 | console.log('OK: List of supported languages match content server config') 59 | } catch(e) { 60 | var different = diffLists(actual, expect).join(',') 61 | console.log('****************************************************************************') 62 | console.log('* FIXME! List of supported languages not in synch with content server config') 63 | console.log('* They differ on: %s', different) 64 | console.log('****************************************************************************') 65 | } 66 | }) 67 | } 68 | 69 | main() 70 | -------------------------------------------------------------------------------- /lib/routes/index.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var crypto = require('crypto') 6 | 7 | var P = require('../promise') 8 | var uuid = require('uuid') 9 | var isA = require('joi') 10 | var url = require('url') 11 | 12 | module.exports = function ( 13 | log, 14 | error, 15 | serverPublicKeys, 16 | signer, 17 | db, 18 | mailer, 19 | Password, 20 | config, 21 | customs, 22 | metricsContext 23 | ) { 24 | var isPreVerified = require('../preverifier')(error, config) 25 | var defaults = require('./defaults')(log, P, db, error) 26 | var idp = require('./idp')(log, serverPublicKeys) 27 | var checkPassword = require('./utils/password_check')(log, config, Password, customs, db) 28 | var push = require('../push')(log, db) 29 | var devices = require('../devices')(log, db, push) 30 | var account = require('./account')( 31 | log, 32 | crypto, 33 | P, 34 | uuid, 35 | isA, 36 | error, 37 | db, 38 | mailer, 39 | Password, 40 | config, 41 | customs, 42 | isPreVerified, 43 | checkPassword, 44 | push, 45 | metricsContext, 46 | devices 47 | ) 48 | var password = require('./password')( 49 | log, 50 | isA, 51 | error, 52 | db, 53 | Password, 54 | config.smtp.redirectDomain, 55 | mailer, 56 | config.verifierVersion, 57 | customs, 58 | checkPassword, 59 | push 60 | ) 61 | var session = require('./session')(log, isA, error, db) 62 | var sign = require('./sign')(log, P, isA, error, signer, db, config.domain, devices) 63 | var util = require('./util')( 64 | log, 65 | crypto, 66 | isA, 67 | config, 68 | config.smtp.redirectDomain 69 | ) 70 | 71 | var basePath = url.parse(config.publicUrl).path 72 | if (basePath === '/') { basePath = '' } 73 | 74 | var v1Routes = [].concat( 75 | account, 76 | password, 77 | session, 78 | sign, 79 | util 80 | ) 81 | v1Routes.forEach(function(r) { r.path = basePath + '/v1' + r.path }) 82 | defaults.forEach(function(r) { r.path = basePath + r.path }) 83 | var allRoutes = defaults.concat(idp, v1Routes) 84 | 85 | return allRoutes 86 | } 87 | -------------------------------------------------------------------------------- /test/mailbox.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var P = require('../lib/promise') 6 | var request = require('request') 7 | const EventEmitter = require('events').EventEmitter 8 | 9 | /* eslint-disable no-console */ 10 | module.exports = function (host, port) { 11 | 12 | host = host || '127.0.0.1' 13 | port = port || 9001 14 | 15 | const eventEmitter = new EventEmitter() 16 | 17 | function waitForCode(email) { 18 | return waitForEmail(email) 19 | .then( 20 | function (emailData) { 21 | return emailData.headers['x-verify-code'] || 22 | emailData.headers['x-recovery-code'] 23 | } 24 | ) 25 | } 26 | 27 | function loop(name, tries, cb) { 28 | var url = 'http://' + host + ':' + port + '/mail/' + encodeURIComponent(name) 29 | console.log('checking mail', url) 30 | request({ url: url, method: 'GET' }, 31 | function (err, res, body) { 32 | console.log('mail status', res && res.statusCode, 'tries', tries) 33 | var json = null 34 | try { 35 | json = JSON.parse(body)[0] 36 | } 37 | catch (e) { 38 | return cb(e) 39 | } 40 | 41 | if(!json) { 42 | if (tries === 0) { 43 | return cb(new Error('could not get mail for ' + url)) 44 | } 45 | return setTimeout(loop.bind(null, name, --tries, cb), 1000) 46 | } 47 | console.log('deleting mail', url) 48 | request({ url: url, method: 'DELETE' }, 49 | function (err, res, body) { 50 | cb(err, json) 51 | } 52 | ) 53 | } 54 | ) 55 | } 56 | 57 | function waitForEmail(email) { 58 | var d = P.defer() 59 | loop(email.split('@')[0], 20, function (err, json) { 60 | if (err) { 61 | eventEmitter.emit('email:error', email, err) 62 | return d.reject(err) 63 | } 64 | eventEmitter.emit('email:message', email, json) 65 | return d.resolve(json) 66 | }) 67 | return d.promise 68 | } 69 | 70 | return { 71 | waitForEmail: waitForEmail, 72 | waitForCode: waitForCode, 73 | eventEmitter: eventEmitter 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /test/remote/account_unlock_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var TestServer = require('../test_server') 7 | var Client = require('../client') 8 | 9 | var config = require('../../config').getProperties() 10 | 11 | TestServer.start(config) 12 | .then(function main(server) { 13 | 14 | test( 15 | '/account/lock is no longer supported', 16 | function (t) { 17 | return Client.create(config.publicUrl, server.uniqueEmail(), 'password') 18 | .then( 19 | function (c) { 20 | return c.lockAccount() 21 | } 22 | ) 23 | .then( 24 | function () { 25 | t.fail('should get an error') 26 | }, 27 | function (e) { 28 | t.equal(e.code, 410, 'correct error status code') 29 | } 30 | ) 31 | } 32 | ) 33 | 34 | test( 35 | '/account/unlock/resend_code is no longer supported', 36 | function (t) { 37 | return Client.create(config.publicUrl, server.uniqueEmail(), 'password') 38 | .then( 39 | function (c) { 40 | return c.resendAccountUnlockCode('en') 41 | } 42 | ) 43 | .then( 44 | function () { 45 | t.fail('should get an error') 46 | }, 47 | function (e) { 48 | t.equal(e.code, 410, 'correct error status code') 49 | } 50 | ) 51 | } 52 | ) 53 | 54 | test( 55 | '/account/unlock/verify_code is no longer supported', 56 | function (t) { 57 | return Client.create(config.publicUrl, server.uniqueEmail(), 'password') 58 | .then( 59 | function (c) { 60 | return c.verifyAccountUnlockCode('bigscaryuid', 'bigscarycode') 61 | } 62 | ) 63 | .then( 64 | function () { 65 | t.fail('should get an error') 66 | }, 67 | function (e) { 68 | t.equal(e.code, 410, 'correct error status code') 69 | } 70 | ) 71 | } 72 | ) 73 | 74 | test( 75 | 'teardown', 76 | function (t) { 77 | server.stop() 78 | t.end() 79 | } 80 | ) 81 | }) 82 | -------------------------------------------------------------------------------- /test/bench/bot.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /* eslint-disable no-console */ 6 | var Client = require('../client') 7 | 8 | var config = { 9 | origin: 'http://127.0.0.1:9000', 10 | email: Math.random() + 'benchmark@example.com', 11 | password: 'password', 12 | duration: 120000 13 | } 14 | 15 | var key = { 16 | algorithm: 'RS', 17 | n: '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123', 18 | e: '65537' 19 | } 20 | 21 | function bindApply(fn, args) { 22 | return function() { 23 | return fn.apply(null, args) 24 | } 25 | } 26 | 27 | function times(fn, n) { 28 | return function () { 29 | var args = arguments 30 | var p = fn.apply(null, args) 31 | for (var i = 1; i < n; i++) { 32 | p = p.then(bindApply(fn, args)) 33 | } 34 | return p 35 | } 36 | } 37 | 38 | function session(c) { 39 | return c.login() 40 | .then(c.emailStatus.bind(c)) 41 | .then(c.keys.bind(c)) 42 | .then(c.devices.bind(c)) 43 | .then(times(c.sign.bind(c, key, 10000), 10)) 44 | .then(c.destroySession.bind(c)) 45 | } 46 | 47 | function run(c) { 48 | return c.create() 49 | .then(times(session, 10)) 50 | .then(c.changePassword.bind(c, 'newPassword')) 51 | .then( 52 | function () { 53 | return c.destroyAccount() 54 | }, 55 | function (err) { 56 | console.error('Error during run:', err.message) 57 | return c.destroyAccount() 58 | } 59 | ) 60 | } 61 | 62 | var client = new Client(config.origin) 63 | client.options.preVerified = true 64 | 65 | client.setupCredentials(config.email, config.password) 66 | .then( 67 | function () { 68 | var begin = Date.now() 69 | 70 | function loop(ms) { 71 | run(client) 72 | .done( 73 | function () { 74 | if (Date.now() - begin < ms) { 75 | loop(ms) 76 | } 77 | }, 78 | function (err) { 79 | console.error('Error during cleanup:', err.message) 80 | } 81 | ) 82 | } 83 | 84 | loop(config.duration) 85 | } 86 | ) 87 | -------------------------------------------------------------------------------- /test/local/forgot_password_token_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var log = { trace: function() {} } 7 | 8 | var timestamp = Date.now() 9 | 10 | var PasswordForgotToken = require('../../lib/tokens/password_forgot_token')( 11 | log, 12 | require('util').inherits, 13 | require('../../lib/tokens')(log), 14 | require('crypto'), 15 | 1000 * 60 * 15 // 15 minutes 16 | ) 17 | 18 | 19 | var ACCOUNT = { 20 | uid: 'xxx', 21 | email: Buffer('test@example.com').toString('hex') 22 | } 23 | 24 | 25 | test( 26 | 're-creation from tokenData works', 27 | function (t) { 28 | var token = null 29 | return PasswordForgotToken.create(ACCOUNT) 30 | .then( 31 | function (x) { 32 | token = x 33 | } 34 | ) 35 | .then( 36 | function () { 37 | return PasswordForgotToken.fromHex(token.data, ACCOUNT) 38 | } 39 | ) 40 | .then( 41 | function (token2) { 42 | t.deepEqual(token.data, token2.data) 43 | t.deepEqual(token.id, token2.id) 44 | t.deepEqual(token.authKey, token2.authKey) 45 | t.deepEqual(token.bundleKey, token2.bundleKey) 46 | t.deepEqual(token.uid, token2.uid) 47 | t.deepEqual(token.email, token2.email) 48 | } 49 | ) 50 | } 51 | ) 52 | 53 | 54 | test( 55 | 'ttl "works"', 56 | function (t) { 57 | return PasswordForgotToken.create(ACCOUNT) 58 | .then( 59 | function (token) { 60 | token.createdAt = timestamp 61 | t.equal(token.ttl(timestamp), 900) 62 | t.equal(token.ttl(timestamp + 1000), 899) 63 | t.equal(token.ttl(timestamp + 2000), 898) 64 | } 65 | ) 66 | } 67 | ) 68 | 69 | 70 | test( 71 | 'failAttempt decrements `tries`', 72 | function (t) { 73 | return PasswordForgotToken.create(ACCOUNT) 74 | .then( 75 | function (x) { 76 | t.equal(x.tries, 3) 77 | t.equal(x.failAttempt(), false) 78 | t.equal(x.tries, 2) 79 | t.equal(x.failAttempt(), false) 80 | t.equal(x.tries, 1) 81 | t.equal(x.failAttempt(), true) 82 | } 83 | ) 84 | } 85 | ) 86 | -------------------------------------------------------------------------------- /lib/tokens/key_fetch_token.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | module.exports = function (log, inherits, Token, P, error) { 6 | 7 | function KeyFetchToken(keys, details) { 8 | Token.call(this, keys, details) 9 | this.keyBundle = details.keyBundle 10 | this.emailVerified = !!details.emailVerified 11 | 12 | // Tokens are considered verified if no tokenVerificationId exists 13 | this.tokenVerificationId = details.tokenVerificationId || null 14 | this.tokenVerified = this.tokenVerificationId ? false : true 15 | } 16 | inherits(KeyFetchToken, Token) 17 | 18 | KeyFetchToken.tokenTypeID = 'keyFetchToken' 19 | 20 | KeyFetchToken.create = function (details) { 21 | log.trace({ op: 'KeyFetchToken.create', uid: details && details.uid }) 22 | return Token.createNewToken(KeyFetchToken, details || {}) 23 | .then( 24 | function (token) { 25 | return token.bundleKeys(details.kA, details.wrapKb) 26 | .then( 27 | function (keyBundle) { 28 | token.keyBundle = Buffer(keyBundle, 'hex') //TODO see if we can skip hex 29 | return token 30 | } 31 | ) 32 | } 33 | ) 34 | } 35 | 36 | KeyFetchToken.fromId = function (id, details) { 37 | log.trace({ op: 'KeyFetchToken.fromId' }) 38 | return P.resolve(new KeyFetchToken({ tokenId: id, authKey: details.authKey }, details)) 39 | } 40 | 41 | KeyFetchToken.fromHex = function (string, details) { 42 | log.trace({ op: 'KeyFetchToken.fromHex' }) 43 | return Token.createTokenFromHexData(KeyFetchToken, string, details || {}) 44 | } 45 | 46 | KeyFetchToken.prototype.bundleKeys = function (kA, wrapKb) { 47 | log.trace({ op: 'keyFetchToken.bundleKeys', id: this.id }) 48 | return this.bundle('account/keys', Buffer.concat([kA, wrapKb])) 49 | } 50 | 51 | KeyFetchToken.prototype.unbundleKeys = function (bundle) { 52 | log.trace({ op: 'keyFetchToken.unbundleKeys', id: this.id }) 53 | return this.unbundle('account/keys', bundle) 54 | .then( 55 | function (plaintext) { 56 | return { 57 | kA: plaintext.slice(0, 32), 58 | wrapKb: plaintext.slice(32, 64) 59 | } 60 | } 61 | ) 62 | } 63 | 64 | return KeyFetchToken 65 | } 66 | -------------------------------------------------------------------------------- /lib/crypto/password.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var P = require('../promise') 6 | var hkdf = require('./hkdf') 7 | var butil = require('./butil') 8 | 9 | module.exports = function(log, config) { 10 | 11 | var scrypt = require('./scrypt')(log, config) 12 | 13 | var hashVersions = { 14 | 0: function (authPW, authSalt) { 15 | return P.resolve(butil.xorBuffers(authPW, authSalt)) 16 | }, 17 | 1: function (authPW, authSalt) { 18 | return scrypt.hash(authPW, authSalt, 65536, 8, 1, 32) 19 | } 20 | } 21 | 22 | function Password(authPW, authSalt, version) { 23 | version = typeof(version) === 'number' ? version : 1 24 | this.authPW = authPW 25 | this.authSalt = authSalt 26 | this.version = version 27 | this.stretchPromise = hashVersions[version](authPW, authSalt) 28 | this.verifyHashPromise = this.stretchPromise.then(hkdfVerify) 29 | } 30 | 31 | Password.prototype.stretchedPassword = function () { 32 | return this.stretchPromise 33 | } 34 | 35 | Password.prototype.verifyHash = function () { 36 | return this.verifyHashPromise 37 | } 38 | 39 | Password.prototype.matches = function (verifyHash) { 40 | return this.verifyHash().then( 41 | function (hash) { 42 | return butil.buffersAreEqual(hash, verifyHash) 43 | } 44 | ) 45 | } 46 | 47 | Password.prototype.unwrap = function (wrapped, context) { 48 | context = context || 'wrapwrapKey' 49 | return this.stretchedPassword().then( 50 | function (stretched) { 51 | return hkdf(stretched, context, null, 32) 52 | .then( 53 | function (wrapper) { 54 | return butil.xorBuffers(wrapper, wrapped) 55 | } 56 | ) 57 | } 58 | ) 59 | } 60 | Password.prototype.wrap = Password.prototype.unwrap 61 | 62 | function hkdfVerify(stretched) { 63 | return hkdf(stretched, 'verifyHash', null, 32) 64 | } 65 | 66 | Password.stat = function () { 67 | // Reset the high-water-mark whenever it is read. 68 | var numPendingHWM = scrypt.numPendingHWM 69 | scrypt.numPendingHWM = scrypt.numPending 70 | return { 71 | stat: 'scrypt', 72 | maxPending: scrypt.maxPending, 73 | numPending: scrypt.numPending, 74 | numPendingHWM: numPendingHWM 75 | } 76 | } 77 | 78 | return Password 79 | } 80 | -------------------------------------------------------------------------------- /test/local/password_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var log = {} 7 | var config = {} 8 | var Password = require('../../lib/crypto/password')(log, config) 9 | 10 | test( 11 | 'password version zero', 12 | function (t) { 13 | var pwd = Buffer('aaaaaaaaaaaaaaaa') 14 | var salt = Buffer('bbbbbbbbbbbbbbbb') 15 | var p1 = new Password(pwd, salt, 0) 16 | t.equal(p1.version, 0, 'should be using version zero') 17 | var p2 = new Password(pwd, salt, 0) 18 | t.equal(p2.version, 0, 'should be using version zero') 19 | return p1.verifyHash() 20 | .then( 21 | function (hash) { 22 | return p2.matches(hash) 23 | } 24 | ) 25 | .then( 26 | function (matched) { 27 | t.ok(matched, 'identical passwords should match') 28 | } 29 | ) 30 | } 31 | ) 32 | 33 | test( 34 | 'password version one', 35 | function (t) { 36 | var pwd = Buffer('aaaaaaaaaaaaaaaa') 37 | var salt = Buffer('bbbbbbbbbbbbbbbb') 38 | var p1 = new Password(pwd, salt, 1) 39 | t.equal(p1.version, 1, 'should be using version one') 40 | var p2 = new Password(pwd, salt, 1) 41 | t.equal(p2.version, 1, 'should be using version one') 42 | return p1.verifyHash() 43 | .then( 44 | function (hash) { 45 | return p2.matches(hash) 46 | } 47 | ) 48 | .then( 49 | function (matched) { 50 | t.ok(matched, 'identical passwords should match') 51 | } 52 | ) 53 | } 54 | ) 55 | 56 | test( 57 | 'passwords of different versions should not match', 58 | function (t) { 59 | var pwd = Buffer('aaaaaaaaaaaaaaaa') 60 | var salt = Buffer('bbbbbbbbbbbbbbbb') 61 | var p1 = new Password(pwd, salt, 0) 62 | var p2 = new Password(pwd, salt, 1) 63 | return p1.verifyHash() 64 | .then( 65 | function (hash) { 66 | return p2.matches(hash) 67 | } 68 | ) 69 | .then( 70 | function (matched) { 71 | t.ok(!matched, 'passwords should not match') 72 | } 73 | ) 74 | } 75 | ) 76 | 77 | test( 78 | 'scrypt queue stats can be reported', 79 | function (t) { 80 | var stat = Password.stat() 81 | t.equal(stat.stat, 'scrypt') 82 | t.ok(stat.hasOwnProperty('numPending')) 83 | t.ok(stat.hasOwnProperty('numPendingHWM')) 84 | t.end() 85 | } 86 | ) 87 | -------------------------------------------------------------------------------- /test/remote/email_validity_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var TestServer = require('../test_server') 7 | var Client = require('../client') 8 | var P = require('../../lib/promise') 9 | 10 | var config = require('../../config').getProperties() 11 | 12 | TestServer.start(config) 13 | .then(function main(server) { 14 | 15 | test( 16 | '/account/create with a variety of malformed email addresses', 17 | function (t) { 18 | var pwd = '123456' 19 | 20 | var emails = [ 21 | 'notAnEmailAddress', 22 | '\n@example.com', 23 | 'me@hello world.com', 24 | 'me@hello+world.com', 25 | 'me@.example', 26 | 'me@example', 27 | 'me@example.com-', 28 | 'me@example..com', 29 | 'me@example-.com', 30 | 'me@example.-com', 31 | '\uD83D\uDCA9@unicodepooforyou.com' 32 | ] 33 | emails.forEach(function(email, i) { 34 | emails[i] = Client.create(config.publicUrl, email, pwd) 35 | .then( 36 | t.fail, 37 | function (err) { 38 | t.equal(err.code, 400, 'http 400 : malformed email is rejected') 39 | } 40 | ) 41 | }) 42 | 43 | return P.all(emails) 44 | } 45 | ) 46 | 47 | test( 48 | '/account/create with a variety of unusual but valid email addresses', 49 | function (t) { 50 | var pwd = '123456' 51 | 52 | var emails = [ 53 | 'tim@tim-example.net', 54 | 'a+b+c@example.com', 55 | '#!?-@t-e-s-t.c-o-m', 56 | String.fromCharCode(1234) + '@example.com', 57 | 'test@' + String.fromCharCode(5678) + '.com' 58 | ] 59 | 60 | emails.forEach(function(email, i) { 61 | emails[i] = Client.create(config.publicUrl, email, pwd) 62 | .then( 63 | function(c) { 64 | t.pass('Email ' + email + ' is valid') 65 | return c.destroyAccount() 66 | }, 67 | function (err) { 68 | t.fail('Email address ' + email + " should have been allowed, but it wasn't") 69 | } 70 | ) 71 | }) 72 | 73 | return P.all(emails) 74 | } 75 | ) 76 | 77 | test( 78 | 'teardown', 79 | function (t) { 80 | server.stop() 81 | t.end() 82 | } 83 | ) 84 | }) 85 | -------------------------------------------------------------------------------- /test/mail_helper.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /* eslint-disable no-console */ 6 | var config = require('../config').getProperties() 7 | 8 | // SMTP half 9 | 10 | var MailParser = require('mailparser').MailParser 11 | var users = {} 12 | 13 | function emailName(emailAddress) { 14 | return emailAddress.split('@')[0] 15 | } 16 | 17 | require('simplesmtp').createSimpleServer( 18 | { 19 | SMTPBanner: 'FXATEST' 20 | }, 21 | function (req) { 22 | var mp = new MailParser({ defaultCharset: 'utf8' }) 23 | mp.on('end', 24 | function (mail) { 25 | var link = mail.headers['x-link'] 26 | var rc = mail.headers['x-recovery-code'] 27 | var vc = mail.headers['x-verify-code'] 28 | var name = emailName(mail.headers.to) 29 | if (vc) { 30 | console.log('\x1B[32m', link, '\x1B[39m') 31 | } 32 | else if (rc) { 33 | console.log('\x1B[34m', link, '\x1B[39m') 34 | } 35 | else { 36 | console.error('\x1B[31mNo verify code match\x1B[39m') 37 | console.error(mail) 38 | } 39 | if (users[name]) { 40 | users[name].push(mail) 41 | } else { 42 | users[name] = [mail] 43 | } 44 | } 45 | ) 46 | req.pipe(mp) 47 | req.accept() 48 | } 49 | ).listen(config.smtp.port, config.smtp.host) 50 | 51 | // HTTP half 52 | 53 | var hapi = require('hapi') 54 | var api = new hapi.Server() 55 | api.connection({ 56 | host: config.smtp.api.host, 57 | port: config.smtp.api.port 58 | }) 59 | 60 | function loop(email, cb) { 61 | var mail = users[email] 62 | if (!mail) { 63 | return setTimeout(loop.bind(null, email, cb), 50) 64 | } 65 | cb(mail) 66 | } 67 | 68 | api.route( 69 | [ 70 | { 71 | method: 'GET', 72 | path: '/mail/{email}', 73 | handler: function (request, reply) { 74 | loop( 75 | request.params.email, 76 | function (emailData) { 77 | reply(emailData) 78 | } 79 | ) 80 | } 81 | }, 82 | { 83 | method: 'DELETE', 84 | path: '/mail/{email}', 85 | handler: function (request, reply) { 86 | delete users[request.params.email] 87 | reply() 88 | } 89 | } 90 | ] 91 | ) 92 | 93 | api.start(function () { 94 | console.log('mail_helper started...') 95 | }) 96 | -------------------------------------------------------------------------------- /test/local/token_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | 'use strict' 6 | 7 | var crypto = require('crypto') 8 | var hkdf = require('../../lib/crypto/hkdf') 9 | var mocks = require('../mocks') 10 | var P = require('../../lib/promise') 11 | var sinon = require('sinon') 12 | var test = require('tap').test 13 | 14 | var Bundle = { 15 | bundle: sinon.spy(), 16 | unbundle: sinon.spy() 17 | } 18 | var log = mocks.spyLog() 19 | var modulePath = '../../lib/tokens/token' 20 | 21 | test('NODE_ENV=dev', function (t) { 22 | process.env.NODE_ENV = 'dev' 23 | var Token = require(modulePath)(log, crypto, P, hkdf, Bundle, null) 24 | 25 | t.plan(4) 26 | 27 | t.test('Token constructor was exported', function (t) { 28 | t.equal(typeof Token, 'function', 'Token is function') 29 | t.equal(Token.name, 'Token', 'function is called Token') 30 | t.equal(Token.length, 2, 'function expects two arguments') 31 | t.end() 32 | }) 33 | 34 | t.test('Token constructor sets createdAt', function (t) { 35 | var now = Date.now() - 1 36 | var token = new Token({}, { createdAt: now }) 37 | 38 | t.equal(token.createdAt, now, 'token.createdAt is correct') 39 | t.end() 40 | }) 41 | 42 | t.test('Token constructor does not set createdAt if it is negative', function (t) { 43 | var notNow = -Date.now() 44 | var token = new Token({}, { createdAt: notNow }) 45 | 46 | t.ok(token.createdAt > 0, 'token.createdAt seems correct') 47 | t.end() 48 | }) 49 | 50 | t.test('Token constructor does not set createdAt if it is in the future', function (t) { 51 | var notNow = Date.now() + 1000 52 | var token = new Token({}, { createdAt: notNow }) 53 | 54 | t.ok(token.createdAt > 0 && token.createdAt < notNow, 'token.createdAt seems correct') 55 | t.end() 56 | }) 57 | }) 58 | 59 | test('NODE_ENV=prod', function (t) { 60 | process.env.NODE_ENV = 'prod' 61 | delete require.cache[require.resolve(modulePath)] 62 | delete require.cache[require.resolve('../../config')] 63 | var Token = require(modulePath)(log, crypto, P, hkdf, Bundle, null) 64 | 65 | t.plan(1) 66 | 67 | t.test('Token constructor does not set createdAt', function (t) { 68 | var notNow = Date.now() - 1 69 | var token = new Token({}, { createdAt: notNow }) 70 | 71 | t.ok(token.createdAt > notNow, 'token.createdAt seems correct') 72 | t.end() 73 | }) 74 | }) 75 | 76 | -------------------------------------------------------------------------------- /lib/devices.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | 'use strict' 6 | 7 | module.exports = function (log, db, push) { 8 | return { 9 | upsert: upsert, 10 | synthesizeName: synthesizeName 11 | } 12 | 13 | function upsert (request, sessionToken, deviceInfo) { 14 | var operation, event, result 15 | if (deviceInfo.id) { 16 | operation = 'updateDevice' 17 | event = 'device.updated' 18 | } else { 19 | operation = 'createDevice' 20 | event = 'device.created' 21 | } 22 | var isPlaceholderDevice = Object.keys(deviceInfo).length === 0 23 | 24 | return db[operation](sessionToken.uid, sessionToken.tokenId, deviceInfo) 25 | .then(function (device) { 26 | result = device 27 | return log.activityEvent(event, request, { 28 | uid: sessionToken.uid.toString('hex'), 29 | device_id: result.id.toString('hex'), 30 | is_placeholder: isPlaceholderDevice 31 | }) 32 | }) 33 | .then(function () { 34 | if (operation === 'createDevice') { 35 | push.notifyDeviceConnected(sessionToken.uid, result.name, result.id.toString('hex')) 36 | if (isPlaceholderDevice) { 37 | log.info({ 38 | op: 'device:createPlaceholder', 39 | uid: sessionToken.uid, 40 | id: result.id 41 | }) 42 | } 43 | return log.notifyAttachedServices('device:create', request, { 44 | uid: sessionToken.uid, 45 | id: result.id, 46 | type: result.type, 47 | timestamp: result.createdAt, 48 | isPlaceholder: isPlaceholderDevice 49 | }) 50 | } 51 | }) 52 | .then(function () { 53 | return result 54 | }) 55 | } 56 | 57 | function synthesizeName (device) { 58 | var browserPart = part('uaBrowser') 59 | var osPart = part('uaOS') 60 | 61 | if (browserPart) { 62 | if (osPart) { 63 | return browserPart + ', ' + osPart 64 | } 65 | 66 | return browserPart 67 | } 68 | 69 | return osPart || '' 70 | 71 | function part (key) { 72 | if (device[key]) { 73 | var versionKey = key + 'Version' 74 | 75 | if (device[versionKey]) { 76 | return device[key] + ' ' + device[versionKey] 77 | } 78 | 79 | return device[key] 80 | } 81 | } 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /lib/sqs.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var AWS = require('aws-sdk') 6 | var inherits = require('util').inherits 7 | var EventEmitter = require('events').EventEmitter 8 | 9 | module.exports = function (log) { 10 | 11 | function SQSReceiver(region, urls) { 12 | this.sqs = new AWS.SQS({ region : region }) 13 | this.queueUrls = urls || [] 14 | EventEmitter.call(this) 15 | } 16 | inherits(SQSReceiver, EventEmitter) 17 | 18 | function checkDeleteError(err) { 19 | if (err) { 20 | log.error({ op: 'deleteMessage', err: err }) 21 | } 22 | } 23 | 24 | SQSReceiver.prototype.fetch = function (url) { 25 | var errTimer = null 26 | this.sqs.receiveMessage( 27 | { 28 | QueueUrl: url, 29 | AttributeNames: [], 30 | MaxNumberOfMessages: 10, 31 | WaitTimeSeconds: 20 32 | }, 33 | function (err, data) { 34 | if (err) { 35 | log.error({ op: 'fetch', url: url, err: err }) 36 | if (!errTimer) { 37 | // unacceptable! this aws lib will call the callback 38 | // more than once with different errors. ಠ_ಠ 39 | errTimer = setTimeout(this.fetch.bind(this, url), 2000) 40 | } 41 | return 42 | } 43 | function deleteMessage(message) { 44 | this.sqs.deleteMessage( 45 | { 46 | QueueUrl: url, 47 | ReceiptHandle: message.ReceiptHandle 48 | }, 49 | checkDeleteError 50 | ) 51 | } 52 | data.Messages = data.Messages || [] 53 | for (var i = 0; i < data.Messages.length; i++) { 54 | var msg = data.Messages[i] 55 | var deleteFromQueue = deleteMessage.bind(this, msg) 56 | try { 57 | var body = JSON.parse(msg.Body) 58 | var message = JSON.parse(body.Message) 59 | message.del = deleteFromQueue 60 | this.emit('data', message) 61 | } 62 | catch (e) { 63 | log.error({ op: 'fetch', url: url, err: e }) 64 | deleteFromQueue() 65 | } 66 | } 67 | this.fetch(url) 68 | }.bind(this) 69 | ) 70 | } 71 | 72 | SQSReceiver.prototype.start = function () { 73 | for (var i = 0; i < this.queueUrls.length; i++) { 74 | this.fetch(this.queueUrls[i]) 75 | } 76 | } 77 | 78 | return SQSReceiver 79 | } 80 | -------------------------------------------------------------------------------- /scripts/e2e-email/localeQuirks.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | // Not all non-english locales have tranlated some things yet. 6 | // So there are these unfortunate bits of custom mappings that 7 | // will need to change over time. 8 | 9 | var translationQuirks = { 10 | // these locales will be expected to have a SMTP 11 | // 'content-language' header of 'en-US' 12 | 'content-language': [ 13 | ], 14 | 15 | 'Verify your Firefox Account': [ 16 | 'en', 17 | 'en-GB', 18 | 'ar', 19 | 'bg', 20 | 'es-AR', 21 | 'ff', 22 | 'he', 23 | 'lt', 24 | ], 25 | 26 | 'Firefox Account Verified': [ 27 | 'en', 28 | 'en-GB', 29 | 'ar', 30 | 'es-AR', 31 | 'ff', 32 | 'he', 33 | 'hi-IN', 34 | 'lt', 35 | ], 36 | 37 | 'Reset your Firefox Account password': [ 38 | 'en', 39 | 'en-GB', 40 | 'ar', 41 | 'bg', 42 | 'es-AR', 43 | 'ff', 44 | 'he', 45 | 'lt', 46 | ], 47 | 48 | 'Re-verify your Firefox Account': [ 49 | 'en', 50 | 'en-GB', 51 | 'ar', 52 | 'bg', 53 | 'es-AR', 54 | 'ff', 55 | 'he', 56 | 'lt', 57 | ], 58 | 59 | 'New sign-in to Firefox': [ 60 | 'en', 61 | 'en-GB', 62 | 'ar', 63 | 'bg', 64 | 'es', 65 | 'es-AR', 66 | 'fa', 67 | 'ff', 68 | 'fy', 69 | 'he', 70 | 'hu', 71 | 'lt', 72 | 'pa', 73 | 'sq', 74 | 'sr', 75 | 'sr-LATN', 76 | 'tr', 77 | ], 78 | 79 | 'Your Firefox Account password has been changed': [ 80 | 'en', 81 | 'en-GB', 82 | 'ar', 83 | 'bg', 84 | 'es-AR', 85 | 'ff', 86 | 'he', 87 | 'lt', 88 | ], 89 | 90 | 'Your Firefox Account password has been reset': [ 91 | 'en', 92 | 'en-GB', 93 | 'ar', 94 | 'bg', 95 | 'es-AR', 96 | 'ff', 97 | 'he', 98 | 'ko', 99 | 'lt', 100 | ], 101 | } 102 | 103 | function ary2map(ary) { 104 | var map = {} 105 | ary.forEach(function(val) { 106 | if (map[val]) { 107 | console.log('Duplicate!:', val) 108 | } 109 | map[val] = 1 110 | }) 111 | return map 112 | } 113 | 114 | Object.keys(translationQuirks).forEach(function(quirk) { 115 | var locales = translationQuirks[quirk] 116 | translationQuirks[quirk] = ary2map(locales) 117 | }) 118 | 119 | module.exports = translationQuirks 120 | -------------------------------------------------------------------------------- /test/bench/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 | 6 | /* eslint-disable no-console */ 7 | var cp = require('child_process') 8 | var split = require('binary-split') 9 | var through = require('through') 10 | 11 | var clientCount = 2 12 | var pathStats = {} 13 | var requests = 0 14 | var pass = 0 // eslint-disable-line no-unused-vars 15 | var fail = 0 16 | var start = null 17 | 18 | var server = cp.spawn( 19 | 'node', 20 | ['../../bin/key_server.js'], 21 | { 22 | cwd: __dirname 23 | } 24 | ) 25 | 26 | server.stderr 27 | .pipe(split()) 28 | .pipe( 29 | through( 30 | function (data) { 31 | try { 32 | this.emit('data', JSON.parse(data)) 33 | } 34 | catch (e) {} 35 | } 36 | ) 37 | ) 38 | .pipe( 39 | through( 40 | function (json) { 41 | if (json.level > 30 && json.op !== 'console') { 42 | console.log(json) 43 | } 44 | if (json.op && json.op === 'request.summary') { 45 | if (!start) start = Date.now() 46 | requests++ 47 | if (json.code === 200) { pass++ } else { fail++ } 48 | var stat = pathStats[json.path] || {} 49 | stat.count = stat.count + 1 || 1 50 | stat.max = Math.max(stat.max || 0, json.t) 51 | stat.min = Math.min(stat.min || Number.MAX_VALUE, json.t) 52 | pathStats[json.path] = stat 53 | this.emit('data', json) 54 | } 55 | else if (json.op === 'server.start.1') { 56 | startClients() 57 | } 58 | } 59 | ) 60 | ) 61 | 62 | function startClient() { 63 | var client = cp.spawn( 64 | 'node', 65 | ['./bot.js'], 66 | { 67 | cwd: __dirname 68 | } 69 | ) 70 | client.stdout.on('data', process.stdout.write.bind(process.stdout)) 71 | client.stderr.on('data', process.stderr.write.bind(process.stderr)) 72 | return client 73 | } 74 | 75 | function clientExit() { 76 | clientCount-- 77 | if (clientCount === 0) { 78 | var seconds = (Date.now() - start) / 1000 79 | var rps = Math.floor(requests / seconds) 80 | console.log('rps: %d requests: %d errors: %d', rps, requests, fail) 81 | console.log(pathStats) 82 | server.kill('SIGINT') 83 | } 84 | } 85 | 86 | function startClients() { 87 | for (var i = 0; i < clientCount; i++) { 88 | var c = startClient() 89 | c.on('exit', clientExit) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/remote/base_path_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | process.env.PUBLIC_URL = 'http://127.0.0.1:9000/auth' 6 | 7 | var test = require('../ptaptest') 8 | var TestServer = require('../test_server') 9 | var Client = require('../client') 10 | var P = require('../../lib/promise') 11 | var request = require('request') 12 | 13 | var config = require('../../config').getProperties() 14 | 15 | TestServer.start(config) 16 | .then(function main(server) { 17 | 18 | function testVersionRoute(path) { 19 | return function (t) { 20 | var d = P.defer() 21 | request(config.publicUrl + path, 22 | function (err, res, body) { 23 | if (err) { d.reject(err) } 24 | t.equal(res.statusCode, 200) 25 | var json = JSON.parse(body) 26 | t.deepEqual(Object.keys(json), ['version', 'commit', 'source']) 27 | t.equal(json.version, require('../../package.json').version, 'package version') 28 | t.ok(json.source && json.source !== 'unknown', 'source repository') 29 | 30 | // check that the git hash just looks like a hash 31 | t.ok(json.commit.match(/^[0-9a-f]{40}$/), 'The git hash actually looks like one') 32 | d.resolve(json) 33 | } 34 | ) 35 | return d.promise 36 | } 37 | } 38 | 39 | test( 40 | 'alternate base path', 41 | function (t) { 42 | var email = Math.random() + '@example.com' 43 | var password = 'ok' 44 | // if this doesn't crash, we're all good 45 | return Client.create(config.publicUrl, email, password, server.mailbox) 46 | } 47 | ) 48 | 49 | test( 50 | '.well-known did not move', 51 | function (t) { 52 | var d = P.defer() 53 | request('http://127.0.0.1:9000/.well-known/browserid', 54 | function (err, res, body) { 55 | if (err) { d.reject(err) } 56 | t.equal(res.statusCode, 200) 57 | var json = JSON.parse(body) 58 | t.equal(json.authentication, '/.well-known/browserid/sign_in.html') 59 | d.resolve(json) 60 | } 61 | ) 62 | return d.promise 63 | } 64 | ) 65 | 66 | test( 67 | '"/" returns valid version information', 68 | testVersionRoute('/') 69 | ) 70 | 71 | test( 72 | '"/__version__" returns valid version information', 73 | testVersionRoute('/__version__') 74 | ) 75 | 76 | test( 77 | 'teardown', 78 | function (t) { 79 | server.stop() 80 | t.end() 81 | } 82 | ) 83 | }) 84 | -------------------------------------------------------------------------------- /scripts/must-reset.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | /*/ 8 | 9 | Usage: 10 | 11 | node scripts/must-reset.js -i ./reset.json 12 | 13 | This script is used to put user accounts into a "must reset" state. It uses the 14 | same config file as key_server.js so should be run from a production instance. 15 | 16 | /*/ 17 | 18 | var butil = require('../lib/crypto/butil') 19 | var commandLineOptions = require('commander') 20 | var config = require('../config').getProperties() 21 | var crypto = require('crypto') 22 | var error = require('../lib/error') 23 | var log = require('../lib/log')(config.log.level) 24 | var P = require('../lib/promise') 25 | var path = require('path') 26 | var Token = require('../lib/tokens')(log, config.tokenLifetimes) 27 | 28 | commandLineOptions 29 | .option('-i, --input ', 'JSON input file') 30 | .parse(process.argv) 31 | 32 | var requiredOptions = [ 33 | 'input' 34 | ] 35 | 36 | requiredOptions.forEach(checkRequiredOption) 37 | 38 | 39 | var DB = require('../lib/db')( 40 | config.db.backend, 41 | log, 42 | error, 43 | Token.SessionToken, 44 | Token.KeyFetchToken, 45 | Token.AccountResetToken, 46 | Token.PasswordForgotToken, 47 | Token.PasswordChangeToken 48 | ) 49 | 50 | DB.connect(config[config.db.backend]) 51 | .then( 52 | function (db) { 53 | var json = require(path.resolve(commandLineOptions.input)) 54 | 55 | var uids = butil.bufferize(json.map(function (entry) { 56 | return entry.uid 57 | }), {inplace: true}) 58 | 59 | return P.all(uids.map( 60 | function (uid) { 61 | return db.resetAccount( 62 | { uid: uid }, 63 | { 64 | authSalt: butil.ONES, 65 | verifyHash: butil.ONES, 66 | wrapWrapKb: crypto.randomBytes(32), 67 | verifierVersion: 1 68 | } 69 | ) 70 | .catch(function (err) { 71 | log.error({ op: 'reset.failed', uid: uid, err: err }) 72 | process.exit(1) 73 | }) 74 | } 75 | )) 76 | .then( 77 | function () { 78 | log.info({ complete: true, uidsReset: uids.length }) 79 | }, 80 | function (err) { 81 | log.error(err) 82 | } 83 | ) 84 | .then(db.close.bind(db)) 85 | } 86 | ) 87 | 88 | function checkRequiredOption(optionName) { 89 | if (! commandLineOptions[optionName]) { 90 | console.error('--' + optionName + ' required') 91 | process.exit(1) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/verification-reminders.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var config = require('../config') 6 | var P = require('./promise') 7 | var reminderConfig = config.get('verificationReminders') 8 | 9 | var LOG_REMINDERS_CREATED = 'verification-reminders.created' 10 | var LOG_REMINDERS_DELETED = 'verification-reminders.deleted' 11 | var LOG_REMINDERS_ERROR_CREATE = 'verification-reminder.create' 12 | var LOG_REMINDERS_ERROR_DELETE = 'verification-reminder.delete' 13 | 14 | module.exports = function (log, db) { 15 | /** 16 | * shouldRemind 17 | * 18 | * Determines if we should create a reminder for this user to verify their account. 19 | * 20 | * @returns {boolean} 21 | */ 22 | function shouldRemind() { 23 | // random between 0 and 100, inclusive 24 | var rand = Math.floor(Math.random() * (100 + 1)) 25 | return rand <= (reminderConfig.rate * 100) 26 | } 27 | 28 | return { 29 | /** 30 | * Create a new reminder 31 | * @param reminderData 32 | * @param {string} reminderData.uid - The uid to remind. 33 | */ 34 | create: function createReminder(reminderData) { 35 | if (! shouldRemind()) { 36 | // resolves if not part of the verification roll out 37 | return P.resolve(false) 38 | } 39 | 40 | reminderData.type = 'first' 41 | var firstReminder = db.createVerificationReminder(reminderData) 42 | reminderData.type = 'second' 43 | var secondReminder = db.createVerificationReminder(reminderData) 44 | 45 | return P.all([firstReminder, secondReminder]) 46 | .then( 47 | function () { 48 | log.increment(LOG_REMINDERS_CREATED) 49 | }, 50 | function (err) { 51 | log.error({ op: LOG_REMINDERS_ERROR_CREATE, err: err }) 52 | } 53 | ) 54 | }, 55 | /** 56 | * Delete the reminder. Used if the user verifies their account. 57 | * 58 | * @param reminderData 59 | * @param {string} reminderData.uid - The uid for the reminder. 60 | */ 61 | 'delete': function deleteReminder(reminderData) { 62 | reminderData.type = 'first' 63 | var firstReminder = db.deleteVerificationReminder(reminderData) 64 | reminderData.type = 'second' 65 | var secondReminder = db.deleteVerificationReminder(reminderData) 66 | 67 | return P.all([firstReminder, secondReminder]) 68 | .then( 69 | function () { 70 | log.increment(LOG_REMINDERS_DELETED) 71 | }, 72 | function (err) { 73 | log.error({ op: LOG_REMINDERS_ERROR_DELETE, err: err }) 74 | } 75 | ) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/local/mailer_locales_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('tap').test 6 | var config = require('../../config').getProperties() 7 | var log = {} 8 | 9 | require('../../lib/mailer')(config, log) 10 | .done( 11 | function(mailer) { 12 | 13 | test( 14 | 'All configured supportedLanguages are available', 15 | function (t) { 16 | var locales = config.i18n.supportedLanguages 17 | locales.forEach(function(lang) { 18 | // sr-LATN is sr, but in Latin characters, not Cyrillic 19 | if (lang === 'sr-LATN') { 20 | t.equal('sr-Latn', mailer.translator(lang).language) 21 | } else { 22 | t.equal(lang, mailer.translator(lang).language) 23 | } 24 | }) 25 | t.end() 26 | } 27 | ) 28 | 29 | test( 30 | 'unsupported languages get default/fallback content', 31 | function (t) { 32 | // These are locales for which we do not have explicit translations 33 | var locales = [ 34 | // [ locale, expected result ] 35 | [ '', 'en' ], 36 | [ 'en-US', 'en' ], 37 | [ 'en-CA', 'en' ], 38 | [ 'db-LB', 'en' ], 39 | [ 'el-GR', 'en' ], 40 | [ 'es-BO', 'es' ], 41 | [ 'fr-FR', 'fr' ], 42 | [ 'fr-CA', 'fr' ], 43 | ] 44 | 45 | locales.forEach(function(lang) { 46 | t.equal(lang[1], mailer.translator(lang[0]).language) 47 | }) 48 | t.end() 49 | } 50 | ) 51 | 52 | test( 53 | 'accept-language handled correctly', 54 | function (t) { 55 | // These are the Accept-Language headers from Firefox 37 L10N builds 56 | var locales = [ 57 | // [ accept-language, expected result ] 58 | [ 'bogus-value', 'en' ], 59 | [ 'en-US,en;q=0.5', 'en' ], 60 | [ 'es-AR,es;q=0.8,en-US;q=0.5,en;q=0.3', 'es-AR' ], 61 | [ 'es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3', 'es' ], 62 | [ 'sv-SE,sv;q=0.8,en-US;q=0.5,en;q=0.3', 'sv-SE' ], 63 | [ 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'zh-CN' ] 64 | ] 65 | 66 | locales.forEach(function(lang) { 67 | t.equal(lang[1], mailer.translator(lang[0]).language) 68 | }) 69 | t.end() 70 | } 71 | ) 72 | 73 | test( 74 | 'teardown', 75 | function (t) { 76 | mailer.stop() 77 | t.end() 78 | } 79 | ) 80 | } 81 | ) 82 | 83 | 84 | -------------------------------------------------------------------------------- /test/remote/session_destroy_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('tap').test 6 | var TestServer = require('../test_server') 7 | var Client = require('../client') 8 | 9 | var config = require('../../config').getProperties() 10 | 11 | TestServer.start(config) 12 | .then(function main(server) { 13 | 14 | test( 15 | 'session destroy', 16 | function (t) { 17 | var email = server.uniqueEmail() 18 | var password = 'foobar' 19 | var client = null 20 | var sessionToken = null 21 | return Client.createAndVerify(config.publicUrl, email, password, server.mailbox) 22 | .then( 23 | function (x) { 24 | client = x 25 | return client.sessionStatus() 26 | } 27 | ) 28 | .then( 29 | function () { 30 | sessionToken = client.sessionToken 31 | return client.destroySession() 32 | } 33 | ) 34 | .then( 35 | function () { 36 | t.equal(client.sessionToken, null, 'session token deleted') 37 | client.sessionToken = sessionToken 38 | return client.sessionStatus() 39 | } 40 | ) 41 | .then( 42 | function (status) { 43 | t.fail('got status with destroyed session') 44 | }, 45 | function (err) { 46 | t.equal(err.errno, 110, 'session is invalid') 47 | } 48 | ) 49 | } 50 | ) 51 | 52 | test( 53 | 'session status with valid token', 54 | function (t) { 55 | var email = server.uniqueEmail() 56 | var password = 'testx' 57 | var uid = null 58 | return Client.create(config.publicUrl, email, password) 59 | .then( 60 | function (c) { 61 | uid = c.uid 62 | return c.login() 63 | .then( 64 | function () { 65 | return c.api.sessionStatus(c.sessionToken) 66 | } 67 | ) 68 | } 69 | ) 70 | .then( 71 | function (x) { 72 | t.deepEqual(x, { uid: uid }, 'good status') 73 | } 74 | ) 75 | } 76 | ) 77 | 78 | test( 79 | 'session status with invalid token', 80 | function (t) { 81 | var client = new Client(config.publicUrl) 82 | return client.api.sessionStatus('0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF') 83 | .then( 84 | t.fail, 85 | function (err) { 86 | t.equal(err.errno, 110, 'invalid token') 87 | } 88 | ) 89 | } 90 | ) 91 | 92 | test( 93 | 'teardown', 94 | function (t) { 95 | server.stop() 96 | t.end() 97 | } 98 | ) 99 | }) 100 | -------------------------------------------------------------------------------- /lib/routes/defaults.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var path = require('path') 6 | var cp = require('child_process') 7 | const util = require('util') 8 | 9 | var version = require('../../package.json').version 10 | var commitHash 11 | var sourceRepo 12 | 13 | // Production and stage provide './config/version.json'. Try to load this at 14 | // startup; punt on failure. For dev environments, we'll get this from `git` 15 | // for dev environments. 16 | try { 17 | var versionJson = path.join(__dirname, '..', '..', 'config', 'version.json') 18 | var info = require(versionJson) 19 | commitHash = info.version.hash 20 | sourceRepo = info.version.source 21 | } catch (e) { 22 | /* ignore */ 23 | } 24 | 25 | module.exports = function (log, P, db, error) { 26 | 27 | function versionHandler(request, reply) { 28 | log.begin('Defaults.root', request) 29 | 30 | function sendReply() { 31 | reply( 32 | { 33 | version: version, 34 | commit: commitHash, 35 | source: sourceRepo 36 | } 37 | ).spaces(2).suffix('\n') 38 | } 39 | 40 | // if we already have the commitHash, send the reply and return 41 | if (commitHash) { 42 | return sendReply() 43 | } 44 | 45 | // ignore errors and default to 'unknown' if not found 46 | var gitDir = path.resolve(__dirname, '..', '..', '.git') 47 | var cmd = util.format('git --git-dir=%s rev-parse HEAD', gitDir) 48 | cp.exec(cmd, function(err, stdout1) { 49 | var configPath = path.join(gitDir, 'config') 50 | var cmd = util.format('git config --file %s --get remote.origin.url', configPath) 51 | cp.exec(cmd, function(err, stdout2) { 52 | commitHash = (stdout1 && stdout1.trim()) || 'unknown' 53 | sourceRepo = (stdout2 && stdout2.trim()) || 'unknown' 54 | return sendReply() 55 | }) 56 | }) 57 | } 58 | 59 | var routes = [ 60 | { 61 | method: 'GET', 62 | path: '/', 63 | handler: versionHandler 64 | }, 65 | { 66 | method: 'GET', 67 | path: '/__version__', 68 | handler: versionHandler 69 | }, 70 | { 71 | method: 'GET', 72 | path: '/__heartbeat__', 73 | handler: function heartbeat(request, reply) { 74 | log.begin('Defaults.heartbeat', request) 75 | db.ping() 76 | .done( 77 | function () { 78 | reply({}) 79 | }, 80 | function (err) { 81 | log.error({ op: 'heartbeat', err: err }) 82 | reply(error.serviceUnavailable()) 83 | } 84 | ) 85 | } 86 | }, 87 | { 88 | method: '*', 89 | path: '/v0/{p*}', 90 | handler: function v0(request, reply) { 91 | log.begin('Defaults.v0', request) 92 | reply(error.gone()) 93 | } 94 | } 95 | ] 96 | 97 | return routes 98 | } 99 | -------------------------------------------------------------------------------- /lib/tokens/bundle.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | 6 | /* Utility functions for working with encrypted data bundles. 7 | * 8 | * This module provides 'bundle' and 'unbundle' functions that perform the 9 | * simple encryption operations required by the fxa-auth-server API. The 10 | * encryption works as follows: 11 | * 12 | * * Input is some master key material, a string identifying the context 13 | * of the data, and a payload to be encrypted. 14 | * 15 | * * HKDF is used to derive a 32-byte HMAC key and an encryption key of 16 | * length equal to the payload. The context string is used to ensure 17 | * that these keys are unique to this encryption context. 18 | * 19 | * * The payload is XORed with the encryption key, then HMACed using the 20 | * HMAC key. 21 | * 22 | * * Output is the hex-encoded concatenation of the ciphertext and HMAC. 23 | * 24 | */ 25 | 26 | 27 | module.exports = function (crypto, P, hkdf, butil, error) { 28 | 29 | 30 | var HASH_ALGORITHM = 'sha256' 31 | 32 | 33 | function Bundle() {} 34 | 35 | 36 | // Encrypt the given buffer into a hex ciphertext string. 37 | // 38 | Bundle.bundle = function (key, keyInfo, payload) { 39 | return deriveBundleKeys(key, keyInfo, payload.length) 40 | .then( 41 | function (keys) { 42 | var ciphertext = butil.xorBuffers(payload, keys.xorKey) 43 | var hmac = crypto.createHmac(HASH_ALGORITHM, keys.hmacKey) 44 | hmac.update(ciphertext) 45 | var mac = hmac.digest() 46 | return Buffer.concat([ciphertext, mac]).toString('hex') 47 | } 48 | ) 49 | } 50 | 51 | 52 | // Decrypt the given hex string into a buffer of plaintext data. 53 | // 54 | Bundle.unbundle = function (key, keyInfo, payload) { 55 | payload = Buffer(payload, 'hex') 56 | var ciphertext = payload.slice(0, -32) 57 | var expectedHmac = payload.slice(-32) 58 | return deriveBundleKeys(key, keyInfo, ciphertext.length) 59 | .then( 60 | function (keys) { 61 | var hmac = crypto.createHmac(HASH_ALGORITHM, keys.hmacKey) 62 | hmac.update(ciphertext) 63 | var mac = hmac.digest() 64 | if (!butil.buffersAreEqual(mac, expectedHmac)) { 65 | throw error.invalidSignature() 66 | } 67 | return butil.xorBuffers(ciphertext, keys.xorKey) 68 | } 69 | ) 70 | } 71 | 72 | 73 | // Derive the HMAC and XOR keys required to encrypt a given size of payload. 74 | // 75 | function deriveBundleKeys(key, keyInfo, payloadSize) { 76 | return hkdf(key, keyInfo, null, 32 + payloadSize) 77 | .then( 78 | function (keyMaterial) { 79 | return { 80 | hmacKey: keyMaterial.slice(0, 32), 81 | xorKey: keyMaterial.slice(32) 82 | } 83 | } 84 | ) 85 | } 86 | 87 | return Bundle 88 | } 89 | -------------------------------------------------------------------------------- /test/local/butil_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('tap').test 6 | var butil = require('../../lib/crypto/butil') 7 | 8 | test( 9 | 'buffersAreEqual returns false if lengths are different', 10 | function (t) { 11 | t.equal(butil.buffersAreEqual(Buffer(2), Buffer(4)), false) 12 | t.end() 13 | } 14 | ) 15 | 16 | test( 17 | 'buffersAreEqual returns true if buffers have same bytes', 18 | function (t) { 19 | var b1 = Buffer('abcd', 'hex') 20 | var b2 = Buffer('abcd', 'hex') 21 | t.equal(butil.buffersAreEqual(b1, b2), true) 22 | t.end() 23 | } 24 | ) 25 | 26 | test( 27 | 'xorBuffers throws an Error if lengths are different', 28 | function (t) { 29 | try { 30 | butil.xorBuffers(Buffer(2), Buffer(4)) 31 | } 32 | catch (e) { 33 | return t.end() 34 | } 35 | t.fail('did not throw') 36 | } 37 | ) 38 | 39 | test( 40 | 'xorBuffers works', 41 | function (t) { 42 | var b1 = Buffer('e5', 'hex') 43 | var b2 = Buffer('5e', 'hex') 44 | t.deepEqual(butil.xorBuffers(b1, b2), Buffer('bb', 'hex')) 45 | t.end() 46 | } 47 | ) 48 | 49 | test( 50 | 'bufferize works', 51 | function (t) { 52 | var argument = { foo: 'bar', baz: 'f00d' } 53 | var result = butil.bufferize(argument) 54 | t.notEqual(result, argument) 55 | t.equal(Object.keys(result).length, Object.keys(argument).length) 56 | t.equal(typeof result.foo, 'string') 57 | t.equal(result.foo, argument.foo) 58 | t.ok(Buffer.isBuffer(result.baz)) 59 | t.equal(result.baz.length, 2) 60 | t.equal(result.baz[0], 0xf0) 61 | t.equal(result.baz[1], 0x0d) 62 | t.end() 63 | } 64 | ) 65 | 66 | test( 67 | 'bufferize works in-place', 68 | function (t) { 69 | var argument = { foo: 'beef', bar: 'baz' } 70 | var result = butil.bufferize(argument, { inplace: true }) 71 | t.equal(result, argument) 72 | t.equal(Object.keys(result).length, 2) 73 | t.ok(Buffer.isBuffer(result.foo)) 74 | t.equal(result.foo.length, 2) 75 | t.equal(result.foo[0], 0xbe) 76 | t.equal(result.foo[1], 0xef) 77 | t.equal(typeof result.bar, 'string') 78 | t.equal(result.bar, 'baz') 79 | t.end() 80 | } 81 | ) 82 | 83 | test( 84 | 'bufferize ignores exceptions', 85 | function (t) { 86 | var argument = { foo: 'bar', baz: 'f00d', qux: 'beef' } 87 | var result = butil.bufferize(argument, { ignore: [ 'baz' ] }) 88 | t.notEqual(argument, result) 89 | t.equal(Object.keys(result).length, Object.keys(argument).length) 90 | t.equal(typeof result.foo, 'string') 91 | t.equal(result.foo, argument.foo) 92 | t.equal(typeof result.baz, 'string') 93 | t.equal(result.baz, argument.baz) 94 | t.ok(Buffer.isBuffer(result.qux)) 95 | t.equal(result.qux.length, 2) 96 | t.equal(result.qux[0], 0xbe) 97 | t.equal(result.qux[1], 0xef) 98 | t.end() 99 | } 100 | ) 101 | 102 | -------------------------------------------------------------------------------- /lib/routes/utils/request_helper.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /** 6 | * Returns `true` if request has a keys=true query param. 7 | * 8 | * @param request 9 | * @returns {boolean} 10 | */ 11 | function wantsKeys (request) { 12 | return request.query.keys === 'true' 13 | } 14 | 15 | /** 16 | * Returns whether or not to use token-verification feature on a request. 17 | * 18 | * @param account 19 | * @param config 20 | * @param request 21 | * @returns {boolean} 22 | */ 23 | function shouldEnableTokenVerification(account, config, request) { 24 | 25 | var confirmLogin = config.signinConfirmation && config.signinConfirmation.enabled 26 | if (!confirmLogin) { 27 | return false 28 | } 29 | 30 | // Always create unverified tokens if customs-server 31 | // has said the request is suspicious. 32 | if (request.app.isSuspiciousRequest) { 33 | return true 34 | } 35 | 36 | // Or if the email address matching one of these regexes. 37 | var email = account.email 38 | var isValidEmail = config.signinConfirmation.forceEmailRegex.some(function (reg) { 39 | var emailReg = new RegExp(reg) 40 | return emailReg.test(email) 41 | }) 42 | 43 | if (isValidEmail) { 44 | return true 45 | } 46 | 47 | // While we're testing this feature, there may be some funky 48 | // edge-cases in device login flows that haven't been fully tested. 49 | // Temporarily avoid them for regular users by checking the `context` flag, 50 | // and create pre-verified sessions for unsupported clients. 51 | // This check will go away in the final version of this feature. 52 | var context = request.payload && request.payload.metricsContext && request.payload.metricsContext.context 53 | var isValidContext = context && (config.signinConfirmation.supportedClients.indexOf(context) > -1) 54 | if (!isValidContext) { 55 | return false 56 | } 57 | 58 | // Check to see if user in roll-out cohort. 59 | // Cohort is determined by user's uid. 60 | var uid = account.uid.toString('hex') 61 | var uidNum = parseInt(uid.substr(0, 4), 16) % 100 62 | return uidNum < (config.signinConfirmation.sample_rate * 100) 63 | } 64 | 65 | /** 66 | * Returns whether or not to send the verify account email on a login 67 | * attempt. This never sends a verification email to an already verified email. 68 | * 69 | * @param request 70 | * @returns {boolean} 71 | */ 72 | function shouldSendVerifyAccountEmail(account, request) { 73 | 74 | var sendEmailIfUnverified = request.query.sendEmailIfUnverified 75 | 76 | // Only the content-server sends metrics context. For all non content-server 77 | // requests, send the verification email. 78 | var context = !!(request.payload && request.payload.metricsContext) 79 | 80 | return (!context || !!sendEmailIfUnverified) && !account.emailVerified 81 | } 82 | 83 | module.exports = { 84 | wantsKeys: wantsKeys, 85 | shouldEnableTokenVerification: shouldEnableTokenVerification, 86 | shouldSendVerifyAccountEmail: shouldSendVerifyAccountEmail 87 | } 88 | -------------------------------------------------------------------------------- /lib/pool.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var P = require('./promise') 6 | var Poolee = require('poolee') 7 | 8 | function parseUrl(url) { 9 | var match = /([a-zA-Z]+):\/\/(\S+)/.exec(url) 10 | if (match) { 11 | return { 12 | protocol: match[1], 13 | host: match[2] 14 | } 15 | } 16 | throw new Error('url is invalid: ' + url) 17 | } 18 | 19 | function Pool(url, options) { 20 | options = options || {} 21 | var parsedUrl = parseUrl(url) 22 | var protocol = require(parsedUrl.protocol) 23 | this.poolee = new Poolee( 24 | protocol, 25 | [parsedUrl.host], 26 | { 27 | timeout: options.timeout || 5000, 28 | keepAlive: true, 29 | maxRetries: 0 30 | } 31 | ) 32 | } 33 | 34 | Pool.prototype.request = function (method, path, data) { 35 | var d = P.defer() 36 | this.poolee.request( 37 | { 38 | method: method || 'GET', 39 | path: path, 40 | headers: { 41 | 'Content-Type': 'application/json' 42 | }, 43 | data: data ? JSON.stringify(data) : undefined 44 | }, 45 | handleResponse 46 | ) 47 | return d.promise 48 | 49 | function handleResponse (err, res, body) { 50 | var parsedBody = safeParse(body) 51 | 52 | if (err) { 53 | return d.reject(err) 54 | } 55 | 56 | if (res.statusCode < 200 || res.statusCode >= 300) { 57 | var error = parsedBody || new Error(body) 58 | error.statusCode = res.statusCode 59 | return d.reject(error) 60 | } 61 | 62 | if (! body) { 63 | return d.resolve() 64 | } 65 | 66 | if (! parsedBody) { 67 | return d.reject(new Error('Invalid JSON')) 68 | } 69 | 70 | d.resolve(parsedBody) 71 | } 72 | } 73 | 74 | Pool.prototype.post = function (path, data) { 75 | return this.request('POST', path, data) 76 | } 77 | 78 | Pool.prototype.put = function (path, data) { 79 | return this.request('PUT', path, data) 80 | } 81 | 82 | Pool.prototype.get = function (path) { 83 | return this.request('GET', path) 84 | } 85 | 86 | Pool.prototype.del = function (path, data) { 87 | return this.request('DELETE', path, data) 88 | } 89 | 90 | Pool.prototype.head = function (path) { 91 | return this.request('HEAD', path) 92 | } 93 | 94 | Pool.prototype.close = function () { 95 | /*/ 96 | This is a hack to coax the server to close its existing connections 97 | /*/ 98 | var socketCount = this.poolee.options.maxSockets || 20 99 | function noop() {} 100 | for (var i = 0; i < socketCount; i++) { 101 | this.poolee.request( 102 | { 103 | method: 'GET', 104 | path: '/', 105 | headers: { 106 | 'Connection': 'close' 107 | } 108 | }, 109 | noop 110 | ) 111 | } 112 | } 113 | 114 | module.exports = Pool 115 | 116 | function safeParse (json) { 117 | try { 118 | return JSON.parse(json) 119 | } 120 | catch (e) { 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fxa-auth-server", 3 | "version": "1.69.0", 4 | "description": "Firefox Accounts, an identity provider for Mozilla cloud services", 5 | "bin": { 6 | "fxa-auth": "./bin/key_server.js" 7 | }, 8 | "directories": { 9 | "test": "test" 10 | }, 11 | "scripts": { 12 | "test": "NODE_ENV=dev CORS_ORIGIN=http://foo,http://bar scripts/test-local.sh", 13 | "start": "NODE_ENV=dev scripts/start-local.sh 2>&1", 14 | "start-mysql": "NODE_ENV=dev scripts/start-local-mysql.sh 2>&1", 15 | "test-quick": "npm run tq", 16 | "test-e2e": "NODE_ENV=dev tap test/e2e 2>/dev/null", 17 | "tq": "NODE_ENV=dev tap test/local 2>/dev/null && NODE_ENV=dev CORS_ORIGIN=https://bar scripts/test-remote-quick.js", 18 | "test-remote": "MAILER_HOST=restmail.net MAILER_PORT=80 CORS_ORIGIN=http://baz tap --timeout=300 --tap test/remote", 19 | "prepush": "grunt quicklint" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/mozilla/fxa-auth-server.git" 24 | }, 25 | "engines": { 26 | "node": ">=4.5.0" 27 | }, 28 | "bugs": "https://github.com/mozilla/fxa-auth-server/issues/", 29 | "homepage": "https://github.com/mozilla/fxa-auth-server/", 30 | "license": "MPL-2.0", 31 | "author": "Mozilla (https://mozilla.org/)", 32 | "readmeFilename": "README.md", 33 | "dependencies": { 34 | "ajv": "4.1.7", 35 | "aws-sdk": "2.2.10", 36 | "base64url": "1.0.6", 37 | "binary-split": "0.1.2", 38 | "bluebird": "2.10.2", 39 | "buffer-equal-constant-time": "1.0.1", 40 | "convict": "1.3.0", 41 | "email-addresses": "2.0.2", 42 | "envc": "2.4.0", 43 | "fxa-auth-mailer": "git+https://github.com/mozilla/fxa-auth-mailer.git#master", 44 | "fxa-geodb": "0.0.7", 45 | "fxa-jwtool": "0.7.1", 46 | "fxa-shared": "1.0.2", 47 | "hapi": "15.0.3", 48 | "hapi-auth-hawk": "3.0.1", 49 | "hapi-fxa-oauth": "2.2.0", 50 | "hkdf": "0.0.2", 51 | "inert": "4.0.2", 52 | "joi": "9.0.4", 53 | "memcached": "2.2.2", 54 | "mozlog": "2.0.5", 55 | "newrelic": "1.30.1", 56 | "node-statsd": "0.1.1", 57 | "node-uap": "git+https://github.com/vladikoff/node-uap.git#9cdd16247", 58 | "poolee": "1.0.1", 59 | "request": "2.74.0", 60 | "scrypt-hash": "1.1.13", 61 | "through": "2.3.8", 62 | "uuid": "1.4.1", 63 | "web-push": "2.1.1" 64 | }, 65 | "devDependencies": { 66 | "commander": "2.9.0", 67 | "eslint-config-fxa": "2.1.0", 68 | "fxa-auth-db-mysql": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#master", 69 | "fxa-conventional-changelog": "1.1.0", 70 | "grunt": "1.0.1", 71 | "grunt-bump": "0.8.0", 72 | "grunt-conventional-changelog": "6.1.0", 73 | "grunt-copyright": "0.3.0", 74 | "grunt-eslint": "19.0.0", 75 | "grunt-newer": "1.2.0", 76 | "grunt-nsp": "2.3.1", 77 | "hawk": "2.3.1", 78 | "husky": "0.11.7", 79 | "jws": "3.1.3", 80 | "leftpad": "0.0.0", 81 | "load-grunt-tasks": "3.5.2", 82 | "mailparser": "0.6.1", 83 | "nock": "8.0.0", 84 | "proxyquire": "1.7.10", 85 | "simplesmtp": "0.3.35", 86 | "sinon": "1.17.5", 87 | "sjcl": "1.0.6", 88 | "tap": "7.1.2", 89 | "ws": "1.1.1" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /scripts/reset-send-create-batches.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | var commandLineOptions = require('commander') 8 | var fs = require('fs') 9 | var P = require('../lib/promise') 10 | var path = require('path') 11 | 12 | commandLineOptions 13 | .option('-b, --batchsize [size]', 'Number of emails to send in a batch. Defaults to 500', parseInt) 14 | .option('-i, --input ', 'JSON input file') 15 | .option('-o, --output ', 'Directory batch files should be stored.') 16 | .parse(process.argv) 17 | 18 | var BATCH_SIZE = commandLineOptions.batchsize || 500 19 | 20 | var requiredOptions = [ 21 | 'input', 22 | 'output' 23 | ] 24 | 25 | requiredOptions.forEach(checkRequiredOption) 26 | 27 | P.resolve() 28 | .then(readRecords) 29 | .then(createBatches) 30 | .then(writeBatches) 31 | .then(function (batches) { 32 | var msg = batches.length + ' batch' + (batches.length === 1 ? '' : 'es') + ' created' 33 | console.log(msg) 34 | process.exit(0) 35 | }) 36 | 37 | 38 | function checkRequiredOption(optionName) { 39 | if (! commandLineOptions[optionName]) { 40 | console.error('--' + optionName + ' required') 41 | process.exit(1) 42 | } 43 | } 44 | 45 | function readRecords() { 46 | var inputFileName = path.resolve(commandLineOptions.input) 47 | var fsStats 48 | try { 49 | fsStats = fs.statSync(inputFileName) 50 | } catch (e) { 51 | console.error(inputFileName, 'invalid filename') 52 | process.exit(1) 53 | } 54 | 55 | if (! fsStats.isFile()) { 56 | console.error(inputFileName, 'is not a file') 57 | process.exit(1) 58 | } 59 | 60 | var records = [] 61 | try { 62 | records = require(inputFileName) 63 | } catch(e) { 64 | console.error(inputFileName, 'does not contain JSON') 65 | process.exit(1) 66 | } 67 | 68 | if (! records.length) { 69 | console.error('uh oh, no records found') 70 | process.exit(1) 71 | } 72 | 73 | return records 74 | } 75 | 76 | function createBatches(records) { 77 | var batches = [] 78 | 79 | while (records.length) { 80 | var batch = records.splice(0, BATCH_SIZE) 81 | batches.push(batch) 82 | } 83 | 84 | return batches 85 | } 86 | 87 | function writeBatches(batches) { 88 | var outputDirectory = path.resolve(commandLineOptions.output) 89 | ensureOutputDirExists(outputDirectory) 90 | 91 | batches.forEach(function (batch, index) { 92 | var outputPath = path.join(outputDirectory, index + '.json') 93 | fs.writeFileSync(outputPath, JSON.stringify(batch, null, 2)) 94 | }) 95 | 96 | return batches 97 | } 98 | 99 | function ensureOutputDirExists(outputDir) { 100 | var dirStats 101 | try { 102 | dirStats = fs.statSync(outputDir) 103 | } catch (e) { 104 | fs.mkdirSync(outputDir) 105 | return 106 | } 107 | 108 | if (! dirStats.isDirectory()) { 109 | console.error(outputDir + ' is not a directory') 110 | process.exit(1) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/bounces.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var eaddrs = require('email-addresses') 6 | var P = require('./promise') 7 | 8 | module.exports = function (log, error) { 9 | 10 | return function start(bounceQueue, db) { 11 | 12 | function accountDeleted(uid, email) { 13 | log.info({ op: 'accountDeleted', uid: uid.toString('hex'), email: email }) 14 | } 15 | 16 | function gotError(email, err) { 17 | log.error({ op: 'databaseError', email: email, err: err }) 18 | } 19 | 20 | function findEmailRecord(email) { 21 | return db.emailRecord(email) 22 | .catch(function (err) { 23 | // The mail agent may have mangled the email address 24 | // that's being reported in the bounce notification. 25 | // Try looking up by normalized form as well. 26 | if (err.errno !== error.ERRNO.ACCOUNT_UNKNOWN) { 27 | throw err 28 | } 29 | var normalizedEmail = eaddrs.parseOneAddress(email).address 30 | if (normalizedEmail === email) { 31 | throw err 32 | } 33 | return db.emailRecord(normalizedEmail) 34 | }) 35 | } 36 | 37 | function deleteAccountIfUnverified(record) { 38 | if (!record.emailVerified) { 39 | return db.deleteAccount(record) 40 | .then( 41 | accountDeleted.bind(null, record.uid, record.email), 42 | gotError.bind(null, record.email) 43 | ) 44 | } else { 45 | // A previously-verified email is now bouncing. 46 | // We don't know what to do here, yet. 47 | // But we can measure it! 48 | log.increment('account.email_bounced.already_verified') 49 | } 50 | } 51 | 52 | function handleBounce(message) { 53 | var recipients = [] 54 | if (message.bounce && message.bounce.bounceType === 'Permanent') { 55 | recipients = message.bounce.bouncedRecipients 56 | } 57 | else if (message.complaint && message.complaint.complaintFeedbackType === 'abuse') { 58 | recipients = message.complaint.complainedRecipients 59 | } 60 | return P.each(recipients, function (recipient) { 61 | var email = recipient.emailAddress 62 | log.info({ 63 | op: 'handleBounce', 64 | action: recipient.action, 65 | email: email, 66 | bounce: !!message.bounce, 67 | diagnosticCode: recipient.diagnosticCode, 68 | status: recipient.status 69 | }) 70 | log.increment('account.email_bounced') 71 | return findEmailRecord(email) 72 | .then( 73 | deleteAccountIfUnverified, 74 | gotError.bind(null, email) 75 | ) 76 | }).then( 77 | function () { 78 | // We always delete the message, even if handling some addrs failed. 79 | message.del() 80 | } 81 | ) 82 | } 83 | 84 | bounceQueue.on('data', handleBounce) 85 | bounceQueue.start() 86 | 87 | return { 88 | bounceQueue: bounceQueue, 89 | handleBounce: handleBounce 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/remote/recovery_email_verify_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var url = require('url') 7 | var Client = require('../client') 8 | var TestServer = require('../test_server') 9 | 10 | var config = require('../../config').getProperties() 11 | 12 | TestServer.start(config) 13 | .then(function main(server) { 14 | 15 | test( 16 | 'create account verify with incorrect code', 17 | function (t) { 18 | var email = server.uniqueEmail() 19 | var password = 'allyourbasearebelongtous' 20 | var client = null 21 | return Client.create(config.publicUrl, email, password) 22 | .then( 23 | function (x) { 24 | client = x 25 | } 26 | ) 27 | .then( 28 | function () { 29 | return client.emailStatus() 30 | } 31 | ) 32 | .then( 33 | function (status) { 34 | t.equal(status.verified, false, 'new account is not verified') 35 | } 36 | ) 37 | .then( 38 | function () { 39 | return client.verifyEmail('00000000000000000000000000000000') 40 | } 41 | ) 42 | .then( 43 | function () { 44 | t.fail('verified email with bad code') 45 | }, 46 | function (err) { 47 | t.equal(err.message.toString(), 'Invalid verification code', 'bad attempt') 48 | } 49 | ) 50 | .then( 51 | function () { 52 | return client.emailStatus() 53 | } 54 | ) 55 | .then( 56 | function (status) { 57 | t.equal(status.verified, false, 'account not verified') 58 | } 59 | ) 60 | } 61 | ) 62 | 63 | test( 64 | 'verification email link', 65 | function (t) { 66 | var email = server.uniqueEmail() 67 | var password = 'something' 68 | var client = null // eslint-disable-line no-unused-vars 69 | var options = { 70 | redirectTo: 'https://sync.' + config.smtp.redirectDomain, 71 | service: 'sync' 72 | } 73 | return Client.create(config.publicUrl, email, password, options) 74 | .then( 75 | function (c) { 76 | client = c 77 | } 78 | ) 79 | .then( 80 | function () { 81 | return server.mailbox.waitForEmail(email) 82 | } 83 | ) 84 | .then( 85 | function (emailData) { 86 | var link = emailData.headers['x-link'] 87 | var query = url.parse(link, true).query 88 | t.ok(query.uid, 'uid is in link') 89 | t.ok(query.code, 'code is in link') 90 | t.equal(query.redirectTo, options.redirectTo, 'redirectTo is in link') 91 | t.equal(query.service, options.service, 'service is in link') 92 | } 93 | ) 94 | } 95 | ) 96 | 97 | test( 98 | 'teardown', 99 | function (t) { 100 | server.stop() 101 | t.end() 102 | } 103 | ) 104 | }) 105 | -------------------------------------------------------------------------------- /test/local/error_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var messages = require('joi/lib/language') 7 | var AppError = require('../../lib/error') 8 | 9 | test( 10 | 'tightly-coupled joi message hack is okay', 11 | function (t) { 12 | t.equal(typeof messages.errors.any.required, 'string') 13 | t.not(messages.errors.any.required, '') 14 | t.end() 15 | } 16 | ) 17 | 18 | test( 19 | 'exported functions exist', 20 | function (t) { 21 | t.equal(typeof AppError, 'function') 22 | t.equal(AppError.length, 3) 23 | t.equal(typeof AppError.translate, 'function') 24 | t.equal(AppError.translate.length, 1) 25 | t.equal(typeof AppError.invalidRequestParameter, 'function') 26 | t.equal(AppError.invalidRequestParameter.length, 1) 27 | t.equal(typeof AppError.missingRequestParameter, 'function') 28 | t.equal(AppError.missingRequestParameter.length, 1) 29 | t.end() 30 | } 31 | ) 32 | 33 | test( 34 | 'error.translate with missing required parameters', 35 | function (t) { 36 | var result = AppError.translate({ 37 | output: { 38 | payload: { 39 | message: 'foo' + messages.errors.any.required, 40 | validation: { 41 | keys: [ 'bar', 'baz' ] 42 | } 43 | } 44 | } 45 | }) 46 | t.ok(result instanceof AppError, 'instanceof AppError') 47 | t.equal(result.errno, 108) 48 | t.equal(result.message, 'Missing parameter in request body: bar') 49 | t.equal(result.output.statusCode, 400) 50 | t.equal(result.output.payload.error, 'Bad Request') 51 | t.equal(result.output.payload.errno, result.errno) 52 | t.equal(result.output.payload.message, result.message) 53 | t.equal(result.output.payload.param, 'bar') 54 | t.end() 55 | } 56 | ) 57 | 58 | test( 59 | 'error.translate with invalid parameter', 60 | function (t) { 61 | var result = AppError.translate({ 62 | output: { 63 | payload: { 64 | validation: 'foo' 65 | } 66 | } 67 | }) 68 | t.ok(result instanceof AppError, 'instanceof AppError') 69 | t.equal(result.errno, 107) 70 | t.equal(result.message, 'Invalid parameter in request body') 71 | t.equal(result.output.statusCode, 400) 72 | t.equal(result.output.payload.error, 'Bad Request') 73 | t.equal(result.output.payload.errno, result.errno) 74 | t.equal(result.output.payload.message, result.message) 75 | t.equal(result.output.payload.validation, 'foo') 76 | t.end() 77 | } 78 | ) 79 | 80 | test( 81 | 'tooManyRequests', 82 | function (t) { 83 | var result = AppError.tooManyRequests(900, 'in 15 minutes') 84 | t.ok(result instanceof AppError, 'instanceof AppError') 85 | t.equal(result.errno, 114) 86 | t.equal(result.message, 'Client has sent too many requests') 87 | t.equal(result.output.statusCode, 429) 88 | t.equal(result.output.payload.error, 'Too Many Requests') 89 | t.equal(result.output.payload.retryAfter, 900) 90 | t.equal(result.output.payload.retryAfterLocalized, 'in 15 minutes') 91 | 92 | result = AppError.tooManyRequests(900) 93 | t.equal(result.output.payload.retryAfter, 900) 94 | t.notOk(result.output.payload.retryAfterLocalized) 95 | 96 | t.end() 97 | } 98 | ) 99 | -------------------------------------------------------------------------------- /test/remote/account_locale_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var TestServer = require('../test_server') 7 | var Client = require('../client') 8 | 9 | var config = require('../../config').getProperties() 10 | var key = { 11 | 'algorithm': 'RS', 12 | 'n': '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123', 13 | 'e': '65537' 14 | } 15 | 16 | TestServer.start(config) 17 | .then(function main(server) { 18 | 19 | test( 20 | 'signing a cert against an account with no locale will save the locale', 21 | function (t) { 22 | var email = server.uniqueEmail() 23 | var password = 'ilikepancakes' 24 | var client 25 | return Client.createAndVerify(config.publicUrl, email, password, server.mailbox) 26 | .then( 27 | function (c) { 28 | client = c 29 | return c.api.accountStatus(c.uid, c.sessionToken) 30 | } 31 | ) 32 | .then( 33 | function (response) { 34 | t.ok(!response.locale, 'account has no locale') 35 | return client.login() 36 | } 37 | ) 38 | .then( 39 | function () { 40 | return client.api.certificateSign( 41 | client.sessionToken, 42 | key, 43 | 1000, 44 | 'en-US' 45 | ) 46 | } 47 | ) 48 | .then( 49 | function () { 50 | return client.api.accountStatus(client.uid, client.sessionToken) 51 | } 52 | ) 53 | .then( 54 | function (response) { 55 | t.equal(response.locale, 'en-US', 'account has a locale') 56 | } 57 | ) 58 | } 59 | ) 60 | 61 | test( 62 | 'a really long (invalid) locale', 63 | function (t) { 64 | var email = server.uniqueEmail() 65 | var password = 'ilikepancakes' 66 | return Client.create( 67 | config.publicUrl, 68 | email, 69 | password, 70 | { lang: Buffer(128).toString('hex') } 71 | ) 72 | .then( 73 | function (c) { 74 | return c.api.accountStatus(c.uid, c.sessionToken) 75 | } 76 | ) 77 | .then( 78 | function (response) { 79 | t.ok(!response.locale, 'account has no locale') 80 | } 81 | ) 82 | } 83 | ) 84 | 85 | test( 86 | 'a really long (valid) locale', 87 | function (t) { 88 | var email = server.uniqueEmail() 89 | var password = 'ilikepancakes' 90 | return Client.create( 91 | config.publicUrl, 92 | email, 93 | password, 94 | { lang: 'en-US,en;q=0.8,' + Buffer(128).toString('hex') } 95 | ) 96 | .then( 97 | function (c) { 98 | return c.api.accountStatus(c.uid, c.sessionToken) 99 | } 100 | ) 101 | .then( 102 | function (response) { 103 | t.equal(response.locale, 'en-US,en;q=0.8', 'account has no locale') 104 | } 105 | ) 106 | } 107 | ) 108 | 109 | test( 110 | 'teardown', 111 | function (t) { 112 | server.stop() 113 | t.end() 114 | } 115 | ) 116 | }) 117 | -------------------------------------------------------------------------------- /lib/userAgent.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | 'use strict' 6 | 7 | var ua = require('node-uap') 8 | 9 | var MOBILE_OS_FAMILIES = { 10 | 'Android': null, 11 | 'Bada': null, 12 | 'BlackBerry OS': null, 13 | 'BlackBerry Tablet OS': null, 14 | 'Brew MP': null, 15 | 'Firefox OS': null, 16 | 'iOS': null, 17 | 'Maemo': null, 18 | 'MeeGo': null, 19 | 'Symbian OS': null, 20 | 'Symbian^3': null, 21 | 'Symbian^3 Anna': null, 22 | 'Symbian^3 Belle': null, 23 | 'Windows CE': null, 24 | 'Windows Mobile': null, 25 | 'Windows Phone': null 26 | } 27 | 28 | var ELLIPSIS = '\u2026' 29 | 30 | module.exports = function (userAgentString, log) { 31 | var userAgentData = ua.parse(userAgentString) 32 | 33 | this.uaBrowser = getFamily(userAgentData.ua) || null 34 | this.uaBrowserVersion = getVersion(userAgentData.ua) || null 35 | this.uaOS = getFamily(userAgentData.os) || null 36 | this.uaOSVersion = getVersion(userAgentData.os) || null 37 | this.uaDeviceType = getDeviceType(userAgentData) || null 38 | 39 | if (! this.uaBrowser && ! this.uaOS) { 40 | // In the worst case, fall back to a truncated user agent string 41 | this.uaBrowser = truncate(userAgentString || '', log) 42 | } 43 | 44 | return this 45 | } 46 | 47 | function getFamily (data) { 48 | if (data.family && data.family !== 'Other') { 49 | return data.family 50 | } 51 | } 52 | 53 | function getVersion (data) { 54 | if (! data.major) { 55 | return 56 | } 57 | 58 | if (! data.minor || parseInt(data.minor) === 0) { 59 | return data.major 60 | } 61 | 62 | return data.major + '.' + data.minor 63 | } 64 | 65 | function getDeviceType (data) { 66 | if (getFamily(data.device) || isMobileOS(data.os)) { 67 | if (isTablet(data)) { 68 | return 'tablet' 69 | } else { 70 | return 'mobile' 71 | } 72 | } 73 | } 74 | 75 | function isMobileOS (os) { 76 | return os.family in MOBILE_OS_FAMILIES 77 | } 78 | 79 | function isTablet(data) { 80 | // 'tablets' are iPads and Android devices with no word 'Mobile' in them. 81 | // Ref: https://webmasters.googleblog.com/2011/03/mo-better-to-also-detect-mobile-user.html 82 | if (getFamily(data.device)) { 83 | if (data.device.family === 'iPad' || 84 | (data.os && data.os.family === 'Android' && data.userAgent.indexOf('Mobile') === -1) 85 | ) { 86 | return true 87 | } 88 | } 89 | 90 | return false 91 | } 92 | 93 | function truncate (userAgentString, log) { 94 | log.info({ 95 | op: 'userAgent:truncate', 96 | userAgent: userAgentString 97 | }) 98 | 99 | // Completely arbitrary truncation length. This should be a very rare 100 | // condition, so we just want something that is long enough to convey 101 | // some meaningful information without being too messy. 102 | var length = 60 103 | 104 | if (userAgentString.length < length) { 105 | return userAgentString 106 | } 107 | 108 | if (/.+\(.+\)/.test(userAgentString)) { 109 | var openingIndex = userAgentString.indexOf('(') 110 | var closingIndex = userAgentString.indexOf(')') 111 | 112 | if (openingIndex < closingIndex && closingIndex < 100) { 113 | // If there is a closing parenthesis within a reasonable length, 114 | // allow the string to be a bit longer than our arbitrary maximum. 115 | length = closingIndex + 1 116 | } 117 | } 118 | 119 | return userAgentString.substr(0, length) + ELLIPSIS 120 | } 121 | 122 | -------------------------------------------------------------------------------- /test/remote/verification_reminder_db_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var tap = require('tap') 6 | var test = tap.test 7 | var uuid = require('uuid') 8 | var log = { trace: console.log, info: console.log } // eslint-disable-line no-console 9 | 10 | var config = require('../../config').getProperties() 11 | var TestServer = require('../test_server') 12 | var Token = require('../../lib/tokens')(log) 13 | var DB = require('../../lib/db')( 14 | config.db.backend, 15 | log, 16 | Token.error, 17 | Token.SessionToken, 18 | Token.KeyFetchToken, 19 | Token.AccountResetToken, 20 | Token.PasswordForgotToken, 21 | Token.PasswordChangeToken 22 | ) 23 | 24 | var zeroBuffer16 = Buffer('00000000000000000000000000000000', 'hex') 25 | var zeroBuffer32 = Buffer('0000000000000000000000000000000000000000000000000000000000000000', 'hex') 26 | 27 | function createTestAccount() { 28 | return { 29 | uid: uuid.v4('binary'), 30 | email: 'reminder' + Math.random() + '@bar.com', 31 | emailCode: zeroBuffer16, 32 | emailVerified: false, 33 | verifierVersion: 1, 34 | verifyHash: zeroBuffer32, 35 | authSalt: zeroBuffer32, 36 | kA: zeroBuffer32, 37 | wrapWrapKb: zeroBuffer32, 38 | acceptLanguage: 'bg-BG,en-US;q=0.7,ar-BH;q=0.3' 39 | } 40 | } 41 | 42 | var mockLog = require('../mocks').mockLog 43 | 44 | var dbServer, reminderConfig 45 | var dbConn = TestServer.start(config) 46 | .then( 47 | function (server) { 48 | dbServer = server 49 | reminderConfig = process.env.VERIFICATION_REMINDER_RATE 50 | process.env.VERIFICATION_REMINDER_RATE = 1 51 | return DB.connect(config[config.db.backend]) 52 | } 53 | ) 54 | 55 | test( 56 | 'create', 57 | function (t) { 58 | var thisMockLog = mockLog({ 59 | increment: function (name) { 60 | t.equal(name, 'verification-reminders.created') 61 | } 62 | }) 63 | 64 | dbConn.then(function (db) { 65 | var account = createTestAccount() 66 | var reminder = { uid: account.uid.toString('hex') } 67 | 68 | var verificationReminder = require('../../lib/verification-reminders')(thisMockLog, db) 69 | return verificationReminder.create(reminder).then( 70 | function () { 71 | t.end() 72 | }, 73 | function () { 74 | t.fail() 75 | } 76 | ) 77 | }) 78 | } 79 | ) 80 | 81 | test( 82 | 'delete', 83 | function (t) { 84 | var thisMockLog = mockLog({ 85 | increment: function (name) { 86 | if (name === 'verification-reminders.deleted') { 87 | t.ok(true, 'correct log message') 88 | } 89 | } 90 | }) 91 | 92 | dbConn.then(function (db) { 93 | var verificationReminder = require('../../lib/verification-reminders')(thisMockLog, db) 94 | var account = createTestAccount() 95 | var reminder = { uid: account.uid.toString('hex') } 96 | 97 | return verificationReminder.create(reminder) 98 | .then(function () { 99 | return verificationReminder.delete(reminder) 100 | }) 101 | .then(function () { 102 | t.end() 103 | }, function (err) { 104 | t.notOk(err) 105 | }) 106 | }) 107 | } 108 | ) 109 | 110 | test( 111 | 'teardown', 112 | function (t) { 113 | return dbConn.then(function(db) { 114 | return db.close() 115 | }).then(function() { 116 | return dbServer.stop() 117 | }).then(function () { 118 | process.env.VERIFICATION_REMINDER_RATE = reminderConfig 119 | t.end() 120 | }) 121 | } 122 | ) 123 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Anyone is welcome to help with Firefox Accounts. Feel free to get in touch with other community members on IRC, the 4 | mailing list or through issues here on GitHub. 5 | 6 | - IRC: `#fxa` on `irc.mozilla.org` 7 | - Mailing list: 8 | - and of course, [the issues list](https://github.com/mozilla/fxa-auth-server/issues) 9 | 10 | ## Bug Reports ## 11 | 12 | You can file issues here on GitHub. Please try to include as much information as you can and under what conditions 13 | you saw the issue. 14 | 15 | ## Sending Pull Requests ## 16 | 17 | Patches should be submitted as pull requests (PR). 18 | 19 | Before submitting a PR: 20 | - Your code must run and pass all the automated tests before you submit your PR for review. "Work in progress" pull requests are allowed to be submitted, but should be clearly labeled as such and should not be merged until all tests pass and the code has been reviewed. 21 | - Run `grunt eslint` to make sure your code passes linting. 22 | - Run `npm test` to make sure all tests still pass. 23 | - Your patch should include new tests that cover your changes. It is your and your reviewer's responsibility to ensure your patch includes adequate tests. 24 | 25 | When submitting a PR: 26 | - You agree to license your code under the project's open source license ([MPL 2.0](/LICENSE)). 27 | - Base your branch off the current `master` (see below for an example workflow). 28 | - Add both your code and new tests if relevant. 29 | - Run `grunt eslint` and `npm test` to make sure your code passes linting and tests. 30 | - Please do not include merge commits in pull requests; include only commits with the new relevant code. 31 | - Your commit message must follow the 32 | [commit guidelines](https://github.com/mozilla/fxa/blob/master/CONTRIBUTING.md#git-commit-guidelines). 33 | 34 | After your PR is merged: 35 | - Add yourself to the [AUTHORS](/AUTHORS) file so we can publicly recognize your contribution. 36 | 37 | See the main [README.md](/README.md) for information on prerequisites, installing, running and testing. 38 | 39 | ## Code Review ## 40 | 41 | This project is production Mozilla code and subject to our [engineering practices and quality standards](https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Committing_Rules_and_Responsibilities). Every patch must be peer reviewed. This project is part of the [Firefox Accounts module](https://wiki.mozilla.org/Modules/Other#Firefox_Accounts), and your patch must be reviewed by one of the listed module owners or peers. 42 | 43 | ## Example Workflow ## 44 | 45 | This is an example workflow to make it easier to submit Pull Requests. Imagine your username is `user1`: 46 | 47 | 1. Fork this repository via the GitHub interface 48 | 49 | 2. The clone the upstream (as origin) and add your own repo as a remote: 50 | 51 | ```sh 52 | $ git clone https://github.com/mozilla/fxa-auth-server.git 53 | $ cd fxa-auth-server 54 | $ git remote add user1 git@github.com:user1/fxa-auth-server.git 55 | ``` 56 | 57 | 3. Create a branch for your fix/feature and make sure it's your currently checked-out branch: 58 | 59 | ```sh 60 | $ git checkout -b add-new-feature 61 | ``` 62 | 63 | 4. Add/fix code, add tests then commit and push this branch to your repo: 64 | 65 | ```sh 66 | $ git add 67 | $ git commit 68 | $ git push user1 add-new-feature 69 | ``` 70 | 71 | 5. From the GitHub interface for your repo, click the `Review Changes and Pull Request` which appears next to your new branch. 72 | 73 | 6. Click `Send pull request`. 74 | 75 | ### Keeping up to Date ### 76 | 77 | The main reason for creating a new branch for each feature or fix is so that you can track master correctly. If you need 78 | to fetch the latest code for a new fix, try the following: 79 | 80 | ```sh 81 | $ git checkout master 82 | $ git pull 83 | ``` 84 | 85 | Now you're ready to branch again for your new feature (from step 3 above). 86 | -------------------------------------------------------------------------------- /test/remote/account_preverified_token_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('tap').test 6 | var TestServer = require('../test_server') 7 | var Client = require('../client') 8 | var JWTool = require('fxa-jwtool') 9 | 10 | var config = require('../../config').getProperties() 11 | process.env.TRUSTED_JKUS = 'http://127.0.0.1:9000/.well-known/public-keys' 12 | process.env.SIGNIN_CONFIRMATION_ENABLED = false 13 | 14 | var secretKey = JWTool.JWK.fromFile( 15 | config.secretKeyFile, 16 | { 17 | jku: config.publicUrl + '/.well-known/public-keys', 18 | kid: 'dev-1' 19 | } 20 | ) 21 | 22 | function fail() { throw new Error('call succeeded when it should have failed')} 23 | 24 | function nowSeconds() { 25 | return Math.floor(Date.now() / 1000) 26 | } 27 | 28 | TestServer.start(config) 29 | .then(function main(server) { 30 | 31 | test( 32 | 'a valid preVerifyToken creates a verified account', 33 | function (t) { 34 | var email = server.uniqueEmail() 35 | var password = 'ok' 36 | var token = secretKey.signSync( 37 | { 38 | exp: nowSeconds() + 10, 39 | aud: config.domain, 40 | sub: email 41 | } 42 | ) 43 | return Client.create(config.publicUrl, email, password, { preVerifyToken: token, keys: true }) 44 | .then( 45 | function (c) { 46 | return c.keys() 47 | } 48 | ) 49 | .then( 50 | function (keys) { 51 | t.ok(Buffer.isBuffer(keys.kA), 'kA exists') 52 | t.ok(Buffer.isBuffer(keys.wrapKb), 'wrapKb exists') 53 | } 54 | ) 55 | } 56 | ) 57 | 58 | test( 59 | 'an invalid preVerifyToken return an invalid verification code error', 60 | function (t) { 61 | var email = server.uniqueEmail() 62 | var password = 'ok' 63 | var token = secretKey.signSync( 64 | { 65 | exp: nowSeconds() + 10, 66 | aud: config.domain, 67 | sub: 'wrong@example.com' 68 | } 69 | ) 70 | return Client.create(config.publicUrl, email, password, { preVerifyToken: token }) 71 | .then( 72 | fail, 73 | function (err) { 74 | t.equal(err.errno, 105, 'invalid verification code') 75 | } 76 | ) 77 | } 78 | ) 79 | 80 | test( 81 | 're-signup against an unverified email', 82 | function (t) { 83 | var email = server.uniqueEmail() 84 | var password = 'abcdef' 85 | return Client.create(config.publicUrl, email, password) 86 | .then( 87 | function () { 88 | // delete the first verification email 89 | return server.mailbox.waitForEmail(email) 90 | } 91 | ) 92 | .then( 93 | function () { 94 | var token = secretKey.signSync( 95 | { 96 | exp: nowSeconds() + 10, 97 | aud: config.domain, 98 | sub: email 99 | } 100 | ) 101 | return Client.create(config.publicUrl, email, password, { preVerifyToken: token }) 102 | } 103 | ) 104 | .then( 105 | function (client) { 106 | t.ok(client.uid, 'account created') 107 | return client.keys() 108 | } 109 | ) 110 | .then( 111 | function (keys) { 112 | t.ok(Buffer.isBuffer(keys.kA), 'kA exists') 113 | t.ok(Buffer.isBuffer(keys.wrapKb), 'wrapKb exists') 114 | } 115 | ) 116 | } 117 | ) 118 | 119 | test( 120 | 'teardown', 121 | function (t) { 122 | server.stop() 123 | t.end() 124 | } 125 | ) 126 | }) 127 | -------------------------------------------------------------------------------- /test/remote/flow_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('tap').test 6 | var Client = require('../client') 7 | var TestServer = require('../test_server') 8 | var jwtool = require('fxa-jwtool') 9 | 10 | var config = require('../../config').getProperties() 11 | process.env.SIGNIN_CONFIRMATION_ENABLED = false 12 | 13 | var pubSigKey = jwtool.JWK.fromFile(config.publicKeyFile) 14 | 15 | TestServer.start(config) 16 | .then(function main(server) { 17 | 18 | var email1 = server.uniqueEmail() 19 | 20 | test( 21 | 'Create account flow', 22 | function (t) { 23 | var email = email1 24 | var password = 'allyourbasearebelongtous' 25 | var client = null 26 | var publicKey = { 27 | 'algorithm': 'RS', 28 | 'n': '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123', 29 | 'e': '65537' 30 | } 31 | var duration = 1000 * 60 * 60 * 24 // 24 hours 32 | return Client.createAndVerify(config.publicUrl, email, password, server.mailbox, {keys:true}) 33 | .then( 34 | function (x) { 35 | client = x 36 | return client.keys() 37 | } 38 | ) 39 | .then( 40 | function (keys) { 41 | t.ok(Buffer.isBuffer(keys.kA), 'kA exists') 42 | t.ok(Buffer.isBuffer(keys.wrapKb), 'wrapKb exists') 43 | t.ok(Buffer.isBuffer(keys.kB), 'kB exists') 44 | t.equal(client.kB.length, 32, 'kB exists, has the right length') 45 | } 46 | ) 47 | .then( 48 | function () { 49 | return client.sign(publicKey, duration) 50 | } 51 | ) 52 | .then( 53 | function (cert) { 54 | t.equal(typeof(cert), 'string', 'cert exists') 55 | var payload = jwtool.verify(cert, pubSigKey.pem) 56 | t.equal(payload.principal.email.split('@')[0], client.uid, 'cert has correct uid') 57 | } 58 | ) 59 | } 60 | ) 61 | 62 | test( 63 | 'Login flow', 64 | function (t) { 65 | var email = email1 66 | var password = 'allyourbasearebelongtous' 67 | var client = null 68 | var publicKey = { 69 | 'algorithm': 'RS', 70 | 'n': '4759385967235610503571494339196749614544606692567785790953934768202714280652973091341316862993582789079872007974809511698859885077002492642203267408776123', 71 | 'e': '65537' 72 | } 73 | var duration = 1000 * 60 * 60 * 24 // 24 hours 74 | return Client.login(config.publicUrl, email, password, server.mailbox, {keys:true}) 75 | .then( 76 | function (x) { 77 | client = x 78 | t.ok(client.authAt, 'authAt was set') 79 | t.ok(client.uid, 'got a uid') 80 | return client.keys() 81 | } 82 | ) 83 | .then( 84 | function (keys) { 85 | t.ok(Buffer.isBuffer(keys.kA), 'kA exists') 86 | t.ok(Buffer.isBuffer(keys.wrapKb), 'wrapKb exists') 87 | t.ok(Buffer.isBuffer(keys.kB), 'kB exists') 88 | t.equal(client.kB.length, 32, 'kB exists, has the right length') 89 | } 90 | ) 91 | .then( 92 | function () { 93 | return client.sign(publicKey, duration) 94 | } 95 | ) 96 | .then( 97 | function (cert) { 98 | t.equal(typeof(cert), 'string', 'cert exists') 99 | } 100 | ) 101 | } 102 | ) 103 | 104 | test( 105 | 'teardown', 106 | function (t) { 107 | server.stop() 108 | t.end() 109 | } 110 | ) 111 | }) 112 | -------------------------------------------------------------------------------- /docs/pushpayloads.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title":"FxA Push Payload schema", 3 | "description":"This schema defines what is acceptable to send as a payload data with the Push API from the FxA servers to a device connected to FxA", 4 | "$schema":"http://json-schema.org/draft-04/schema#", 5 | "type":"object", 6 | "allOf":[ 7 | { 8 | "type":"object", 9 | "required":[ 10 | "version", 11 | "command" 12 | ], 13 | "properties":{ 14 | "version":{ 15 | "type":"integer", 16 | "description":"The version of this push payload data instance. Bump this if you make changes to any part of this schema." 17 | }, 18 | "command":{ 19 | "type":"string", 20 | "description":"Helps the receiving device discriminate payloads" 21 | } 22 | } 23 | }, 24 | { 25 | "type":"object", 26 | "anyOf":[ 27 | { "$ref":"#/definitions/deviceConnected" }, 28 | { "$ref":"#/definitions/deviceDisconnected" }, 29 | { "$ref":"#/definitions/collectionsChanged" }, 30 | { "$ref":"#/definitions/passwordChanged" }, 31 | { "$ref":"#/definitions/passwordReset" } 32 | ] 33 | } 34 | ], 35 | "definitions":{ 36 | "deviceConnected":{ 37 | "type":"object", 38 | "required":[ 39 | "data" 40 | ], 41 | "properties":{ 42 | "command":{ 43 | "enum":[ 44 | "fxaccounts:device_connected" 45 | ] 46 | }, 47 | "data":{ 48 | "type":"object", 49 | "required":[ 50 | "deviceName" 51 | ], 52 | "properties":{ 53 | "deviceName":{ 54 | "type":"string", 55 | "description":"The name of the device who joined this account" 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | "deviceDisconnected":{ 62 | "type":"object", 63 | "required":[ 64 | "data" 65 | ], 66 | "properties":{ 67 | "command":{ 68 | "enum":[ 69 | "fxaccounts:device_disconnected" 70 | ] 71 | }, 72 | "data":{ 73 | "type":"object", 74 | "required":[ 75 | "id" 76 | ], 77 | "properties":{ 78 | "id":{ 79 | "type":"string", 80 | "description":"The id of the device who was disconnected remotely" 81 | } 82 | } 83 | } 84 | } 85 | }, 86 | "collectionsChanged":{ 87 | "type":"object", 88 | "required":[ 89 | "data" 90 | ], 91 | "properties":{ 92 | "command":{ 93 | "enum":[ 94 | "sync:collection_changed" 95 | ] 96 | }, 97 | "data":{ 98 | "type":"object", 99 | "required":[ 100 | "collections" 101 | ], 102 | "properties":{ 103 | "collections":{ 104 | "type":"array", 105 | "minItems": 1, 106 | "uniqueItems": true, 107 | "description":"A list of collections that were changed", 108 | "items": { 109 | "enum": [ "addons", "bookmarks", "history", "forms", "prefs", 110 | "tabs", "passwords", "clients" ] 111 | } 112 | } 113 | } 114 | } 115 | } 116 | }, 117 | "passwordChanged":{ 118 | "type":"object", 119 | "properties":{ 120 | "command":{ 121 | "enum":[ 122 | "fxaccounts:password_changed" 123 | ] 124 | } 125 | } 126 | }, 127 | "passwordReset":{ 128 | "type":"object", 129 | "properties":{ 130 | "command":{ 131 | "enum":[ 132 | "fxaccounts:password_reset" 133 | ] 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/mailer.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var P = require('./promise') 6 | var createMailer = require('fxa-auth-mailer') 7 | 8 | module.exports = function (config, log) { 9 | var defaultLanguage = config.i18n.defaultLanguage 10 | 11 | return createMailer( 12 | log, 13 | { 14 | locales: config.i18n.supportedLanguages, 15 | defaultLanguage: defaultLanguage, 16 | mail: config.smtp 17 | } 18 | ) 19 | .then( 20 | function (mailer) { 21 | mailer.sendVerifyCode = function (account, code, opts) { 22 | return P.resolve(mailer.verifyEmail( 23 | { 24 | email: account.email, 25 | uid: account.uid.toString('hex'), 26 | code: code.toString('hex'), 27 | service: opts.service, 28 | redirectTo: opts.redirectTo, 29 | resume: opts.resume, 30 | acceptLanguage: opts.acceptLanguage || defaultLanguage 31 | } 32 | )) 33 | } 34 | mailer.sendVerifyLoginEmail = function (account, code, opts) { 35 | return P.resolve(mailer.verifyLoginEmail( 36 | { 37 | acceptLanguage: opts.acceptLanguage || defaultLanguage, 38 | code: code.toString('hex'), 39 | email: account.email, 40 | ip: opts.ip, 41 | location: opts.location, 42 | redirectTo: opts.redirectTo, 43 | resume: opts.resume, 44 | service: opts.service, 45 | timeZone: opts.timeZone, 46 | uaBrowser: opts.uaBrowser, 47 | uaBrowserVersion: opts.uaBrowserVersion, 48 | uaOS: opts.uaOS, 49 | uaOSVersion: opts.uaOSVersion, 50 | uid: account.uid.toString('hex') 51 | } 52 | )) 53 | } 54 | mailer.sendRecoveryCode = function (token, code, opts) { 55 | return P.resolve(mailer.recoveryEmail( 56 | { 57 | email: token.email, 58 | token: token.data.toString('hex'), 59 | code: code.toString('hex'), 60 | service: opts.service, 61 | redirectTo: opts.redirectTo, 62 | resume: opts.resume, 63 | acceptLanguage: opts.acceptLanguage || defaultLanguage 64 | } 65 | )) 66 | } 67 | mailer.sendPasswordChangedNotification = function (email, opts) { 68 | return P.resolve(mailer.passwordChangedEmail( 69 | { 70 | email: email, 71 | acceptLanguage: opts.acceptLanguage || defaultLanguage 72 | } 73 | )) 74 | } 75 | mailer.sendPasswordResetNotification = function (email, opts) { 76 | return P.resolve(mailer.passwordResetEmail( 77 | { 78 | email: email, 79 | acceptLanguage: opts.acceptLanguage || defaultLanguage 80 | } 81 | )) 82 | } 83 | mailer.sendNewDeviceLoginNotification = function (email, opts) { 84 | return P.resolve(mailer.newDeviceLoginEmail( 85 | { 86 | acceptLanguage: opts.acceptLanguage || defaultLanguage, 87 | email: email, 88 | ip: opts.ip, 89 | location: opts.location, 90 | timeZone: opts.timeZone, 91 | uaBrowser: opts.uaBrowser, 92 | uaBrowserVersion: opts.uaBrowserVersion, 93 | uaOS: opts.uaOS, 94 | uaOSVersion: opts.uaOSVersion 95 | } 96 | )) 97 | } 98 | mailer.sendPostVerifyEmail = function (email, opts) { 99 | return P.resolve(mailer.postVerifyEmail( 100 | { 101 | email: email, 102 | acceptLanguage: opts.acceptLanguage || defaultLanguage 103 | } 104 | )) 105 | } 106 | return mailer 107 | } 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /test/test_server.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var cp = require('child_process') 6 | var crypto = require('crypto') 7 | var P = require('../lib/promise') 8 | var request = require('request') 9 | var mailbox = require('./mailbox') 10 | var createDBServer = require('fxa-auth-db-mysql') 11 | 12 | /* eslint-disable no-console */ 13 | function TestServer(config, printLogs) { 14 | this.printLogs = printLogs === false ? false : true 15 | this.config = config 16 | this.server = null 17 | this.mail = null 18 | this.oauth = null 19 | this.mailbox = mailbox(config.smtp.api.host, config.smtp.api.port) 20 | } 21 | 22 | function waitLoop(testServer, url, cb) { 23 | request( 24 | url + '/__heartbeat__', 25 | function (err, res, body) { 26 | if (err) { 27 | if (err.errno !== 'ECONNREFUSED') { 28 | console.log('ERROR: unexpected result from ' + url) 29 | console.log(err) 30 | return cb(err) 31 | } 32 | if (!testServer.server) { 33 | console.log('starting...') 34 | testServer.start() 35 | } 36 | console.log('waiting...') 37 | return setTimeout(waitLoop.bind(null, testServer, url, cb), 100) 38 | } 39 | cb() 40 | } 41 | ) 42 | } 43 | 44 | TestServer.start = function (config, printLogs) { 45 | var d = P.defer() 46 | createDBServer().then( 47 | function (db) { 48 | db.listen(config.httpdb.url.split(':')[2]) 49 | db.on('error', function () {}) 50 | var testServer = new TestServer(config, printLogs) 51 | testServer.db = db 52 | waitLoop(testServer, config.publicUrl, function (err) { 53 | return err ? d.reject(err) : d.resolve(testServer) 54 | }) 55 | } 56 | ) 57 | return d.promise 58 | } 59 | 60 | TestServer.prototype.start = function () { 61 | this.server = cp.spawn( 62 | 'node', 63 | ['./key_server_stub.js'], 64 | { 65 | cwd: __dirname, 66 | stdio: this.printLogs ? 'pipe' : 'ignore' 67 | } 68 | ) 69 | 70 | if (this.printLogs) { 71 | this.server.stdout.on('data', process.stdout.write.bind(process.stdout)) 72 | this.server.stderr.on('data', process.stderr.write.bind(process.stderr)) 73 | } 74 | 75 | // if another instance is already running this will just die which is ok 76 | this.mail = cp.spawn( 77 | 'node', 78 | ['./mail_helper.js'], 79 | { 80 | cwd: __dirname, 81 | stdio: this.printLogs ? 'pipe' : 'ignore' 82 | } 83 | ) 84 | if (this.printLogs) { 85 | this.mail.stdout.on('data', process.stdout.write.bind(process.stdout)) 86 | this.mail.stderr.on('data', process.stderr.write.bind(process.stderr)) 87 | } 88 | if (this.config.oauth.url) { 89 | this.oauth = cp.spawn( 90 | 'node', 91 | ['./oauth_helper.js'], 92 | { 93 | cwd: __dirname, 94 | stdio: this.printLogs ? 'pipe' : 'ignore' 95 | } 96 | ) 97 | if (this.printLogs) { 98 | this.oauth.stdout.on('data', process.stdout.write.bind(process.stdout)) 99 | this.oauth.stderr.on('data', process.stderr.write.bind(process.stderr)) 100 | } 101 | } 102 | } 103 | 104 | TestServer.prototype.stop = function () { 105 | try { this.db.close() } catch (e) {} 106 | if (this.server) { 107 | this.server.kill('SIGINT') 108 | this.mail.kill() 109 | if (this.oauth) { 110 | this.oauth.kill() 111 | } 112 | } 113 | } 114 | 115 | TestServer.prototype.uniqueEmail = function () { 116 | return crypto.randomBytes(10).toString('hex') + '@restmail.net' 117 | } 118 | 119 | TestServer.prototype.uniqueUnicodeEmail = function () { 120 | return crypto.randomBytes(10).toString('hex') + 121 | String.fromCharCode(1234) + 122 | '@' + 123 | String.fromCharCode(5678) + 124 | 'restmail.net' 125 | } 126 | 127 | module.exports = TestServer 128 | -------------------------------------------------------------------------------- /test/local/verification_reminder_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var tap = require('tap') 6 | var proxyquire = require('proxyquire') 7 | var uuid = require('uuid') 8 | 9 | var test = tap.test 10 | var P = require('../../lib/promise') 11 | var mockLog = require('../mocks').mockLog 12 | 13 | var zeroBuffer16 = Buffer('00000000000000000000000000000000', 'hex') 14 | 15 | var ACCOUNT = { 16 | uid: uuid.v4('binary'), 17 | email: 'reminder' + Math.random() + '@bar.com', 18 | emailCode: zeroBuffer16, 19 | acceptLanguage: 'bg-BG,en-US;q=0.7,ar-BH;q=0.3' 20 | } 21 | 22 | var reminderData = { 23 | email: ACCOUNT.email 24 | } 25 | 26 | var mockDb = { 27 | createVerificationReminder: function () { 28 | return P.resolve() 29 | } 30 | } 31 | 32 | test( 33 | 'creates reminders with valid options and rate', 34 | function (t) { 35 | var moduleMocks = { 36 | '../config': { 37 | 'get': function (item) { 38 | if (item === 'verificationReminders') { 39 | return { 40 | rate: 1 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | var addedTimes = 0 48 | var thisMockLog = mockLog({ 49 | increment: function (name) { 50 | if (name === 'verification-reminders.created') { 51 | addedTimes++ 52 | if (addedTimes === 5) { 53 | t.end() 54 | } 55 | } 56 | } 57 | }) 58 | 59 | var verificationReminder = proxyquire('../../lib/verification-reminders', moduleMocks)(thisMockLog, mockDb) 60 | 61 | verificationReminder.create(reminderData) 62 | verificationReminder.create(reminderData) 63 | verificationReminder.create(reminderData) 64 | verificationReminder.create(reminderData) 65 | verificationReminder.create(reminderData) 66 | } 67 | ) 68 | 69 | test( 70 | 'does not create reminders when rate is 0', 71 | function (t) { 72 | var moduleMocks = { 73 | '../config': { 74 | 'get': function (item) { 75 | if (item === 'verificationReminders') { 76 | return { 77 | rate: 0 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | var verificationReminder = proxyquire('../../lib/verification-reminders', moduleMocks)(mockLog, mockDb) 85 | verificationReminder.create(reminderData) 86 | .then(function (result) { 87 | if (result === false) { 88 | t.end() 89 | } 90 | }) 91 | } 92 | ) 93 | 94 | test( 95 | 'deletes reminders', 96 | function (t) { 97 | var thisMockLog = mockLog({ 98 | increment: function (name) { 99 | if (name === 'verification-reminders.deleted') { 100 | t.end() 101 | } 102 | } 103 | }) 104 | var thisMockDb = { 105 | deleteVerificationReminder: function (reminderData) { 106 | t.ok(reminderData.email) 107 | t.ok(reminderData.type) 108 | return P.resolve() 109 | } 110 | } 111 | 112 | var verificationReminder = proxyquire('../../lib/verification-reminders', {})(thisMockLog, thisMockDb) 113 | verificationReminder.delete(reminderData) 114 | } 115 | ) 116 | 117 | test( 118 | 'deletes reminders can catch errors', 119 | function (t) { 120 | var thisMockLog = mockLog({ 121 | error: function (logErr) { 122 | t.equal(logErr.op, 'verification-reminder.delete') 123 | t.ok(logErr.err.message) 124 | t.end() 125 | } 126 | }) 127 | var thisMockDb = { 128 | deleteVerificationReminder: function () { 129 | return P.reject(new Error('Something is wrong')) 130 | } 131 | } 132 | 133 | var verificationReminder = proxyquire('../../lib/verification-reminders', {})(thisMockLog, thisMockDb) 134 | verificationReminder.delete(reminderData) 135 | } 136 | ) 137 | 138 | -------------------------------------------------------------------------------- /lib/metrics/statsd.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var StatsD = require('node-statsd') 6 | var uaParser = require('node-uap') 7 | 8 | var STATSD_PREFIX = 'fxa.auth.' 9 | var TIMING_SUFFIX = '.time' 10 | var HISTOGRAM_SUFFIX = '.hist' 11 | 12 | function getGenericTags(info) { 13 | var tags = [] 14 | if (info.userAgent) { 15 | var agent = uaParser.parse(info.userAgent) 16 | if (agent) { 17 | if (agent.ua) { 18 | tags = tags.concat([ 19 | 'agent_ua_family:' + agent.ua.family, // -> "Safari" 20 | 'agent_ua_version:' + agent.ua.toVersionString(), // -> "5.0.1" 21 | 'agent_ua_version_major:' + agent.ua.major // -> "5" 22 | ]) 23 | } 24 | 25 | if (agent.os) { 26 | tags = tags.concat([ 27 | 'agent_os_version:' + agent.os.toVersionString(), // -> "5.1" 28 | 'agent_os_family:' + agent.os.family, // -> "iOS" 29 | 'agent_os_major:' + agent.os.major // -> "5" 30 | ]) 31 | } 32 | } 33 | } 34 | 35 | return tags 36 | } 37 | 38 | function StatsDCollector(log) { 39 | if (! log) { 40 | throw new Error('Log is required') 41 | } 42 | 43 | var config = require('../../config') 44 | var statsdConfig = config.get('statsd') 45 | 46 | this.host = statsdConfig.host 47 | this.port = statsdConfig.port 48 | this.sampleRate = statsdConfig.sample_rate 49 | this.connected = false 50 | this.client = null 51 | this.log = log 52 | } 53 | 54 | StatsDCollector.prototype = { 55 | /** 56 | * Initializes a StatsD socket client 57 | */ 58 | init: function () { 59 | var self = this 60 | var client = this.client = new StatsD(this.host, this.port) 61 | 62 | if (client.socket) { 63 | this.connected = true 64 | client.socket.on('error', function (error) { 65 | self.connected = false 66 | self.log.error({ op: 'statsd', err: error }) 67 | }) 68 | } else { 69 | self.log.error({ op: 'statsd', err: new Error('StatsD failed to connect to ' + this.host + ':' + this.port) }) 70 | this.connected = false 71 | } 72 | 73 | if (! this.connected) { 74 | self.log.error({ op: 'statsd', err: new Error('StatsD not connected.') }) 75 | } 76 | }, 77 | 78 | /** 79 | * Send a formatted metrics object to StatsD 80 | * 81 | * @param {Object} info 82 | */ 83 | write: function (info) { 84 | var tags = getGenericTags(info) 85 | this.increment(info.event, tags) 86 | }, 87 | 88 | increment: function (name, tags) { 89 | if (this.client) { 90 | this.client.increment( 91 | STATSD_PREFIX + name, 92 | 1, 93 | this.sampleRate, 94 | tags, 95 | handleErrors(this, 'increment') 96 | ) 97 | } 98 | }, 99 | 100 | timing: function (name, timing, tags) { 101 | if (this.client) { 102 | this.client.timing( 103 | STATSD_PREFIX + name + TIMING_SUFFIX, 104 | timing, 105 | this.sampleRate, 106 | tags, 107 | handleErrors(this, 'timing') 108 | ) 109 | } 110 | }, 111 | 112 | histogram: function (name, value, tags) { 113 | if (this.client) { 114 | this.client.histogram( 115 | STATSD_PREFIX + name + HISTOGRAM_SUFFIX, 116 | value, 117 | this.sampleRate, 118 | tags, 119 | handleErrors(this, 'histogram') 120 | ) 121 | } 122 | }, 123 | 124 | /** 125 | * Close the client 126 | */ 127 | close: function () { 128 | if (this.client) { 129 | this.client.close() 130 | this.connected = false 131 | } 132 | } 133 | } 134 | 135 | function handleErrors (self, method) { 136 | return function (error) { 137 | if (error) { 138 | self.log.error({ 139 | op: 'statsd.' + method, 140 | err: error 141 | }) 142 | } 143 | } 144 | } 145 | 146 | module.exports = StatsDCollector 147 | -------------------------------------------------------------------------------- /test/remote/account_signin_verification_enable_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('../ptaptest') 6 | var TestServer = require('../test_server') 7 | var Client = require('../client') 8 | 9 | test( 10 | 'signin confirmation can be disabled', 11 | function (t) { 12 | var config = require('../../config').getProperties() 13 | process.env.SIGNIN_CONFIRMATION_ENABLED = false 14 | var server, email, client 15 | var password = 'allyourbasearebelongtous' 16 | 17 | TestServer.start(config) 18 | .then(function main(serverObj) { 19 | server = serverObj 20 | email = server.uniqueEmail() 21 | }) 22 | .then(function() { 23 | return Client.createAndVerify(config.publicUrl, email, password, server.mailbox, {keys:true}) 24 | }) 25 | .then( 26 | function (x) { 27 | client = x 28 | t.ok(client.authAt, 'authAt was set') 29 | } 30 | ) 31 | .then( 32 | function () { 33 | return client.emailStatus() 34 | } 35 | ) 36 | .then( 37 | function (status) { 38 | t.equal(status.verified, true, 'account is verified') 39 | } 40 | ) 41 | .then( 42 | function () { 43 | return client.login({keys:true}) 44 | } 45 | ) 46 | .then( 47 | function (response) { 48 | t.notEqual(response.verificationMethod, 'email', 'verification method not set') 49 | t.notEqual(response.verificationReason, 'login', 'verification reason not set') 50 | } 51 | ) 52 | .then( 53 | function () { 54 | return client.emailStatus() 55 | } 56 | ) 57 | .then( 58 | function (status) { 59 | t.equal(status.verified, true, 'account is verified') 60 | } 61 | ) 62 | .done(function() { 63 | server.stop() 64 | t.end() 65 | }) 66 | } 67 | ) 68 | 69 | test( 70 | 'signin confirmation can be enabled', 71 | function (t) { 72 | process.env.SIGNIN_CONFIRMATION_ENABLED = true 73 | process.env.SIGNIN_CONFIRMATION_RATE = 1.0 74 | var config = require('../../config').getProperties() 75 | var server, email, client 76 | var password = 'allyourbasearebelongtous' 77 | 78 | TestServer.start(config) 79 | .then(function main(serverObj) { 80 | server = serverObj 81 | email = server.uniqueEmail() 82 | }) 83 | .then(function() { 84 | return Client.createAndVerify(config.publicUrl, email, password, server.mailbox, {keys:true}) 85 | }) 86 | .then( 87 | function (x) { 88 | client = x 89 | t.ok(client.authAt, 'authAt was set') 90 | } 91 | ) 92 | .then( 93 | function () { 94 | return client.emailStatus() 95 | } 96 | ) 97 | .then( 98 | function (status) { 99 | t.equal(status.verified, true, 'account is verified') 100 | } 101 | ) 102 | .then( 103 | function () { 104 | return client.login({keys:true}) 105 | } 106 | ) 107 | .then( 108 | function (response) { 109 | t.equal(response.verificationMethod, 'email', 'verification method set') 110 | t.equal(response.verificationReason, 'login', 'verification reason set') 111 | } 112 | ) 113 | .then( 114 | function () { 115 | return client.emailStatus() 116 | } 117 | ) 118 | .then( 119 | function (status) { 120 | t.equal(status.verified, false, 'account is not verified') 121 | t.equal(status.emailVerified, true, 'email is verified') 122 | t.equal(status.sessionVerified, false, 'session is not verified') 123 | } 124 | ) 125 | .done(function() { 126 | server.stop() 127 | t.end() 128 | }) 129 | } 130 | ) 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Firefox Accounts Server 2 | ======================= 3 | 4 | [![Build Status](https://travis-ci.org/mozilla/fxa-auth-server.svg?branch=master)](https://travis-ci.org/mozilla/fxa-auth-server) 5 | 6 | This project implements the core server-side API for Firefox Accounts. It 7 | provides account, device and encryption-key management for the Mozilla Cloud 8 | Services ecosystem. 9 | 10 | [Overview](/docs/overview.md) 11 | 12 | [Detailed design document](https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol) 13 | 14 | [Detailed API spec](/docs/api.md) 15 | 16 | [Guidelines for Contributing](/CONTRIBUTING.md) 17 | 18 | ## Prerequisites 19 | 20 | * node 4.5.0 or higher 21 | * npm 22 | * Grunt 23 | 24 | ## Install 25 | 26 | On some systems running the server as root will cause working directory permissions issues with node. It is recommended that you create a separate, standard user to ensure a clean and more secure installation. 27 | 28 | Clone the git repository and install dependencies: 29 | 30 | git clone git://github.com/mozilla/fxa-auth-server.git 31 | cd fxa-auth-server 32 | npm install 33 | 34 | To start the server in dev memory store mode (ie. `NODE_ENV=dev`), run: 35 | 36 | npm start 37 | 38 | This runs a script `scripts/start-local.sh` as defined in `package.json`. This will start up 39 | 4 services, three of which listen on the following ports (by default): 40 | 41 | * `bin/key_server.js` on port 9000 42 | * `test/mail_helper.js` on port 9001 43 | * `./node_modules/fxa-customs-server/bin/customs_server.js` on port 7000 44 | * `bin/notifier.js` (no port) 45 | 46 | When you `Ctrl-c` your server, all 4 processes will be stopped. 47 | 48 | To start the server in dev MySQL store mode (ie. `NODE_ENV=dev`), run: 49 | 50 | npm run start-mysql 51 | 52 | ## Testing 53 | 54 | Run tests with: 55 | 56 | npm test 57 | 58 | To select a specific glob of tests to run: 59 | 60 | npm test -- test/local/account_routes.js test/local/password_* 61 | 62 | * Note: stop the auth-server before running tests. Otherwise, they will fail with obscure errors. 63 | 64 | ## Reference Client 65 | 66 | https://github.com/mozilla/fxa-js-client 67 | 68 | 69 | ## Dev Deployment 70 | 71 | Refer to https://github.com/mozilla/fxa-dev.git. 72 | 73 | 74 | ## Configuration 75 | 76 | Configuration of this project 77 | is managed by [convict](https://github.com/mozilla/node-convict), 78 | using the schema in 79 | [`config/index.js`](https://github.com/mozilla/fxa-auth-server/blob/master/config/index.js). 80 | 81 | Default values from this schema 82 | can be overridden in two ways: 83 | 84 | 1. By setting individual environment variables, 85 | as indicated by the `env` property 86 | for each item in the schema. 87 | 88 | For example: 89 | ```sh 90 | export CONTENT_SERVER_URL="http://your.content.server.org" 91 | ``` 92 | 93 | 2. By specifying the path 94 | to a conforming JSON file, 95 | or a comma-separated list of paths, 96 | using the `CONFIG_FILES` environment variable. 97 | Files specified in this way 98 | are loaded when the server starts. 99 | If the server fails to start, 100 | it usually indicates that one of these JSON files 101 | does not conform to the schema; 102 | check the error message 103 | for more information. 104 | 105 | For example: 106 | ```sh 107 | export CONFIG_FILES="~/fxa-content-server.json,~/fxa-db.json" 108 | ``` 109 | 110 | ## Troubleshooting 111 | 112 | Firefox Accounts authorization is a complicated flow. You can get verbose logging by adjusting the log level in the `config.json` on your deployed instance. Add a stanza like: 113 | 114 | "log": { 115 | "level": "trace" 116 | } 117 | 118 | Valid `level` values (from least to most verbose logging) include: `"fatal", "error", "warn", "info", "trace", "debug"`. 119 | 120 | ## Database integration 121 | 122 | This server depends on a database server 123 | from the [`fxa-auth-db-mysql` repo](https://github.com/mozilla/fxa-auth-db-mysql/). 124 | When running the tests, it uses a memory-store 125 | that mocks behaviour of the production MySQL server. 126 | 127 | ## License 128 | 129 | MPL 2.0 130 | -------------------------------------------------------------------------------- /test/remote/verifier_upgrade_tests.js: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | var test = require('tap').test 6 | var TestServer = require('../test_server') 7 | var Client = require('../client') 8 | var createDBServer = require('fxa-auth-db-mysql') 9 | var log = { trace: console.log } // eslint-disable-line no-console 10 | 11 | var config = require('../../config').getProperties() 12 | 13 | process.env.VERIFIER_VERSION = '0' 14 | process.env.SIGNIN_CONFIRMATION_ENABLED = false 15 | 16 | var Token = require('../../lib/tokens')(log) 17 | var DB = require('../../lib/db')( 18 | config.db.backend, 19 | log, 20 | Token.error, 21 | Token.SessionToken, 22 | Token.KeyFetchToken, 23 | Token.AccountResetToken, 24 | Token.PasswordForgotToken, 25 | Token.PasswordChangeToken 26 | ) 27 | 28 | createDBServer().then( 29 | function (db_server) { 30 | db_server.listen(config.httpdb.url.split(':')[2]) 31 | db_server.on('error', function () {}) 32 | 33 | var email = Math.random() + '@example.com' 34 | var password = 'ok' 35 | var uid = null 36 | 37 | test( 38 | 'upgrading verifierVersion upgrades the account on password change', 39 | function (t) { 40 | return TestServer.start(config) 41 | .then( 42 | function main(server) { 43 | return Client.create(config.publicUrl, email, password, { preVerified: true, keys: true }) 44 | .then( 45 | function (c) { 46 | uid = Buffer(c.uid, 'hex') 47 | return server.stop() 48 | } 49 | ) 50 | } 51 | ) 52 | .then( 53 | function () { 54 | return DB.connect(config[config.db.backend]) 55 | .then( 56 | function (db) { 57 | return db.account(uid) 58 | .then( 59 | function (account) { 60 | t.equal(account.verifierVersion, 0, 'wrong version') 61 | } 62 | ) 63 | .then( 64 | function () { 65 | return db.close() 66 | } 67 | ) 68 | } 69 | ) 70 | } 71 | ) 72 | .then( 73 | function () { 74 | process.env.VERIFIER_VERSION = '1' 75 | return TestServer.start(config) 76 | } 77 | ) 78 | .then( 79 | function (server) { 80 | var client 81 | return Client.login(config.publicUrl, email, password, server.mailbox) 82 | .then( 83 | function (x) { 84 | client = x 85 | return client.keys() 86 | } 87 | ) 88 | .then( 89 | function () { 90 | return client.changePassword(password) 91 | } 92 | ) 93 | .then( 94 | function () { 95 | return server.stop() 96 | } 97 | ) 98 | } 99 | ) 100 | .then( 101 | function () { 102 | return DB.connect(config[config.db.backend]) 103 | .then( 104 | function (db) { 105 | return db.account(uid) 106 | .then( 107 | function (account) { 108 | t.equal(account.verifierVersion, 1, 'wrong upgrade version') 109 | } 110 | ) 111 | .then( 112 | function () { 113 | return db.close() 114 | } 115 | ) 116 | } 117 | ) 118 | } 119 | ) 120 | .then( 121 | function () { 122 | try { 123 | db_server.close() 124 | } catch (e) { 125 | // This connection may already be dead if a real mysql server is 126 | // already bound to :8000. 127 | } 128 | } 129 | ) 130 | }) 131 | }) 132 | --------------------------------------------------------------------------------