├── .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 | [](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 |
--------------------------------------------------------------------------------