diff --git a/.circleci/README.md b/.circleci/README.md index 5235970584b..82284de3e6e 100644 --- a/.circleci/README.md +++ b/.circleci/README.md @@ -2,13 +2,6 @@ `config.yml` defines the jobs and workflows of our CircleCI deployment. -## Special Cases - -`fxa-email-service` isn't tested for most PRs because it doesn't change -often and is relatively resource intensive. In order to -trigger these tests the PR branch should be prefixed with `email-service-`. -PRs that change those packages on other branches will (intentionally) fail. - ## Scripts This directory contains scripts used by `config.yml` to run jobs. More diff --git a/.circleci/assert-branch.sh b/.circleci/assert-branch.sh index 595b1dabd60..516e2301ccb 100755 --- a/.circleci/assert-branch.sh +++ b/.circleci/assert-branch.sh @@ -8,10 +8,3 @@ if [[ $CIRCLE_BRANCH =~ ^train-.* ]]; then echo "Train branch, skipping package check" exit 0 fi - -if grep -e 'fxa-email-service' ../packages/test.list; then - if [[ ! $CIRCLE_BRANCH =~ ^email-service-.* ]]; then - echo "Please create a new PR from a branch name starting with 'email-service-'" - exit 1 - fi -fi diff --git a/.circleci/config.yml b/.circleci/config.yml index e61c526d1db..6076c6c8c36 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -137,14 +137,12 @@ jobs: 'fxa-admin-panel' \ 'fxa-support-panel' \ 'fxa-event-broker' \ - 'fxa-auth-db-mysql' \ 'fxa-profile-server' \ '123done' \ 'browserid-verifier' \ 'fortress' \ 'fxa-auth-client' \ 'fxa-geodb' \ - 'fxa-email-event-proxy' \ 'fxa-customs-server' \ ) for p in "${PACKAGES[@]}"; do @@ -195,23 +193,6 @@ jobs: - test-content-server-part: index: 5 - test-email-service: - resource_class: large - docker: - - image: circleci/rust:latest-node - - image: mysql:5.7.27 - environment: - - MYSQL_DATABASE: fxa - - MYSQL_ALLOW_EMPTY_PASSWORD: yes - - MYSQL_ROOT_PASSWORD: '' - - image: redis - steps: - - base-install - - run: cargo install cargo-audit - - run: ./packages/fxa-email-service/scripts/test-ci.sh - - store_artifacts: - path: artifacts - deploy-packages: resource_class: small docker: @@ -364,17 +345,6 @@ workflows: ignore: main tags: ignore: /.*/ - - test-email-service: - # since email-service is expensive - # to build and rarely changes - # we only run it on branches - # starting with "email-service-" - filters: - branches: - only: - - /^email-service-.*/ - tags: - ignore: /.*/ - build-and-deploy-storybooks: filters: branches: diff --git a/.circleci/modules-to-test.js b/.circleci/modules-to-test.js index 179480349e4..92bd54a934a 100644 --- a/.circleci/modules-to-test.js +++ b/.circleci/modules-to-test.js @@ -68,9 +68,7 @@ async function modulesToSkip(org, repo, prNumber, branch) { `https://api.github.com/repos/${org}/${repo}/issues/${prNumber}/labels` ); const labels = await labelRes.json(); - const skipModules = branch.startsWith('email-service') - ? [] - : ['fxa-email-service']; + const skipModules = []; if (Array.isArray(labels) && labels.find((l) => l.name === '🙈 skip ci')) { const skipLabels = labels .filter((l) => l.name.startsWith('fxa-')) diff --git a/.dockerignore b/.dockerignore index 02b92ba012f..bc890d0a947 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,6 +12,4 @@ packages/*/build packages/fxa-auth-server/.mail_output packages/fxa-dev-launcher -packages/fxa-email-event-proxy -packages/fxa-email-service packages/fxa-content-server/dist diff --git a/.github/.codecov.yaml b/.github/.codecov.yaml index 9ccaef894f1..a94f3937e40 100644 --- a/.github/.codecov.yaml +++ b/.github/.codecov.yaml @@ -38,7 +38,6 @@ flags: many: paths: - packages/fxa-admin-panel - - packages/fxa-auth-db-mysql - packages/fxa-customs-server - packages/fxa-payments-server - packages/fxa-profile-server diff --git a/.gitignore b/.gitignore index 23f5a8b3776..e68864654ed 100644 --- a/.gitignore +++ b/.gitignore @@ -75,12 +75,7 @@ Thumbs.db packages/browserid-verifier/loadtest/venv packages/browserid-verifier/loadtest/*.pyc -# fxa-auth-db-mysql -packages/fxa-auth-db-mysql/config/dev.js -packages/fxa-auth-db-mysql/sandbox - # fxa-auth-server -packages/fxa-auth-server/fxa-auth-db-mysql packages/fxa-auth-server/sandbox packages/fxa-auth-server/config/key.json packages/fxa-auth-server/config/oldKey.json @@ -114,16 +109,6 @@ packages/fxa-content-server/.tscompiled # fxa-customs-server packages/fxa-customs-server/test/mocks/temp.netset -# fxa-email-event-proxy -packages/fxa-email-event-proxy/*.zip - -# fxa-email-service -packages/fxa-email-service/**/*.rs.bk -packages/fxa-email-service/config/local.* -packages/fxa-email-service/fxa-auth-db-mysql -packages/fxa-email-service/target -packages/fxa-email-service/.sourcehash - # fxa-payments-server packages/fxa-payments-server/fxa-content-server-l10n/ diff --git a/_dev/docker/docker-compose.yml b/_dev/docker/docker-compose.yml index 0963daad45c..2acdaa0d596 100644 --- a/_dev/docker/docker-compose.yml +++ b/_dev/docker/docker-compose.yml @@ -20,21 +20,6 @@ services: init: true ports: - "5050:5050" - authdb: - image: fxa-auth-db-mysql:build - entrypoint: /bin/bash -c - command: ["/fxa/_scripts/check-mysql.sh 3306 mysql && node bin/db_patcher.js && node bin/server.js"] - environment: - - HOST=0.0.0.0 - - PORT=8000 - - NODE_ENV=dev - - MYSQL_HOST=mysql - - MYSQL_SLAVE_HOST=mysql - init: true - ports: - - "8000:8000" - depends_on: - - mysql auth: image: fxa-auth-server:build entrypoint: /bin/bash -c @@ -156,11 +141,3 @@ services: image: jdlk7/firestore-emulator ports: - "9090:9090" - email: - image: mozilla/fxa-email-service - environment: - - NODE_ENV=dev - - FXA_EMAIL_ENV=dev - - FXA_EMAIL_LOG_LEVEL=debug - ports: - - "8001:8001" diff --git a/_scripts/base-docker.sh b/_scripts/base-docker.sh index bdd473de8ea..0edb0f27d1e 100755 --- a/_scripts/base-docker.sh +++ b/_scripts/base-docker.sh @@ -39,7 +39,6 @@ npx yarn workspaces focus --production \ browserid-verifier \ fxa-admin-panel \ fxa-admin-server \ - fxa-auth-db-mysql \ fxa-auth-server \ fxa-content-server \ fxa-customs-server \ diff --git a/_scripts/check-ports.sh b/_scripts/check-ports.sh index 17cd1119969..d74855f3652 100755 --- a/_scripts/check-ports.sh +++ b/_scripts/check-ports.sh @@ -8,7 +8,6 @@ PORTS=( 8085 # google-pubsub-emulator 9090 # google-firestore-emulator 5000 # sync server - 8001 # email-service 8000 # auth-server db mysql 9000 # auth-server key server 3030 # content-server diff --git a/_scripts/fxa-email-service.sh b/_scripts/fxa-email-service.sh deleted file mode 100755 index dae1a2e9995..00000000000 --- a/_scripts/fxa-email-service.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -ex - -# This docker image doesn't react to SIGINTs (Ctrl+C) which is used by -# pm2 to kill processes. -# We go around this by defining a SIGINT handler, running docker in the -# background (but still logging on the screen), and running a read to keep -# the script running. - -function on_sigint() { - echo "fxa-email-service shutting down." - docker stop fxa_email_service - exit 0 -} - -trap on_sigint INT - -docker run --rm --name fxa_email_service \ - -e NODE_ENV=dev \ - -e FXA_EMAIL_ENV=dev \ - -e FXA_EMAIL_LOG_LEVEL=debug \ - -e RUST_BACKTRACE=1 \ - -p 8001:8001 mozilla/fxa-email-service & - -while :; do read -r; done diff --git a/_scripts/gh-pages.sh b/_scripts/gh-pages.sh index fc0df725af3..7376a5c2542 100755 --- a/_scripts/gh-pages.sh +++ b/_scripts/gh-pages.sh @@ -9,9 +9,6 @@ fi echo "Building docs." -cd packages/fxa-email-service -cargo doc --no-deps - cd ../../packages/fxa-payments-server yarn workspaces focus fxa-payments-server npm run build-storybook @@ -21,9 +18,6 @@ git clone --branch gh-pages git@github.com:mozilla/fxa.git docs-build cd docs-build -rm -rf ./fxa-email-service -mv ../packages/fxa-email-service/target/doc fxa-email-service - rm -rf ./fxa-payments-server mv ../packages/fxa-payments-server/storybook-static fxa-payments-server diff --git a/_scripts/test-package.sh b/_scripts/test-package.sh index f90811be8eb..d95974cf31d 100755 --- a/_scripts/test-package.sh +++ b/_scripts/test-package.sh @@ -6,7 +6,6 @@ Usage: npm test [all|*] examples: npm test fxa-shared -npm test fxa-auth-db-mysql fxa-auth-server npm test all " diff --git a/docs/adr/0028-retiring-email-service.md b/docs/adr/0028-retiring-email-service.md new file mode 100644 index 00000000000..f15fc655a20 --- /dev/null +++ b/docs/adr/0028-retiring-email-service.md @@ -0,0 +1,55 @@ +# Retiring fxa-email-service + +- Deciders: Danny Coates, Vijay Budhram, Jon Buckley +- Date: 2021-10-06 + +## Context and Problem Statement + +The goal of fxa-email-service was to spin off the email sending responsibilities of FxA into a shared service that multiple Mozilla projects could use. From its readme: + +> The FxA team had an OKR for Q2 2018 about decoupling the auth server from SES and making it possible to send email via different providers. Subsequently, some other teams expressed an interest in depending on a standalone email service too. + +> This repo started as our experiment to see what a decoupled email-sending service would look like, written in Rust. It is now handling all FxA email traffic in production, and we are gradually separating it from the FxA stack to run as a standalone service in its own right. + +Had it achieved the goal of fully decoupling from FxA and being more widely used we'd likely continue using it. However, in the approximately 4 years of its existance it has only really been used as an intermediate step between auth-server and SES. Fortunately in that time it hasn't required much maintenance so its "weight", being a fairly large codebase for what it does and our only Rust service, has never been a concern. Recent work to eliminate fxa-auth-db-mysql meant we either needed make changes to it or rethink how FxA sends email. It turned out the work to update email-service was larger than eliminating it and replacing it by sending email directly from auth-server via SES or SMTP. + +## Decision Drivers + +- Future maintenance +- Email provider flexability + +## Considered Options + +- Keep email-service and update db access +- Eliminate email-service and send directly from auth-server + +## Decision Outcome + +We will eliminate email-service. + +## Pros and Cons of Options + +### Keep email-service + +Pros: + +- Change is scary + +Cons: + +- More code to maintain +- Less team experience with Rust + +### Eliminate email-service + +Pros: + +- Significantly reduces our code footprint +- No Rust simplifies our build process +- Future email work will be easier to implement and maintain in JS/TS + +Cons: + +- Lose (at least temporarily) the ability to use multiple email providers simultaneously + - We've never used this beyond testing so far + - We could add it to auth-server if required diff --git a/packages/fxa-auth-db-mysql/.eslintrc b/packages/fxa-auth-db-mysql/.eslintrc deleted file mode 100644 index 18257676d72..00000000000 --- a/packages/fxa-auth-db-mysql/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": ["plugin:fxa/server"], - "plugins": ["fxa"], - "rules": { - "no-useless-escape": "off" - } -} diff --git a/packages/fxa-auth-db-mysql/.migration-lint-ignore.json b/packages/fxa-auth-db-mysql/.migration-lint-ignore.json deleted file mode 100644 index 5a9bc87e41f..00000000000 --- a/packages/fxa-auth-db-mysql/.migration-lint-ignore.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "procedures": [ - "accountEmails_4", - "accountExists_2", - "consumeRecoveryCode_2", - "createAccount_7", - "createEmail_2", - "createEmailBounce_1", - "deleteEmail_4", - "emailRecord_4", - "fetchEmailBounces_1", - "fetchVerificationReminders_2", - "getSecondaryEmail_1", - "prune_7", - "sessionWithDevice_15", - "setPrimaryEmail_3" - ], - "tables": [ - "deviceCommands", - "securityEvents" - ] -} diff --git a/packages/fxa-auth-db-mysql/.nsprc b/packages/fxa-auth-db-mysql/.nsprc deleted file mode 100644 index 9357ea286a4..00000000000 --- a/packages/fxa-auth-db-mysql/.nsprc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "comment_1179": "1179 is prototype pollution in minimist, used by eslint, mocha. Doesn't affect us, as we don't pass untrusted external inputs to those libraries.", - "comment_1500": "1500 is prototype pollution in yargs-parser, used by mocha, nyc. Doesn't affect us, as we don't pass untrusted external inputs to those libraries.", - "exceptions": [ - "https://npmjs.com/advisories/1179", - "https://npmjs.com/advisories/1500" - ] -} diff --git a/packages/fxa-auth-db-mysql/.prettierignore b/packages/fxa-auth-db-mysql/.prettierignore deleted file mode 100644 index 91b0b8f11bd..00000000000 --- a/packages/fxa-auth-db-mysql/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -AUTHORS -LICENSE -.* -*.sql -*.sh -Dockerfile* diff --git a/packages/fxa-auth-db-mysql/.vscode/launch.json b/packages/fxa-auth-db-mysql/.vscode/launch.json deleted file mode 100644 index 86f3f374101..00000000000 --- a/packages/fxa-auth-db-mysql/.vscode/launch.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Mocha All", - "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", - "args": [ - "--timeout", - "999999", - "--colors", - "${workspaceFolder}/test/local", - "${workspaceFolder}/test/mem", - "${workspaceFolder}/test/backend", - "${workspaceFolder}/db-server/test/local" - ], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "preLaunchTask": "Stop PM2 Auth Db Server", - "postDebugTask": "Start PM2 Auth Db Server" - }, - { - "type": "node", - "request": "launch", - "name": "Mocha Current File", - "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", - "args": [ - "--timeout", - "999999", - "--colors", - "${workspaceFolder}/${relativeFile}" - ], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "preLaunchTask": "Stop PM2 Auth Db Server", - "postDebugTask": "Start PM2 Auth Db Server" - } - ] -} diff --git a/packages/fxa-auth-db-mysql/.vscode/tasks.json b/packages/fxa-auth-db-mysql/.vscode/tasks.json deleted file mode 100644 index b939027e36e..00000000000 --- a/packages/fxa-auth-db-mysql/.vscode/tasks.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Current Auth Db MySQL Local Test", - "type": "shell", - "command": "./scripts/mocha-coverage.js", - "args": [ - "-R", - "dot", - "--recursive", - "--timeout", - "5000", - "--exit", - "${relativeFile}" - ], - "group": "test", - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "dedicated" - }, - "options": { - "env": { - "NODE_ENV": "dev", - "VERIFIER_VERSION": "0", - "NO_COVERAGE": "1", - "CORS_ORIGIN": "http://foo,http://bar" - } - } - }, - { - "label": "DB Patcher", - "type": "shell", - "command": "node ./bin/db_patcher.js >/dev/null", - }, - { - "label": "Stop PM2 Auth Db Server", - "type": "shell", - "command": "pm2 stop 'auth-server db mysql PORT 8000'", - "dependsOn": "DB Patcher" - }, - { - "label": "Start PM2 Auth Db Server", - "type": "shell", - "command": "pm2 start 'auth-server db mysql PORT 8000'" - } - ], -} diff --git a/packages/fxa-auth-db-mysql/AUTHORS b/packages/fxa-auth-db-mysql/AUTHORS deleted file mode 100644 index 9e10e99abad..00000000000 --- a/packages/fxa-auth-db-mysql/AUTHORS +++ /dev/null @@ -1,22 +0,0 @@ -Andrew Chilton -Danny Coates -Edouard Oger -Edouard Oger -Greg Guthe -Jamon Camisso -Jamon Camisso -John Morrison -Jon Buckley -Jon Buckley -Michiel de Jong -Peter deHaan -Phil Booth -Ryan Kelly -Ryan Kelly -Sai Pc -Sean McArthur -Shane Tomlinson -Vijay Budhram -Vijay Budhram -Vlad Filippov -vladikoff diff --git a/packages/fxa-auth-db-mysql/CHANGELOG-server.md b/packages/fxa-auth-db-mysql/CHANGELOG-server.md deleted file mode 100644 index f82703da0b6..00000000000 --- a/packages/fxa-auth-db-mysql/CHANGELOG-server.md +++ /dev/null @@ -1,132 +0,0 @@ -train-42 - -- feat(api): Test to check passwordChangeToken returns the account.email address - #153 -- fix(db): ensure that requests for bad paths are logged - #145 -- refactor(tests): expect verifyHash to be absent from responses - #143 -- chore(travis): Remove chilts from travis notifications - #156 - -train-41 - -- fix(db-api): propagate all error fields - #151 -- chore(travis): Tell Travis to use #fxa-bots - #152 - -train-40 - -- feat(db-api): Add AppError.incorrectPassword for the backends to use in checkPassword - #146 -- chore(lint): Clean up some syntax errors reported by ESLint - #147 -- chore(lint): Remove more semi colons - #149 -- chore(lint): Replace JSHint with ESLint - #149 - -train-39 - -- add check password api - #141 -- docs(api): Server_API.md docs - #145 -- chore(license): Update license to be SPDX compliant - #142 -- refactor(lib): Move source files into lib/ to tidy up - #140 -- build(travis): Test on both io.js v1 and v2 - #139 - -train-36 - -- chore(test): Test on node.js v0.10, v0.12 and the latest io.js - #138 -- chore(copyright): Update to grunt-copyright v0.2.0 - #137 -- refactor(tests): Generate test UIDs a different way (from crypto.randomBytes) - #136 -- docs(db): Add docs to help with developing the db repos - #126 - -train-35 - -- fix(shrinkwrap): as discussed, only "top-level" repos get shrunkwrapped -- docs(changelog): belatedly add changelog note for train-34 - -train-34 - -- fix(db): set createdAt, verifierSetAt and normalizedEmail in the tests - #130 -- fix(release): add tasks "grunt version" and "grunt version:patch" to create release tags - #132 -- docs(readme): better readme for help implementing a storage backend - #129 - -train-33 - -- Tweak logging for compatibility with mozlog - #127 -- Emit memory stats for operational logging - #128 - -train-32 - -- Add ability to mark an account as "locked" for security reasons - #123, #124 - -train-31 - -- Implemented the reverse backend, so now storage repos load the server - -train-29 - -- Add a CONTRIBUTING.md and an AUTHORS file - #104, #117 -- Remove references to the old .fxa_dbrc config file - #107, #113 -- Failed stored procedures return errors correctly - #95, #94 -- Add a unique index to passwordChangeTokens(uid) - #100 -- fix(build): Adding --force flag onto grunt validate-shrinkwrap task - #98 -- Update convict to newer version - #96 -- NOTE: This train will require the addition of stored procedures and a new - index on passwordChangeTokens to the stage and production databases - -train-24 - -- Use the DB stored procedures, instead of raw SQL - #84 -- NOTE: This train will require the addition of stored procedures to the stage and production databases - -train-23 - -- no-op. Rebuild to use nodejs 0.10.32 - -train-22 - -- licence, jshint miscellany - -train-21 - -- remove patchLevel from config, now in code - #69, #70 -- update node-ass version - #75 -- Use named mysql error constants rather than raw magic numbers - #74 -- show prune messages at loglevel info - #73 - -train-20 - -- add code to remove stale tokens from - fxa.{accountReset,passwordForgot,passwordChange}Tokens tables - issue #2 - - adds PROCEDURE `prune` to fxa database - - controlled by options `pruneEvery' and`enablePruning' -- pass err.stack so bunyan actually prints more than "uncaughtException" - #68 -- updates to use `mysql-patcher' modules in place of db_patcher script - #71, #72 - -train-19 - -- Switch to convict for config -- update ass version -- update restify and request for new qs module - -train-18 - -- use a version of ass that does not pull in gh-badges at all: #58 -- email argument is already a buffer: #56, #57 -- add shrinkwrap; npm shrinkwrap --dev: #54 -- fixed log object not having 'stat' in tests - -train-17 - -- added locale to accounts #53 - -train-16 - -- NSTR - -train-15 - -- code cleanup and test addition - -train-14 - -- fix #33 - retryable is matching errno on the wrong object in some cases -- more tests -- fix coverage stats - -train-13 - -- initial version diff --git a/packages/fxa-auth-db-mysql/CHANGELOG.md b/packages/fxa-auth-db-mysql/CHANGELOG.md deleted file mode 100644 index 81825ffd3d3..00000000000 --- a/packages/fxa-auth-db-mysql/CHANGELOG.md +++ /dev/null @@ -1,2603 +0,0 @@ -## 1.223.2 - -### Other changes - -- deps: switch from git to https for deps (#11587) ([2611a980d](https://github.com/mozilla/fxa/commit/2611a980d)) - -## 1.223.1 - -No changes. - -## 1.223.0 - -No changes. - -## 1.222.0 - -### New features - -- fxa-shared: Allows sentry events for critical endpoints to be 'tagged' as such. Because: ([6abd9bd3e](https://github.com/mozilla/fxa/commit/6abd9bd3e)) - -### Other changes - -- deps: bump @sentry/node from 6.15.0 to 6.16.1 ([d6e82ae9c](https://github.com/mozilla/fxa/commit/d6e82ae9c)) -- 2d519d084 Feedback ([2d519d084](https://github.com/mozilla/fxa/commit/2d519d084)) - -## 1.221.3 - -No changes. - -## 1.221.2 - -No changes. - -## 1.221.1 - -No changes. - -## 1.221.0 - -No changes. - -## 1.220.5 - -No changes. - -## 1.220.4 - -No changes. - -## 1.220.3 - -No changes. - -## 1.220.2 - -No changes. - -## 1.220.1 - -No changes. - -## 1.220.0 - -### Other changes - -- deps-dev: bump nock from 13.1.4 to 13.2.1 (#11121) ([7ecc6f0e7](https://github.com/mozilla/fxa/commit/7ecc6f0e7)) -- deps: bump @sentry/node from 6.14.3 to 6.15.0 (#11036) ([cb0f09d41](https://github.com/mozilla/fxa/commit/cb0f09d41)) -- deps: bump @sentry/node from 6.14.1 to 6.14.3 (#11020) ([507aef4b4](https://github.com/mozilla/fxa/commit/507aef4b4)) - -## 1.219.5 - -No changes. - -## 1.219.4 - -No changes. - -## 1.219.3 - -No changes. - -## 1.219.2 - -No changes. - -## 1.219.1 - -No changes. - -## 1.219.0 - -### Other changes - -- deps: bump @sentry/node from 6.13.3 to 6.14.1 (#10943) ([bff4cedc9](https://github.com/mozilla/fxa/commit/bff4cedc9)) - -## 1.218.9 - -No changes. - -## 1.218.8 - -No changes. - -## 1.218.7 - -No changes. - -## 1.218.6 - -No changes. - -## 1.218.5 - -### Other changes - -- deps-dev: bump nock from 13.1.3 to 13.1.4 (#10884) ([d9dc1d08c](https://github.com/mozilla/fxa/commit/d9dc1d08c)) - -## 1.218.4 - -No changes. - -## 1.218.3 - -No changes. - -## 1.218.2 - -No changes. - -## 1.218.1 - -No changes. - -## 1.218.0 - -### Other changes - -- deps: bump convict from 6.2.0 to 6.2.1 ([ef7842bc0](https://github.com/mozilla/fxa/commit/ef7842bc0)) - -## 1.217.2 - -No changes. - -## 1.217.1 - -No changes. - -## 1.217.0 - -### Other changes - -- deps: updated dependencies (#10638) ([f57031d15](https://github.com/mozilla/fxa/commit/f57031d15)) -- deps: bump @sentry/node from 6.12.0 to 6.13.2 (#10583) ([cb19efb3c](https://github.com/mozilla/fxa/commit/cb19efb3c)) - -## 1.216.3 - -No changes. - -## 1.216.2 - -No changes. - -## 1.216.1 - -No changes. - -## 1.216.0 - -### Other changes - -- deps: update pm2; dedupe (#10557) ([89e549a74](https://github.com/mozilla/fxa/commit/89e549a74)) - -## 1.215.2 - -No changes. - -## 1.215.1 - -### Bug fixes - -- db: remove patch check from auth-db-mysql ([9572ca983](https://github.com/mozilla/fxa/commit/9572ca983)) - -## 1.215.0 - -### Refactorings - -- db: created a new package for all db migrations ([9e7814418](https://github.com/mozilla/fxa/commit/9e7814418)) - -### Other changes - -- deps: bump @sentry/node from 6.11.0 to 6.12.0 ([4809fc2cc](https://github.com/mozilla/fxa/commit/4809fc2cc)) - -## 1.214.1 - -No changes. - -## 1.214.0 - -### Other changes - -- deps-dev: bump nock from 13.1.1 to 13.1.3 ([7c467acc0](https://github.com/mozilla/fxa/commit/7c467acc0)) - -## 1.213.11 - -No changes. - -## 1.213.10 - -No changes. - -## 1.213.9 - -No changes. - -## 1.213.8 - -No changes. - -## 1.213.7 - -No changes. - -## 1.213.6 - -No changes. - -## 1.213.5 - -No changes. - -## 1.213.4 - -No changes. - -## 1.213.3 - -No changes. - -## 1.213.2 - -No changes. - -## 1.213.1 - -No changes. - -## 1.213.0 - -### Other changes - -- deps: bump @sentry/node from 6.10.0 to 6.11.0 ([25f24a897](https://github.com/mozilla/fxa/commit/25f24a897)) - -## 1.212.2 - -No changes. - -## 1.212.1 - -No changes. - -## 1.212.0 - -### New features - -- emails: Add the finish account setup email for passwordless accounts ([445738953](https://github.com/mozilla/fxa/commit/445738953)) - -### Other changes - -- deps: updated base deps for train-212 ([8a391693f](https://github.com/mozilla/fxa/commit/8a391693f)) -- deps: bump convict from 6.1.0 to 6.2.0 ([99be156b7](https://github.com/mozilla/fxa/commit/99be156b7)) -- deps: bump convict-format-with-moment from 6.0.1 to 6.2.0 ([27490228a](https://github.com/mozilla/fxa/commit/27490228a)) - -## 1.211.2 - -No changes. - -## 1.211.1 - -No changes. - -## 1.211.0 - -### Bug fixes - -- emails: send email for cancelation due to failed payments ([3142a4e53](https://github.com/mozilla/fxa/commit/3142a4e53)) - -### Other changes - -- deps: bump convict-format-with-validator from 6.0.1 to 6.2.0 ([a43649dcb](https://github.com/mozilla/fxa/commit/a43649dcb)) -- deps: bump @sentry/node from 6.7.2 to 6.9.0 ([10020fb87](https://github.com/mozilla/fxa/commit/10020fb87)) - -## 1.210.3 - -No changes. - -## 1.210.2 - -No changes. - -## 1.210.1 - -No changes. - -## 1.210.0 - -### Other changes - -- deps-dev: bump nock from 13.1.0 to 13.1.1 ([2f720ea98](https://github.com/mozilla/fxa/commit/2f720ea98)) -- auth-server: fly away bluebird ([5f63d848b](https://github.com/mozilla/fxa/commit/5f63d848b)) - -## 1.209.1 - -No changes. - -## 1.209.0 - -### New features - -- admin: disable account ([4c995b603](https://github.com/mozilla/fxa/commit/4c995b603)) -- auth: convert remaining auth-server db use to direct db access ([a561ae1f3](https://github.com/mozilla/fxa/commit/a561ae1f3)) - -### Other changes - -- deps: update deps and start ignoring @types/\* in dependabot ([694ff5f6a](https://github.com/mozilla/fxa/commit/694ff5f6a)) -- deps: bump @sentry/node from 6.7.0 to 6.7.1 ([b78095131](https://github.com/mozilla/fxa/commit/b78095131)) -- deps: bump @sentry/node from 6.5.1 to 6.7.0 ([b6119a2c7](https://github.com/mozilla/fxa/commit/b6119a2c7)) -- deps: update pm2 / dedupe ([5d7653fa6](https://github.com/mozilla/fxa/commit/5d7653fa6)) - -## 1.208.4 - -No changes. - -## 1.208.3 - -No changes. - -## 1.208.2 - -No changes. - -## 1.208.1 - -No changes. - -## 1.208.0 - -### Other changes - -- deps: updated some deps ([fa895572c](https://github.com/mozilla/fxa/commit/fa895572c)) -- deps: updated pm2 ([34704ba14](https://github.com/mozilla/fxa/commit/34704ba14)) -- deps: updated sentry/\* packages ([9095a1c13](https://github.com/mozilla/fxa/commit/9095a1c13)) -- deps-dev: bump nock from 13.0.11 to 13.1.0 ([4d52527d8](https://github.com/mozilla/fxa/commit/4d52527d8)) - -## 1.207.1 - -No changes. - -## 1.207.0 - -### New features - -- auth: create script to send subscription renewal reminder emails ([178cec80a](https://github.com/mozilla/fxa/commit/178cec80a)) - -### Other changes - -- deps: bump mocha from 7.2.0 to 8.4.0 ([4b11eab5f](https://github.com/mozilla/fxa/commit/4b11eab5f)) -- deps: update some deps ([6fce48032](https://github.com/mozilla/fxa/commit/6fce48032)) -- deps: added "yarn outdated" plugin + updated some deps ([952e4f388](https://github.com/mozilla/fxa/commit/952e4f388)) - -## 1.206.1 - -No changes. - -## 1.206.0 - -### Other changes - -- deps: bump mozlog from 3.0.1 to 3.0.2 ([f46bd3472](https://github.com/mozilla/fxa/commit/f46bd3472)) -- deps: updated pm2 ([0847e2545](https://github.com/mozilla/fxa/commit/0847e2545)) - -## 1.205.0 - -### New features - -- emails: add tables and models for email history ([16212769d](https://github.com/mozilla/fxa/commit/16212769d)) - -## 1.204.7 - -No changes. - -## 1.204.6 - -No changes. - -## 1.204.5 - -No changes. - -## 1.204.4 - -No changes. - -## 1.204.3 - -No changes. - -## 1.204.2 - -No changes. - -## 1.204.1 - -### Bug fixes - -- release: Add changelog notes and bump version for 204 ([5b8356e11](https://github.com/mozilla/fxa/commit/5b8356e11)) - -## 1.204.0 - -No changes. - -## 1.203.5 - -No changes. - -## 1.203.4 - -No changes. - -## 1.203.3 - -No changes. - -## 1.203.2 - -No changes. - -## 1.203.1 - -No changes. - -## 1.203.0 - -### Other changes - -- deps: update convict ([52e626866](https://github.com/mozilla/fxa/commit/52e626866)) - -## 1.202.3 - -No changes. - -## 1.202.2 - -No changes. - -## 1.202.1 - -No changes. - -## 1.202.0 - -### Other changes - -- deps-dev: bump nock from 13.0.7 to 13.0.11 ([f38836bf8](https://github.com/mozilla/fxa/commit/f38836bf8)) -- d426b981e Fix use of MySQL cluster read-only nodes ([d426b981e](https://github.com/mozilla/fxa/commit/d426b981e)) - -## 1.201.1 - -No changes. - -## 1.201.0 - -### Other changes - -- deps-dev: bump nock from 13.0.5 to 13.0.7 ([411638723](https://github.com/mozilla/fxa/commit/411638723)) - -## 1.200.0 - -### Other changes - -- deps-dev: bump restify-clients from 2.6.9 to 3.1.0 ([961ab2f2bd](https://github.com/mozilla/fxa/commit/961ab2f2bd)) - -## 1.199.0 - -### Other changes - -- deps: bump @sentry/node from 6.0.0 to 6.0.1 ([3b6838b18](https://github.com/mozilla/fxa/commit/3b6838b18)) -- deps: bump @sentry/node from 5.29.1 to 6.0.0 ([147825a5b](https://github.com/mozilla/fxa/commit/147825a5b)) - -## 1.198.2 - -No changes. - -## 1.198.1 - -### Other changes - -- 4e70b3f04 merge main->train-198 ([4e70b3f04](https://github.com/mozilla/fxa/commit/4e70b3f04)) - -## 1.198.0 - -### Other changes - -- deps: update eslint to v7 ([7cf502be2](https://github.com/mozilla/fxa/commit/7cf502be2)) - -## 1.197.3 - -No changes. - -## 1.197.2 - -No changes. - -## 1.197.1 - -No changes. - -## 1.197.0 - -No changes. - -## 1.196.0 - -### New features - -- db: Create table to store PayPal customer information ([10f4cae5a](https://github.com/mozilla/fxa/commit/10f4cae5a)) - -### Other changes - -- deps: bump @sentry/node from 5.23.0 to 5.29.1 ([0bc414ad2](https://github.com/mozilla/fxa/commit/0bc414ad2)) - -## 1.195.4 - -No changes. - -## 1.195.3 - -No changes. - -## 1.195.2 - -No changes. - -## 1.195.1 - -No changes. - -## 1.195.0 - -No changes. - -## 1.194.0 - -No changes. - -## 1.193.1 - -No changes. - -## 1.193.0 - -### Other changes - -- deps: update node version to 14 ([6c2b253c1](https://github.com/mozilla/fxa/commit/6c2b253c1)) - -## 1.192.0 - -No changes. - -## 1.191.1 - -No changes. - -## 1.191.0 - -No changes. - -## 1.190.1 - -No changes. - -## 1.190.0 - -No changes. - -## 1.189.1 - -No changes. - -## 1.189.0 - -### New features - -- db: Create table to store user-customer relationship Because: ([d994e2f56](https://github.com/mozilla/fxa/commit/d994e2f56)) - -### Other changes - -- deps: bump base64url from 3.0.0 to 3.0.1 ([33aa6370b](https://github.com/mozilla/fxa/commit/33aa6370b)) -- deps: update mozlog ([a68310952](https://github.com/mozilla/fxa/commit/a68310952)) - -## 1.188.1 - -No changes. - -## 1.188.0 - -No changes. - -## 1.187.3 - -No changes. - -## 1.187.2 - -No changes. - -## 1.187.1 - -No changes. - -## 1.187.0 - -No changes. - -## 1.186.2 - -No changes. - -## 1.186.1 - -No changes. - -## 1.186.0 - -### Other changes - -- deps: update yarn version and root level deps ([da2e99729](https://github.com/mozilla/fxa/commit/da2e99729)) - -## 1.185.1 - -No changes. - -## 1.185.0 - -### Other changes - -- dependency updates ([aaa549ed6](https://github.com/mozilla/fxa/commit/aaa549ed6)) - -## 1.184.1 - -No changes. - -## 1.184.0 - -No changes. - -## 1.183.1 - -No changes. - -## 1.183.0 - -No changes. - -## 1.182.2 - -No changes. - -## 1.182.1 - -No changes. - -## 1.182.0 - -No changes. - -## 1.181.2 - -No changes. - -## 1.181.1 - -No changes. - -## 1.181.0 - -No changes. - -## 1.180.1 - -### Bug fixes - -- db: Set collation on stored procedure email fields, so mysql will use indexes. ([044d7280e](https://github.com/mozilla/fxa/commit/044d7280e)) - -## 1.180.0 - -### New features - -- aet: Add ecosystemAnonId to auth-db and mysql ([49917be6c](https://github.com/mozilla/fxa/commit/49917be6c)) - -## 1.179.4 - -No changes. - -## 1.179.3 - -No changes. - -## 1.179.2 - -No changes. - -## 1.179.1 - -No changes. - -## 1.179.0 - -No changes. - -## 1.178.1 - -No changes. - -## 1.178.0 - -### Other changes - -- deps: update deps ([27cd24c63](https://github.com/mozilla/fxa/commit/27cd24c63)) -- docs: Replace 'master' with 'main' throughout ([20a0acf8b](https://github.com/mozilla/fxa/commit/20a0acf8b)) - -## 1.177.1 - -No changes. - -## 1.177.0 - -### Other changes - -- deps: updated dependencies ([3fa952919](https://github.com/mozilla/fxa/commit/3fa952919)) -- pm2: Add ISO timestamp to pm2 log lines ([2c5630adb](https://github.com/mozilla/fxa/commit/2c5630adb)) - -## 1.176.0 - -No changes. - -## 1.175.0 - -### New features - -- auth: handle a password change requirement in login ([c495177e8](https://github.com/mozilla/fxa/commit/c495177e8)) - -### Other changes - -- README files: Fix dead links in READMEs ([38624143e](https://github.com/mozilla/fxa/commit/38624143e)) - -## 1.174.2 - -No changes. - -## 1.174.1 - -No changes. - -## 1.174.0 - -### Bug fixes - -- local-dev: added fxa-shared and fxa-react to pm2 ([c3780546b](https://github.com/mozilla/fxa/commit/c3780546b)) - -## 1.173.0 - -### Bug fixes - -- build: fix paths to fxa-shared ([21fe09b72](https://github.com/mozilla/fxa/commit/21fe09b72)) - -### Refactorings - -- tsconfig: consolidate common tsconfig options ([e565285b7](https://github.com/mozilla/fxa/commit/e565285b7)) -- packages: use workspace references ([81575019a](https://github.com/mozilla/fxa/commit/81575019a)) - -### Other changes - -- deps: update some dependencies ([fec460f6d](https://github.com/mozilla/fxa/commit/fec460f6d)) -- format: mass reformat with prettier 2 and single config ([cc595fc2b](https://github.com/mozilla/fxa/commit/cc595fc2b)) -- deps: updated mocha to 7.1.2 ([a5c1a339c](https://github.com/mozilla/fxa/commit/a5c1a339c)) - -## 1.172.2 - -No changes. - -## 1.172.1 - -No changes. - -## 1.172.0 - -No changes. - -## 1.171.1 - -No changes. - -## 1.171.0 - -### Bug fixes - -- deps: Add exception for yargs-parser nsp advisory 1500 ([b54877911](https://github.com/mozilla/fxa/commit/b54877911)) - -## 1.170.3 - -No changes. - -## 1.170.2 - -No changes. - -## 1.170.1 - -No changes. - -## 1.170.0 - -### Other changes - -- all: update readmes across all packages to improve testing documentation ([099163e94](https://github.com/mozilla/fxa/commit/099163e94)) - -## 1.169.3 - -No changes. - -## 1.169.2 - -No changes. - -## 1.169.1 - -No changes. - -## 1.169.0 - -### New features - -- build: add a default dockerfile template to build.sh ([4dd0b0007](https://github.com/mozilla/fxa/commit/4dd0b0007)) - -## 1.168.3 - -No changes. - -## 1.168.2 - -No changes. - -## 1.168.1 - -No changes. - -## 1.168.0 - -### New features - -- docker: created fxa-builder docker image ([d4da8a360](https://github.com/mozilla/fxa/commit/d4da8a360)) -- db: modified procedures to set verifiedAt field when email gets verified ([710542f6d](https://github.com/mozilla/fxa/commit/710542f6d)) - -## 1.167.1 - -No changes. - -## 1.167.0 - -### Refactorings - -- config: replace 127.0.0.1 with localhost ([1dd1b038d](https://github.com/mozilla/fxa/commit/1dd1b038d)) -- pm2: restructure our pm2 configs ([3a054dfc3](https://github.com/mozilla/fxa/commit/3a054dfc3)) - -## 1.166.2 - -No changes. - -## 1.166.1 - -No changes. - -## 1.166.0 - -### Refactorings - -- emails: move all email normalization and equality checks to helper functions ([ce1930f4b](https://github.com/mozilla/fxa/commit/ce1930f4b)) - -## 1.165.1 - -No changes. - -## 1.165.0 - -No changes. - -## 1.164.1 - -No changes. - -## 1.164.0 - -### Bug fixes - -- docs: update MySQL version and node version ([dd56076df](https://github.com/mozilla/fxa/commit/dd56076df)) - -## 1.163.2 - -No changes. - -## 1.163.1 - -No changes. - -## 1.163.0 - -### Other changes - -- deps: Updates to address nsp advisory 1179 ([a5649db18](https://github.com/mozilla/fxa/commit/a5649db18)) - -## 1.162.3 - -No changes. - -## 1.162.2 - -No changes. - -## 1.162.1 - -No changes. - -## 1.162.0 - -### Bug fixes - -- monorepo: update default node version across packages ([0f2d54071](https://github.com/mozilla/fxa/commit/0f2d54071)) - -### Other changes - -- cleanup: remove obsolete docker files ([863e56163](https://github.com/mozilla/fxa/commit/863e56163)) -- deps: Updates to address nsp advisory 1488 ([e47bc55ba](https://github.com/mozilla/fxa/commit/e47bc55ba)) - -## 1.161.2 - -No changes. - -## 1.161.1 - -No changes. - -## 1.161.0 - -### Bug fixes - -- mysql: Force MySQL connections to always use UTC timezone. ([c97f9e5b8](https://github.com/mozilla/fxa/commit/c97f9e5b8)) - -### Other changes - -- skip some subscription-related tests on content-server for now ([e573b52f5](https://github.com/mozilla/fxa/commit/e573b52f5)) -- subscriptions: remove accountSubscriptions table and procedures ([cd0521557](https://github.com/mozilla/fxa/commit/cd0521557)) -- contributing: update contact information to reflect move to Matrix ([4e7082856](https://github.com/mozilla/fxa/commit/4e7082856)) - -## 1.160.1 - -No changes. - -## 1.160.0 - -No changes. - -## 1.159.0 - -### Bug fixes - -- docker: don't rm /tmp after npm i ([6fc34fc45](https://github.com/mozilla/fxa/commit/6fc34fc45)) - -## 1.158.1 - -No changes. - -## 1.158.0 - -### New features - -- keys: Add ability to enable/disable recovery key ([dba5ee65d](https://github.com/mozilla/fxa/commit/dba5ee65d)) -- coverage: Add coveralls coverage ([932b70c3c](https://github.com/mozilla/fxa/commit/932b70c3c)) - -### Other changes - -- mem: Remove auth server db memory database ([2fa9dce43](https://github.com/mozilla/fxa/commit/2fa9dce43)) - -## 1.157.0 - -No changes. - -## 1.156.0 - -No changes. - -## 1.155.0 - -### Refactorings - -- git: merge all package gitignores into single root-level gitignore (a238c3d27) - -## 1.154.0 - -No changes. - -## 1.153.0 - -### Other changes - -- monorepo: remove stale references to travisci (9b4789125) -- node: updated node to v12 (7169a367e) - -## 1.152.1 - -No changes. - -## 1.152.0 - -No changes. - -## 1.151.5 - -No changes. - -## 1.151.4 - -No changes. - -## 1.151.3 - -No changes. - -## 1.151.2 - -No changes. - -## 1.151.1 - -No changes. - -## 1.151.0 - -### New features - -- audit: run npm audit on push instead of in ci (ccd3c2b07) - -### Bug fixes - -- deps: Fix a bunch of audit warnings (f8a1da3be) - -### Other changes - -- deps: Remove stale nsp exceptions from .nsprc files (f7324a1b2) -- deps: Get audit-filter working for all packages in monorepo (1b0141e2b) -- monorepo: eslint consolidation (0a5e3950f) - -## 1.150.9 - -No changes. - -## 1.150.8 - -No changes. - -## 1.150.7 - -No changes. - -## 1.150.6 - -No changes. - -## 1.150.5 - -No changes. - -## 1.150.4 - -No changes. - -## 1.150.3 - -No changes. - -## 1.150.2 - -No changes. - -## 1.150.1 - -### New features - -- keys: Explicitly track timestamp of last key rotation. (f8dbdfad9) - -### Bug fixes - -- tests: Fix secondary-emails test to account for nondeterministic result order. (59c9a8c1c) - -## 1.150.0 - -No changes. - -## 1.149.4 - -No changes. - -## 1.149.3 - -No changes. - -## 1.149.2 - -No changes. - -## 1.149.1 - -No changes. - -## 1.149.0 - -### Other changes - -- deps: move auth server from shrinkwrap to package-lock (8e4af3095) - -## 1.148.8 - -No changes. - -## 1.148.7 - -No changes. - -## 1.148.6 - -No changes. - -## 1.148.5 - -No changes. - -## 1.148.4 - -No changes. - -## 1.148.3 - -No changes. - -## 1.148.2 - -### Other changes - -- release: Merge branch 'train-147' into train-148-merge-147 (66e170d45) - -## 1.148.1 - -No changes. - -## 1.148.0 - -### New features - -- add vscode tasks for running tests and debugger (dac5e8b98) - -## 1.147.5 - -No changes. - -## 1.147.4 - -No changes. - -## 1.147.3 - -No changes. - -## 1.147.2 - -No changes. - -## 1.147.1 - -No changes. - -## 1.147.0 - -### Bug fixes - -- build: npm audit fix (4839fcc5e) -- db: Reset `keysChangedAt` to NULL if we don't know its correct value. (89a8423d4) - -## 1.146.4 - -No changes. - -## 1.146.3 - -No changes. - -## 1.146.2 - -No changes. - -## 1.146.1 - -No changes. - -## 1.146.0 - -No changes. - -## 1.145.5 - -No changes. - -## 1.145.4 - -No changes. - -## 1.145.3 - -No changes. - -## 1.145.2 - -No changes. - -## 1.145.1 - -No changes. - -## 1.145.0 - -### Bug fixes - -- subscriptions: bump account profileUpdatedAt when subscriptions are changed (8c21351b4) - -### Refactorings - -- db: rename productName to productId (5d709f96d) - -### Other changes - -- deps: remove newrelic step one (675c08924) - -## 1.144.4 - -No changes. - -## 1.144.3 - -No changes. - -## 1.144.2 - -No changes. - -## 1.144.1 - -No changes. - -## 1.144.0 - -No changes. - -## 1.143.4 - -No changes. - -## 1.143.3 - -No changes. - -## 1.143.2 - -No changes. - -## 1.143.1 - -### New features - -- recovery: Clear recovery keys when resetting account (f1f93cc19) - -## 1.143.0 - -### Other changes - -- support-panel: call out stored procedures with specific grants (4450eccc9) -- ci: Remove CI config from within packages subdir. (66990a8f4) - -## 1.142.1 - -No changes. - -## 1.142.0 - -### New features - -- support-panel: support live user queries (79534bc49) -- routes: securityEvents GET and DELETE added with uid (90750377b) - -### Bug fixes - -- docs: remove extra code indents that messed up formatting (ae014390d) - -## 1.141.8 - -No changes. - -## 1.141.7 - -No changes. - -## 1.141.6 - -No changes. - -## 1.141.5 - -No changes. - -## 1.141.4 - -No changes. - -## 1.141.3 - -No changes. - -## 1.141.2 - -### Other changes - -- package: manually bump version strings to 1.141.1 (737265b25) - -## 1.141.1 - -No changes. - -## 1.141.0 - -### New features - -- subscriptions: implement reactivation of cancelled subscriptions (e0391a658) -- script: script for reading security events from db (ea21cf4e9) - -### Bug fixes - -- tests: add remote db tests for subscription cancellation (1bd4b2607) -- scripts: expect semi-colons in db migration script (1d1c630c1) -- format: fixed up COTRIBUTING.md files (a0422c6ae) - -### Other changes - -- subs: remove `|| []` from call to db.fetchAccountSubscriptions (4f816d103) -- style: added prettier precommit hook (2820ac733) -- style: added prettier to fxa-auth-db-mysql (963cdd235) - -## 1.140.3 - -No changes. - -## 1.140.2 - -No changes. - -## 1.140.1 - -No changes. - -## 1.140.0 - -### New features - -- clients: Add a route for listing all attached clients. (13f0e20ad) - -## 1.139.2 - -No changes. - -## 1.139.1 - -No changes. - -## 1.139.0 - -No changes. - -## 1.138.4 - -No changes. - -## 1.138.3 - -No changes. - -## 1.138.2 - -No changes. - -## 1.138.1 - -No changes. - -## 1.138.0 - -### New features - -- subscriptions: support deferred cancellation of subscriptions (4ee71842d) - -### Refactorings - -- tests: switch from insist to chai for assertions (e93fdf9aa) - -## 1.137.4 - -No changes. - -## 1.137.3 - -No changes. - -## 1.137.2 - -No changes. - -## 1.137.1 - -No changes. - -## 1.137.0 - -### Bug fixes - -- url: base, homepage, bug url updated for all packages in package.json (cee3dc741) - -## 1.136.6 - -No changes. - -## 1.136.5 - -No changes. - -## 1.136.4 - -No changes. - -## 1.136.3 - -No changes. - -## 1.136.2 - -No changes. - -## 1.136.1 - -No changes. - -## 1.136.0 - -No changes. - -## 1.135.6 - -No changes. - -## 1.135.5 - -No changes. - -## 1.135.4 - -No changes. - -## 1.135.3 - -No changes. - -## 1.135.2 - -No changes. - -## 1.135.1 - -No changes. - -## 1.135.0 - -### New features - -- accounts: add ability to associate subscriptions with an account (e9ffe4374) - -### Bug fixes - -- package: update grunt to fix nsp warning in fxa-auth-db-mysql (0591237c0) - -### Other changes - -- db: remove old scrypt-hash dependency from auth db (42816c67a) -- packages: remove old release tagging scripts and docs (6f168c244) - -## 1.134.5 - -No changes. - -## 1.134.4 - -No changes. - -## 1.134.3 - -No changes. - -## 1.134.2 - -No changes. - - - -## [1.133.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.133.0...v1.133.1) (2019-03-19) - -### Features - -- **devices:** Add ability to associate a device record with a refesh token. ([1123e32](https://github.com/mozilla/fxa-auth-db-mysql/commit/1123e32)) - - - -# [1.133.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.132.0...v1.133.0) (2019-03-19) - -### chore - -- **devices:** Add explicit deletes to replace `ON DELETE CASCADE`. ([75aba96](https://github.com/mozilla/fxa-auth-db-mysql/commit/75aba96)) -- **package:** update shrinkwrap ([f629704](https://github.com/mozilla/fxa-auth-db-mysql/commit/f629704)) - - - -# [1.132.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.130.0...v1.132.0) (2019-03-05) - -### chore - -- **deploy:** upgrade to node 10 ([f3bc954](https://github.com/mozilla/fxa-auth-db-mysql/commit/f3bc954)) -- **deps:** update nyc ([db987c3](https://github.com/mozilla/fxa-auth-db-mysql/commit/db987c3)) -- **routes:** Remove last vestiges of `sessionWithDevice` route. ([0e5115b](https://github.com/mozilla/fxa-auth-db-mysql/commit/0e5115b)) - -### Features - -- **account:** Add `profileChangedAt` and `keysChangedAt` to the `accounts` table. ([02e944c](https://github.com/mozilla/fxa-auth-db-mysql/commit/02e944c)) - -### test - -- **demo:** add some comments to pt-osc demo ([c85cc7a](https://github.com/mozilla/fxa-auth-db-mysql/commit/c85cc7a)) -- **demo:** set up triggers like pt-osc and check ([ecb87b3](https://github.com/mozilla/fxa-auth-db-mysql/commit/ecb87b3)) - - - -# [1.130.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.129.0...v1.130.0) (2019-02-05) - -### chore - -- **ci:** run tests on node 10 ([5467e2f](https://github.com/mozilla/fxa-auth-db-mysql/commit/5467e2f)) - -### Refactor - -- **crypto:** fall back to node's scrypt implementation ([932f2dd](https://github.com/mozilla/fxa-auth-db-mysql/commit/932f2dd)) - - - -# [1.129.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.128.1...v1.129.0) (2019-01-24) - -### Bug Fixes - -- **test:** add a test script to add account rows ([3aa09cd](https://github.com/mozilla/fxa-auth-db-mysql/commit/3aa09cd)) - - - -## [1.128.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.128.0...v1.128.1) (2019-01-09) - -### chore - -- **deps:** reshrink to get ramda deps ([260063b](https://github.com/mozilla/fxa-auth-db-mysql/commit/260063b)) - - - -# [1.128.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.127.0...v1.128.0) (2019-01-08) - -### Bug Fixes - -- **query:** remove `ROW_COUNT()` from remaining procedures ([4e8b058](https://github.com/mozilla/fxa-auth-db-mysql/commit/4e8b058)) -- **query:** update set primary email query to not check if email is verified ([b9bc3c7](https://github.com/mozilla/fxa-auth-db-mysql/commit/b9bc3c7)) - -### Features - -- **npm:** update shrink script ([96b3ce5](https://github.com/mozilla/fxa-auth-db-mysql/commit/96b3ce5)) - - - -# [1.127.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.126.0...v1.127.0) (2018-12-11) - -### chore - -- **scripts:** ignore newly failing stored procedures ([edf0bb4](https://github.com/mozilla/fxa-auth-db-mysql/commit/edf0bb4)) - -### Features - -- **scripts:** check for FOREIGN KEY in migration lint script ([82170eb](https://github.com/mozilla/fxa-auth-db-mysql/commit/82170eb)) -- **scripts:** check for missing expected encodings on procedure args ([daf2677](https://github.com/mozilla/fxa-auth-db-mysql/commit/daf2677)) -- **scripts:** lint-ignore tables that already have foreign keys ([3aeca8e](https://github.com/mozilla/fxa-auth-db-mysql/commit/3aeca8e)) - -### Refactor - -- **scripts:** harmonise row count stuff with rest of lint script ([6065fe8](https://github.com/mozilla/fxa-auth-db-mysql/commit/6065fe8)) - - - -# [1.126.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.125.0...v1.126.0) (2018-11-27) - -### Bug Fixes - -- **account:** don't use `LOWER(uid)` in account query ([d2cfe49](https://github.com/mozilla/fxa-auth-db-mysql/commit/d2cfe49)) -- **account:** update accountRecord to specify charset for inEmail ([a45c8a0](https://github.com/mozilla/fxa-auth-db-mysql/commit/a45c8a0)) -- **tests:** Don't put binary data into fake email addresses. ([5c83dec](https://github.com/mozilla/fxa-auth-db-mysql/commit/5c83dec)) - - - -# [1.125.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.124.1...v1.125.0) (2018-11-14) - -### Bug Fixes - -- **scripts:** stop the explain script tripping over git grep colours ([ff0ac5c](https://github.com/mozilla/fxa-auth-db-mysql/commit/ff0ac5c)) - -### chore - -- **db:** use mariadb-friendly drop index syntax ([f01b520](https://github.com/mozilla/fxa-auth-db-mysql/commit/f01b520)) -- **scripts:** lint-ignore consumeRecoveryCode_2 and setPrimaryEmail_3 ([5ddf863](https://github.com/mozilla/fxa-auth-db-mysql/commit/5ddf863)) - -### Features - -- **scripts:** add ROW_COUNT() checks to the procedure-linting script ([0eb0142](https://github.com/mozilla/fxa-auth-db-mysql/commit/0eb0142)) - - - -## [1.124.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.124.0...v1.124.1) (2018-11-02) - -### Bug Fixes - -- **package:** update deps ([d44e10f](https://github.com/mozilla/fxa-auth-db-mysql/commit/d44e10f)) - - - -# [1.124.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.123.3...v1.124.0) (2018-10-30) - - - -## [1.123.3](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.123.2...v1.123.3) (2018-10-30) - -### Bug Fixes - -- **accountRecord:** Rollback `accountRecord_4` due to unexplained performance issues. ([034b3b0](https://github.com/mozilla/fxa-auth-db-mysql/commit/034b3b0)) -- **migration:** Fix typo in SP name in reverse migration for 91. ([5b08dba](https://github.com/mozilla/fxa-auth-db-mysql/commit/5b08dba)) - - - -## [1.123.2](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.123.1...v1.123.2) (2018-10-26) - -### Bug Fixes - -- **account:** rollback `profileChangedAt` migration ([4b4f7d4](https://github.com/mozilla/fxa-auth-db-mysql/commit/4b4f7d4)) - - - -## [1.123.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.122.1...v1.123.1) (2018-10-22) - - - -# [1.123.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.121.0...v1.123.0) (2018-10-16) - -### Bug Fixes - -- **account:** delete recovery codes, recovery keys, security events on account delete ([a8d0467](https://github.com/mozilla/fxa-auth-db-mysql/commit/a8d0467)) -- **mem:** ensure emailBounces are stored most-recent first ([ccf6c3c](https://github.com/mozilla/fxa-auth-db-mysql/commit/ccf6c3c)) -- **performance:** Add index for scanning signinCodes by uid. ([905e716](https://github.com/mozilla/fxa-auth-db-mysql/commit/905e716)) - -### chore - -- **deps:** Update deps to fix security warnings, remove nsp ([5581297](https://github.com/mozilla/fxa-auth-db-mysql/commit/5581297)) - - - -## [1.122.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.121.1...v1.122.1) (2018-10-22) - - - -# [1.122.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.121.0...v1.122.0) (2018-10-02) - -### Features - -- **account:** add `profileChangedAt` property to account table ([24917b7](https://github.com/mozilla/fxa-auth-db-mysql/commit/24917b7)) - - - -## [1.121.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.121.0...v1.121.1) (2018-10-18) - -### Bug Fixes - -- **account:** update stored procedures to be more replication friendly ([3c1dd5a](https://github.com/mozilla/fxa-auth-db-mysql/commit/3c1dd5a)) - - - -# [1.121.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.120.0...v1.121.0) (2018-09-18) - -### chore - -- **scripts:** disable the explain script in production ([52447bb](https://github.com/mozilla/fxa-auth-db-mysql/commit/52447bb)) -- **scripts:** tweak some old migrations to fix explain errors ([9e9457c](https://github.com/mozilla/fxa-auth-db-mysql/commit/9e9457c)) - -### Features - -- **scripts:** add an ignore file for the explain script ([b90688c](https://github.com/mozilla/fxa-auth-db-mysql/commit/b90688c)) -- **scripts:** add script to automate MySQL EXPLAIN checks ([31fff59](https://github.com/mozilla/fxa-auth-db-mysql/commit/31fff59)) - - - -# [1.120.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.118.1...v1.120.0) (2018-09-06) - -### Bug Fixes - -- **devices:** Reinstate device commands, with performance fixes. (#389) r=@vladikoff,@philboot ([a01e4aa](https://github.com/mozilla/fxa-auth-db-mysql/commit/a01e4aa)), closes [#384](https://github.com/mozilla/fxa-auth-db-mysql/issues/384) [#384](https://github.com/mozilla/fxa-auth-db-mysql/issues/384) -- **recovery:** hash recovery key ([fe12332](https://github.com/mozilla/fxa-auth-db-mysql/commit/fe12332)) -- **scripts:** remove nonsense (but harmless) comparison of bool to -1 (#394) r=@vladikoff ([13ca415](https://github.com/mozilla/fxa-auth-db-mysql/commit/13ca415)) - -### chore - -- **db:** ensure mem db behaves like mysql db ([8d5d55f](https://github.com/mozilla/fxa-auth-db-mysql/commit/8d5d55f)) -- **docs:** update mysql docs (#391) r=@rfk ([64634d4](https://github.com/mozilla/fxa-auth-db-mysql/commit/64634d4)) - - - -## [1.119.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.118.1...v1.119.1) (2018-08-23) - -### Bug Fixes - -- **devices:** Reinstate device commands, with performance fixes. (#389) r=@vladikoff,@philboot ([a01e4aa](https://github.com/mozilla/fxa-auth-db-mysql/commit/a01e4aa)), closes [#384](https://github.com/mozilla/fxa-auth-db-mysql/issues/384) [#384](https://github.com/mozilla/fxa-auth-db-mysql/issues/384) - -### chore - -- **db:** ensure mem db behaves like mysql db ([8d5d55f](https://github.com/mozilla/fxa-auth-db-mysql/commit/8d5d55f)) - - - -# [1.119.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.118.1...v1.119.0) (2018-08-21) - -### chore - -- **db:** ensure mem db behaves like mysql db ([8d5d55f](https://github.com/mozilla/fxa-auth-db-mysql/commit/8d5d55f)) - - - -## [1.118.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.118.0...v1.118.1) (2018-08-18) - -### chore - -- **db:** stop calling the upsertAvailableCommands procedure ([06554f5](https://github.com/mozilla/fxa-auth-db-mysql/commit/06554f5)) - - - -# [1.118.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.117.0...v1.118.0) (2018-08-14) - -### Bug Fixes - -- **restify:** set keepAliveTimeout correctly on api.server object (#381) ([afc376c](https://github.com/mozilla/fxa-auth-db-mysql/commit/afc376c)) -- **restify:** set server.keepAliveTimeout to 120s, similar to in node6 (#380) ([5ece670](https://github.com/mozilla/fxa-auth-db-mysql/commit/5ece670)) - - - -# [1.117.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.116.0...v1.117.0) (2018-07-24) - -### Bug Fixes - -- **tests:** move local utils tests so they get run by npm t (#377) r=@vladikoff ([677d02b](https://github.com/mozilla/fxa-auth-db-mysql/commit/677d02b)) - -### Features - -- **ci:** update to circle 2 (#375) r=@vbudhram ([5d7b35b](https://github.com/mozilla/fxa-auth-db-mysql/commit/5d7b35b)) -- **recovery:** update account recovery GET/DEL to not accept recoveryKeyId (#374), r=@rfk ([29b9b4b](https://github.com/mozilla/fxa-auth-db-mysql/commit/29b9b4b)) - - - -# [1.116.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.115.0...v1.116.0) (2018-07-11) - -### chore - -- **package:** update shrinkwrap ([98755f7](https://github.com/mozilla/fxa-auth-db-mysql/commit/98755f7)) -- **release:** Merge mozilla/train-115 into master r=@shane-tomlinson ([b5c0f0e](https://github.com/mozilla/fxa-auth-db-mysql/commit/b5c0f0e)) - -### Features - -- **scripts:** add boilerplate to detect missing migrations ([7ef4c66](https://github.com/mozilla/fxa-auth-db-mysql/commit/7ef4c66)) - -### Refactor - -- **recovery:** Use base32 for recovery code generation (#372), r=@vbudhram ([77a6fdd](https://github.com/mozilla/fxa-auth-db-mysql/commit/77a6fdd)) - - - -# [1.115.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.114.1...v1.115.0) (2018-06-27) - - - -## [1.114.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.114.0...v1.114.1) (2018-06-13) - -### Bug Fixes - -- **docker:** base image node:8-alpine and upgrade to npm6 ([c66d3f0](https://github.com/mozilla/fxa-auth-db-mysql/commit/c66d3f0)) - - - -# [1.114.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.113.1...v1.114.0) (2018-06-13) - -### Features - -- **devices:** Allow devices to register "available commands". (#354); r=philbooth,eoger ([10bb799](https://github.com/mozilla/fxa-auth-db-mysql/commit/10bb799)) - - - -## [1.113.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.113.0...v1.113.1) (2018-05-30) - -### Reverts - -- **devices:** Revert "available commands" for train-113. (#360); r=jrgm ([cbe7981](https://github.com/mozilla/fxa-auth-db-mysql/commit/cbe7981)) - - - -# [1.113.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.112.0...v1.113.0) (2018-05-30) - -### chore - -- **ci:** Remove coveralls from travis config. (#355) ([c94fe0b](https://github.com/mozilla/fxa-auth-db-mysql/commit/c94fe0b)) - -### Features - -- **devices:** Allow devices to register "available commands". (#354); r=philbooth,eoger ([69816f6](https://github.com/mozilla/fxa-auth-db-mysql/commit/69816f6)) -- **recovery:** Add initial account recovery support (#357), r=@rfk, @philbooth ([f6716ad](https://github.com/mozilla/fxa-auth-db-mysql/commit/f6716ad)) - - - -# [1.112.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.111.0...v1.112.0) (2018-05-16) - -### Bug Fixes - -- **deps:** update to restify 7.1 and mysql 2.15 (#351), r=@rfk ([4415850](https://github.com/mozilla/fxa-auth-db-mysql/commit/4415850)) -- **restify:** set a sane max param length value for restify ([d84c827](https://github.com/mozilla/fxa-auth-db-mysql/commit/d84c827)) -- **restify:** update param size ([bb78be2](https://github.com/mozilla/fxa-auth-db-mysql/commit/bb78be2)) - -### Features - -- **changelog:** Add an "acknowledgements" section to some changelog entries. (#350) ([5a27b0a](https://github.com/mozilla/fxa-auth-db-mysql/commit/5a27b0a)) - - - -# [1.111.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.110.0...v1.111.0) (2018-05-02) - -### Bug Fixes - -- **npm:** update shrinkwrap to npm 5.8 (#344) r=@jrgm ([a841d06](https://github.com/mozilla/fxa-auth-db-mysql/commit/a841d06)) -- **tests:** increase timeout on recovery code tests (#339), r=@jrgm ([f202197](https://github.com/mozilla/fxa-auth-db-mysql/commit/f202197)) - -### Features - -- **node:** update to node 8 (#341) r=@jrgm ([8bcc7dd](https://github.com/mozilla/fxa-auth-db-mysql/commit/8bcc7dd)) - -### Refactor - -- **db:** Fixes #340 Remove column createdAt on recoveryCode table (#342), r=@vbudhram ([1b59224](https://github.com/mozilla/fxa-auth-db-mysql/commit/1b59224)), closes [#340](https://github.com/mozilla/fxa-auth-db-mysql/issues/340) [(#342](https://github.com/(/issues/342) - - - -# [1.110.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.109.0...v1.110.0) (2018-04-18) - -### Bug Fixes - -- **codes:** remove current recovery codes before applying migration (#337), r=@rfk ([23cbc61](https://github.com/mozilla/fxa-auth-db-mysql/commit/23cbc61)) -- **codes:** update recovery code requirements (#333), r=@philbooth ([2ca7d9f](https://github.com/mozilla/fxa-auth-db-mysql/commit/2ca7d9f)) -- **devices:** Rename pushbox capability to messages and add messages.sendtab capability (#335) ([5a1535a](https://github.com/mozilla/fxa-auth-db-mysql/commit/5a1535a)) - - - -# [1.109.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.107.1...v1.109.0) (2018-04-04) - -### Bug Fixes - -- **codes:** drop all codes when one is consumed (#326) r=@rfk ([f6ab498](https://github.com/mozilla/fxa-auth-db-mysql/commit/f6ab498)) -- **node:** Use Node.js v6.14.0 (#332) ([1400a26](https://github.com/mozilla/fxa-auth-db-mysql/commit/1400a26)) -- **unblock:** update consume unblock code (#330) r=@vladikoff ([9bdb47b](https://github.com/mozilla/fxa-auth-db-mysql/commit/9bdb47b)) -- **verify:** update verifyWithMethod to update a session verification status (#329), r=@philb ([9c433ba](https://github.com/mozilla/fxa-auth-db-mysql/commit/9c433ba)) - -### Features - -- **mysql:** Add config option for REQUIRED_SQL_MODES. (#334) r=@philbooth,@vladikoff ([a229ddc](https://github.com/mozilla/fxa-auth-db-mysql/commit/a229ddc)) -- **mysql:** STRICT_ALL_TABLES and NO_ENGINE_SUBSTITUTION required in sql (#327) r=@vladikoff ([c226b07](https://github.com/mozilla/fxa-auth-db-mysql/commit/c226b07)) - -### Acknowledgements - -Thanks to Yusuf Yazir for suggesting a security improvement -in the handling of unblock codes ([Bug 1368827](https://bugzilla.mozilla.org/show_bug.cgi?id=1368827)). - - - -# [1.108.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.107.0...v1.108.0) (2018-03-20) - -### Bug Fixes - -- **buffers:** convert remaining Buffer to Buffer.from r=@vladikoff ([5092779](https://github.com/mozilla/fxa-auth-db-mysql/commit/5092779)), closes [#316](https://github.com/mozilla/fxa-auth-db-mysql/issues/316) -- **db:** remove database configuration option, hardcode 'fxa'  (#314) r=@vladikoff ([c2e21dd](https://github.com/mozilla/fxa-auth-db-mysql/commit/c2e21dd)), closes [#290](https://github.com/mozilla/fxa-auth-db-mysql/issues/290) -- **email:** Use email buffer for DEL ‘/email/:email’ route (#315), r=@vladikoff, @vbudhram ([cc6e08b](https://github.com/mozilla/fxa-auth-db-mysql/commit/cc6e08b)) -- **test:** correct promises error handling (#325) r=@eoger ([7effcb3](https://github.com/mozilla/fxa-auth-db-mysql/commit/7effcb3)) - -### chore - -- **api:** remove bufferization from db layer ([818edcf](https://github.com/mozilla/fxa-auth-db-mysql/commit/818edcf)) - -### Features - -- **devices:** Devices capabilities (#320) r=@philbooth ([4808a1c](https://github.com/mozilla/fxa-auth-db-mysql/commit/4808a1c)) -- **node:** update to node v6.13.1 r=@jbuck ([7727d88](https://github.com/mozilla/fxa-auth-db-mysql/commit/7727d88)) -- **totp:** initial recovery codes (#319), r=@philbooth ([995d52b](https://github.com/mozilla/fxa-auth-db-mysql/commit/995d52b)) - - - -# [1.108.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.107.0...v1.108.0) (2018-03-20) - -### Bug Fixes - -- **buffers:** convert remaining Buffer to Buffer.from r=@vladikoff ([5092779](https://github.com/mozilla/fxa-auth-db-mysql/commit/5092779)), closes [#316](https://github.com/mozilla/fxa-auth-db-mysql/issues/316) -- **db:** remove database configuration option, hardcode 'fxa'  (#314) r=@vladikoff ([c2e21dd](https://github.com/mozilla/fxa-auth-db-mysql/commit/c2e21dd)), closes [#290](https://github.com/mozilla/fxa-auth-db-mysql/issues/290) -- **email:** Use email buffer for DEL ‘/email/:email’ route (#315), r=@vladikoff, @vbudhram ([cc6e08b](https://github.com/mozilla/fxa-auth-db-mysql/commit/cc6e08b)) -- **test:** correct promises error handling (#325) r=@eoger ([7effcb3](https://github.com/mozilla/fxa-auth-db-mysql/commit/7effcb3)) - -### chore - -- **api:** remove bufferization from db layer ([818edcf](https://github.com/mozilla/fxa-auth-db-mysql/commit/818edcf)) - -### Features - -- **devices:** Devices capabilities (#320) r=@philbooth ([4808a1c](https://github.com/mozilla/fxa-auth-db-mysql/commit/4808a1c)) -- **node:** update to node v6.13.1 r=@jbuck ([7727d88](https://github.com/mozilla/fxa-auth-db-mysql/commit/7727d88)) -- **totp:** initial recovery codes (#319), r=@philbooth ([995d52b](https://github.com/mozilla/fxa-auth-db-mysql/commit/995d52b)) - - - -# [1.107.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.107.0...v1.107.1) (2018-03-21) - -### Bug Fixes - -- **emails:** Make all request paths containing an email use hex encoding. (#1); r=philbooth ([6059aca](https://github.com/mozilla/fxa-auth-db-mysql/commit/6059aca)) - - - -# [1.107.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.106.0...v1.107.0) (2018-03-07) - -### chore - -- **tests:** cleanup `sessionToken` endpoints and docs, r=@philbooth, @rfk ([da2e9ef](https://github.com/mozilla/fxa-auth-db-mysql/commit/da2e9ef)) - -### Features - -- **totp:** Add initial totp session verification logic (#309), r=@philbooth ([ee19e1b](https://github.com/mozilla/fxa-auth-db-mysql/commit/ee19e1b)) -- **totp:** vlad updates for totp (#313) r=@vladikoff ([f6d603c](https://github.com/mozilla/fxa-auth-db-mysql/commit/f6d603c)) - - - -# [1.106.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.105.0...v1.106.0) (2018-02-21) - -### Bug Fixes - -- **token:** Fix mem verifyTokenCode (#303), r=@rfk, @philbooth ([6a4fb67](https://github.com/mozilla/fxa-auth-db-mysql/commit/6a4fb67)), closes [(#303](https://github.com/(/issues/303) - -### chore - -- **deps:** update deps, fix nsp (#308) r=@philbooth ([0d874f9](https://github.com/mozilla/fxa-auth-db-mysql/commit/0d874f9)), closes [(#308](https://github.com/(/issues/308) - -### Features - -- **sessions:** Add support for reauth on an existing session. (#305); r=philbooth ([fdff3e9](https://github.com/mozilla/fxa-auth-db-mysql/commit/fdff3e9)) -- **totp:** Add totp management api (#299), r=@philbooth ([9b8efcb](https://github.com/mozilla/fxa-auth-db-mysql/commit/9b8efcb)) - - - -# [1.105.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.104.0...v1.105.0) (2018-02-06) - -### Features - -- **tests:** make tests more independent (#293), r=@philbooth, @rfk ([c7d3638](https://github.com/mozilla/fxa-auth-db-mysql/commit/c7d3638)) - - - -# [1.104.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.103.0...v1.104.0) (2018-01-23) - -### Bug Fixes - -- **pruning:** Avoid accidental full-table scans when pruning session tokens. (#295); r=philboo ([5c6622c](https://github.com/mozilla/fxa-auth-db-mysql/commit/5c6622c)) -- **scripts:** add SET NAMES to reverse migration boilerplate (#296), r=@vbudhram ([0790b89](https://github.com/mozilla/fxa-auth-db-mysql/commit/0790b89)) - -### Features - -- **devices:** return session token id from deleteDevice ([a2dd244](https://github.com/mozilla/fxa-auth-db-mysql/commit/a2dd244)) - - - -# [1.103.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.101.0...v1.103.0) (2018-01-09) - -### Bug Fixes - -- **node:** use node 6.12.3 (#291) r=@vladikoff ([6080c0c](https://github.com/mozilla/fxa-auth-db-mysql/commit/6080c0c)) - -### Features - -- **logs:** add Sentry for errors (#292) r=@vbudhram ([6348a95](https://github.com/mozilla/fxa-auth-db-mysql/commit/6348a95)), closes [#288](https://github.com/mozilla/fxa-auth-db-mysql/issues/288) - - - -# [1.101.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.100.0...v1.101.0) (2017-11-29) - -### Features - -- **codes:** add support for verifying token short code (#287) r=@vladikoff,@rfk ([ac0b814](https://github.com/mozilla/fxa-auth-db-mysql/commit/ac0b814)) - -### Refactor - -- **dbserver:** clean up the db server package (#289) r=@rfk ([c3d8e6e](https://github.com/mozilla/fxa-auth-db-mysql/commit/c3d8e6e)) - - - -# [1.100.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.98.0...v1.100.0) (2017-11-15) - -### Bug Fixes - -- **newrelic:** futureproofing comment and up to newrelic@2.3.2 with npm run shrink (#285) r=@vl ([bfc1963](https://github.com/mozilla/fxa-auth-db-mysql/commit/bfc1963)) -- **newrelic:** newrelic native requires make, python, gyp, c++; update node 6.12.0 (#286) r=@vl ([4b7e696](https://github.com/mozilla/fxa-auth-db-mysql/commit/4b7e696)) -- **travis:** run tests with 6 and current stable (failure not allowed anymore) ([c4e0e98](https://github.com/mozilla/fxa-auth-db-mysql/commit/c4e0e98)) - - - -# [1.98.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.97.0...v1.98.0) (2017-10-26) - -### chore - -- **docker:** Update to node v6.11.5 for security fix ([7cc3251](https://github.com/mozilla/fxa-auth-db-mysql/commit/7cc3251)) - - - -# [1.97.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.96.1...v1.97.0) (2017-10-04) - -### Features - -- **db:** prune session tokens (again) ([67bd8fb](https://github.com/mozilla/fxa-auth-db-mysql/commit/67bd8fb)) - - - -## [1.96.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.96.0...v1.96.1) (2017-09-20) - -### Bug Fixes - -- **db:** call latest version of the prune stored procedure (#281) r=vladikoff ([2c34f2e](https://github.com/mozilla/fxa-auth-db-mysql/commit/2c34f2e)) - - - -# [1.96.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.95.1...v1.96.0) (2017-09-19) - -### Bug Fixes - -- **tokens:** revert session-token pruning ([ecde71b](https://github.com/mozilla/fxa-auth-db-mysql/commit/ecde71b)) - - - -## [1.95.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.95.0...v1.95.1) (2017-09-12) - -### Bug Fixes - -- **mysql:** update all device procedures to use utf8mb4 (#276) r=jbuck,rfk ([7d22ad8](https://github.com/mozilla/fxa-auth-db-mysql/commit/7d22ad8)) -- **tokens:** prune old session tokens that have no device record ([8fad575](https://github.com/mozilla/fxa-auth-db-mysql/commit/8fad575)) - - - -# [1.95.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.94.1...v1.95.0) (2017-09-06) - -### chore - -- **docs:** update node version in docs to 6 ([63fbdf2](https://github.com/mozilla/fxa-auth-db-mysql/commit/63fbdf2)) - -### Features - -- **schema:** add a pushEndpointExpired column to devices ([d8e93c4](https://github.com/mozilla/fxa-auth-db-mysql/commit/d8e93c4)) - - - -## [1.94.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.94.0...v1.94.1) (2017-08-23) - -### Features - -- **db:** add utf8mb4 support (#267) r=rfk ([549d39f](https://github.com/mozilla/fxa-auth-db-mysql/commit/549d39f)) - - - -# [1.94.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.93.0...v1.94.0) (2017-08-21) - -### chore - -- **ci:** remove node4 test targets from travis-ci (#270) r=vladikoff ([9523d02](https://github.com/mozilla/fxa-auth-db-mysql/commit/9523d02)) -- **email:** Remove emailRecord depreciation (#269), r=@philbooth ([0a7c2c6](https://github.com/mozilla/fxa-auth-db-mysql/commit/0a7c2c6)) - -### Features - -- **schema:** add a uaFormFactor column to sessionTokens (#271) r=vladikoff ([774b6c1](https://github.com/mozilla/fxa-auth-db-mysql/commit/774b6c1)) - - - -# [1.93.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.92.0...v1.93.0) (2017-08-09) - -### Features - -- **docker:** update to node 6 (#266) r=jbuck ([7b13cea](https://github.com/mozilla/fxa-auth-db-mysql/commit/7b13cea)) - - - -# [1.92.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.91.2...v1.92.0) (2017-07-26) - -### chore - -- **scripts:** add a script to generate migration boilerplate (#261) r=vladikoff ([45949c5](https://github.com/mozilla/fxa-auth-db-mysql/commit/45949c5)) -- **tests:** don't make eslint a prerequisite for the tests (#258), r=@vbudhram ([ddae438](https://github.com/mozilla/fxa-auth-db-mysql/commit/ddae438)) - - - -## [1.91.2](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.91.1...v1.91.2) (2017-07-17) - -### Features - -- **schema:** drop the uaFormFactor column from sessionTokens (#262), r=@vbudhram ([f23098a](https://github.com/mozilla/fxa-auth-db-mysql/commit/f23098a)) - - - -## [1.91.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.91.0...v1.91.1) (2017-07-12) - -### Bug Fixes - -- **nodejs:** upgrade to 4.8.4 for security fixes ([450e931](https://github.com/mozilla/fxa-auth-db-mysql/commit/450e931)) - - - -# [1.91.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.90.0...v1.91.0) (2017-07-12) - -### Features - -- **email:** Add change email (#254), r=@philbooth ([7253d09](https://github.com/mozilla/fxa-auth-db-mysql/commit/7253d09)) -- **email:** correctly return `createdAt` when using accountRecord (#256), r=@philbooth ([70a1a39](https://github.com/mozilla/fxa-auth-db-mysql/commit/70a1a39)) -- **schema:** add a uaFormFactor column to sessionTokens ([e99bc19](https://github.com/mozilla/fxa-auth-db-mysql/commit/e99bc19)) - - - -# [1.90.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.89.1...v1.90.0) (2017-06-28) - -### chore - -- **eslint:** update to latest eslint (#252) r=vbudhram ([1157bb2](https://github.com/mozilla/fxa-auth-db-mysql/commit/1157bb2)) -- **train:** uplift train 89 (#253), r=@philbooth ([06944e8](https://github.com/mozilla/fxa-auth-db-mysql/commit/06944e8)) - -### Features - -- **db:** store flowIds with signinCodes ([3fac7d7](https://github.com/mozilla/fxa-auth-db-mysql/commit/3fac7d7)) -- **email:** Update procedures to use email table (#245), r=@philbooth, @rfk ([b896063](https://github.com/mozilla/fxa-auth-db-mysql/commit/b896063)) -- **tokens:** Add ability to reset accounts tokens (#249), r=@philbooth ([92199bc](https://github.com/mozilla/fxa-auth-db-mysql/commit/92199bc)) - - - -## [1.89.3](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.89.2...v1.89.3) (2017-06-21) - -### Features - -- **email:** Don't use subquery on email verify update (#251), r=@jbuck ([102dea4](https://github.com/mozilla/fxa-auth-db-mysql/commit/102dea4)) - - - -## [1.89.2](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.89.1...v1.89.2) (2017-06-21) - -### Features - -- **email:** Remove temporary table from `accountEmails` query (#250), r=@rfk, @jbuck ([e9d0335](https://github.com/mozilla/fxa-auth-db-mysql/commit/e9d0335)) - - - -## [1.89.1](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.89.0...v1.89.1) (2017-06-14) - -### Features - -- **email:** Add email table migration script (#247), r=@rfk, @jbuck ([9ef8cbf](https://github.com/mozilla/fxa-auth-db-mysql/commit/9ef8cbf)) - - - -# [1.89.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.87.0...v1.89.0) (2017-06-13) - -### Features - -- **db:** enable signinCode expiry ([2b53553](https://github.com/mozilla/fxa-auth-db-mysql/commit/2b53553)) -- **email:** Keep account email and emails table in sync (#241), r=@rfk, @philbooth ([78d5559](https://github.com/mozilla/fxa-auth-db-mysql/commit/78d5559)) - -### Refactor - -- **test:** refactor our tests to use Mocha instead of TAP ([0441ea9](https://github.com/mozilla/fxa-auth-db-mysql/commit/0441ea9)) - - - -# [1.87.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.85.0...v1.87.0) (2017-05-17) - -### Bug Fixes - -- **docs:** update authors and node.js version in README ([5610b92](https://github.com/mozilla/fxa-auth-db-mysql/commit/5610b92)) -- **email:** Use correct delete account procedure (#231) ([4a16bf3](https://github.com/mozilla/fxa-auth-db-mysql/commit/4a16bf3)) - -### chore - -- **docker:** Use official node image & update to Node.js v4.8.2 (#225) r=vladikoff ([2298e38](https://github.com/mozilla/fxa-auth-db-mysql/commit/2298e38)) - -### Features - -- **docker:** add custom feature branch (#237) r=jrgm ([d21a8df](https://github.com/mozilla/fxa-auth-db-mysql/commit/d21a8df)) -- **email:** Add get email endpoint (#227), r=@vladikoff, @rfk ([8f5653c](https://github.com/mozilla/fxa-auth-db-mysql/commit/8f5653c)) -- **signinCodes:** migration and endpoints for signinCodes table (#235), r=@vbudhram ([b740793](https://github.com/mozilla/fxa-auth-db-mysql/commit/b740793)) -- **tokens:** prune tokens older than 3 months (#224) r=vladikoff ([fdc19c1](https://github.com/mozilla/fxa-auth-db-mysql/commit/fdc19c1)), closes [#219](https://github.com/mozilla/fxa-auth-db-mysql/issues/219) - - - -# [1.86.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v1.85.0...v1.86.0) (2017-05-01) - -### Bug Fixes - -- **docs:** update authors and node.js version in README ([6d89d30](https://github.com/mozilla/fxa-auth-db-mysql/commit/6d89d30)) - -### chore - -- **docker:** Use official node image & update to Node.js v4.8.2 (#225) r=vladikoff ([2298e38](https://github.com/mozilla/fxa-auth-db-mysql/commit/2298e38)) - -### Features - -- **email:** Add get email endpoint (#227), r=@vladikoff, @rfk ([8f5653c](https://github.com/mozilla/fxa-auth-db-mysql/commit/8f5653c)) -- **tokens:** prune tokens older than 3 months (#224) r=vladikoff ([fdc19c1](https://github.com/mozilla/fxa-auth-db-mysql/commit/fdc19c1)), closes [#219](https://github.com/mozilla/fxa-auth-db-mysql/issues/219) - - - -# [1.85.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.83.0...v1.85.0) (2017-04-18) - -### Bug Fixes - -- **install:** add formatter to main package.json (#222) ([f4cb995](https://github.com/mozilla/fxa-auth-db-mysql/commit/f4cb995)) -- **security:** escape json output (#220) r=vladikoff ([13b9f70](https://github.com/mozilla/fxa-auth-db-mysql/commit/13b9f70)) - -### chore - -- **dependencies:** update all our production dependencies (#217) r=vladikoff ([e008849](https://github.com/mozilla/fxa-auth-db-mysql/commit/e008849)) - - - -# [0.83.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.82.0...v0.83.0) (2017-03-21) - -### Bug Fixes - -- **config:** Add environment variable for ipHmacKey ([65f6d78](https://github.com/mozilla/fxa-auth-db-mysql/commit/65f6d78)) -- **emailBounces:** receive the email parameter in the url as hex ([e1c078b](https://github.com/mozilla/fxa-auth-db-mysql/commit/e1c078b)) -- **security-events:** Correctly handle tokenless security events in mem backend (#215) r=vladikoff,sea ([0f816cb](https://github.com/mozilla/fxa-auth-db-mysql/commit/0f816cb)) - -### Features - -- **email:** Add support for adding additional emails (#211), r=@seanmonstar, @rfk ([1c436c9](https://github.com/mozilla/fxa-auth-db-mysql/commit/1c436c9)) - - - -# [0.82.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.81.0...v0.82.0) (2017-03-06) - -### Features - -- **docker:** add docker via Circle CI (#212) r=jbuck,seanmonstar ([8f913be](https://github.com/mozilla/fxa-auth-db-mysql/commit/8f913be)), closes [#208](https://github.com/mozilla/fxa-auth-db-mysql/issues/208) -- **sessions:** update the sessions query to include device information (#203) r=vbudhram ([70dcc5b](https://github.com/mozilla/fxa-auth-db-mysql/commit/70dcc5b)) - - - -# [0.81.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.76.0...v0.81.0) (2017-02-23) - -### Bug Fixes - -- **email:** Return `createdAt` when calling db.emailRecord (#209), r=@rfk ([1a226cc](https://github.com/mozilla/fxa-auth-db-mysql/commit/1a226cc)) -- **reminders:** adjust mysql procedures (#200) r=rfk ([4b6a92d](https://github.com/mozilla/fxa-auth-db-mysql/commit/4b6a92d)) -- **style:** replace tab char with a space (#207) r=rfk ([44470ad](https://github.com/mozilla/fxa-auth-db-mysql/commit/44470ad)) - -### Features - -- **db:** add emailBounces table ([4fe29fa](https://github.com/mozilla/fxa-auth-db-mysql/commit/4fe29fa)) -- **tokens:** add prune token maxAge and update pruning (#206); r=rfk ([699c352](https://github.com/mozilla/fxa-auth-db-mysql/commit/699c352)) -- **tokens:** get the device associated with a tokenVerificationId (#204) r=vladikoff ([7f45075](https://github.com/mozilla/fxa-auth-db-mysql/commit/7f45075)) - - - -# [0.76.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.75.0...v0.76.0) (2016-12-13) - -### Bug Fixes - -- **schema:** Complete final phase of several previous migrations ([7eddbc9](https://github.com/mozilla/fxa-auth-db-mysql/commit/7eddbc9)) - -### chore - -- **deps:** add new shrinkwrap command (#193) ([b33c750](https://github.com/mozilla/fxa-auth-db-mysql/commit/b33c750)), closes [#189](https://github.com/mozilla/fxa-auth-db-mysql/issues/189) - - - -# [0.75.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.74.0...v0.75.0) (2016-11-30) - -### Bug Fixes - -- **bufferize:** Only bufferize params we explicitly want as buffers. (#182); r=philbooth ([a461769](https://github.com/mozilla/fxa-auth-db-mysql/commit/a461769)) -- **bufferize:** Only bufferize params we explicitly want as buffers. (#187) r=vladikoff ([aad12bb](https://github.com/mozilla/fxa-auth-db-mysql/commit/aad12bb)) - -### Reverts - -- **bufferize:** revert the extra bufferize logic ([e913a66](https://github.com/mozilla/fxa-auth-db-mysql/commit/e913a66)) - - - -# [0.74.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.72.0...v0.74.0) (2016-11-15) - -### chore - -- **lint:** Include ./bin/\*.js in eslint coverage ([6c8eeba](https://github.com/mozilla/fxa-auth-db-mysql/commit/6c8eeba)) -- **securityEvents:** Stop writing to the `securityEvents.tokenId` column. ([1e3763d](https://github.com/mozilla/fxa-auth-db-mysql/commit/1e3763d)) - -### Features - -- **eventLog:** Remove the unused "eventLog" feature. ([a138e76](https://github.com/mozilla/fxa-auth-db-mysql/commit/a138e76)) - - - -# [0.72.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.71.0...v0.72.0) (2016-10-19) - -### Bug Fixes - -- **securityEvents:** Tweak securityEvents db queries based on @jrgm feedback ([ffa5561](https://github.com/mozilla/fxa-auth-db-mysql/commit/ffa5561)) - - - -# [0.71.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.70.0...v0.71.0) (2016-10-05) - -### Bug Fixes - -- **travis:** drop node 0.10 test config ([c1b1841](https://github.com/mozilla/fxa-auth-db-mysql/commit/c1b1841)) - -### chore - -- **travis:** add node 6 explicitly to travis (#175) r=vladikoff ([c1556ab](https://github.com/mozilla/fxa-auth-db-mysql/commit/c1556ab)) - -### Features - -- **unblock:** add unblockCode support ([12fb9df](https://github.com/mozilla/fxa-auth-db-mysql/commit/12fb9df)) - - - -# [0.70.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.69.0...v0.70.0) (2016-09-24) - -### Bug Fixes - -- **security:** Fix the endpoints for /securityEvents. ([5dfd5f8](https://github.com/mozilla/fxa-auth-db-mysql/commit/5dfd5f8)), closes [#171](https://github.com/mozilla/fxa-auth-db-mysql/issues/171) - -### Features - -- **db:** return account.email from accountDevices ([b090367](https://github.com/mozilla/fxa-auth-db-mysql/commit/b090367)) -- **security:** add security events ([cc31172](https://github.com/mozilla/fxa-auth-db-mysql/commit/cc31172)) - - - -# [0.69.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.68.0...v0.69.0) (2016-09-09) - -### Bug Fixes - -- **db:** don't return zombie devices from accountDevices ([6e5c2db](https://github.com/mozilla/fxa-auth-db-mysql/commit/6e5c2db)) -- **db:** Fix the typo ([7bfdf91](https://github.com/mozilla/fxa-auth-db-mysql/commit/7bfdf91)) -- **db:** Update resetAccount to not delete from accountUnlockCodes ([616602a](https://github.com/mozilla/fxa-auth-db-mysql/commit/616602a)) -- **shrinkwrap:** refresh shrinkwrap ([83d94d4](https://github.com/mozilla/fxa-auth-db-mysql/commit/83d94d4)) - -### feature - -- **newrelic:** add optional newrelic integration ([fca7e2e](https://github.com/mozilla/fxa-auth-db-mysql/commit/fca7e2e)) - -### Refactor - -- **db:** Remove account unlock related code. ([340e299](https://github.com/mozilla/fxa-auth-db-mysql/commit/340e299)) - - - -# [0.68.0](https://github.com/mozilla/fxa-auth-db-mysql/compare/v0.67.0...v0.68.0) (2016-08-24) - -### Bug Fixes - -- **db:** ensure that devices get deleted with session tokens ([840dda6](https://github.com/mozilla/fxa-auth-db-mysql/commit/840dda6)) -- **db:** use an index when deleting device records by sessionToken id. ([f5bbb60](https://github.com/mozilla/fxa-auth-db-mysql/commit/f5bbb60)) -- **scripts:** add process.exit to populate script ([7820fdc](https://github.com/mozilla/fxa-auth-db-mysql/commit/7820fdc)) -- **scripts:** ensure changelog is updated sanely ([24376cc](https://github.com/mozilla/fxa-auth-db-mysql/commit/24376cc)) - -### Features - -- **scripts:** add device records to the populate script ([c235696](https://github.com/mozilla/fxa-auth-db-mysql/commit/c235696)) - -# 0.67.0 - -- fix(deps): update dev dependencies #143 -- fix(deps): update prod dependencies #144 -- chore(readme): update travis status badge url -- fix(tests): switch coverage tool, add coveralls #145 -- chore(deps): update to latest request and sinon #148 -- feat(db): Remove account lockout #147 -- fix(db): remove createAccountResetToken stored procedure and endpoint #154 -- refactor(db): remove openId #153 -- feat(db): Record whether we _must_ verify each unverified token #155 - -# 0.63.0 - -- feat(db): implement verification state for key fetch tokens #138 -- chore(travis): drop node 0.12 support #139 -- feat(reminders): add verification reminders #127 -- chore(mozlog): update from mozlog@2.0.3 to 2.0.5 #140 -- chore(scripts): sort scripts alphabetically #140 -- chore(shrinkwrap): add "npm run shrinkwrap" script #140 - -# 0.62.0 - -- feat(mx-stats): Add a script to print stats on popular mail providers #134 -- feat(db): store push keys according to the current implementation #133 -- feat(db): implement new token verification logic #132 - -# 0.59.0 - -- fix(logging): log connection config and charset info at startup #131 -- fix(tests): adjust notifier tests monkeypatching to accept mozlog signature #130 -- fix(logging): adjust logging method calls to use mozlog signature #130 -- fix(tests): enforce mozlog rules in test logger #130 - -# 0.58.0 - -- fix(db): expunge devices in resetAccount sproc #128 - -# 0.57.0 - -- feat(devices): added sessionWithDevice endpoint -- chore(dependencies): upgrade mozlog to 2.0.3 - -# 0.55.0 - -- feat(docker): Additional Dockerfile for self-hosting #121 -- docs(contributing): Mention git commit guidelines #122 - -# train-53 - -- chore(deps): Update mysql package dependency to latest version #112 -- fix(tests): Upgrade test runner and fix some test declarations #112 - -# train-51 - -- fix(travis): build and test on 0.10, 0.12 and 4.x, and allow failure on >= 5.x -- chore(shrinkwrap): update npm-shrinkwrap.json - -# train-50.1 - -- fix(db): fix memory-store initialisation of device fields to null #117 -- fix(version): print out constructor class name; adds /**version** alias #118 - -# train-50 - -- chore(nsp): re-added shrinkwrap validation to travis -- fix(server): fix bad route parameter name -- feat(db): update devices to match new requirements - -# train-49 - -- reverted some dependencies to previous versions due to #113 - -# train-48 - -- feat(db): add device registration and management endpoints #110 - -# train-46 - -- feat(db): add endpoint to return a user's sessions #102 -- feat(db): return accountCreatedAt from sessionToken stored procedure #105 -- chore(metadata): Update package metadata for stand-alone server lib. #106 - -# train-45 - -- fix(metrics): measure request count and time in perf tests - #97 -- fix(metrics): append delimiter to metrics output - #94 -- chore(version): generate legacy-format output for ./config/version.json - #101 -- chore(metrics): add script for creating dummy session tokens - #100 -- chore(metrics): report latency in performance tests - #99 -- chore(eslint): change complexity rule - #96 -- chore(metrics): add scripts for perf-testing metrics queries - #88 - -# train-44 - -- There are no longer separate fxa-auth-db-mysql and fxa-auth-db-server repositories - assemble all db repos - #56 -- preliminary support for authenticating with OpenID - #78 -- feat(db): add script for reporting metrics #80 -- feat(db): store user agent and last-access time in sessionTokens - #65 -- refactor(config): Use human-readable duration values in config - #62 -- fix(tests): used a randomized openid url - #92 -- fix(db): default user-agent fields to null in memory backend - #90 -- fix(server): prevent insane bufferization of non-hex parameters - #89 -- chore(configs): eliminate sub-directory dotfiles - #69 -- chore(package): expose scripts for running and testing db-mem - #71 -- chore(project): merge db-server project admin/config stuff to top level - #74 -- chore(docs): update readme and api docs for merged repos - #76 -- reshuffle package.json (use file paths, not file: url) - #77 -- chore(coverage): exclude fxa-auth-db-server/node_modules from coverage checks - #82 - -# train-42 - -- fix(tests): pass server object to backend tests - #63 -- refactor(db): remove verifyHash from responses - #48 -- chore(shrinkwrap): update shrinkwrap for verifyHash removal - #61 -- chore(shrinkwrap): update shrinkwrap, principally to head of fxa-auth-db-server - #63 - -# train-41 - -- feat(api): Return the account email address on passwordChangeToken - #59 -- chore(travis): Tell Travis to use #fxa-bots - #60 - -# train-40 - -- fix(notifications): always return a promise from db.processUnpublishedEvents, fixes #49 - #52 -- fix(npm): Update npm-shrinkwrap to include the last version of fxa-auth-db-server - #50 -- chore(cleanup): Fixed some syntax errors reported by ESLint - #55 -- fix(db): Return 400 on incorrect password - #53 -- refactor(db): Remove old stored procedures that are no longer used - #57 - -# train-39 - -- fix(npm): Update npm-shrinkwrap to include the last version of fxa-auth-db-server - #50 -- Added checkPassword_1 stored procedure - #45 -- Use array for Mysql read() bound parameters - #45 -- chore(license): Update license to be SPDX compliant - #46 - -# train-37 - -- refactor(lib): move most things into lib/ -- build(travis): Test on both io.js v1 and v2 -- chore(shrinkwrap): update shrinkwrap picking up lib changes in fxa-auth-db-server - -# train-36 - -- refactor(db): Change table access in stored procedures to be consistent - #36 -- fix(db): Fix reverse patches 8->7 and 9->8 - #38 -- fix(package): Remove uuid completely since no longer needed - #37 -- chore(package): Update to mysql-patcher@0.7.0 - #39 -- chore(copyright): Update to grunt-copyright v0.2.0 - #40 -- chore(test): Test on node.js v0.10, v0.12 and the latest io.js - #41 - -# train-35 - -- there was no train-35 for fxa-auth-db-mysql - -# train-34 - -- feat(events): Publish account events to notification server in a background loop - #25 - - Note: this feature is disabled by default (see 'config.notifications.publishUrl'), - and will not be enabled in train-34 -- fix(notifier): allow us to use the json secret key from the auth-server directly for the notifier - #29 -- fix(db): do not set createdAt, verifierSetAt or normalizedEmail here - #31 -- fix(logging): load the logger from the new location - #32 -- fix(release): add tasks "grunt version" and "grunt version:patch" to - #34 -- chore(tests): Remove console logging during test run - #25 -- chore(tests): Don't assume log.info message order during tests - #25 -- chore(tests): Remove some apparently-unused files in 'test' directory - #25 -- chore(package.json): add extra fields related to the repo - #30 -- chore(shrinkwrap): update shrinkwrap - #33 - -# train-33 - -- Log account activity events for later publishing to notification service - #20 -- Fix tests to do more reliable error-message detection - #20 -- Correctly pass pool name when getting a connection - #23 -- Use mozlog for logging - #21 -- Log memory-usage stats emitted by fxa-auth-db-server - #24 -- Some documentation and packaging tweaks - #17, #18 - -# train-32 - -- Add ability to mark an account as "locked" for security reasons - #7 -- Add support for docker-based development workflow - #13 - -# train-31 - -- Only fail with a DB patch level less than the one expected -- (hotfix) regenerated npm-shrinkwrap.json that uses the correct version of fxa-auth-db-server - #15 diff --git a/packages/fxa-auth-db-mysql/CONTRIBUTING.md b/packages/fxa-auth-db-mysql/CONTRIBUTING.md deleted file mode 100644 index 632f59848a6..00000000000 --- a/packages/fxa-auth-db-mysql/CONTRIBUTING.md +++ /dev/null @@ -1,90 +0,0 @@ -# Contributing - -Anyone is welcome to help with Firefox Accounts. Feel free to get in touch with other community members on Matrix, the -mailing list or through issues here on GitHub. - -- Matrix: [#fxa:mozilla.org](https://chat.mozilla.org/#/room/#fxa:mozilla.org) -- Mailing list: -- and of course, [the issues list](https://github.com/mozilla/fxa-auth-db-mysql/issues) - -UPDATE: On March 2020, Mozilla moved from IRC to Matrix. For more information on Matrix, check out the following wiki article: . - -## Bug Reports - -You can file issues here on GitHub. Please try to include as much information as you can and under what conditions -you saw the issue. - -## Sending Pull Requests - -Patches should be submitted as pull requests (PR). - -Before submitting a PR: - -- 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. - - Run `grunt eslint` to make sure your code passes linting. - - Run `npm test` to make sure all tests still pass. -- 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. - -When submitting a PR: - -- You agree to license your code under the project's open source license ([MPL 2.0](/LICENSE)). -- Base your branch off the current `main` (see below for an example workflow). -- Add both your code and new tests if relevant. -- Run `grunt eslint` and `npm test` to make sure your code passes linting and tests. -- Please do not include merge commits in pull requests; include only commits with the new relevant code. -- Your commit message must follow the - [commit guidelines](https://github.com/mozilla/fxa/blob/main/CONTRIBUTING.md#git-commit-guidelines). - -After your PR is merged: - -- Add yourself to the [AUTHORS](/AUTHORS) file so we can publicly recognize your contribution. - -See the main [README.md](/README.md) for information on prerequisites, installing, running and testing. - -## Code Review - -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. - -## Example Workflow - -This is an example workflow to make it easier to submit Pull Requests. Imagine your username is `user1`: - -1. Fork this repository via the GitHub interface - -2. The clone the upstream (as origin) and add your own repo as a remote: - - ```sh - $ git clone https://github.com/mozilla/fxa-auth-db-mysql.git - $ cd fxa-auth-db-mysql - $ git remote add user1 git@github.com:user1/fxa-auth-db-mysql.git - ``` - -3. Create a branch for your fix/feature and make sure it's your currently checked-out branch: - - ```sh - $ git checkout -b add-new-feature - ``` - -4. Add/fix code, add tests then commit and push this branch to your repo: - - ```sh - $ git add - $ git commit - $ git push user1 add-new-feature - ``` - -5. From the GitHub interface for your repo, click the `Review Changes and Pull Request` which appears next to your new branch. - -6. Click `Send pull request`. - -### Keeping up to Date - -The main reason for creating a new branch for each feature or fix is so that you can track main correctly. If you need -to fetch the latest code for a new fix, try the following: - -```sh -$ git checkout main -$ git pull -``` - -Now you're ready to branch again for your new feature (from step 3 above). diff --git a/packages/fxa-auth-db-mysql/LICENSE b/packages/fxa-auth-db-mysql/LICENSE deleted file mode 100644 index a612ad9813b..00000000000 --- a/packages/fxa-auth-db-mysql/LICENSE +++ /dev/null @@ -1,373 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. diff --git a/packages/fxa-auth-db-mysql/README.md b/packages/fxa-auth-db-mysql/README.md deleted file mode 100644 index b153e74aa0a..00000000000 --- a/packages/fxa-auth-db-mysql/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# Firefox Accounts database service - -# Deprecated! - -**This service is no longer used by auth-server and is in the process of being dismantled. DB migration `.sql` files should be the only new additions here for the time being. New DB API work should be done in `fxa-shared/db`.** - ---- - -Node.js-based database service -for Firefox Accounts. -Includes: - -- The [API server](#api-server). -- A [MySQL backend](#mysql-backend). - Used in production. - -## Prerequisites - -- node.js 14 -- npm -- MySQL (we use version 5.6.42 in production) - -## Testing - -This package uses [Mocha](https://mochajs.org/) to test its code. By default `npm test` will run all NPM test scripts and then lint the code: - -- `npm run test-mysql` will test database code under `test/backend` and `test/local`. -- `npm run test-server` will test server code under `db-server/test/local`. - -Test specific tests with the following commands: - -```bash -# Test only test/lib/log.js -./scripts/mocha-coverage.js test/lib/log.js - -# Grep for "error module" under db-server/test -./scripts/mocha-coverage.js db-server/test/*/** -g "error module" -``` - -Refer to Mocha's [CLI documentation](https://mochajs.org/#command-line-usage) for more advanced test configuration. - -## API Server - -See the [API documentation][apidocs]. -Backend implementers should also read -the [database documentation][dbdocs]. - -To run the server tests: - -```sh -npm run test-server -``` - -## MySQL backend - -Implements the [backend API][dbdocs] -as a MySQL database. - -To run the MySQL tests: - -```sh -npm run test-mysql -``` - -### Configuration - -Both the server -and the database patcher -read values from a config file -`config/$NODE_ENV.json`, -where `NODE_ENV` is an environment variable -set in the shell. - -For local development, -set `NODE_ENV` to `dev` -then create a new JSON file -called `config/dev.json`. -In there, -you can set any values -that you'd like to override -the master config file, -`config/config.js`. - -For instance: - -```json -{ - "master": { - "user": "root", - "password": "foo" - }, - "slave": { - "user": "root", - "password": "bar" - } -} -``` - -### Starting the server - -You can start the server like so: - -```sh -npm start -``` - -This will set up the database for you -then start the server on whichever port -is configured in `config/$NODE_ENV.json` -(port 8000 by default). - -If the server fails to start, -check that MySQL is running -and that your active config -has the correct settings -to connect to the database. - -### Setting-up the database separately - -If you want to run -the database patcher on its own, -use the following command: - -```sh -node bin/db_patcher.js -``` - -This command creates the database -if it doesn't exist, -then runs migrations -from `lib/db/schema` -in the appropriate order. -Both forward and reverse migrations -are contained in this directory, -but note that the reverse migrations -are commented out -as a precaution against -accidental execution. - -If the command fails, -check that MySQL is running -and that your active config -has the correct settings -to connect to the database. - -### Clean-up - -If you want to clean the database, -just drop it in MySQL: - -```sh -mysql -u root -p -e 'DROP DATABASE fxa' -``` - -It will be recreated automatically -next time you run `npm start`. - -## License - -[MPL 2.0][license] - -[apidocs]: docs/API.md -[dbdocs]: docs/DB_API.md -[server-readme]: db-server/README.md -[license]: LICENSE diff --git a/packages/fxa-auth-db-mysql/bin/db_patcher.js b/packages/fxa-auth-db-mysql/bin/db_patcher.js deleted file mode 100755 index 86d5b631641..00000000000 --- a/packages/fxa-auth-db-mysql/bin/db_patcher.js +++ /dev/null @@ -1,31 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -var path = require('path'); -var mysql = require('mysql'); -var config = require('../config'); -var logger = require('../lib/logging')('bin.db_patcher'); -var patcher = require('mysql-patcher'); - -var patch = require('../lib/db/patch'); -const constants = require('../lib/constants'); - -// set some options -var options = Object.assign({}, config.master); -options.dir = path.join(__dirname, '..', 'lib', 'db', 'schema'); -options.patchKey = config.patchKey; -options.metaTable = 'dbMetadata'; -options.patchLevel = patch.level; -options.mysql = mysql; -options.createDatabase = true; -options.reversePatchAllowed = false; -options.database = constants.DATABASE_NAME; - -patcher.patch(options, function (err) { - if (err) { - logger.error('patch-error', { err: '' + err }); - process.exit(2); - } - logger.info('patched', { level: options.patchLevel }); -}); diff --git a/packages/fxa-auth-db-mysql/bin/metrics.js b/packages/fxa-auth-db-mysql/bin/metrics.js deleted file mode 100755 index 1590f366dfb..00000000000 --- a/packages/fxa-auth-db-mysql/bin/metrics.js +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env node - -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -'use strict'; - -var fs = require('fs'); - -module.exports = { - run: run, - countAccounts: [ - 'SELECT COUNT(*) AS count', - 'FROM accounts', - 'WHERE createdAt < ?;', - ].join('\n'), - countVerifiedAccounts: [ - 'SELECT COUNT(*) AS count', - 'FROM accounts', - 'WHERE createdAt < ?', - 'AND emailVerified = true;', - ].join('\n'), - countAccountsWithTwoOrMoreDevices: [ - 'SELECT COUNT(*) AS count', - 'FROM (', - ' SELECT a.uid', - ' FROM accounts AS a', - ' INNER JOIN sessionTokens AS s', - ' ON a.uid = s.uid', - ' WHERE a.createdAt < ?', - ' GROUP BY (a.uid)', - ' HAVING COUNT(s.tokenId) > 1', - ') AS sub;', - ].join('\n'), - countAccountsWithThreeOrMoreDevices: [ - 'SELECT COUNT(*) AS count', - 'FROM (', - ' SELECT a.uid', - ' FROM accounts AS a', - ' INNER JOIN sessionTokens AS s', - ' ON a.uid = s.uid', - ' WHERE a.createdAt < ?', - ' GROUP BY (a.uid)', - ' HAVING COUNT(s.tokenId) > 2', - ') AS sub;', - ].join('\n'), - countAccountsWithMobileDevice: [ - 'SELECT COUNT(DISTINCT a.uid) AS count', - 'FROM accounts AS a', - 'INNER JOIN sessionTokens AS s', - 'ON a.uid = s.uid', - 'WHERE a.createdAt < ?', - "AND s.uaDeviceType = 'mobile';", - ].join('\n'), -}; - -if (require.main === module) { - module.exports.run( - parseConfigFile(process.argv[2] || '/etc/gather_basic_metrics.conf') - ); -} - -function run(config, now) { - now = now || new Date(); - var lastMidnight = Date.UTC( - now.getUTCFullYear(), - now.getUTCMonth(), - now.getUTCDate(), - 0, - 0, - 0, - 0 - ); - var os = require('os'); - var log = require('../lib/logging')('bin.metrics'); - var mysql = require('../lib/db/mysql')(log, require('../db-server').errors); - var self = this; - var db; - - return mysql - .connect({ - master: { - host: config.General.db_dnsname, - user: config.General.db_username, - password: config.General.db_password, - database: config.General.db_name, - }, - slave: { - host: config.General.db_dnsname, - user: config.General.db_username, - password: config.General.db_password, - database: config.General.db_name, - }, - patchKey: 'schema-patch-level', - }) - .then(function (result) { - db = result; - return db.readMultiple( - [ - { sql: 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED' }, - { sql: 'START TRANSACTION' }, - metricsQuery('countAccounts'), - metricsQuery('countVerifiedAccounts'), - metricsQuery('countAccountsWithTwoOrMoreDevices'), - metricsQuery('countAccountsWithThreeOrMoreDevices'), - metricsQuery('countAccountsWithMobileDevice'), - ], - { sql: 'COMMIT' } - ); - }) - .then(function (results) { - assertResults(results, 2, 6); - fs.appendFileSync( - '/media/ephemeral0/fxa-admin/basic_metrics.log', - JSON.stringify({ - hostname: os.hostname(), - pid: process.pid, - op: 'account_totals', - total_accounts: results[2][0].count, - total_verified_accounts: results[3][0].count, - total_accounts_with_two_or_more_devices: results[4][0].count, - total_accounts_with_three_or_more_devices: results[5][0].count, - total_accounts_with_mobile_device: results[6][0].count, - time: new Date(lastMidnight).toISOString(), - v: 0, - }) + '\n' - ); - db.close(); - }) - .catch(function (error) { - log.error('metrics.run', error); - db.close(); - }); - - function metricsQuery(queryName) { - return { sql: self[queryName], params: [lastMidnight] }; - } -} - -function assertResults(results, firstIndex, lastIndex) { - results - .filter(function (result, index) { - return index >= firstIndex && index <= lastIndex; - }) - .forEach(assertResult); -} - -function assertResult(result) { - if (Array.isArray(result) && result.length === 1 && result[0].count >= 0) { - return; - } - - throw new Error( - 'unexpected metrics query result format, should be [ { count: n } ]' - ); -} - -// Very rudimentary parser for the following config file format: -// [General] -// db_dnsname: foo -// db_username: bar -// db_password: baz -// db_name: qux -function parseConfigFile(path) { - var currentSection; - - return fs - .readFileSync(path, { encoding: 'utf8' }) - .split('\n') - .map(trim) - .filter(filterConfig) - .reduce(reduceConfig, {}); - - function reduceConfig(parsed, line) { - var isSectionName = line.match(/^\[(.+)\]$/); - - if (isSectionName) { - currentSection = isSectionName[1]; - - if (!parsed[currentSection]) { - parsed[currentSection] = {}; - } - } else { - var setting = line.split(':').map(trim); - - if (!currentSection || setting.length === 0 || setting.length > 2) { - throw new Error('unexpected config file format'); - } - - parsed[currentSection][setting[0]] = setting[1]; - } - - return parsed; - } -} - -function trim(string) { - return string.trim(); -} - -function filterConfig(line) { - return line && line[0] !== '#' && line[0] !== ';'; -} diff --git a/packages/fxa-auth-db-mysql/bin/server.js b/packages/fxa-auth-db-mysql/bin/server.js deleted file mode 100644 index f4390af4fe9..00000000000 --- a/packages/fxa-auth-db-mysql/bin/server.js +++ /dev/null @@ -1,87 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// This MUST be the first require in the program. -// Only `require()` the newrelic module if explicity enabled. -// If required, modules will be instrumented. -require('../lib/newrelic')(); - -var config = require('../config'); -var dbServer = require('../db-server'); -var error = dbServer.errors; -var logger = require('../lib/logging')('bin.server'); -var DB = require('../lib/db/mysql')(logger, error); -var restify = require('restify'); -// configure Sentry -var Sentry = require('@sentry/node'); -const { tagCriticalEvent } = require('fxa-shared/tags/sentry'); - -const sentryDsn = config.sentryDsn; - -if (sentryDsn) { - Sentry.init({ - dsn: sentryDsn, - beforeSend: tagCriticalEvent, - }); - logger.info('sentryEnabled'); -} else { - logger.info('sentryDisabled'); -} - -function logCharsetInfo(db, poolName) { - // Record some information about mysql connection configuration and - // charset at startup. - db._showVariables(poolName) - .then(function (variables) { - logger.info(['variables', poolName].join('.'), variables); - }) - .then(function () { - return db._connectionConfig(poolName); - }) - .then(function (config) { - logger.info(['connectionConfig', poolName].join('.'), config); - }) - .catch(function (err) { - logger.error('error', { error: err }); - }); -} - -DB.connect(config).done(function (db) { - // Serve the HTTP API. - var server = dbServer.createServer(db); - server.listen(config.port, config.hostname, function () { - logger.info('start', { port: config.port }); - }); - - server.on('uncaughtException', function (req, res, route, err) { - if (sentryDsn) { - Sentry.captureException(err); - } - res.send(new restify.errors.InternalServerError('Server Error')); - }); - - server.on('error', function (err) { - if (sentryDsn) { - Sentry.captureException(err); - } - logger.error('start', { message: err.message }); - }); - server.on('success', function (d) { - logger.info('summary', d); - }); - server.on('failure', function (err) { - if (err.statusCode >= 500) { - logger.error('summary', err); - } else { - logger.warn('summary', err); - } - }); - server.on('mem', function (stats) { - logger.info('mem', stats); - }); - - // Log connection config and charset info - logCharsetInfo(db, 'MASTER'); - logCharsetInfo(db, 'SLAVE'); -}); diff --git a/packages/fxa-auth-db-mysql/config/config.js b/packages/fxa-auth-db-mysql/config/config.js deleted file mode 100644 index e1c06273836..00000000000 --- a/packages/fxa-auth-db-mysql/config/config.js +++ /dev/null @@ -1,199 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -module.exports = function (fs, path, url, convict) { - var conf = convict({ - env: { - doc: 'The current node.js environment', - default: 'prod', - format: ['dev', 'test', 'stage', 'prod'], - env: 'NODE_ENV', - }, - hostname: { - doc: 'The IP address the server should bind to', - default: 'localhost', - env: 'HOST', - }, - port: { - doc: 'The port the server should bind to', - default: 8000, - format: 'port', - env: 'PORT', - }, - logging: { - app: { - default: 'fxa-auth-db-server', - }, - fmt: { - format: ['heka', 'pretty'], - default: 'heka', - }, - level: { - env: 'LOG_LEVEL', - default: 'info', - }, - uncaught: { - format: ['exit', 'log', 'ignore'], - default: 'exit', - }, - }, - patchKey: { - doc: - 'The name of the row in the dbMetadata table which stores the patch level', - default: 'schema-patch-level', - env: 'SCHEMA_PATCH_KEY', - }, - enablePruning: { - doc: 'Enables (true) or disables (false) pruning', - default: false, - format: Boolean, - env: 'ENABLE_PRUNING', - }, - pruneEvery: { - doc: 'Approximate time between prunes (in ms)', - default: '1 hour', - format: 'duration', - env: 'PRUNE_EVERY', - }, - pruneTokensMaxAge: { - // This setting must always be older than token lifetimes in the fxa-auth-server - doc: - 'Time after which to prune account, password and unblock tokens (in ms)', - default: '3 months', - format: 'duration', - env: 'PRUNE_TOKENS_MAX_AGE', - }, - signinCodesMaxAge: { - doc: 'Maximum age for signinCodes, after which they will expire', - default: '2 days', - format: 'duration', - env: 'SIGNIN_CODES_MAX_AGE', - }, - requiredSQLModes: { - doc: - 'Comma-separated list of SQL mode flags to enforce on each connection', - default: '', - format: 'String', - env: 'REQUIRED_SQL_MODES', - }, - master: { - user: { - doc: 'The user to connect to for MySql', - default: 'root', - env: 'MYSQL_USER', - }, - password: { - doc: 'The password to connect to for MySql', - default: '', - env: 'MYSQL_PASSWORD', - }, - host: { - doc: 'The host to connect to for MySql', - default: 'localhost', - env: 'MYSQL_HOST', - }, - port: { - doc: 'The port to connect to for MySql', - default: 3306, - format: 'port', - env: 'MYSQL_PORT', - }, - connectionLimit: { - doc: 'The maximum number of connections to create at once.', - default: 10, - format: 'nat', - env: 'MYSQL_CONNECTION_LIMIT', - }, - waitForConnections: { - doc: - "Determines the pool's action when no connections are available and the limit has been reached.", - default: true, - format: Boolean, - env: 'MYSQL_WAIT_FOR_CONNECTIONS', - }, - queueLimit: { - doc: - "Determines the maximum size of the pool's waiting-for-connections queue.", - default: 100, - format: 'nat', - env: 'MYSQL_QUEUE_LIMIT', - }, - }, - slave: { - user: { - doc: 'The user to connect to for MySql', - default: 'root', - env: 'MYSQL_SLAVE_USER', - }, - password: { - doc: 'The password to connect to for MySql', - default: '', - env: 'MYSQL_SLAVE_PASSWORD', - }, - host: { - doc: 'The host to connect to for MySql', - default: 'localhost', - env: 'MYSQL_SLAVE_HOST', - }, - port: { - doc: 'The port to connect to for MySql', - default: 3306, - format: 'port', - env: 'MYSQL_SLAVE_PORT', - }, - connectionLimit: { - doc: 'The maximum number of connections to create at once.', - default: 10, - format: 'nat', - env: 'MYSQL_SLAVE_CONNECTION_LIMIT', - }, - waitForConnections: { - doc: - "Determines the pool's action when no connections are available and the limit has been reached.", - default: true, - format: Boolean, - env: 'MYSQL_SLAVE_WAIT_FOR_CONNECTIONS', - }, - queueLimit: { - doc: - "Determines the maximum size of the pool's waiting-for-connections queue.", - default: 100, - format: 'nat', - env: 'MYSQL_SLAVE_QUEUE_LIMIT', - }, - }, - ipHmacKey: { - doc: 'A secret to hash IP addresses for security history events', - default: 'changeme', - env: 'IP_HMAC_KEY', - }, - sentryDsn: { - doc: 'Sentry DSN for error and log reporting', - default: '', - format: 'String', - env: 'SENTRY_DSN', - }, - recoveryCodes: { - length: { - doc: 'The length of a recovery code', - default: 10, - format: 'nat', - env: 'RECOVERY_CODE_LENGTH', - }, - }, - }); - - // handle configuration files. you can specify a CSV list of configuration - // files to process, which will be overlayed in order, in the CONFIG_FILES - // environment variable. By default, the ./config/.json file is loaded. - - var envConfig = path.join(__dirname, conf.get('env') + '.json'); - envConfig = envConfig + ',' + process.env.CONFIG_FILES; - - var files = envConfig.split(',').filter(fs.existsSync); - conf.loadFile(files); - conf.validate({ allowed: 'strict' }); - - return conf.getProperties(); -}; diff --git a/packages/fxa-auth-db-mysql/config/index.js b/packages/fxa-auth-db-mysql/config/index.js deleted file mode 100644 index 7be1e725ea0..00000000000 --- a/packages/fxa-auth-db-mysql/config/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -var fs = require('fs'); -var path = require('path'); -var url = require('url'); -var convict = require('convict'); -convict.addFormats(require('convict-format-with-moment')); -convict.addFormats(require('convict-format-with-validator')); - -module.exports = require('./config')(fs, path, url, convict); diff --git a/packages/fxa-auth-db-mysql/db-server/index.js b/packages/fxa-auth-db-mysql/db-server/index.js deleted file mode 100644 index 4f34fdbb462..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/index.js +++ /dev/null @@ -1,412 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -var restify = require('restify'); -var bufferize = require('./lib/bufferize'); -var version = require('../package.json').version; -var errors = require('./lib/error'); -const safeJsonFormatter = require('./lib/safeJsonFormatter'); - -function createServer(db) { - var implementation = db.constructor.name || '__anonymousconstructor__'; - - function reply(fn) { - return function (req, res, next) { - fn(req.params, req.body, req.query) - .then( - handleSuccess.bind(null, req, res), - handleError.bind(null, req, res) - ) - .then(next, next); - }; - } - - function withIdAndBody(fn) { - return reply(function (params, body, query) { - return fn.call(db, params.id, body); - }); - } - - function withBodyAndQuery(fn) { - return reply(function (params, body, query) { - return fn.call(db, body, query); - }); - } - - function withParams(fn) { - return reply(function (params, body, query) { - return fn.call(db, params); - }); - } - - function withSpreadParams(fn) { - return reply(function (params, body, query) { - return fn.apply( - db, - Object.keys(params).map((k) => params[k]) - ); - }); - } - - function withParamsAndBody(fn) { - return reply(function (params, body, query) { - return fn.call(db, params, body); - }); - } - - function withSpreadParamsAndBody(fn) { - return reply(function (params, body, query) { - return fn.apply( - db, - Object.keys(params) - .map((k) => params[k]) - .concat([body]) - ); - }); - } - - const api = restify.createServer({ - formatters: { - 'application/json; q=0.9': safeJsonFormatter, - }, - // Auth-server accepts 255 unicode email address and sends them over has hex encoded values. - // These values could be as large as 1530 characters. - maxParamLength: 1530, - }); - - // Allow Keep-Alive connections from the auth-server to be idle up to two - // minutes before closing the connection. If this is not set, the default - // idle-time is 5 seconds. This can cause a lot of unneeded churn in server - // connections. Setting this to 120s makes node8 behave more like node6. - - // https://nodejs.org/docs/latest-v8.x/api/http.html#http_server_keepalivetimeout - api.server.keepAliveTimeout = 120000; - - api.use(restify.plugins.bodyParser()); - api.use(restify.plugins.queryParser()); - api.use( - bufferize.bufferizeRequest.bind( - null, - new Set([ - // These are all the different params that we handle as binary Buffers, - // but are passed into the API as hex strings. - 'authKey', - 'authSalt', - 'data', - 'deviceId', - 'emailCode', - 'flowId', - 'id', - 'kA', - 'keyBundle', - 'passCode', - 'recoveryKeyId', - 'sessionTokenId', - 'refreshTokenId', - 'tokenId', - 'tokenVerificationId', - 'uid', - 'verifyHash', - 'wrapWrapKb', - ]) - ) - ); - - api.get('/account/:id', withIdAndBody(db.account)); - api.del('/account/:id', withIdAndBody(db.deleteAccount)); - api.put('/account/:id', withIdAndBody(db.createAccount)); - api.post('/account/:id/checkPassword', withIdAndBody(db.checkPassword)); - api.post('/account/:id/reset', withIdAndBody(db.resetAccount)); - api.post('/account/:id/resetTokens', withIdAndBody(db.resetAccountTokens)); - api.post( - '/account/:id/verifyEmail/:emailCode', - op(function (req) { - return db.verifyEmail(req.params.id, req.params.emailCode); - }) - ); - api.post('/account/:id/locale', withIdAndBody(db.updateLocale)); - api.get('/account/:id/sessions', withIdAndBody(db.sessions)); - api.put( - '/account/:id/ecosystemAnonId', - withIdAndBody(db.updateEcosystemAnonId) - ); - - api.get('/account/:id/emails', withIdAndBody(db.accountEmails)); - api.post('/account/:id/emails', withIdAndBody(db.createEmail)); - api.del( - '/account/:id/emails/:email', - op(function (req) { - return db.deleteEmail( - req.params.id, - bufferize.hexToUtf8(req.params.email) - ); - }) - ); - - api.get( - '/email/:email', - op(function (req) { - return db.getSecondaryEmail(bufferize.hexToUtf8(req.params.email)); - }) - ); - api.get( - '/email/:email/account', - op(function (req) { - return db.accountRecord(bufferize.hexToUtf8(req.params.email)); - }) - ); - api.post( - '/email/:email/account/:id', - op(function (req) { - return db.setPrimaryEmail( - req.params.id, - bufferize.hexToUtf8(req.params.email) - ); - }) - ); - - api.get('/sessionToken/:id', withIdAndBody(db.sessionToken)); - api.del('/sessionToken/:id', withIdAndBody(db.deleteSessionToken)); - api.put('/sessionToken/:id', withIdAndBody(db.createSessionToken)); - api.post('/sessionToken/:id/update', withIdAndBody(db.updateSessionToken)); - - api.get('/keyFetchToken/:id', withIdAndBody(db.keyFetchToken)); - api.del('/keyFetchToken/:id', withIdAndBody(db.deleteKeyFetchToken)); - api.put('/keyFetchToken/:id', withIdAndBody(db.createKeyFetchToken)); - - api.get( - '/keyFetchToken/:id/verified', - withIdAndBody(db.keyFetchTokenWithVerificationStatus) - ); - api.post('/tokens/:id/verify', withIdAndBody(db.verifyTokens)); - api.post( - '/tokens/:id/verifyWithMethod', - withIdAndBody(db.verifyTokensWithMethod) - ); - api.post('/tokens/:code/verifyCode', withParamsAndBody(db.verifyTokenCode)); - - api.get('/accountResetToken/:id', withIdAndBody(db.accountResetToken)); - api.del('/accountResetToken/:id', withIdAndBody(db.deleteAccountResetToken)); - - api.get('/passwordChangeToken/:id', withIdAndBody(db.passwordChangeToken)); - api.del( - '/passwordChangeToken/:id', - withIdAndBody(db.deletePasswordChangeToken) - ); - api.put( - '/passwordChangeToken/:id', - withIdAndBody(db.createPasswordChangeToken) - ); - - api.get('/passwordForgotToken/:id', withIdAndBody(db.passwordForgotToken)); - api.del( - '/passwordForgotToken/:id', - withIdAndBody(db.deletePasswordForgotToken) - ); - api.put( - '/passwordForgotToken/:id', - withIdAndBody(db.createPasswordForgotToken) - ); - api.post( - '/passwordForgotToken/:id/update', - withIdAndBody(db.updatePasswordForgotToken) - ); - api.post( - '/passwordForgotToken/:id/verified', - withIdAndBody(db.forgotPasswordVerified) - ); - - api.get('/verificationReminders', withBodyAndQuery(db.fetchReminders)); - api.post( - '/verificationReminders', - withBodyAndQuery(db.createVerificationReminder) - ); - api.del('/verificationReminders', withBodyAndQuery(db.deleteReminder)); - - api.get('/securityEvents/:id/ip/:ipAddr', withParams(db.securityEvents)); - api.post('/securityEvents', withBodyAndQuery(db.createSecurityEvent)); - api.get( - '/securityEvents/:id', - op((req) => { - return db.securityEventsByUid(req.params.id); - }) - ); - api.del( - '/securityEvents/:id', - op((req) => { - return db.deleteSecurityEventsByUid(req.params.id); - }) - ); - - api.get('/emailBounces/:id', withIdAndBody(db.fetchEmailBounces)); - api.post('/emailBounces', withBodyAndQuery(db.createEmailBounce)); - - api.get('/emailRecord/:id', withIdAndBody(db.emailRecord)); - api.head('/emailRecord/:id', withIdAndBody(db.accountExists)); - - api.get('/totp/:id', withIdAndBody(db.totpToken)); - api.del('/totp/:id', withIdAndBody(db.deleteTotpToken)); - api.put('/totp/:id', withIdAndBody(db.createTotpToken)); - api.post('/totp/:id/update', withIdAndBody(db.updateTotpToken)); - - api.get('/__heartbeat__', withIdAndBody(db.ping)); - - api.get('/account/:id/devices', withIdAndBody(db.accountDevices)); - api.get('/account/:uid/device/:deviceId', withSpreadParams(db.device)); - api.put( - '/account/:uid/device/:deviceId', - withSpreadParamsAndBody(db.createDevice) - ); - api.post( - '/account/:uid/device/:deviceId/update', - withSpreadParamsAndBody(db.updateDevice) - ); - api.del('/account/:uid/device/:deviceId', withSpreadParams(db.deleteDevice)); - - function op(fn) { - return function (req, res, next) { - fn.call(null, req) - .then( - handleSuccess.bind(null, req, res), - handleError.bind(null, req, res) - ) - .then(next, next); - }; - } - - api.get( - '/account/:uid/tokens/:tokenVerificationId/device', - op(function (req) { - return db.deviceFromTokenVerificationId( - req.params.uid, - req.params.tokenVerificationId - ); - }) - ); - - api.put( - '/account/:uid/unblock/:code', - op(function (req) { - return db.createUnblockCode(req.params.uid, req.params.code); - }) - ); - - api.del( - '/account/:uid/unblock/:code', - op(function (req) { - return db.consumeUnblockCode(req.params.uid, req.params.code); - }) - ); - - api.put( - '/signinCodes/:code', - op((req) => - db.createSigninCode( - req.params.code, - req.body.uid, - req.body.createdAt, - req.body.flowId - ) - ) - ); - - api.post( - '/signinCodes/:code/consume', - op((req) => db.consumeSigninCode(req.params.code)) - ); - - api.post( - '/account/:id/recoveryCodes', - op((req) => { - return db.replaceRecoveryCodes(req.params.id, req.body.count); - }) - ); - - api.post( - '/account/:id/recoveryCodes/:code', - op((req) => db.consumeRecoveryCode(req.params.id, req.params.code)) - ); - - api.get( - '/account/:id/recoveryKey/:recoveryKeyId', - withParams(db.getRecoveryKey) - ); - api.get('/account/:id/recoveryKey', withIdAndBody(db.recoveryKeyExists)); - api.del('/account/:id/recoveryKey', withParams(db.deleteRecoveryKey)); - api.post('/account/:id/recoveryKey', withIdAndBody(db.createRecoveryKey)); - api.post( - '/account/:id/recoveryKey/update', - withIdAndBody(db.updateRecoveryKey) - ); - - api.get('/', function (req, res, next) { - res.send({ version: version, implementation: implementation }); - next(); - }); - - api.get('/__version__', function (req, res, next) { - res.send({ version: version, implementation: implementation }); - next(); - }); - - function handleSuccess(req, res, result) { - api.emit('success', { - code: 200, - route: req.route.name, - method: req.method, - path: req.url, - t: Date.now() - req.time(), - }); - if (Array.isArray(result)) { - res.send(result.map(bufferize.unbuffer)); - } else { - // When performing a `HEAD` request, the content type is not - // set, manually set to application/json - if (req.method === 'HEAD') { - res.setHeader('Content-Type', 'application/json'); - } - - res.send(bufferize.unbuffer(result || {})); - } - } - - function handleError(req, res, err) { - if (typeof err !== 'object') { - err = { message: err || 'none' }; - } - - var statusCode = err.code || 500; - - api.emit('failure', { - code: statusCode, - route: req.route ? req.route.name : 'unknown', - method: req.method, - path: req.url, - err: err, - t: Date.now() - req.time(), - }); - - res.send(statusCode, { - message: err.message, - errno: err.errno, - error: err.error, - code: err.code, - }); - } - - var memInterval = setInterval(function () { - api.emit('mem', process.memoryUsage()); - }, 15000); - memInterval.unref(); - - api.on('NotFound', function (req, res) { - handleError(req, res, errors.notFound()); - }); - - return api; -} - -module.exports = { - createServer: createServer, - errors: errors, -}; diff --git a/packages/fxa-auth-db-mysql/db-server/lib/bufferize.js b/packages/fxa-auth-db-mysql/db-server/lib/bufferize.js deleted file mode 100644 index c6676be975b..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/lib/bufferize.js +++ /dev/null @@ -1,65 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -var HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/; - -function unbuffer(object) { - var keys = Object.keys(object); - for (var i = 0; i < keys.length; i++) { - var x = object[keys[i]]; - if (Buffer.isBuffer(x)) { - object[keys[i]] = x.toString('hex'); - } - } - return object; -} - -function bufferize(object, onlyTheseKeys) { - var keys = Object.keys(object); - if (onlyTheseKeys) { - keys = keys.filter((key) => onlyTheseKeys.has(key)); - } - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - var value = object[key]; - // Don't convert things with no value, but we still want - // to bufferize falsy things like the empty string. - if (typeof value !== 'undefined' && value !== null) { - if (typeof value !== 'string' || !HEX_STRING.test(value)) { - throw new Error('Invalid hex data for ' + key + ': "' + value + '"'); - } - object[key] = Buffer.from(value, 'hex'); - } - } - return object; -} - -function bufferizeRequest(keys, req, res, next) { - try { - if (req.body) { - req.body = bufferize(req.body, keys); - } - if (req.params) { - req.params = bufferize(req.params, keys); - } - } catch (err) { - // Failure here means invalid hex data in a bufferized field. - if (!err.statusCode) { - err.statusCode = 400; - } - return next(err); - } - return next(); -} - -function hexToUtf8(value) { - return Buffer.from(value, 'hex').toString('utf8'); -} - -module.exports = { - unbuffer: unbuffer, - bufferize: bufferize, - bufferizeRequest: bufferizeRequest, - hexToUtf8: hexToUtf8, -}; diff --git a/packages/fxa-auth-db-mysql/db-server/lib/error.js b/packages/fxa-auth-db-mysql/db-server/lib/error.js deleted file mode 100644 index f13be61dbc9..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/lib/error.js +++ /dev/null @@ -1,117 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -var inherits = require('util').inherits; - -function AppError(options) { - this.message = options.message; - this.errno = options.errno; - this.error = options.error; - this.code = options.code; - if (options.stack) { - this.stack = options.stack; - } -} -inherits(AppError, Error); - -AppError.prototype.toString = function () { - return 'Error: ' + this.message; -}; - -AppError.duplicate = function () { - return new AppError({ - code: 409, - error: 'Conflict', - errno: 101, - message: 'Record already exists', - }); -}; - -AppError.notFound = function () { - return new AppError({ - code: 404, - error: 'Not Found', - errno: 116, - message: 'Not Found', - }); -}; - -AppError.incorrectPassword = function () { - return new AppError({ - code: 400, - error: 'Bad request', - errno: 103, - message: 'Incorrect password', - }); -}; - -AppError.cannotDeletePrimaryEmail = function () { - return new AppError({ - code: 400, - error: 'Bad request', - errno: 136, - message: 'Can not delete primary email', - }); -}; - -AppError.expiredTokenVerificationCode = function () { - return new AppError({ - code: 400, - error: 'Bad request', - errno: 137, - message: 'Expired token verification code', - }); -}; - -AppError.invalidVerificationMethod = function () { - return new AppError({ - code: 400, - error: 'Bad request', - errno: 138, - message: 'Invalid verification method', - }); -}; - -AppError.recoveryKeyInvalid = () => { - return new AppError({ - code: 400, - error: 'Bad Request', - errno: 159, - message: 'Recovery key is not valid', - }); -}; - -AppError.cannotSetUnverifiedPrimaryEmail = function () { - return new AppError({ - code: 400, - error: 'Bad request', - errno: 147, - message: 'Can not set unverified primary email', - }); -}; - -AppError.cannotSetUnownedPrimaryEmail = function () { - return new AppError({ - code: 400, - error: 'Bad request', - errno: 148, - message: 'Can not set primary email to email that is not owned by account', - }); -}; - -AppError.wrap = function (err) { - // Don't re-wrap! - if (err instanceof AppError) { - return err; - } - return new AppError({ - code: 500, - error: 'Internal Server Error', - errno: err.errno, - message: err.code, - stack: err.stack, - }); -}; - -module.exports = AppError; diff --git a/packages/fxa-auth-db-mysql/db-server/lib/safeJsonFormatter.js b/packages/fxa-auth-db-mysql/db-server/lib/safeJsonFormatter.js deleted file mode 100644 index 0b9287a09f6..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/lib/safeJsonFormatter.js +++ /dev/null @@ -1,14 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -module.exports = (req, res, body) => { - let data = body ? JSON.stringify(body) : 'null'; - data = data - .replace(//g, '\\u003e') - .replace(/&/g, '\\u0026'); - - res.setHeader('Content-Length', Buffer.byteLength(data)); - return data; -}; diff --git a/packages/fxa-auth-db-mysql/db-server/test/.eslintrc b/packages/fxa-auth-db-mysql/db-server/test/.eslintrc deleted file mode 100644 index 333c534448f..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/test/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -extends: ../../.eslintrc - -env: - mocha: true - -rules: - fxa/async-crypto-random: 0 diff --git a/packages/fxa-auth-db-mysql/db-server/test/backend/db_tests.js b/packages/fxa-auth-db-mysql/db-server/test/backend/db_tests.js deleted file mode 100644 index 7f0812513cb..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/test/backend/db_tests.js +++ /dev/null @@ -1,4417 +0,0 @@ -/* eslint-disable no-prototype-builtins */ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -'use strict'; - -const { assert } = require('chai'); -const crypto = require('crypto'); -const P = require('bluebird'); -const util = require('../../../lib/db/util'); -const { normalizeEmail } = require('fxa-shared').email.helpers; - -const zeroBuffer16 = Buffer.from('00000000000000000000000000000000', 'hex'); -const zeroBuffer32 = Buffer.from( - '0000000000000000000000000000000000000000000000000000000000000000', - 'hex' -); -const now = Date.now(); - -function newUuid() { - return crypto.randomBytes(16); -} - -function unblockCode() { - return crypto.randomBytes(4).toString('hex'); -} - -function createAccount() { - const account = { - uid: newUuid(), - email: ('' + Math.random()).substr(2) + '@bar.com', - emailCode: zeroBuffer16, - emailVerified: false, - verifierVersion: 1, - verifyHash: zeroBuffer32, - authSalt: zeroBuffer32, - kA: zeroBuffer32, - wrapWrapKb: zeroBuffer32, - verifierSetAt: now, - createdAt: now, - locale: 'en_US', - }; - account.normalizedEmail = normalizeEmail(account.email); - account.emailBuffer = Buffer.from(account.email); - return account; -} - -function createEmail(data) { - const email = { - email: ('' + Math.random()).substr(2) + '@bar.com', - uid: data.uid, - emailCode: data.emailCode || crypto.randomBytes(16), - isVerified: data.isVerified || false, - verifiedAt: data.verifiedAt || null, - isPrimary: false, - createdAt: Date.now(), - }; - email.email = data.email || email.email; - email.normalizedEmail = normalizeEmail(email.email); - - return email; -} - -function hex(len) { - return Buffer.from(crypto.randomBytes(len).toString('hex'), 'hex'); -} -function hex6() { - return hex(6); -} -function hex16() { - return hex(16); -} -function hex32() { - return hex(32); -} -// function hex64() { return hex(64) } -function hex96() { - return hex(96); -} - -function makeMockSessionToken(uid, mustVerify) { - const sessionToken = { - tokenId: hex32(), - data: hex32(), - uid: uid, - createdAt: Date.now(), - uaBrowser: 'mock browser', - uaBrowserVersion: 'mock browser version', - uaOS: 'mock OS', - uaOSVersion: 'mock OS version', - uaDeviceType: 'mock device type', - mustVerify: mustVerify, - tokenVerificationId: hex16(), - tokenVerificationCode: unblockCode(), - tokenVerificationCodeExpiresAt: Date.now() + 20000, - }; - return sessionToken; -} - -function makeMockDevice(tokenId) { - const device = { - sessionTokenId: tokenId, - name: 'Test Device', - type: 'mobile', - createdAt: Date.now(), - callbackURL: 'https://push.server', - callbackPublicKey: 'foo', - callbackAuthKey: 'bar', - callbackIsExpired: false, - availableCommands: { - 'https://identity.mozilla.com/cmd/display-uri': 'metadata-bundle', - }, - }; - device.deviceId = newUuid(); - return device; -} - -function makeMockRefreshToken(uid) { - const refreshToken = { - tokenId: hex32(), - uid: uid, - }; - return refreshToken; -} - -function makeMockOAuthDevice(tokenId) { - const device = { - refreshTokenId: tokenId, - name: 'Test OAuth Device', - type: 'mobile', - createdAt: Date.now(), - callbackURL: 'https://push.server', - callbackPublicKey: 'foo', - callbackAuthKey: 'bar', - callbackIsExpired: false, - availableCommands: { - 'https://identity.mozilla.com/cmd/display-uri': 'metadata-bundle', - }, - }; - device.deviceId = newUuid(); - return device; -} - -function makeMockForgotPasswordToken(uid) { - const token = { - data: hex32(), - tokenId: hex32(), - uid: uid, - passCode: hex16(), - tries: 1, - createdAt: Date.now(), - }; - return token; -} - -function makeMockKeyFetchToken(uid, verified) { - const keyFetchToken = { - authKey: hex32(), - uid: uid, - keyBundle: hex96(), - createdAt: now + 2, - tokenVerificationId: verified ? undefined : hex16(), - }; - keyFetchToken.tokenId = hex32(); - return keyFetchToken; -} - -function makeMockChangePasswordToken(uid) { - const token = { - data: hex32(), - uid: uid, - createdAt: Date.now(), - }; - token.tokenId = hex32(); - return token; -} - -function makeMockAccountResetToken(uid, tokenId) { - const token = { - tokenId: tokenId || hex32(), - data: hex32(), - uid: uid, - createdAt: now + 5, - }; - return token; -} - -function createRecoveryData() { - const data = { - recoveryKeyId: hex(16), - recoveryData: crypto.randomBytes(32).toString('hex'), - enabled: true, - }; - return data; -} - -// To run these tests from a new backend, pass the config and an already created -// DB API for them to be run against. -module.exports = function (config, DB) { - describe('db_tests', () => { - let db, accountData; - before(() => { - return DB.connect(config).then((db_) => { - db = db_; - return db.ping(); - }); - }); - - beforeEach(() => { - accountData = createAccount(); - return db.createAccount(accountData.uid, accountData); - }); - - describe('db.account', () => { - beforeEach(() => { - return db - .accountExists(accountData.emailBuffer) - .then((exists) => - assert(exists, 'account exists for this email address') - ); - }); - - it('should create account', () => { - const anotherAccountData = createAccount(); - return db - .accountExists(anotherAccountData.emailBuffer) - .then(assert.fail, (err) => assert.equal(err.code, 404, 'Not found')) - .then(() => - db.createAccount(anotherAccountData.uid, anotherAccountData) - ) - .then((account) => { - assert.deepEqual( - account, - {}, - 'Returned an empty object on account creation' - ); - return db - .accountExists(Buffer.from(anotherAccountData.email)) - .then( - (exists) => - assert(exists, 'account exists for this email address'), - assert.fail - ); - }); - }); - - it('should fail with duplicate account', () => { - return db - .createAccount(accountData.uid, accountData) - .then(assert.fail, (err) => { - assert(err, 'trying to create the same account produces an error'); - assert.equal(err.code, 409, 'error code'); - assert.equal(err.errno, 101, 'error errno'); - assert.equal(err.message, 'Record already exists', 'message'); - assert.equal(err.error, 'Conflict', 'error'); - }); - }); - - it('should return account', () => { - return db.account(accountData.uid).then((account) => { - assert.deepEqual(account.uid, accountData.uid, 'uid'); - assert.equal(account.email, accountData.email, 'email'); - assert.deepEqual( - account.emailCode, - accountData.emailCode, - 'emailCode' - ); - assert.equal( - !!account.emailVerified, - accountData.emailVerified, - 'emailVerified' - ); - assert.deepEqual(account.kA, accountData.kA, 'kA'); - assert.deepEqual( - account.wrapWrapKb, - accountData.wrapWrapKb, - 'wrapWrapKb' - ); - assert(!account.verifyHash, 'verifyHash field should be absent'); - assert.deepEqual(account.authSalt, accountData.authSalt, 'authSalt'); - assert.equal( - account.verifierVersion, - accountData.verifierVersion, - 'verifierVersion' - ); - assert.equal(account.createdAt, accountData.createdAt, 'createdAt'); - assert.equal( - account.verifierSetAt, - accountData.createdAt, - 'verifierSetAt has been set to the same as createdAt' - ); - assert.equal(account.locale, accountData.locale, 'locale'); - assert.equal( - account.profileChangedAt, - account.createdAt, - 'profileChangedAt set to createdAt' - ); - assert.equal( - account.ecosystemAnonId, - accountData.ecosystemAnonId, - 'ecosystemAnonId' - ); - }); - }); - - it('should return email record', () => { - return db.emailRecord(accountData.emailBuffer).then((account) => { - assert.deepEqual(account.uid, accountData.uid, 'uid'); - assert.equal(account.email, accountData.email, 'email'); - assert.deepEqual( - account.emailCode, - accountData.emailCode, - 'emailCode' - ); - assert.equal( - !!account.emailVerified, - accountData.emailVerified, - 'emailVerified' - ); - assert.deepEqual(account.kA, accountData.kA, 'kA'); - assert.deepEqual( - account.wrapWrapKb, - accountData.wrapWrapKb, - 'wrapWrapKb' - ); - assert(!account.verifyHash, 'verifyHash field should be absent'); - assert.deepEqual(account.authSalt, accountData.authSalt, 'authSalt'); - assert.equal( - account.verifierVersion, - accountData.verifierVersion, - 'verifierVersion' - ); - assert.equal( - account.verifierSetAt, - accountData.verifierSetAt, - 'verifierSetAt' - ); - assert.equal( - account.hasOwnProperty('locale'), - false, - 'locale not returned' - ); - }); - }); - }); - - describe('db.checkPassword', () => { - it('should fail with incorrect password', () => { - return db - .checkPassword(accountData.uid, { - verifyHash: Buffer.from(crypto.randomBytes(32)), - }) - .then(assert.fail, (err) => { - assert(err, 'incorrect password produces an error'); - assert.equal(err.code, 400, 'error code'); - assert.equal(err.errno, 103, 'error errno'); - assert.equal(err.message, 'Incorrect password', 'message'); - assert.equal(err.error, 'Bad request', 'error'); - return db.checkPassword(accountData.uid, { - verifyHash: zeroBuffer32, - }); - }); - }); - - it('should be successful with correct password', () => { - return db - .checkPassword(accountData.uid, { verifyHash: zeroBuffer32 }) - .then((account) => { - assert.deepEqual(account.uid, account.uid, 'uid'); - assert.lengthOf(Object.keys(account), 1); - }); - }); - }); - - describe('db.updateEcosystemAnonId', () => { - const newAnonId = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkdWRlbmVzc0Bmb28uYmFyIiwibmFtZSI6IkZvbyBCYXJtYW4iLCJpYXQiOjE1MTYyMzkwMjJ9.hVQ6sj219nUiwN8B5uClxcVpoq-SmRLQdZmXjS0w3CA'; - it('should update ecosystemAnonId', () => { - return db - .updateEcosystemAnonId(accountData.uid, { - ecosystemAnonId: newAnonId, - }) - .then(() => db.account(accountData.uid)) - .then((result) => { - assert.equal( - result.ecosystemAnonId, - newAnonId, - 'ecosystemAnonId was updated' - ); - }); - }); - - it('should return not found error if the uid is not in the database', () => { - const randomUid = newUuid(); - return db - .updateEcosystemAnonId(randomUid, newAnonId) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'should return not found errno'); - assert.equal(err.code, 404, 'should return not found code'); - }); - }); - }); - - describe('session token handling', () => { - let sessionTokenData; - beforeEach(() => { - sessionTokenData = makeMockSessionToken(accountData.uid, false); - return db.createSessionToken( - sessionTokenData.tokenId, - sessionTokenData - ); - }); - - it('should get sessions', () => { - return db.sessions(accountData.uid).then((sessions) => { - assert.isArray(sessions); - assert.lengthOf(sessions, 1); - - assert.lengthOf(Object.keys(sessions[0]), 20); - assert.equal( - sessions[0].tokenId.toString('hex'), - sessionTokenData.tokenId.toString('hex'), - 'tokenId is correct' - ); - assert.equal( - sessions[0].uid.toString('hex'), - accountData.uid.toString('hex'), - 'uid is correct' - ); - assert.equal( - sessions[0].createdAt, - sessionTokenData.createdAt, - 'createdAt is correct' - ); - assert.equal( - sessions[0].uaBrowser, - sessionTokenData.uaBrowser, - 'uaBrowser is correct' - ); - assert.equal( - sessions[0].uaBrowserVersion, - sessionTokenData.uaBrowserVersion, - 'uaBrowserVersion is correct' - ); - assert.equal( - sessions[0].uaOS, - sessionTokenData.uaOS, - 'uaOS is correct' - ); - assert.equal( - sessions[0].uaOSVersion, - sessionTokenData.uaOSVersion, - 'uaOSVersion is correct' - ); - assert.equal( - sessions[0].uaDeviceType, - sessionTokenData.uaDeviceType, - 'uaDeviceType is correct' - ); - assert.equal( - sessions[0].uaFormFactor, - sessionTokenData.uaFormFactor, - 'uaFormFactor is correct' - ); - assert.equal( - sessions[0].lastAccessTime, - sessionTokenData.createdAt, - 'lastAccessTime is correct' - ); - assert.equal( - sessions[0].authAt, - sessionTokenData.createdAt, - 'authAt is correct' - ); - }); - }); - - it('should create session', () => { - accountData = createAccount(); - sessionTokenData = makeMockSessionToken(accountData.uid, false); - return db - .createAccount(accountData.uid, accountData) - .then(() => - db.createSessionToken(sessionTokenData.tokenId, sessionTokenData) - ) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on session token creation' - ); - return db.sessions(accountData.uid); - }) - .then((sessions) => { - assert.isArray(sessions); - assert.lengthOf(sessions, 1); - }); - }); - - it('should get session token', () => { - return db.sessionToken(sessionTokenData.tokenId).then((token) => { - assert.isFalse(token.hasOwnProperty('tokenId')); - assert.deepEqual( - token.tokenData, - sessionTokenData.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - sessionTokenData.createdAt, - 'createdAt is correct' - ); - assert.equal( - token.uaBrowser, - sessionTokenData.uaBrowser, - 'uaBrowser is correct' - ); - assert.equal( - token.uaBrowserVersion, - sessionTokenData.uaBrowserVersion, - 'uaBrowserVersion is correct' - ); - assert.equal(token.uaOS, sessionTokenData.uaOS, 'uaOS is correct'); - assert.equal( - token.uaOSVersion, - sessionTokenData.uaOSVersion, - 'uaOSVersion is correct' - ); - assert.equal( - token.uaDeviceType, - sessionTokenData.uaDeviceType, - 'uaDeviceType is correct' - ); - assert.equal( - token.uaFormFactor, - sessionTokenData.uaFormFactor, - 'uaFormFactor is correct' - ); - assert.equal( - token.lastAccessTime, - sessionTokenData.createdAt, - 'lastAccessTime was set' - ); - assert.equal( - token.authAt, - sessionTokenData.createdAt, - 'authAt is correct' - ); - assert.equal( - !!token.emailVerified, - accountData.emailVerified, - 'token emailVerified is same as account emailVerified' - ); - assert.equal( - token.email, - accountData.email, - 'token email same as account email' - ); - assert.deepEqual( - token.emailCode, - accountData.emailCode, - 'token emailCode same as account emailCode' - ); - assert.equal( - token.verifierSetAt, - accountData.verifierSetAt, - 'verifierSetAt is correct' - ); - assert.equal( - token.accountCreatedAt, - accountData.createdAt, - 'accountCreatedAt is correct' - ); - assert.equal( - token.profileChangedAt, - accountData.createdAt, - 'profileChangedAt is correct' - ); - }); - }); - - it('should update token', () => { - const sessionTokenUpdates = { - uaBrowser: 'foo', - uaBrowserVersion: '1', - uaOS: 'bar', - uaOSVersion: '2', - uaDeviceType: 'baz', - lastAccessTime: 42, - authAt: 1234567, - }; - return db - .updateSessionToken(sessionTokenData.tokenId, sessionTokenUpdates) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on session token update' - ); - return db.sessionToken(sessionTokenData.tokenId); - }) - .then((token) => { - assert.deepEqual( - token.tokenData, - sessionTokenData.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - sessionTokenData.createdAt, - 'createdAt is correct' - ); - assert.equal(token.uaBrowser, 'foo', 'uaBrowser is correct'); - assert.equal( - token.uaBrowserVersion, - '1', - 'uaBrowserVersion is correct' - ); - assert.equal(token.uaOS, 'bar', 'uaOS is correct'); - assert.equal(token.uaOSVersion, '2', 'uaOSVersion is correct'); - assert.equal(token.uaDeviceType, 'baz', 'uaDeviceType is correct'); - assert.equal( - token.uaFormFactor, - sessionTokenData.uaFormFactor, - 'uaFormFactor is correct' - ); - assert.equal(token.lastAccessTime, 42, 'lastAccessTime is correct'); - assert.equal(token.authAt, 1234567, 'authAt is correct'); - assert.equal( - !!token.emailVerified, - accountData.emailVerified, - 'token emailVerified is same as account emailVerified' - ); - assert.equal( - token.email, - accountData.email, - 'token email same as account email' - ); - assert.deepEqual( - token.emailCode, - accountData.emailCode, - 'token emailCode same as account emailCode' - ); - assert.equal( - token.verifierSetAt, - accountData.verifierSetAt, - 'verifierSetAt is correct' - ); - assert.equal( - token.accountCreatedAt, - accountData.createdAt, - 'accountCreatedAt is correct' - ); - assert.equal( - token.mustVerify, - sessionTokenData.mustVerify, - 'mustVerify is set' - ); - assert.deepEqual( - token.tokenVerificationId, - sessionTokenData.tokenVerificationId, - 'tokenVerificationId is set' - ); - }); - }); - - it('should update mustVerify to true, but not to false', () => { - return db - .sessionToken(sessionTokenData.tokenId) - .then((token) => { - assert.equal( - token.mustVerify, - false, - 'mustVerify starts out as false' - ); - assert.equal( - token.uaBrowser, - 'mock browser', - 'other fields have their default values' - ); - return db.updateSessionToken(sessionTokenData.tokenId, { - mustVerify: true, - }); - }) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on session token update' - ); - return db.sessionToken(sessionTokenData.tokenId); - }) - .then((token) => { - assert.equal( - token.mustVerify, - true, - 'mustVerify was correctly updated to true' - ); - assert.equal( - token.uaBrowser, - 'mock browser', - 'other fields were not updated' - ); - return db.updateSessionToken(sessionTokenData.tokenId, { - mustVerify: false, - }); - }) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on session token update' - ); - return db.sessionToken(sessionTokenData.tokenId); - }) - .then((token) => { - assert.equal( - token.mustVerify, - true, - 'mustVerify was not reset back to false' - ); - assert.equal( - token.uaBrowser, - 'mock browser', - 'other fields were not updated' - ); - }); - }); - - it('should get verification state', () => { - return db.sessionToken(sessionTokenData.tokenId).then((token) => { - assert.deepEqual( - token.tokenData, - sessionTokenData.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - sessionTokenData.createdAt, - 'createdAt is correct' - ); - assert.equal( - token.uaBrowser, - sessionTokenData.uaBrowser, - 'uaBrowser is correct' - ); - assert.equal( - token.uaBrowserVersion, - sessionTokenData.uaBrowserVersion, - 'uaBrowserVersion is correct' - ); - assert.equal(token.uaOS, sessionTokenData.uaOS, 'uaOS is correct'); - assert.equal( - token.uaOSVersion, - sessionTokenData.uaOSVersion, - 'uaOSVersion is correct' - ); - assert.equal( - token.uaDeviceType, - sessionTokenData.uaDeviceType, - 'uaDeviceType is correct' - ); - assert.equal( - token.uaFormFactor, - sessionTokenData.uaFormFactor, - 'uaFormFactor is correct' - ); - assert.equal( - token.lastAccessTime, - sessionTokenData.createdAt, - 'lastAccessTime was set' - ); - assert.equal( - token.authAt, - sessionTokenData.createdAt, - 'authAt is correct' - ); - assert.equal( - !!token.emailVerified, - accountData.emailVerified, - 'token emailVerified is same as account emailVerified' - ); - assert.equal( - token.email, - accountData.email, - 'token email same as account email' - ); - assert.deepEqual( - token.emailCode, - accountData.emailCode, - 'token emailCode same as account emailCode' - ); - assert.equal( - token.verifierSetAt, - accountData.verifierSetAt, - 'verifierSetAt is correct' - ); - assert.equal( - token.accountCreatedAt, - accountData.createdAt, - 'accountCreatedAt is correct' - ); - assert.equal( - token.mustVerify, - sessionTokenData.mustVerify, - 'mustVerify is correct' - ); - assert.deepEqual( - token.tokenVerificationId, - sessionTokenData.tokenVerificationId, - 'tokenVerificationId is correct' - ); - }); - }); - - it('should fail session verification for invalid tokenId', () => { - return db - .verifyTokens(hex16(), accountData) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - - return db.sessionToken(sessionTokenData.tokenId); - }) - .then((token) => { - assert.equal( - token.mustVerify, - sessionTokenData.mustVerify, - 'mustVerify is correct' - ); - assert.deepEqual( - token.tokenVerificationId, - sessionTokenData.tokenVerificationId, - 'tokenVerificationId is correct' - ); - }); - }); - - it('should fail session verification for invalid uid', () => { - return db - .verifyTokens(sessionTokenData.tokenVerificationId, { uid: hex16() }) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - - return db.sessionToken(sessionTokenData.tokenId); - }) - .then((token) => { - assert.equal( - token.mustVerify, - sessionTokenData.mustVerify, - 'mustVerify is correct' - ); - assert.deepEqual( - token.tokenVerificationId, - sessionTokenData.tokenVerificationId, - 'tokenVerificationId is correct' - ); - }); - }); - - it('should verify session token', () => { - return db - .verifyTokens(sessionTokenData.tokenVerificationId, accountData) - .then(() => { - return db.sessionToken(sessionTokenData.tokenId); - }, assert.fail) - .then((token) => { - assert.equal(!!token.mustVerify, false, 'mustVerify is null'); - assert.isNull(token.tokenVerificationId); - }); - }); - - describe('db.accountDevices', () => { - let deviceData; - beforeEach(() => { - deviceData = makeMockDevice(sessionTokenData.tokenId); - return db.createDevice( - accountData.uid, - deviceData.deviceId, - deviceData - ); - }); - - it('should get device count', () => { - return db.accountDevices(accountData.uid).then((results) => { - assert.lengthOf(results, 1); - }); - }); - - it('db.sessions should contain device data', () => { - return db.sessions(accountData.uid).then((sessions) => { - assert.lengthOf(sessions, 1); - // the next session has a device attached to it - assert.equal( - sessions[0].deviceId.toString('hex'), - deviceData.deviceId.toString('hex') - ); - assert.equal(sessions[0].deviceName, 'Test Device'); - assert.equal(sessions[0].deviceType, 'mobile'); - assert(sessions[0].deviceCreatedAt); - assert.equal(sessions[0].deviceCallbackURL, 'https://push.server'); - assert.equal(sessions[0].deviceCallbackPublicKey, 'foo'); - assert.equal(sessions[0].deviceCallbackAuthKey, 'bar'); - assert.equal(sessions[0].deviceCallbackIsExpired, false); - }); - }); - - it('db.deleteSessionToken should delete device', () => { - return db - .accountDevices(accountData.uid) - .then((results) => { - assert.lengthOf(results, 1); - return db.deleteSessionToken(sessionTokenData.tokenId); - }) - .then(() => db.accountDevices(accountData.uid)) - .then((results) => { - assert.lengthOf(results, 0); - }); - }); - }); - - describe('db.deleteSessionToken', () => { - beforeEach(() => { - return db - .deleteSessionToken(sessionTokenData.tokenId) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on forgot session token deletion' - ); - }, assert.fail); - }); - - it('should delete session', () => { - return db - .sessionToken(sessionTokenData.tokenId) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - }); - }); - - it('should fail to verify deleted session', () => { - return db - .verifyTokens(sessionTokenData.tokenVerificationId, accountData) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - }); - }); - }); - }); - - describe('key fetch token handling', () => { - let keyFetchTokenData; - beforeEach(() => { - keyFetchTokenData = makeMockKeyFetchToken(accountData.uid, false); - return db.createKeyFetchToken( - keyFetchTokenData.tokenId, - keyFetchTokenData - ); - }); - - it('should have created unverified keyfetch token', () => { - keyFetchTokenData = makeMockKeyFetchToken(accountData.uid, false); - return db - .createKeyFetchToken(keyFetchTokenData.tokenId, keyFetchTokenData) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on key fetch token creation' - ); - return db.keyFetchToken(keyFetchTokenData.tokenId); - }) - .then((token) => { - assert.isUndefined(token.tokenId); - assert.deepEqual( - token.authKey, - keyFetchTokenData.authKey, - 'authKey matches' - ); - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - keyFetchTokenData.createdAt, - 'createdAt is ok' - ); - assert.equal( - !!token.emailVerified, - accountData.emailVerified, - 'emailVerified is correct' - ); - assert.isUndefined(token.email); - assert.isUndefined(token.emailCode); - assert.equal( - token.verifierSetAt, - accountData.verifierSetAt, - 'verifierSetAt is correct' - ); - assert.isUndefined(token.tokenVerificationId); - }); - }); - - it('should have created verified key fetch token', () => { - keyFetchTokenData = makeMockKeyFetchToken(accountData.uid, true); - return db - .createKeyFetchToken(keyFetchTokenData.tokenId, keyFetchTokenData) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on key fetch token creation' - ); - return db.keyFetchTokenWithVerificationStatus( - keyFetchTokenData.tokenId - ); - }) - .then((token) => { - assert.isUndefined(token.tokenId); - assert.deepEqual( - token.authKey, - keyFetchTokenData.authKey, - 'authKey matches' - ); - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - keyFetchTokenData.createdAt, - 'createdAt is ok' - ); - assert.equal( - !!token.emailVerified, - accountData.emailVerified, - 'emailVerified is correct' - ); - assert.isUndefined(token.email); - assert.isUndefined(token.emailCode); - assert.equal( - token.verifierSetAt, - accountData.verifierSetAt, - 'verifierSetAt is correct' - ); - assert.equal( - token.tokenVerificationId, - keyFetchTokenData.tokenVerificationId, - 'tokenVerificationId is undefined' - ); - }); - }); - - it('should get keyfetch token verification status', () => { - return db - .keyFetchTokenWithVerificationStatus(keyFetchTokenData.tokenId) - .then((token) => { - assert.deepEqual( - token.authKey, - keyFetchTokenData.authKey, - 'authKey matches' - ); - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - keyFetchTokenData.createdAt, - 'createdAt is ok' - ); - assert.equal( - !!token.emailVerified, - accountData.emailVerified, - 'emailVerified is correct' - ); - assert.equal( - token.verifierSetAt, - accountData.verifierSetAt, - 'verifierSetAt is correct' - ); - assert.deepEqual( - token.tokenVerificationId, - keyFetchTokenData.tokenVerificationId, - 'tokenVerificationId is correct' - ); - }); - }); - - it('should fail keyfetch token verficiation for invalid tokenVerficationId', () => { - return db - .verifyTokens(hex16(), accountData) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - }); - }); - - it('should fail keyfetch token verficiation for invalid uid', () => { - return db - .verifyTokens(keyFetchTokenData.tokenVerificationId, { uid: hex16() }) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - }); - }); - - it('should verify keyfetch token', () => { - return db - .keyFetchTokenWithVerificationStatus(keyFetchTokenData.tokenId) - .then((token) => { - assert.deepEqual( - token.tokenVerificationId, - keyFetchTokenData.tokenVerificationId, - 'tokenVerificationId is correct' - ); - return db.verifyTokens( - keyFetchTokenData.tokenVerificationId, - accountData - ); - }) - .then(() => { - return db.keyFetchTokenWithVerificationStatus( - keyFetchTokenData.tokenId - ); - }) - .then((token) => { - assert.isNull(token.tokenVerificationId); - }); - }); - - it('should delete key fetch token', () => { - return db - .deleteKeyFetchToken(keyFetchTokenData.tokenId) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on token delete' - ); - return db - .keyFetchToken(keyFetchTokenData.tokenVerificationId) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - }); - }); - }); - }); - - describe('forgot password token handling', () => { - let forgotPasswordTokenData; - beforeEach(() => { - forgotPasswordTokenData = makeMockForgotPasswordToken(accountData.uid); - return db.createPasswordForgotToken( - forgotPasswordTokenData.tokenId, - forgotPasswordTokenData - ); - }); - - it('should have created password forgot token', () => { - return db - .passwordForgotToken(forgotPasswordTokenData.tokenId) - .then((token) => { - assert.deepEqual( - token.tokenData, - forgotPasswordTokenData.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - forgotPasswordTokenData.createdAt, - 'createdAt same' - ); - assert.deepEqual( - token.passCode, - forgotPasswordTokenData.passCode, - 'token passCode same' - ); - assert.equal( - token.tries, - forgotPasswordTokenData.tries, - 'Tries is correct' - ); - assert.equal( - token.email, - accountData.email, - 'token email same as account email' - ); - assert.equal( - token.verifierSetAt, - accountData.verifierSetAt, - 'verifierSetAt is set correctly' - ); - }); - }); - - it('should update password forgot token tries', () => { - forgotPasswordTokenData.tries = 9; - return db - .updatePasswordForgotToken( - forgotPasswordTokenData.tokenId, - forgotPasswordTokenData - ) - .then((result) => { - assert.deepEqual( - result, - {}, - 'The returned object from the token update is empty' - ); - return db.passwordForgotToken(forgotPasswordTokenData.tokenId); - }) - .then((token) => { - assert.equal(token.tries, 9, 'token now has had 9 tries'); - }); - }); - - it('should delete password forgot token', () => { - return db - .deletePasswordForgotToken(forgotPasswordTokenData.tokenId) - .then((result) => { - assert.deepEqual( - result, - {}, - 'The returned object from the token delete is empty' - ); - return db - .passwordForgotToken(forgotPasswordTokenData.tokenId) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - }); - }); - }); - }); - - describe('change password token handling', () => { - let changePasswordTokenData; - beforeEach(() => { - changePasswordTokenData = makeMockChangePasswordToken(accountData.uid); - - return db.createPasswordChangeToken( - changePasswordTokenData.tokenId, - changePasswordTokenData - ); - }); - - it('should have created password change token', () => { - return db - .passwordChangeToken(changePasswordTokenData.tokenId) - .then((token) => { - assert.equal( - token.hasOwnProperty('tokenId'), - false, - 'tokenId is not returned' - ); - assert.deepEqual( - token.tokenData, - changePasswordTokenData.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - changePasswordTokenData.createdAt, - 'createdAt is correct' - ); - assert.equal( - token.verifierSetAt, - accountData.verifierSetAt, - 'verifierSetAt is set correctly' - ); - }); - }); - - it('should override change password token when creating with same uid', () => { - const anotherChangePasswordTokenData = makeMockChangePasswordToken( - accountData.uid - ); - return db - .createPasswordChangeToken( - anotherChangePasswordTokenData.tokenId, - anotherChangePasswordTokenData - ) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on change password token creation' - ); - - // Fails to retrieve original change token since it was over written - return db - .passwordChangeToken(changePasswordTokenData.tokenId) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - return db.passwordChangeToken( - anotherChangePasswordTokenData.tokenId - ); - }); - }) - .then((token) => { - assert.deepEqual( - token.tokenData, - anotherChangePasswordTokenData.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - anotherChangePasswordTokenData.createdAt, - 'createdAt is correct' - ); - assert.equal( - token.verifierSetAt, - accountData.verifierSetAt, - 'verifierSetAt is set correctly' - ); - }); - }); - - it('should have deleted token', () => { - return db - .deletePasswordChangeToken( - changePasswordTokenData.tokenId, - changePasswordTokenData - ) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on forgot password change deletion' - ); - return db - .passwordChangeToken(changePasswordTokenData.tokenId) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - }); - }); - }); - }); - - it('should verify account email with legacy db.verifyEmail', () => { - return db - .emailRecord(accountData.emailBuffer) - .then((emailRecord) => { - assert.equal( - emailRecord.emailVerified, - false, - 'account should be emailVerified false' - ); - assert.equal( - emailRecord.emailVerified, - 0, - 'account should be emailVerified (0)' - ); - return db.verifyEmail(emailRecord.uid, emailRecord.emailCode); - }) - .then(function (result) { - assert.deepEqual( - result, - {}, - 'Returned an empty object email verification' - ); - return db.accountEmails(accountData.uid); - }) - .then(function (emails) { - assert.lengthOf(emails, 1); - assert.equal(!!emails[0].isVerified, true, 'email is verified'); - assert.notEqual(emails[0].verifiedAt, null); - assert.isAbove(emails[0].verifiedAt, emails[0].createdAt); - assert.equal(!!emails[0].isPrimary, true, 'email is primary'); - return db.account(accountData.uid); - }) - .then(function (account) { - assert( - account.emailVerified, - 'account should now be emailVerified (truthy)' - ); - assert.equal( - account.emailVerified, - 1, - 'account should now be emailVerified (1)' - ); - assert.isAbove(account.profileChangedAt, account.createdAt); - }); - }); - - it('should change account locale', () => { - return db - .account(accountData.uid) - .then((account) => { - assert.equal(account.locale, 'en_US', 'correct locale set'); - accountData.locale = 'en_NZ'; - return db.updateLocale(accountData.uid, accountData); - }) - .then(function (result) { - assert.deepEqual( - result, - {}, - 'Returned an empty object on locale update' - ); - return db.account(accountData.uid); - }) - .then(function (account) { - assert.equal( - account.locale, - 'en_NZ', - 'account should now have new locale' - ); - }); - }); - - describe('account reset token handling', () => { - let accountResetTokenData, forgotPasswordTokenData; - beforeEach(() => { - forgotPasswordTokenData = makeMockForgotPasswordToken(accountData.uid); - accountResetTokenData = makeMockAccountResetToken(accountData.uid); - return db - .createPasswordForgotToken( - forgotPasswordTokenData.tokenId, - forgotPasswordTokenData - ) - .then(function () { - return db.forgotPasswordVerified( - forgotPasswordTokenData.tokenId, - accountResetTokenData - ); - }); - }); - - it('db.accountResetToken should create token', () => { - return db - .accountResetToken(accountResetTokenData.tokenId) - .then((token) => { - assert.isFalse(token.hasOwnProperty('tokenId')); - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.deepEqual( - token.tokenData, - accountResetTokenData.data, - 'token data matches' - ); - assert.equal( - token.createdAt, - accountResetTokenData.createdAt, - 'createdAt is correct' - ); - assert( - token.verifierSetAt, - 'verifierSetAt is set to a truthy value' - ); - }); - }); - - it('db.deleteAccountResetToken should delete token', () => { - return db - .deleteAccountResetToken(accountResetTokenData.tokenId) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on account reset deletion' - ); - return db - .accountResetToken(accountResetTokenData.tokenId) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - }); - }); - }); - }); - - describe('db.forgotPasswordVerified', () => { - let forgotPasswordTokenData, - anotherForgotPasswordTokenData, - accountResetTokenData, - anotherAccountResetTokenData; - beforeEach(() => { - forgotPasswordTokenData = makeMockForgotPasswordToken(accountData.uid); - accountResetTokenData = makeMockAccountResetToken( - accountData.uid, - forgotPasswordTokenData.tokenId - ); - anotherForgotPasswordTokenData = makeMockForgotPasswordToken( - accountData.uid - ); - anotherAccountResetTokenData = makeMockAccountResetToken( - accountData.uid, - anotherForgotPasswordTokenData.tokenId - ); - - return db.createPasswordForgotToken( - anotherForgotPasswordTokenData.tokenId, - anotherForgotPasswordTokenData - ); - }); - - it('should override accountResetToken when calling `db.forgotPasswordVerified`', () => { - return db - .forgotPasswordVerified( - anotherForgotPasswordTokenData.tokenId, - anotherAccountResetTokenData - ) - .then( - () => db.accountResetToken(anotherAccountResetTokenData.tokenId), - assert.fail - ) - .then((token) => { - // check a couple of fields - assert.deepEqual( - token.uid, - accountData.uid, - 'token belongs to this account' - ); - assert.deepEqual( - token.tokenData, - anotherAccountResetTokenData.data, - 'token data matches' - ); - assert.equal( - token.createdAt, - anotherAccountResetTokenData.createdAt, - 'createdAt is correct' - ); - assert( - token.verifierSetAt, - 'verifierSetAt is set to a truthy value' - ); - - return db.createPasswordForgotToken( - forgotPasswordTokenData.tokenId, - forgotPasswordTokenData - ); - }) - .then( - () => - db.forgotPasswordVerified( - forgotPasswordTokenData.tokenId, - forgotPasswordTokenData - ), - assert.fail - ) - .then(() => - db.accountResetToken(anotherAccountResetTokenData.tokenId) - ) - .then(assert.fail, (err) => { - // throw away accountResetToken (shouldn't exist any longer) - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - - return db.passwordForgotToken(forgotPasswordTokenData.tokenId); - }) - .then(assert.fail, (err) => { - // throw away passwordForgotToken (shouldn't exist any longer) - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - - return db.deleteAccountResetToken(accountResetTokenData.tokenId); - }) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on account reset deletion' - ); - return db.accountResetToken(accountResetTokenData.tokenId); - }) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'err.errno is correct'); - assert.equal(err.code, 404, 'err.code is correct'); - }); - }); - }); - - describe('db.deviceFromTokenVerificationId', () => { - let sessionTokenData; - beforeEach(() => { - sessionTokenData = makeMockSessionToken(accountData.uid); - return db.createSessionToken( - sessionTokenData.tokenId, - sessionTokenData - ); - }); - - it('should fail for non-existing session', () => { - return db - .deviceFromTokenVerificationId(accountData.uid, hex16()) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - - it('should fail for session with no device', () => { - return db - .deviceFromTokenVerificationId( - accountData.uid, - sessionTokenData.tokenVerificationId - ) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - - it('should return device', () => { - const deviceData = makeMockDevice(sessionTokenData.tokenId); - return db - .createDevice(accountData.uid, deviceData.deviceId, deviceData) - .then(() => { - return db.deviceFromTokenVerificationId( - accountData.uid, - sessionTokenData.tokenVerificationId - ); - }) - .then((sessionDeviceInfo) => { - assert.deepEqual( - sessionDeviceInfo.id, - deviceData.deviceId, - 'We found our device id back' - ); - assert.equal( - sessionDeviceInfo.name, - deviceData.name, - 'We found our device name back' - ); - }); - }); - }); - - describe('db.accountDevices', () => { - let sessionDeviceInfo, - oauthDeviceInfo, - sessionTokenData, - refreshTokenData; - - // A little helper function for finding a specific device record in a list. - function matchById(field, value) { - return (d) => d[field] && d[field].equals(value); - } - - beforeEach(() => { - sessionTokenData = makeMockSessionToken(accountData.uid); - refreshTokenData = makeMockRefreshToken(accountData.uid); - - sessionDeviceInfo = makeMockDevice(sessionTokenData.tokenId); - oauthDeviceInfo = makeMockOAuthDevice(refreshTokenData.tokenId); - - return db - .createSessionToken(sessionTokenData.tokenId, sessionTokenData) - .then(() => - db.createDevice( - accountData.uid, - sessionDeviceInfo.deviceId, - sessionDeviceInfo - ) - ) - .then((result) => { - return assert.deepEqual(result, {}, 'returned empty object'); - }) - .then(() => - db.createDevice( - accountData.uid, - oauthDeviceInfo.deviceId, - oauthDeviceInfo - ) - ) - .then((result) => { - return assert.deepEqual(result, {}, 'returned empty object'); - }); - }); - - it('should have created devices', () => { - return P.resolve() - .then(() => { - return db.device(sessionTokenData.uid, sessionDeviceInfo.deviceId); - }) - .then((d) => { - assert.deepEqual(d.uid, sessionTokenData.uid, 'uid'); - assert.deepEqual(d.id, sessionDeviceInfo.deviceId, 'id'); - assert.equal(d.name, sessionDeviceInfo.name, 'name'); - assert.equal(d.type, sessionDeviceInfo.type, 'type'); - assert.equal(d.createdAt, sessionDeviceInfo.createdAt, 'createdAt'); - assert.equal( - d.callbackURL, - sessionDeviceInfo.callbackURL, - 'callbackURL' - ); - assert.equal( - d.callbackPublicKey, - sessionDeviceInfo.callbackPublicKey, - 'callbackPublicKey' - ); - assert.equal( - d.callbackAuthKey, - sessionDeviceInfo.callbackAuthKey, - 'callbackAuthKey' - ); - assert.equal( - d.callbackIsExpired, - sessionDeviceInfo.callbackIsExpired, - 'callbackIsExpired' - ); - assert.deepEqual( - d.availableCommands, - sessionDeviceInfo.availableCommands, - 'availableCommands' - ); - }) - .then(() => { - return db.device(refreshTokenData.uid, oauthDeviceInfo.deviceId); - }) - .then((d) => { - assert.deepEqual(d.uid, refreshTokenData.uid, 'uid'); - assert.deepEqual(d.id, oauthDeviceInfo.deviceId, 'id'); - assert.equal(d.name, oauthDeviceInfo.name, 'name'); - assert.equal(d.type, oauthDeviceInfo.type, 'type'); - assert.equal(d.createdAt, oauthDeviceInfo.createdAt, 'createdAt'); - assert.equal( - d.callbackURL, - oauthDeviceInfo.callbackURL, - 'callbackURL' - ); - assert.equal( - d.callbackPublicKey, - oauthDeviceInfo.callbackPublicKey, - 'callbackPublicKey' - ); - assert.equal( - d.callbackAuthKey, - oauthDeviceInfo.callbackAuthKey, - 'callbackAuthKey' - ); - assert.equal( - d.callbackIsExpired, - oauthDeviceInfo.callbackIsExpired, - 'callbackIsExpired' - ); - assert.deepEqual( - d.availableCommands, - oauthDeviceInfo.availableCommands, - 'availableCommands' - ); - }); - }); - - it('should have linked one device to session token', () => { - return db.sessionToken(sessionTokenData.tokenId).then((s) => { - assert.deepEqual(s.deviceId, sessionDeviceInfo.deviceId, 'id'); - assert.deepEqual(s.uid, sessionTokenData.uid, 'uid'); - assert.equal(s.deviceName, sessionDeviceInfo.name, 'name'); - assert.equal(s.deviceType, sessionDeviceInfo.type, 'type'); - assert.equal( - s.deviceCreatedAt, - sessionDeviceInfo.createdAt, - 'createdAt' - ); - assert.equal( - s.deviceCallbackURL, - sessionDeviceInfo.callbackURL, - 'callbackURL' - ); - assert.equal( - s.deviceCallbackPublicKey, - sessionDeviceInfo.callbackPublicKey, - 'callbackPublicKey' - ); - assert.equal( - s.deviceCallbackAuthKey, - sessionDeviceInfo.callbackAuthKey, - 'callbackAuthKey' - ); - assert.equal( - s.deviceCallbackIsExpired, - sessionDeviceInfo.callbackIsExpired, - 'callbackIsExpired' - ); - assert.deepEqual( - s.deviceAvailableCommands, - sessionDeviceInfo.availableCommands, - 'availableCommands' - ); - assert.equal( - !!s.mustVerify, - !!sessionTokenData.mustVerify, - 'mustVerify is correct' - ); - assert.deepEqual( - s.tokenVerificationId, - sessionTokenData.tokenVerificationId, - 'tokenVerificationId is correct' - ); - }); - }); - - it('should get all devices', () => { - return db.accountDevices(accountData.uid).then((devices) => { - assert.lengthOf(devices, 2); - - let device = devices.find( - matchById('sessionTokenId', sessionTokenData.tokenId) - ); - assert.deepEqual( - device.sessionTokenId, - sessionTokenData.tokenId, - 'sessionTokenId' - ); - assert.isNull(device.refreshTokenId); - assert.equal(device.name, sessionDeviceInfo.name, 'name'); - assert.deepEqual(device.id, sessionDeviceInfo.deviceId, 'id'); - assert.equal( - device.createdAt, - sessionDeviceInfo.createdAt, - 'createdAt' - ); - assert.equal(device.type, sessionDeviceInfo.type, 'type'); - assert.equal( - device.callbackURL, - sessionDeviceInfo.callbackURL, - 'callbackURL' - ); - assert.equal( - device.callbackPublicKey, - sessionDeviceInfo.callbackPublicKey, - 'callbackPublicKey' - ); - assert.equal( - device.callbackAuthKey, - sessionDeviceInfo.callbackAuthKey, - 'callbackAuthKey' - ); - assert.equal( - device.callbackIsExpired, - sessionDeviceInfo.callbackIsExpired, - 'callbackIsExpired' - ); - assert.deepEqual( - device.availableCommands, - sessionDeviceInfo.availableCommands, - 'availableCommands' - ); - assert.isAbove(device.lastAccessTime, 0); - - device = devices.find( - matchById('refreshTokenId', refreshTokenData.tokenId) - ); - assert.isNull(device.sessionTokenId); - assert.deepEqual( - device.refreshTokenId, - refreshTokenData.tokenId, - 'refreshTokenId' - ); - assert.equal(device.name, oauthDeviceInfo.name, 'name'); - assert.deepEqual(device.id, oauthDeviceInfo.deviceId, 'id'); - assert.equal( - device.createdAt, - oauthDeviceInfo.createdAt, - 'createdAt' - ); - assert.equal(device.type, oauthDeviceInfo.type, 'type'); - assert.equal( - device.callbackURL, - oauthDeviceInfo.callbackURL, - 'callbackURL' - ); - assert.equal( - device.callbackPublicKey, - oauthDeviceInfo.callbackPublicKey, - 'callbackPublicKey' - ); - assert.equal( - device.callbackAuthKey, - oauthDeviceInfo.callbackAuthKey, - 'callbackAuthKey' - ); - assert.equal( - device.callbackIsExpired, - oauthDeviceInfo.callbackIsExpired, - 'callbackIsExpired' - ); - assert.deepEqual( - device.availableCommands, - oauthDeviceInfo.availableCommands, - 'availableCommands' - ); - assert.isNull(device.lastAccessTime); - }); - }); - - it('should update device by sessionTokenId', () => { - sessionDeviceInfo.name = 'New New Device'; - sessionDeviceInfo.type = 'desktop'; - sessionDeviceInfo.callbackURL = ''; - sessionDeviceInfo.callbackPublicKey = ''; - sessionDeviceInfo.callbackAuthKey = ''; - sessionDeviceInfo.callbackIsExpired = true; - sessionDeviceInfo.availableCommands = {}; - - const newSessionTokenData = makeMockSessionToken(accountData.uid); - sessionDeviceInfo.sessionTokenId = newSessionTokenData.tokenId; - - return db - .createSessionToken(newSessionTokenData.tokenId, newSessionTokenData) - .then(() => { - return db.updateDevice( - accountData.uid, - sessionDeviceInfo.deviceId, - sessionDeviceInfo - ); - }) - .then((result) => { - assert.deepEqual(result, {}, 'returned empty object'); - return db.accountDevices(accountData.uid); - }) - .then((devices) => { - assert.lengthOf(devices, 2); - const device = devices.find( - matchById('sessionTokenId', newSessionTokenData.tokenId) - ); - assert.ok(device, 'device found under new token id'); - assert.equal(device.name, 'New New Device', 'name updated'); - assert.equal(device.type, 'desktop', 'type unchanged'); - assert.equal(device.callbackURL, '', 'callbackURL unchanged'); - assert.equal( - device.callbackPublicKey, - '', - 'callbackPublicKey unchanged' - ); - assert.equal( - device.callbackAuthKey, - '', - 'callbackAuthKey unchanged' - ); - assert.equal( - device.callbackIsExpired, - true, - 'callbackIsExpired unchanged' - ); - assert.deepEqual( - device.availableCommands, - {}, - 'availableCommands updated' - ); - }); - }); - - it('should update device by refreshTokenId', () => { - oauthDeviceInfo.name = 'New New Device'; - oauthDeviceInfo.type = 'desktop'; - oauthDeviceInfo.callbackURL = ''; - oauthDeviceInfo.callbackPublicKey = ''; - oauthDeviceInfo.callbackAuthKey = ''; - oauthDeviceInfo.callbackIsExpired = true; - oauthDeviceInfo.availableCommands = {}; - - const newRefreshTokenData = makeMockRefreshToken(accountData.uid); - oauthDeviceInfo.refreshTokenId = newRefreshTokenData.tokenId; - - return db - .updateDevice( - accountData.uid, - oauthDeviceInfo.deviceId, - oauthDeviceInfo - ) - .then((result) => { - assert.deepEqual(result, {}, 'returned empty object'); - return db.accountDevices(accountData.uid); - }) - .then((devices) => { - assert.lengthOf(devices, 2); - const device = devices.find( - matchById('refreshTokenId', newRefreshTokenData.tokenId) - ); - assert.ok(device, 'device found under new token id'); - assert.equal(device.name, 'New New Device', 'name updated'); - assert.equal(device.type, 'desktop', 'type unchanged'); - assert.equal(device.callbackURL, '', 'callbackURL unchanged'); - assert.equal( - device.callbackPublicKey, - '', - 'callbackPublicKey unchanged' - ); - assert.equal( - device.callbackAuthKey, - '', - 'callbackAuthKey unchanged' - ); - assert.equal( - device.callbackIsExpired, - true, - 'callbackIsExpired unchanged' - ); - assert.deepEqual( - device.availableCommands, - {}, - 'availableCommands updated' - ); - }); - }); - - it('should fail to return zombie session', () => { - // zombie devices don't have an associated session - sessionDeviceInfo.sessionTokenId = hex16(); - return db - .updateDevice( - accountData.uid, - sessionDeviceInfo.deviceId, - sessionDeviceInfo - ) - .then((result) => { - assert.deepEqual(result, {}, 'returned empty object'); - return db.accountDevices(accountData.uid); - }) - .then((devices) => { - assert.lengthOf(devices, 1); - assert.isNull(devices[0].sessionTokenId); - assert.deepEqual( - devices[0].refreshTokenId, - refreshTokenData.tokenId, - 'refreshTokenId' - ); - }); - }); - - it('should fail to add multiple devices to session', () => { - const anotherDevice = makeMockDevice(sessionTokenData.tokenId); - return db - .createDevice(accountData.uid, anotherDevice.deviceId, anotherDevice) - .then(assert.fail, (err) => { - assert.equal(err.code, 409, 'err.code'); - assert.equal(err.errno, 101, 'err.errno'); - }); - }); - - it('should fail to add multiple devices to refreshToken', () => { - const anotherDevice = makeMockOAuthDevice(refreshTokenData.tokenId); - return db - .createDevice(accountData.uid, anotherDevice.deviceId, anotherDevice) - .then(assert.fail, (err) => { - assert.equal(err.code, 409, 'err.code'); - assert.equal(err.errno, 101, 'err.errno'); - }); - }); - - it('can associate a device record with both sessionToken and refreshToken', () => { - const anotherRefreshToken = makeMockRefreshToken(accountData.uid); - sessionDeviceInfo.refreshTokenId = anotherRefreshToken.tokenId; - return db - .updateDevice( - accountData.uid, - sessionDeviceInfo.deviceId, - sessionDeviceInfo - ) - .then(() => { - return db.accountDevices(accountData.uid); - }) - .then((devices) => { - assert.lengthOf(devices, 2); - const comboDeviceInfo = devices.find( - matchById('sessionTokenId', sessionTokenData.tokenId) - ); - assert.ok(comboDeviceInfo, 'found device record'); - assert.deepEqual( - comboDeviceInfo.refreshTokenId, - anotherRefreshToken.tokenId - ); - }); - }); - - it('should fail to update non-existent device', () => { - return db - .updateDevice(accountData.uid, hex16(), sessionDeviceInfo) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - - it('availableCommands are not cleared if not specified', () => { - const newDevice = Object.assign({}, sessionDeviceInfo); - delete newDevice.availableCommands; - return db - .updateDevice(accountData.uid, sessionDeviceInfo.deviceId, newDevice) - .then(() => { - return db.device(accountData.uid, sessionDeviceInfo.deviceId); - }) - .then((device) => - assert.deepEqual(device.availableCommands, { - 'https://identity.mozilla.com/cmd/display-uri': 'metadata-bundle', - }) - ); - }); - - it('availableCommands are overwritten on update', () => { - const newDevice = Object.assign({}, sessionDeviceInfo, { - availableCommands: { - foo: 'bar', - second: 'command', - }, - }); - return db - .updateDevice(accountData.uid, sessionDeviceInfo.deviceId, newDevice) - .then(() => { - return db.device(accountData.uid, sessionDeviceInfo.deviceId); - }) - .then((device) => - assert.deepEqual(device.availableCommands, { - foo: 'bar', - second: 'command', - }) - ); - }); - - it('availableCommands can update metadata on an existing command', () => { - const newDevice = Object.assign({}, sessionDeviceInfo, { - availableCommands: { - 'https://identity.mozilla.com/cmd/display-uri': 'new-metadata', - }, - }); - return db - .updateDevice(accountData.uid, sessionDeviceInfo.deviceId, newDevice) - .then(() => { - return db.device(accountData.uid, sessionDeviceInfo.deviceId); - }) - .then((device) => - assert.deepEqual(device.availableCommands, { - 'https://identity.mozilla.com/cmd/display-uri': 'new-metadata', - }) - ); - }); - - it('should fail to delete non-existent device', () => { - return db - .deleteDevice(accountData.uid, hex16()) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - - it('should correctly handle multiple devices with different availableCommands maps', () => { - const sessionToken2 = makeMockSessionToken(accountData.uid); - const sessionDeviceInfo2 = Object.assign( - makeMockDevice(sessionToken2.tokenId), - { - availableCommands: { - 'https://identity.mozilla.com/cmd/display-uri': - 'device-two-metadata', - 'extra-command': 'extra-data', - }, - } - ); - const sessionToken3 = makeMockSessionToken(accountData.uid); - const sessionDeviceInfo3 = Object.assign( - makeMockDevice(sessionToken3.tokenId), - { - availableCommands: {}, - } - ); - - return db - .createSessionToken(sessionToken2.tokenId, sessionToken2) - .then(() => - db.createDevice( - accountData.uid, - sessionDeviceInfo2.deviceId, - sessionDeviceInfo2 - ) - ) - .then(() => - db.createSessionToken(sessionToken3.tokenId, sessionToken3) - ) - .then(() => - db.createDevice( - accountData.uid, - sessionDeviceInfo3.deviceId, - sessionDeviceInfo3 - ) - ) - .then(() => db.accountDevices(accountData.uid)) - .then((devices) => { - assert.lengthOf(devices, 4); - - const device1 = devices.find( - matchById('sessionTokenId', sessionTokenData.tokenId) - ); - assert.ok(device1, 'found first device'); - assert.deepEqual( - device1.availableCommands, - sessionDeviceInfo.availableCommands, - 'device1 availableCommands' - ); - - const device2 = devices.find( - matchById('sessionTokenId', sessionToken2.tokenId) - ); - assert.ok(device2, 'found second device'); - assert.deepEqual( - device2.availableCommands, - sessionDeviceInfo2.availableCommands, - 'device2 availableCommands' - ); - - const device3 = devices.find( - matchById('sessionTokenId', sessionToken3.tokenId) - ); - assert.ok(device3, 'found third device'); - assert.deepEqual( - device3.availableCommands, - sessionDeviceInfo3.availableCommands, - 'device3 availableCommands' - ); - - const device4 = devices.find( - matchById('refreshTokenId', refreshTokenData.tokenId) - ); - assert.ok(device4, 'found fourth device'); - assert.deepEqual( - device4.availableCommands, - oauthDeviceInfo.availableCommands, - 'device4 availableCommands' - ); - }); - }); - - it('should correctly handle multiple sessions with different availableCommands maps', () => { - const sessionToken2 = makeMockSessionToken(accountData.uid); - const sessionDeviceInfo2 = Object.assign( - makeMockDevice(sessionToken2.tokenId), - { - availableCommands: { - 'https://identity.mozilla.com/cmd/display-uri': - 'device-two-metadata', - 'extra-command': 'extra-data', - }, - } - ); - const sessionToken3 = makeMockSessionToken(accountData.uid); - - return db - .createSessionToken(sessionToken2.tokenId, sessionToken2) - .then(() => - db.createDevice( - accountData.uid, - sessionDeviceInfo2.deviceId, - sessionDeviceInfo2 - ) - ) - .then(() => - db.createSessionToken(sessionToken3.tokenId, sessionToken3) - ) - .then(() => db.sessions(accountData.uid)) - .then((sessions) => { - assert.lengthOf(sessions, 3); - - const session1 = sessions.find((s) => - s.tokenId.equals(sessionTokenData.tokenId) - ); - assert.ok(session1, 'found first session'); - assert.deepEqual( - session1.deviceAvailableCommands, - sessionDeviceInfo.availableCommands, - 'session1 availableCommands' - ); - - const session2 = sessions.find((s) => - s.tokenId.equals(sessionToken2.tokenId) - ); - assert.ok(session2, 'found second session'); - assert.deepEqual( - session2.deviceAvailableCommands, - sessionDeviceInfo2.availableCommands, - 'session2 availableCommands' - ); - - const session3 = sessions.find((s) => - s.tokenId.equals(sessionToken3.tokenId) - ); - assert.ok(session3, 'found third session'); - assert.deepEqual(session3.deviceId, null, 'session3 deviceId'); - assert.deepEqual( - session3.deviceAvailableCommands, - null, - 'session3 availableCommands' - ); - }); - }); - - it('should delete session when device is deleted', () => { - return db - .deleteDevice(accountData.uid, sessionDeviceInfo.deviceId) - .then((result) => { - assert.deepEqual(result, { - sessionTokenId: sessionTokenData.tokenId, - refreshTokenId: null, - }); - - // Fetch all of the devices for the account - return db.accountDevices(accountData.uid); - }) - .then((devices) => assert.lengthOf(devices, 1)) - .then(() => db.sessionToken(sessionTokenData.tokenId)) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - - it('should return refreshTokenId when device is deleted, so that calling code can delete it', () => { - return db - .deleteDevice(accountData.uid, oauthDeviceInfo.deviceId) - .then((result) => { - assert.deepEqual(result, { - sessionTokenId: null, - refreshTokenId: refreshTokenData.tokenId, - }); - - // Fetch all of the devices for the account - return db.accountDevices(accountData.uid); - }) - .then((devices) => assert.lengthOf(devices, 1)); - }); - }); - - describe('db.resetAccount', () => { - let passwordForgotTokenData, sessionTokenData, sessionDeviceInfo; - beforeEach(() => { - sessionTokenData = makeMockSessionToken(accountData.uid, true); - passwordForgotTokenData = makeMockForgotPasswordToken(accountData.uid); - sessionDeviceInfo = makeMockDevice(sessionTokenData.tokenId); - - return db - .createSessionToken(sessionTokenData.tokenId, sessionTokenData) - .then(() => - db.createDevice( - accountData.uid, - sessionDeviceInfo.deviceId, - sessionDeviceInfo - ) - ) - .then(() => - db.createPasswordForgotToken( - passwordForgotTokenData.tokenId, - passwordForgotTokenData - ) - ); - }); - - it('should verify account upon forgot token creation', () => { - return db - .accountEmails(accountData.uid) - .then((emails) => { - // Account should be unverified - assert.lengthOf(emails, 1); - assert.equal( - !!emails[0].isVerified, - false, - 'email is not verified' - ); - assert.equal(!!emails[0].isPrimary, true, 'email is primary'); - - return db.forgotPasswordVerified( - passwordForgotTokenData.tokenId, - passwordForgotTokenData - ); - }) - .then(() => db.accountEmails(accountData.uid)) - .then((emails) => { - // Account should be verified - assert.lengthOf(emails, 1); - assert.equal(!!emails[0].isVerified, true, 'email is verified'); - assert.notEqual(emails[0].verifiedAt, null); - assert.isAbove(emails[0].verifiedAt, emails[0].createdAt); - assert.equal(!!emails[0].isPrimary, true, 'email is primary'); - }); - }); - - it('should remove devices after account reset', () => { - return db - .accountDevices(accountData.uid) - .then((devices) => { - assert.lengthOf(devices, 1); - return db.resetAccount(accountData.uid, accountData); - }) - .then(() => db.accountDevices(accountData.uid)) - .then((devices) => { - assert.lengthOf(devices, 0); - }); - }); - - it('should remove session after account reset', () => { - return db - .resetAccount(accountData.uid, accountData) - .then(() => db.sessionToken(sessionTokenData.tokenId)) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - - it('should still retrieve account after account reset', () => { - return db - .account(accountData.uid) - .then((account) => { - assert.ok(account, 'account exists'); - accountData.verifierSetAt = now + 1; - return db.resetAccount(accountData.uid, accountData); - }) - .then(() => db.account(accountData.uid)) - .then((account) => { - assert.ok(account, 'account exists'); - assert.equal( - account.profileChangedAt, - account.verifierSetAt, - 'profileChangedAt matches verifierSetAt' - ); - assert.isNull(account.lockedAt); - }); - }); - - it('should track keysChangedAt independently of verifierSetAt', async () => { - const account1 = await db.account(accountData.uid); - assert.equal(account1.verifierSetAt, account1.keysChangedAt); - - // eslint-disable-next-line require-atomic-updates - accountData.verifierSetAt = now + 1; - // eslint-disable-next-line require-atomic-updates - accountData.keysHaveChanged = false; - await db.resetAccount(accountData.uid, accountData); - const account2 = await db.account(accountData.uid); - assert.notEqual(account1.verifierSetAt, account2.verifierSetAt); - assert.equal(account1.keysChangedAt, account2.keysChangedAt); - - // eslint-disable-next-line require-atomic-updates - accountData.verifierSetAt = now + 2; - // eslint-disable-next-line require-atomic-updates - accountData.keysHaveChanged = true; - await db.resetAccount(accountData.uid, accountData); - const account3 = await db.account(accountData.uid); - assert.notEqual(account2.verifierSetAt, account3.verifierSetAt); - assert.notEqual(account2.keysChangedAt, account3.keysChangedAt); - assert.equal(account3.verifierSetAt, now + 2); - assert.equal(account3.keysChangedAt, now + 2); - - delete accountData.keysHaveChanged; - }); - }); - - describe('db.securityEvents', () => { - let session1, session2, session3, uid1, uid2; - const evA = 'account.login', - evB = 'account.create', - evC = 'account.reset'; - const addr1 = '127.0.0.1', - addr2 = '::127.0.0.2'; - - function insert(uid, addr, name, session) { - return db.createSecurityEvent({ - uid: uid, - ipAddr: addr, - name: name, - tokenId: session, - }); - } - - beforeEach(() => { - session1 = makeMockSessionToken(accountData.uid); - session2 = makeMockSessionToken(accountData.uid); - // Make session verified - delete session2.tokenVerificationId; - - session3 = makeMockSessionToken(accountData.uid); - - uid1 = accountData.uid; - uid2 = newUuid(); - - return ( - P.all([ - db.createSessionToken(session1.tokenId, session1), - db.createSessionToken(session2.tokenId, session2), - db.createSessionToken(session3.tokenId, session3), - ]) - // Don't parallelize these, the order of them matters - // because they record timestamps in the db. - .then(() => insert(uid1, addr1, evA, session2.tokenId).delay(10)) - .then(() => insert(uid1, addr1, evB, session1.tokenId).delay(10)) - .then(() => insert(uid1, addr1, evC).delay(10)) - .then(() => insert(uid1, addr2, evA, session3.tokenId).delay(10)) - .then(() => insert(uid2, addr1, evA, hex32())) - ); - }); - - it('should get security event', () => { - return db - .securityEvents({ id: uid1, ipAddr: addr1 }) - .then((results) => { - assert.lengthOf(results, 3); - // The most recent event is returned first. - assert.equal(results[0].name, evC, 'correct event name'); - assert.equal( - !!results[0].verified, - true, - 'event without a session is already verified' - ); - assert.isBelow(results[0].createdAt, Date.now()); - assert.equal(results[1].name, evB, 'correct event name'); - assert.equal( - !!results[1].verified, - false, - 'second session is not verified yet' - ); - assert.isBelow(results[1].createdAt, results[0].createdAt); - assert.equal(results[2].name, evA, 'correct event name'); - assert.equal( - !!results[2].verified, - true, - 'first session is already verified' - ); - assert.isBelow(results[2].createdAt, results[1].createdAt); - }); - }); - - it('should get event after session verified', () => { - return db - .verifyTokens(session1.tokenVerificationId, { uid: uid1 }) - .then(() => db.securityEvents({ id: uid1, ipAddr: addr1 })) - .then((results) => { - assert.lengthOf(results, 3); - assert.isTrue(!!results[0].verified); - assert.isTrue(!!results[1].verified); - assert.isTrue(!!results[2].verified); - }); - }); - - it('should get second address', () => { - return db - .securityEvents({ id: uid1, ipAddr: addr2 }) - .then((results) => { - assert.lengthOf(results, 1); - assert.equal(results[0].name, evA); - assert.isFalse(!!results[0].verified); - }); - }); - - it('should get second addr after deleting unverified session', () => { - return db - .deleteSessionToken(session3.tokenId) - .then(() => db.securityEvents({ id: uid1, ipAddr: addr2 })) - .then((results) => { - assert.lengthOf(results, 1); - assert.equal(results[0].name, evA); - assert.isFalse(!!results[0].verified); - }); - }); - - it('should get with IPv6', () => { - return db - .securityEvents({ id: uid1, ipAddr: '::' + addr1 }) - .then((results) => assert.lengthOf(results, 3)); - }); - - it('should fail with unknown uid', () => { - return db - .securityEvents({ id: newUuid(), ipAddr: addr1 }) - .then((results) => assert.lengthOf(results, 0)); - }); - - it('should delete events when account is deleted', () => { - return db - .deleteAccount(accountData.uid) - .then(() => db.securityEvents({ id: uid1, ipAddr: addr1 })) - .then((res) => { - assert.lengthOf(res, 0); - }); - }); - }); - - describe('db.securityEventsByUid', () => { - let session1, session2, session3, anotherAccountSession, uid, anotherUid; - const evA = 'account.login', - evB = 'account.create', - evC = 'account.reset'; - const addr = '127.0.0.1'; - - function insert(uid, addr, name, session) { - return db.createSecurityEvent({ - uid: uid, - ipAddr: addr, - name: name, - tokenId: session, - }); - } - - beforeEach(() => { - const anotherAccountData = createAccount(); - - session1 = makeMockSessionToken(accountData.uid); - session2 = makeMockSessionToken(accountData.uid); - // Make session verified - delete session2.tokenVerificationId; - - session3 = makeMockSessionToken(accountData.uid); - anotherAccountSession = makeMockSessionToken(anotherAccountData.uid); - - uid = accountData.uid; - anotherUid = anotherAccountData.uid; - - return ( - P.all([ - db.createSessionToken(session1.tokenId, session1), - db.createSessionToken(session2.tokenId, session2), - db.createSessionToken(session3.tokenId, session3), - db.createSessionToken( - anotherAccountSession.tokenId, - anotherAccountSession - ), - ]) - // Don't parallelize these, the order of them matters - // because they record timestamps in the db. - .then(() => insert(uid, addr, evA, session2.tokenId).delay(10)) - .then(() => insert(uid, addr, evB, session1.tokenId).delay(10)) - .then(() => insert(uid, addr, evC).delay(10)) - .then(() => - insert( - anotherUid, - addr, - evA, - anotherAccountSession.tokenId - ).delay(10) - ) - - // create an account in db with anotherAccountData - .then(() => - db.createAccount(anotherAccountData.uid, anotherAccountData) - ) - ); - }); - - it('should get security event', () => { - return db.securityEventsByUid(uid).then((results) => { - assert.lengthOf(results, 3); - // The most recent event is returned first. - assert.equal(results[0].name, evC, 'correct event name'); - assert.equal( - !!results[0].verified, - true, - 'event without a session is already verified' - ); - assert.isBelow(results[0].createdAt, Date.now()); - assert.equal(results[1].name, evB, 'correct event name'); - assert.equal( - !!results[1].verified, - false, - 'second session is not verified yet' - ); - assert.isBelow(results[1].createdAt, results[0].createdAt); - assert.equal(results[2].name, evA, 'correct event name'); - assert.equal( - !!results[2].verified, - true, - 'first session is already verified' - ); - assert.isBelow(results[2].createdAt, results[1].createdAt); - }); - }); - - it('should get security event for another account', () => { - return db.securityEventsByUid(anotherUid).then((results) => { - assert.lengthOf(results, 1); - assert.equal(results[0].name, evA, 'correct event name'); - assert.equal( - results[0].verified, - false, - 'this session is not verified' - ); - assert.isBelow(results[0].createdAt, Date.now()); - }); - }); - - it('should get event after session verified', () => { - return db - .verifyTokens(session1.tokenVerificationId, { uid }) - .then(() => db.securityEventsByUid(uid)) - .then((results) => { - assert.lengthOf(results, 3); - assert.isTrue(!!results[0].verified); - assert.isTrue(!!results[1].verified); - assert.isTrue(!!results[2].verified); - }); - }); - - it('should give no securityEvent with new unknown uid', () => { - const unknownUid = newUuid(); - return db - .securityEventsByUid(unknownUid) - .then((results) => assert.lengthOf(results, 0)); - }); - - it('should get no events when events are deleted', () => { - return db - .deleteSecurityEventsByUid(accountData.uid) - .then((result) => assert.deepEqual(result, {})) - .then(() => db.securityEventsByUid(uid)) - .then((result) => assert.lengthOf(result, 0)); - }); - }); - - describe('db.deleteAccount', () => { - let sessionTokenData; - beforeEach(() => { - sessionTokenData = makeMockSessionToken(accountData.uid); - - return db - .createSessionToken(sessionTokenData.tokenId, sessionTokenData) - .then(() => db.accountExists(accountData.emailBuffer)) - .then((exists) => { - assert.ok(exists, 'account exists'); - return db.deleteAccount(accountData.uid); - }); - }); - - it('should have delete account', () => { - return db - .accountExists(accountData.emailBuffer) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - - it('should fail to verify session', () => { - return db - .verifyTokens(sessionTokenData.tokenVerificationId, accountData) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - - it('should fail to fetch session', () => { - return db - .sessionToken(sessionTokenData.tokenId) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - }); - - describe('reminders', () => { - let accountData2, fetchQuery; - beforeEach(() => { - accountData2 = createAccount(); - return db.createAccount(accountData2.uid, accountData2); - }); - - it('create and delete', () => { - fetchQuery = { - type: 'second', - reminderTime: 1, - reminderTimeOutdated: 100, - limit: 20, - }; - - return db - .createVerificationReminder({ uid: accountData.uid, type: 'second' }) - .then(() => P.delay(20)) - .then(() => db.fetchReminders({}, fetchQuery)) - .then((result) => { - assert.equal(result[0].type, 'second', 'correct type'); - assert.equal( - result[0].uid.toString('hex'), - accountData.uid.toString('hex'), - 'correct uid' - ); - return db.fetchReminders({}, fetchQuery); - }) - .then((result) => assert.lengthOf(result, 0)); - }); - - it('multiple accounts', () => { - fetchQuery = { - type: 'first', - reminderTime: 1, - reminderTimeOutdated: 3000, - limit: 20, - }; - - // create 'first' reminder for account one. - return ( - db - .createVerificationReminder({ uid: accountData.uid, type: 'first' }) - // create 'first' reminder for account two. - .then(() => - db.createVerificationReminder({ - uid: accountData2.uid, - type: 'first', - }) - ) - .then(() => P.delay(20)) - .then(() => db.fetchReminders({}, fetchQuery)) - .then((result) => { - assert.lengthOf(result, 2); - assert.equal(result[0].type, 'first', 'correct type'); - assert.equal(result[1].type, 'first', 'correct type'); - return db.fetchReminders({}, fetchQuery); - }) - .then((result) => assert.lengthOf(result, 0)) - ); - }); - - it('multi fetch', () => { - fetchQuery = { - type: 'first', - reminderTime: 1, - reminderTimeOutdated: 100, - limit: 20, - }; - - // create 'first' reminder for account one. - return ( - db - .createVerificationReminder({ uid: accountData.uid, type: 'first' }) - // create 'first' reminder for account two. - .then(() => - db.createVerificationReminder({ - uid: accountData2.uid, - type: 'first', - }) - ) - .then(() => P.delay(20)) - .then(() => - P.all([ - // only one query should give results - db.fetchReminders({}, fetchQuery), - db.fetchReminders({}, fetchQuery), - db.fetchReminders({}, fetchQuery), - db.fetchReminders({}, fetchQuery), - db.fetchReminders({}, fetchQuery), - db.fetchReminders({}, fetchQuery), - db.fetchReminders({}, fetchQuery), - db.fetchReminders({}, fetchQuery), - db.fetchReminders({}, fetchQuery), - db.fetchReminders({}, fetchQuery), - ]) - ) - .then((results) => { - let found = 0; - results.forEach((result) => { - if (result.length === 2) { - found++; - } - }); - - assert.equal(found, 1, 'only one query has the result'); - }) - ); - }); - }); - - describe('unblockCodes', () => { - let uid1, code1, code2; - beforeEach(() => { - uid1 = newUuid(); - code1 = unblockCode(); - - code2 = unblockCode(); - return P.all([ - db.createUnblockCode(uid1, code1), - db.createUnblockCode(uid1, code2), - ]); - }); - - it('should fail to consume unknown code', () => { - return db - .consumeUnblockCode(newUuid(), code1) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - - it('should fail to consume old unblock code', () => { - return db.consumeUnblockCode(uid1, code1).then((code) => { - assert.ok(code); - return db.consumeUnblockCode(uid1, code2).then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - }); - - it('should consume unblock code', () => { - return db.consumeUnblockCode(uid1, code1).then((code) => { - assert.isAtMost(code.createdAt, Date.now()); - }); - }); - - it('should fail to consume code twice', () => { - return db.consumeUnblockCode(uid1, code1).then((code) => { - assert.isAtMost(code.createdAt, Date.now()); - return db.consumeUnblockCode(uid1, code1).then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - }); - - it('should delete all code when successfully consumed code', () => { - return db - .consumeUnblockCode(uid1, 'NOTREAL') - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - return db.consumeUnblockCode(uid1, code1); - }) - .then((code) => { - assert.isAtMost(code.createdAt, Date.now()); - return db.consumeUnblockCode(uid1, code2); - }, assert.fail) - .then(assert.fail, (err) => { - assert.equal(err.code, 404, 'err.code'); - assert.equal(err.errno, 116, 'err.errno'); - }); - }); - }); - - it('emailBounces', () => { - const email = `${`${Math.random()}`.substr(2)}@example.com`; - return db - .createEmailBounce({ - email, - templateName: 'verify', - bounceType: 'Permanent', - bounceSubType: 'NoEmail', - }) - .then(() => - db.createEmailBounce({ - email, - templateName: 'passwordReset', - bounceType: 'Transient', - bounceSubType: 'General', - }) - ) - .then(() => db.fetchEmailBounces(email)) - .then((bounces) => { - assert.lengthOf(bounces, 2); - assert.equal(bounces[0].email, email); - assert.equal(bounces[0].emailTypeId, 33); - assert.equal(bounces[0].bounceType, 2); - assert.equal(bounces[0].bounceSubType, 2); - assert.equal(bounces[1].email, email); - assert.equal(bounces[0].emailTypeId, 6); - assert.equal(bounces[1].bounceType, 1); - assert.equal(bounces[1].bounceSubType, 3); - }); - }); - - describe('emails', () => { - let secondEmail; - beforeEach(() => { - accountData = createAccount(); - accountData.emailVerified = true; - secondEmail = createEmail({ - uid: accountData.uid, - }); - - return db - .createAccount(accountData.uid, accountData) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on account creation' - ); - - return db.createEmail(accountData.uid, secondEmail); - }) - .then((result) => - assert.deepEqual( - result, - {}, - 'Returned an empty object on email creation' - ) - ); - }); - - it('should return only account email if no secondary email', () => { - const anotherAccountData = createAccount(); - return db - .createAccount(anotherAccountData.uid, anotherAccountData) - .then(() => db.accountEmails(anotherAccountData.uid)) - .then((result) => { - assert.lengthOf(result, 1); - - // Check first email is email from accounts table - assert.equal( - result[0].email, - anotherAccountData.email, - 'matches account email' - ); - assert.isTrue(!!result[0].isPrimary); - assert.equal( - !!result[0].isVerified, - anotherAccountData.emailVerified, - 'matches account emailVerified' - ); - assert.equal( - result[0].verifiedAt, - anotherAccountData.verifiedAt, - 'matches account verifiedAt' - ); - }); - }); - - it('should return secondary emails', () => { - return db.accountEmails(accountData.uid).then((result) => { - assert.lengthOf(result, 2); - - // Check first email is email from accounts table - assert.equal( - result[0].email, - accountData.email, - 'matches account email' - ); - assert.isTrue(!!result[0].isPrimary); - assert.equal( - !!result[0].isVerified, - accountData.emailVerified, - 'matches account emailVerified' - ); - assert.equal( - result[0].verifiedAt, - accountData.verifiedAt, - 'matches account verifiedAt' - ); - - // Check second email is from emails table - assert.equal( - result[1].email, - secondEmail.email, - 'matches secondEmail email' - ); - assert.isFalse(!!result[1].isPrimary); - assert.equal( - !!result[1].isVerified, - secondEmail.isVerified, - 'matches secondEmail isVerified' - ); - assert.equal( - result[1].verifiedAt, - secondEmail.verifiedAt, - 'matches secondEmail verifiedAt' - ); - }); - }); - - it('should get secondary email', () => { - return db.getSecondaryEmail(secondEmail.email).then((result) => { - assert.equal( - result.email, - secondEmail.email, - 'matches secondEmail email' - ); - assert.isFalse(!!result.isPrimary); - assert.equal( - !!result.isVerified, - secondEmail.isVerified, - 'matches secondEmail isVerified' - ); - }); - }); - - it('should verify secondary email', () => { - return db - .verifyEmail(secondEmail.uid, secondEmail.emailCode) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on email verification' - ); - return db.accountEmails(accountData.uid); - }) - .then((result) => { - assert.lengthOf(result, 2); - - // Check second email is from emails table and verified - assert.equal( - result[1].email, - secondEmail.email, - 'matches secondEmail email' - ); - assert.isFalse(!!result[1].isPrimary); - assert.isTrue(!!result[1].isVerified); - assert.notEqual(result[1].verifiedAt, null); - assert.isAbove(result[1].verifiedAt, result[1].createdAt); - return db.account(accountData.uid).then((account) => { - assert.isAbove(account.profileChangedAt, account.createdAt); - }); - }); - }); - - it('should delete email', () => { - return db - .deleteEmail(secondEmail.uid, secondEmail.email) - .then((result) => { - assert.deepEqual( - result, - {}, - 'Returned an empty object on email deletion' - ); - - // Get all emails and check to see if it has been removed - return db.accountEmails(accountData.uid); - }) - .then((result) => { - // Verify that the email has been removed - assert.lengthOf(result, 1); - - // Only email returned should be from users account - assert.equal( - result[0].email, - accountData.email, - 'matches account email' - ); - assert.isTrue(!!result[0].isPrimary); - assert.equal( - !!result[0].isVerified, - accountData.emailVerified, - 'matches account emailVerified' - ); - assert.equal( - result[0].verifiedAt, - accountData.verifiedAt, - 'matches account verifiedAt' - ); - - return db.account(accountData.uid).then((account) => { - assert.isAbove(account.profileChangedAt, account.createdAt); - }); - }); - }); - - it('should free secondary email on account deletion', () => { - let anotherAccountData; - return db - .verifyEmail(secondEmail.uid, secondEmail.emailCode) - .then(() => db.deleteAccount(accountData.uid)) - .then(() => { - anotherAccountData = createAccount(); - anotherAccountData.email = secondEmail.email; - anotherAccountData.normalizedEmail = secondEmail.normalizedEmail; - - return db.createAccount(anotherAccountData.uid, anotherAccountData); - }) - .then((result) => { - assert.deepEqual(result, {}, 'successfully created an account'); - - // Attempt to create secondary email address - return db - .createEmail(accountData.uid, secondEmail) - .then(assert.fail, (err) => - assert.equal(err.errno, 101, 'Correct errno') - ); - }); - }); - - it('should fail to add secondary email that exists on account table', () => { - const anotherEmail = createEmail({ email: accountData.email }); - return db - .createEmail(accountData.uid, anotherEmail) - .then(assert.fail, (err) => { - assert.equal(err.errno, 101, 'should return duplicate entry errno'); - assert.equal(err.code, 409, 'should return duplicate entry code'); - }); - }); - - it('should fail to add duplicate secondary email', () => { - const anotherEmail = createEmail({ email: secondEmail.email }); - return db - .createEmail(accountData.uid, anotherEmail) - .then(assert.fail, (err) => { - assert.equal(err.errno, 101, 'should return duplicate entry errno'); - assert.equal(err.code, 409, 'should return duplicate entry code'); - }); - }); - - it('should fail to delete primary email', () => { - return db - .deleteEmail(accountData.uid, accountData.normalizedEmail) - .then(assert.fail, (err) => { - assert.equal(err.errno, 136, 'should return email delete errno'); - assert.equal(err.code, 400, 'should return email delete code'); - }); - }); - - it('should fail to create account that used a secondary email as primary', () => { - const anotherAccount = createAccount(); - anotherAccount.email = secondEmail.email; - anotherAccount.normalizedEmail = secondEmail.normalizedEmail; - return db - .createAccount(anotherAccount.uid, anotherAccount) - .then(assert.fail, (err) => { - assert.equal(err.errno, 101, 'should return duplicate entry errno'); - assert.equal(err.code, 409, 'should return duplicate entry code'); - }); - }); - - it('should fail to get non-existent secondary email', () => { - return db - .getSecondaryEmail('non-existent@email.com') - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'should return not found errno'); - assert.equal(err.code, 404, 'should return not found code'); - }); - }); - }); - - describe('sign-in codes', () => { - let SIGNIN_CODES, NOW, TIMESTAMPS, FLOW_IDS; - - beforeEach(() => { - SIGNIN_CODES = [hex6(), hex6(), hex6()]; - NOW = Date.now(); - TIMESTAMPS = [NOW - 1, NOW - 2, NOW - config.signinCodesMaxAge - 1]; - FLOW_IDS = [hex32(), hex32(), hex32()]; - - return P.all([ - db.createSigninCode( - SIGNIN_CODES[0], - accountData.uid, - TIMESTAMPS[0], - FLOW_IDS[0] - ), - db.createSigninCode( - SIGNIN_CODES[1], - accountData.uid, - TIMESTAMPS[1], - FLOW_IDS[1] - ), - db.createSigninCode( - SIGNIN_CODES[2], - accountData.uid, - TIMESTAMPS[2], - FLOW_IDS[2] - ), - ]).then((results) => { - results.forEach((r) => - assert.deepEqual( - r, - {}, - 'createSigninCode should return an empty object' - ) - ); - }); - }); - - it('should fail to create duplicate sign-in code', () => { - return db - .createSigninCode(SIGNIN_CODES[0], accountData.uid, TIMESTAMPS[0]) - .then(assert.fail, (err) => { - assert(err, 'db.createSigninCode should reject with an error'); - assert.equal( - err.code, - 409, - 'db.createSigninCode should reject with code 404' - ); - assert.equal( - err.errno, - 101, - 'db.createSigninCode should reject with errno 116' - ); - }); - }); - - it('should consume sign-in code', () => { - return db.consumeSigninCode(SIGNIN_CODES[0]).then((result) => { - assert.deepEqual( - result, - { - email: accountData.email, - flowId: FLOW_IDS[0], - }, - 'db.consumeSigninCode should return an email address and flowId for non-expired codes' - ); - }); - }); - - it('should fail consume sign-in code twice', () => { - return db - .consumeSigninCode(SIGNIN_CODES[0]) - .then(() => db.consumeSigninCode(SIGNIN_CODES[0])) - .then(assert.fail, (err) => { - assert(err, 'db.consumeSigninCode should reject with an error'); - assert.equal( - err.code, - 404, - 'db.consumeSigninCode should reject with code 404' - ); - assert.equal( - err.errno, - 116, - 'db.consumeSigninCode should reject with errno 116' - ); - }); - }); - - it('should fail consume expired sign-in code', () => { - return db - .consumeSigninCode(SIGNIN_CODES[2]) - .then(assert.fail, (err) => { - assert(err, 'db.consumeSigninCode should reject with an error'); - assert.equal( - err.code, - 404, - 'db.consumeSigninCode should reject with code 404' - ); - assert.equal( - err.errno, - 116, - 'db.consumeSigninCode should reject with errno 116' - ); - }); - }); - - it('should fail to use sign-in code from deleted account', () => { - return db.deleteAccount(accountData.uid).then(() => { - return db - .consumeSigninCode(SIGNIN_CODES[1]) - .then(assert.fail, (err) => { - assert(err, 'db.consumeSigninCode should reject with an error'); - assert.equal( - err.code, - 404, - 'db.consumeSigninCode should reject with code 404' - ); - assert.equal( - err.errno, - 116, - 'db.consumeSigninCode should reject with errno 116' - ); - }); - }); - }); - }); - - it('should keep account emails and emails in sync', () => { - return P.all([ - db.accountEmails(accountData.uid), - db.account(accountData.uid), - ]) - .spread(function (emails, account) { - assert.equal( - emails[0].email, - account.email, - 'correct email returned' - ); - assert.equal( - !!emails[0].isVerified, - !!account.emailVerified, - 'correct email verification' - ); - assert.equal( - emails[0].verifiedAt, - account.verifiedAt, - 'correct email verifiedAt' - ); - assert.isTrue(!!emails[0].isPrimary); - - // Verify account email - return db.verifyEmail(account.uid, account.emailCode); - }) - .then(function (result) { - assert.deepEqual( - result, - {}, - 'returned empty response on verify email' - ); - return P.all([ - db.accountEmails(accountData.uid), - db.account(accountData.uid), - ]); - }) - .spread(function (emails, account) { - assert.equal( - emails[0].email, - account.email, - 'correct email returned' - ); - assert.equal( - !!emails[0].isVerified, - !!account.emailVerified, - 'correct email verification' - ); - assert.isTrue(!!emails[0].isPrimary); - }); - }); - - describe('db.resetAccountTokens', () => { - let passwordChangeToken, passwordForgotToken, accountResetToken; - - beforeEach(() => { - accountData.emailVerified = true; - passwordChangeToken = makeMockChangePasswordToken(accountData.uid); - passwordForgotToken = makeMockForgotPasswordToken(accountData.uid); - accountResetToken = makeMockAccountResetToken( - accountData.uid, - passwordForgotToken.tokenId - ); - }); - - it('should remove account reset tokens', () => { - return db - .createPasswordForgotToken( - passwordForgotToken.tokenId, - passwordForgotToken - ) - .then(() => { - // db.forgotPasswordVerified requires a passwordForgotToken to have been made - return db - .forgotPasswordVerified( - passwordForgotToken.tokenId, - accountResetToken - ) - .then(() => { - return db - .accountResetToken(passwordForgotToken.tokenId) - .then((res) => - assert.deepEqual( - res.uid, - accountData.uid, - 'token belongs to account' - ) - ); - }); - }) - .then(() => db.resetAccountTokens(accountData.uid)) - .then(() => { - return db - .accountResetToken(passwordForgotToken.tokenId) - .then(() => - assert.equal( - false, - 'should not have return account reset token token' - ) - ) - .catch((err) => - assert.equal( - err.errno, - 116, - 'did not find password change token' - ) - ); - }); - }); - - it('should remove password change tokens', () => { - return db - .createPasswordChangeToken( - passwordChangeToken.tokenId, - passwordChangeToken - ) - .then(() => { - return db - .passwordChangeToken(passwordChangeToken.tokenId) - .then((res) => - assert.deepEqual( - res.uid, - accountData.uid, - 'token belongs to account' - ) - ); - }) - .then(() => db.resetAccountTokens(accountData.uid)) - .then(() => { - return db - .passwordChangeToken(passwordChangeToken.tokenId) - .then(() => - assert.equal( - false, - 'should not have return password change token' - ) - ) - .catch((err) => - assert.equal( - err.errno, - 116, - 'did not find password change token' - ) - ); - }); - }); - - it('should remove password forgot tokens', () => { - return db - .createPasswordForgotToken( - passwordForgotToken.tokenId, - passwordForgotToken - ) - .then(() => { - return db - .passwordForgotToken(passwordForgotToken.tokenId) - .then((res) => - assert.deepEqual( - res.uid, - accountData.uid, - 'token belongs to account' - ) - ); - }) - .then(() => db.resetAccountTokens(accountData.uid)) - .then(() => { - return db - .passwordForgotToken(passwordForgotToken.tokenId) - .then(() => - assert.equal( - false, - 'should not have return password forgot token' - ) - ) - .catch((err) => - assert.equal( - err.errno, - 116, - 'did not find password forgot token' - ) - ); - }); - }); - }); - - describe('db.setPrimaryEmail', () => { - let account, secondEmail; - - before(() => { - account = createAccount(); - account.emailVerified = true; - secondEmail = createEmail({ - uid: account.uid, - isVerified: true, - verifiedAt: Date.now() + 1000, - }); - return db - .createAccount(account.uid, account) - .then(function () { - return db.verifyEmail(account.uid, account.emailCode); - }) - .then(function (result) { - assert.deepEqual( - result, - {}, - 'returned empty response on verify email' - ); - return db.createEmail(account.uid, secondEmail); - }) - .then(function (result) { - assert.deepEqual( - result, - {}, - 'Returned an empty object on email creation' - ); - return db.accountEmails(account.uid); - }) - .then(function (res) { - assert.lengthOf(res, 2); - assert.equal( - res[0].email, - account.email, - 'primary email is the address that was used to create account' - ); - assert.deepEqual( - res[0].emailCode, - account.emailCode, - 'correct emailCode' - ); - assert.isTrue(!!res[0].isVerified); - assert.notEqual(res[0].verifiedAt, null); - assert.isAbove(res[0].verifiedAt, res[0].createdAt); - assert.isTrue(!!res[0].isPrimary); - - assert.equal( - res[1].email, - secondEmail.email, - 'primary email is the address that was used to create account' - ); - assert.deepEqual( - res[1].emailCode, - secondEmail.emailCode, - 'correct emailCode' - ); - assert.isTrue(!!res[1].isVerified); - assert.notEqual(res[1].verifiedAt, null); - assert.isAbove(res[1].verifiedAt, res[1].createdAt); - assert.isFalse(!!res[1].isPrimary); - }); - }); - - it("should change a user's email", () => { - let sessionTokenData; - return db - .setPrimaryEmail(account.uid, secondEmail.email) - .then(function (res) { - assert.deepEqual( - res, - {}, - 'Returned an empty object on email change' - ); - return db.accountEmails(account.uid); - }) - .then(function (res) { - assert.lengthOf(res, 2); - - assert.equal( - res[0].email, - secondEmail.email, - 'primary email is the secondary email address' - ); - assert.deepEqual( - res[0].emailCode, - secondEmail.emailCode, - 'correct emailCode' - ); - assert.equal( - !!res[0].isVerified, - secondEmail.isVerified, - 'correct verification set' - ); - assert.equal( - res[0].verifiedAt, - secondEmail.verifiedAt, - 'correct verifiedAt set' - ); - assert.isTrue(!!res[0].isPrimary); - - assert.equal( - res[1].email, - account.email, - 'should equal account email' - ); - assert.deepEqual( - res[1].emailCode, - account.emailCode, - 'correct emailCode' - ); - assert.equal( - !!res[1].isVerified, - account.emailVerified, - 'correct verification set' - ); - assert.isFalse(!!res[1].isPrimary); - - // Verify correct email set in session - sessionTokenData = makeMockSessionToken(account.uid); - return db - .createSessionToken(sessionTokenData.tokenId, sessionTokenData) - .then(() => { - return db.sessionToken(sessionTokenData.tokenId); - }); - }) - .then((session) => { - assert.equal( - session.email, - secondEmail.email, - 'should equal new primary email' - ); - assert.deepEqual( - session.emailCode, - secondEmail.emailCode, - 'should equal new primary emailCode' - ); - assert.deepEqual( - session.uid, - account.uid, - 'should equal account uid' - ); - return P.all([ - db.accountRecord(secondEmail.email), - db.accountRecord(account.email), - ]); - }) - .then((res) => { - assert.deepEqual( - res[0], - res[1], - 'should return the same account record regardless of email used' - ); - assert.deepEqual( - res[0].primaryEmail, - secondEmail.email, - 'primary email should be set to update email' - ); - assert.ok(res[0].createdAt, 'should set createdAt'); - assert.deepEqual( - res[0].createdAt, - res[1].createdAt, - 'account records should have the same createdAt' - ); - assert.isAtLeast(res[0].profileChangedAt, res[0].createdAt); - }); - }); - - it("shouldn't set primary email to email that is not owned by account", () => { - const anotherAccount = createAccount(); - anotherAccount.emailVerified = true; - const anotherEmail = createEmail({ - uid: anotherAccount.uid, - isVerified: true, - }); - return db - .createAccount(anotherAccount.uid, anotherAccount) - .then(() => { - return db.createEmail(anotherAccount.uid, anotherEmail); - }) - .then(() => { - return db.setPrimaryEmail(account.uid, anotherEmail.email); - }) - .catch((err) => { - assert.equal(err.errno, 148, 'correct errno set'); - }); - }); - }); - - describe('db.verifyTokenCode', () => { - let account, anotherAccount, sessionToken, tokenVerificationCode, tokenId; - before(() => { - account = createAccount(); - account.emailVerified = true; - return db.createAccount(account.uid, account); - }); - - it('should verify tokenVerificationCode', () => { - tokenId = hex32(); - sessionToken = makeMockSessionToken(account.uid, false); - tokenVerificationCode = sessionToken.tokenVerificationCode; - return db - .createSessionToken(tokenId, sessionToken) - .then(() => { - return db.sessionToken(tokenId); - }) - .then((session) => { - // Returns unverified session - assert.equal( - session.mustVerify, - sessionToken.mustVerify, - 'mustVerify must match sessionToken' - ); - assert.equal( - session.tokenVerificationId.toString('hex'), - sessionToken.tokenVerificationId.toString('hex'), - 'tokenVerificationId must match sessionToken' - ); - - // Verify the session - return db.verifyTokenCode({ code: tokenVerificationCode }, account); - }) - .then(() => { - return db.sessionToken(tokenId); - }) - .then((session) => { - // Returns verified session - assert.isFalse(!!session.mustVerify); - assert.isNull(session.tokenVerificationId); - assert.notOk(session.tokenVerificationCodeHash); - assert.notOk(session.tokenVerificationCodeExpiresAt); - }); - }); - - it("shouldn't verify expired tokenVerificationCode", () => { - tokenId = hex32(); - sessionToken = makeMockSessionToken(account.uid); - sessionToken.tokenVerificationCodeExpiresAt = Date.now() - 2000000000; - tokenVerificationCode = sessionToken.tokenVerificationCode; - return db.createSessionToken(tokenId, sessionToken).then(() => { - return db - .verifyTokenCode({ code: tokenVerificationCode }, account) - .then( - () => { - assert.fail('should not have verified expired token'); - }, - (err) => { - assert.equal(err.errno, 137, 'correct errno, not found'); - } - ); - }); - }); - - it("shouldn't verify unknown tokenVerificationCode", () => { - tokenId = hex32(); - sessionToken = makeMockSessionToken(account.uid); - tokenVerificationCode = 'iamzinvalidz'; - return db.createSessionToken(tokenId, sessionToken).then(() => { - return db - .verifyTokenCode({ code: tokenVerificationCode }, account) - .then( - () => { - assert.fail('should not have verified unknown token'); - }, - (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - } - ); - }); - }); - - it("shouldn't verify tokenVerificationCode and uid mismatch", () => { - tokenId = hex32(); - sessionToken = makeMockSessionToken(account.uid); - tokenVerificationCode = sessionToken.tokenVerificationCode; - anotherAccount = createAccount(); - anotherAccount.emailVerified = true; - return db - .createAccount(anotherAccount.uid, anotherAccount) - .then(() => { - return db.createSessionToken(tokenId, sessionToken); - }) - .then(() => { - return db - .verifyTokenCode({ code: tokenVerificationCode }, anotherAccount) - .then( - () => { - assert.fail('should not have verified unknown token'); - }, - (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - } - ); - }); - }); - }); - - describe('Totp handling', () => { - let sharedSecret, epoch; - beforeEach(() => { - sharedSecret = crypto.randomBytes(40).toString('hex'); - epoch = 0; - return db - .createTotpToken(accountData.uid, { sharedSecret, epoch }) - .then((result) => assert.ok(result, 'token created')); - }); - - it('should create totp token', () => { - return db.totpToken(accountData.uid).then((token) => { - assert.equal( - token.sharedSecret, - sharedSecret, - 'correct sharedSecret' - ); - assert.equal(token.epoch, epoch, 'correct epoch'); - assert.isFalse(!!token.verified); - assert.isTrue(!!token.enabled); - }); - }); - - it('should fail to get unknown totp token', () => { - return db.totpToken(newUuid()).then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - }); - }); - - it('should fail to create second token for same user', () => { - return db - .createTotpToken(accountData.uid, { sharedSecret, epoch }) - .then(assert.fail, (err) => { - assert.equal(err.errno, 101, 'correct errno, duplicate'); - }); - }); - - it('should delete totp token', () => { - return db.deleteTotpToken(accountData.uid).then((result) => { - assert.ok(result); - return db - .totpToken(accountData.uid) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - - return db.account(accountData.uid); - }) - .then((account) => { - assert.isAbove(account.profileChangedAt, account.createdAt); - }); - }); - }); - - it('should update totp token', () => { - return db - .updateTotpToken(accountData.uid, { verified: true, enabled: true }) - .then((result) => { - assert.ok(result); - return db - .totpToken(accountData.uid) - .then((token) => { - assert.equal( - token.sharedSecret, - sharedSecret, - 'correct sharedSecret' - ); - assert.equal(token.epoch, epoch, 'correct epoch'); - assert.isTrue(!!token.verified); - assert.isTrue(!!token.enabled); - - return db.account(accountData.uid); - }) - .then((account) => { - assert.isAbove(account.profileChangedAt, account.createdAt); - }); - }); - }); - - it('should fail to update unknown totp token', () => { - return db - .updateTotpToken(newUuid(), { verified: true, enabled: true }) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - }); - }); - - it('should delete token when account deleted', () => { - return db - .deleteAccount(accountData.uid) - .then(() => db.totpToken(accountData.uid)) - .then( - () => assert.fail('should have deleted totp token'), - (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - } - ); - }); - }); - - describe('db.verifyTokensWithMethod', () => { - let account, sessionToken, tokenId; - before(() => { - account = createAccount(); - account.emailVerified = true; - tokenId = hex32(); - sessionToken = makeMockSessionToken(account.uid, false); - return db - .createAccount(account.uid, account) - .then(() => db.createSessionToken(tokenId, sessionToken)) - .then(() => db.sessionToken(tokenId)) - .then((session) => { - // Returns unverified session - assert.equal( - session.tokenVerificationId.toString('hex'), - sessionToken.tokenVerificationId.toString('hex'), - 'tokenVerificationId must match sessionToken' - ); - assert.isNull(session.verificationMethod); - }); - }); - - it('should fail to verify with unknown sessionId', () => { - const verifyOptions = { - verificationMethod: 'totp-2fa', - }; - return db - .verifyTokensWithMethod(hex32(), verifyOptions) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - }); - }); - - it('should update session verificationMethod', () => { - const verifyOptions = { - verificationMethod: 'totp-2fa', - }; - return db - .verifyTokens(sessionToken.tokenVerificationId, account) - .then(() => { - return db.sessionToken(tokenId); - }, assert.fail) - .then((token) => { - assert.isFalse(!!token.mustVerify); - assert.isNull(token.tokenVerificationId); - assert.isNull(token.verificationMethod); - return db.verifyTokensWithMethod(tokenId, verifyOptions); - }) - .then(() => { - return db.sessionToken(tokenId); - }, assert.fail) - .then((token) => { - assert.isFalse(!!token.mustVerify); - assert.isNull(token.tokenVerificationId); - assert.equal( - token.verificationMethod, - 2, - 'verificationMethod is set' - ); - }); - }); - - it('should fail to verify unknown verification method', () => { - const verifyOptions = { - verificationMethod: 'super-invalid-method', - }; - return db - .verifyTokensWithMethod(tokenId, verifyOptions) - .then(assert.fail, (err) => { - assert.equal( - err.errno, - 138, - 'correct errno, invalid verification method' - ); - }); - }); - - it('should verify with verification method', () => { - const verifyOptions = { - verificationMethod: 'totp-2fa', - }; - return db - .verifyTokensWithMethod(tokenId, verifyOptions) - .then((res) => { - assert.ok(res); - - // Ensure session really has been verified and correct methods set - return db.sessionToken(tokenId); - }) - .then((session) => { - assert.isNull(session.tokenVerificationId); - assert.equal( - session.verificationMethod, - 2, - 'verificationMethod set' - ); - assert.ok(session.verifiedAt, 'verifiedAt set'); - }); - }); - }); - - describe('recovery codes', () => { - let account; - beforeEach(() => { - account = createAccount(); - account.emailVerified = true; - return db.createAccount(account.uid, account); - }); - - it('should fail to generate for unknown user', () => { - return db.replaceRecoveryCodes(hex16(), 2).then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - }); - }); - - const codeLengthTest = [0, 4, 8]; - const codeTest = /^[a-zA-Z0-9]{0,20}$/; - codeLengthTest.forEach((num) => { - it('should generate ' + num + ' recovery codes', () => { - return db.replaceRecoveryCodes(account.uid, num).then( - (codes) => { - assert.lengthOf(codes, num); - codes.forEach((code) => { - assert.match(code, codeTest); - }); - }, - (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - } - ); - }); - }); - - it('should replace recovery codes', () => { - let firstCodes; - return db - .replaceRecoveryCodes(account.uid, 2) - .then((codes) => { - firstCodes = codes; - assert.lengthOf(firstCodes, 2); - - return db.replaceRecoveryCodes(account.uid, 3); - }) - .then((codes) => { - assert.lengthOf(codes, 3); - assert.notDeepEqual(codes, firstCodes, 'codes are different'); - }); - }); - - it('should remove codes when account deleted', () => { - let recoveryCodes; - return db - .replaceRecoveryCodes(account.uid, 2) - .then((codes) => { - recoveryCodes = codes; - return db.deleteAccount(account.uid); - }) - .then((result) => - db.consumeRecoveryCode(account.uid, recoveryCodes[0]) - ) - .then( - () => assert.fail('should have removed codes'), - (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - } - ); - }); - - describe('should consume recovery codes', function () { - // Consuming recovery codes is more time intensive since the scrypt hashes need - // to be compared. Let set timeout higher than 2s default. - this.timeout(12000); - - let recoveryCodes; - beforeEach(() => { - return db.replaceRecoveryCodes(account.uid, 2).then((codes) => { - recoveryCodes = codes; - assert.lengthOf(recoveryCodes, 2); - }); - }); - - it('should fail to consume recovery code with unknown uid', () => { - return db - .consumeRecoveryCode(hex16(), 'recoverycodez') - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - }); - }); - - it('should fail to consume recovery code with unknown code', () => { - return db.replaceRecoveryCodes(account.uid, 3).then(() => { - return db - .consumeRecoveryCode(account.uid, 'notvalidcode') - .then(assert.fail, (err) => { - assert.equal( - err.errno, - 116, - 'correct errno, unknown recovery code' - ); - }); - }); - }); - - it('should fail to consume code twice', () => { - return db - .consumeRecoveryCode(account.uid, recoveryCodes[0]) - .then((result) => { - assert.equal( - result.remaining, - 1, - 'correct number of remaining codes' - ); - - // Should fail to consume code twice - return db - .consumeRecoveryCode(account.uid, recoveryCodes[0]) - .then(assert.fail, (err) => { - assert.equal( - err.errno, - 116, - 'correct errno, unknown recovery code' - ); - }); - }); - }); - - it('should consume code', () => { - return db - .consumeRecoveryCode(account.uid, recoveryCodes[0]) - .then((result) => { - assert.equal( - result.remaining, - 1, - 'correct number of remaining codes' - ); - - return db - .consumeRecoveryCode(account.uid, recoveryCodes[1]) - .then((result) => { - assert.equal( - result.remaining, - 0, - 'correct number of remaining codes' - ); - }); - }); - }); - }); - }); - - describe('account recovery key', () => { - let account, data; - beforeEach(() => { - account = createAccount(); - return db - .createAccount(account.uid, account) - .then(() => { - data = createRecoveryData(); - // Create a valid recovery key - return db.createRecoveryKey(account.uid, data); - }) - .then((res) => { - assert.ok(res, 'empty response'); - }); - }); - - it('should fail to create for unknown user', () => { - return db - .createRecoveryKey('12312312312', data) - .then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'not found'); - }); - }); - - it('should fail to create multiple keys', () => { - data = createRecoveryData(); - return db - .createRecoveryKey(account.uid, data) - .then(assert.fail, (err) => { - assert.equal(err.errno, 101, 'record exists'); - }); - }); - - it('should get account recovery key', () => { - const options = { - id: account.uid, - recoveryKeyId: data.recoveryKeyId, - }; - return db.getRecoveryKey(options).then((res) => { - assert.equal( - res.recoveryData, - data.recoveryData, - 'recovery data set' - ); - const recoveryKeyIdHash = util.createHash(data.recoveryKeyId); - assert.equal( - res.recoveryKeyIdHash.toString('hex'), - recoveryKeyIdHash.toString('hex'), - 'recoveryKeyId set' - ); - assert.ok(res.createdAt); - assert.equal(res.enabled, true); - assert.equal(res.verifiedAt, undefined); - }); - }); - - it('should fail to get key for incorrect user', () => { - const options = { - id: 'unknown', - recoveryKeyId: '123', - }; - return db.getRecoveryKey(options).then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'not found'); - }); - }); - - it('should fail to get non-existent key', () => { - account = createAccount(); - return db.createAccount(account.uid, account).then(() => { - const options = { - id: account.uid, - recoveryKeyId: 'unknown', - }; - return db.getRecoveryKey(options).then(assert.fail, (err) => { - assert.equal(err.errno, 116, 'not found'); - }); - }); - }); - - it('should fail to get key with invalid recoveryKeyId', () => { - const options = { - id: account.uid, - recoveryKeyId: 'incorrect recoveryKeyId', - }; - return db.getRecoveryKey(options).then(assert.fail, (err) => { - assert.equal(err.errno, 159, 'incorrect recoveryKeyId'); - }); - }); - - it('should return true if recovery key exists', () => { - return db.recoveryKeyExists(account.uid).then((res) => { - assert.isTrue(res.exists); - }); - }); - - it("should return false if recovery key doesn't exist", () => { - account = createAccount(); - return db.createAccount(account.uid, account).then(() => { - return db.recoveryKeyExists(account.uid).then((res) => { - assert.isFalse(res.exists); - }); - }); - }); - - it('should throw when checking for recovery key on non-existent user', () => { - return db.recoveryKeyExists('nonexistent').then((res) => { - assert.isFalse(res.exists); - }); - }); - - it('should remove recovery key when account deleted', () => { - const options = { - id: account.uid, - recoveryKeyId: data.recoveryKeyId, - }; - return db - .deleteAccount(account.uid) - .then(() => db.getRecoveryKey(options)) - .then( - () => assert.fail('should have deleted recovery key'), - (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - } - ); - }); - - it('should remove recovery key when account password is reset', () => { - const options = { - id: account.uid, - recoveryKeyId: data.recoveryKeyId, - }; - return db - .resetAccount(account.uid, account) - .then(() => db.getRecoveryKey(options)) - .then( - () => assert.fail('should have deleted recovery key'), - (err) => { - assert.equal(err.errno, 116, 'correct errno, not found'); - } - ); - }); - - it('should create disabled key and then verify it', async () => { - account = createAccount(); - await db.createAccount(account.uid, account); - data = createRecoveryData(); - data.enabled = false; - await db.createRecoveryKey(account.uid, data); - - let res = await db.getRecoveryKey({ - id: account.uid, - recoveryKeyId: data.recoveryKeyId, - }); - assert.ok(res.createdAt); - assert.equal(res.enabled, false); - assert.equal(res.verifiedAt, undefined); - - const updatedKey = Object.assign({}, data, { - verifiedAt: Date.now(), - enabled: true, - }); - await db.updateRecoveryKey(account.uid, updatedKey); - - res = await db.getRecoveryKey({ - id: account.uid, - recoveryKeyId: data.recoveryKeyId, - }); - assert.ok(res.createdAt); - assert.equal(res.enabled, true); - assert.equal(res.verifiedAt, updatedKey.verifiedAt); - }); - - it('should error if verifying unknown key', async () => { - account = createAccount(); - await db.createAccount(account.uid, account); - - data = Object.assign({}, createRecoveryData(), { - verifiedAt: Date.now(), - enabled: true, - }); - - try { - await db.updateRecoveryKey(account.uid, data); - assert.fail('should have failed'); - } catch (err) { - assert.equal(err.errno, 116, 'Not found error'); - } - }); - }); - - after(() => db.close()); - }); -}; diff --git a/packages/fxa-auth-db-mysql/db-server/test/backend/index.js b/packages/fxa-auth-db-mysql/db-server/test/backend/index.js deleted file mode 100644 index 84d215f150c..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/test/backend/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -module.exports = { - dbTests: require('./db_tests'), - remote: require('./remote'), -}; diff --git a/packages/fxa-auth-db-mysql/db-server/test/backend/remote.js b/packages/fxa-auth-db-mysql/db-server/test/backend/remote.js deleted file mode 100644 index 26f49222747..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/test/backend/remote.js +++ /dev/null @@ -1,3015 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -'use strict'; - -const { assert } = require('chai'); -const crypto = require('crypto'); -const fake = require('../fake'); -const P = require('../../../lib/promise'); -const clientThen = require('../client-then'); - -function emailToHex(email) { - return Buffer.from(email).toString('hex'); -} - -// Helper function that performs two tests: -// -// (1) checks that the response is a 200 -// (2) checks that the content-type header is correct -function respOk(r) { - assert.equal(r.res.statusCode, 200, 'returns a 200'); - assert.equal( - r.res.headers['content-type'], - 'application/json', - 'json is returned' - ); -} - -// Helper function that performs three tests: -// -// (1) checks that the response is a 200 -// (2) checks that the content-type header is correct -// (3) checks that the response was an empty object -function respOkEmpty(r) { - assert.equal(r.res.statusCode, 200, 'returns a 200'); - assert.equal( - r.res.headers['content-type'], - 'application/json', - 'json is returned' - ); - assert.deepEqual(r.obj, {}, 'Returned object is empty'); -} - -// Helper function that performs two tests: -// -// (1) checks that the response is a 404 -// (2) checks that the error body for a 404 is consistent -function testNotFound(err) { - assert.equal(err.statusCode, 404, 'returns a 404'); - assert.deepEqual( - err.body, - { - message: 'Not Found', - errno: 116, - error: 'Not Found', - code: 404, - }, - 'Object contains no other fields' - ); -} - -// Helper function that performs two tests: -// -// (1) checks that the response is a 409 -// (2) checks that the error body for a 409 is consistent -function testConflict(err) { - assert.equal(err.statusCode, 409, 'err.statusCode should be 409'); - assert.deepEqual( - err.body, - { - message: 'Record already exists', - errno: 101, - error: 'Conflict', - code: 409, - }, - 'err.body should have correct properties set' - ); -} - -// Helper function that tests for the server failure event. -// -// Returns a promise to be resolved with failure event -// when one is emitted by the server. -function captureFailureEvent(server) { - return new P(function (resolve, reject) { - server.once('failure', resolve); - }); -} - -// Helper test that calls the version route and checks response that checks the version route response. -// -// Takes the client, and a route, either '/' or '/__version__'. -// -function testVersionResponse(client, route) { - return client.getThen(route).then(function (r) { - assert.equal(r.res.statusCode, 200, 'version returns 200 OK'); - assert.match(r.obj.version, /\d+\.\d+\.\d+/); - assert.include(['MySql'], r.obj.implementation); - }); -} - -// To run these tests from a new backend, create a DB instance, start a test server -// and pass the config containing the connection params to this function. The tests -// will run against that server. Second argument is the restify server object, for -// testing of events via `server.on`. -module.exports = function (cfg, makeServer) { - describe('remote', () => { - let client; - let server; - before(() => { - return makeServer().then((s) => { - server = s; - client = clientThen({ url: 'http://' + cfg.hostname + ':' + cfg.port }); - }); - }); - - it('heartbeat', () => { - return client.getThen('/__heartbeat__').then(function (r) { - assert.deepEqual( - r.obj, - {}, - 'Heartbeat contains an empty object and nothing unexpected' - ); - }); - }); - - it('version', () => testVersionResponse(client, '/')); - it('version', () => testVersionResponse(client, '/__version__')); - - it('account not found', () => { - return client.getThen('/account/0123456789ABCDEF0123456789ABCDEF').then( - function (r) { - assert( - false, - 'This request should have failed (instead it suceeded)' - ); - }, - function (err) { - testNotFound(err); - } - ); - }); - - it('add account, add email, get secondary email, get emails, delete email', async () => { - const user = fake.newUserDataHex(); - const secondEmailRecord = user.email; - - let r = await client.putThen('/account/' + user.accountId, user.account); - respOkEmpty(r); - - r = await client.postThen( - '/account/' + user.accountId + '/emails', - user.email - ); - respOkEmpty(r); - - r = await client.getThen('/account/' + user.accountId + '/emails'); - respOk(r); - let result = r.obj; - assert.lengthOf(result, 2); - - // Verify first email is the primary email - assert.equal( - result[0].email, - user.account.email, - 'matches account email' - ); - assert.equal( - !!result[0].isPrimary, - true, - 'isPrimary is true on account email' - ); - assert.equal( - !!result[0].isVerified, - !!user.account.emailVerified, - 'matches account emailVerified' - ); - - // Verify second email is the secondary email - assert.equal( - result[1].email, - secondEmailRecord.email, - 'matches secondEmail email' - ); - assert.equal( - !!result[1].isPrimary, - false, - 'isPrimary is false on secondEmail email' - ); - assert.equal( - !!result[1].isVerified, - false, - 'matches secondEmail isVerified' - ); - - var emailCodeHex = secondEmailRecord.emailCode.toString('hex'); - r = await client.postThen( - '/account/' + user.accountId + '/verifyEmail/' + emailCodeHex - ); - respOkEmpty(r); - - r = await client.getThen('/account/' + user.accountId + '/emails'); - respOk(r); - - result = r.obj; - assert.lengthOf(result, 2); - assert.equal( - result[1].email, - secondEmailRecord.email, - 'matches secondEmail email' - ); - assert.equal( - !!result[1].isPrimary, - false, - 'isPrimary is false on secondEmail email' - ); - assert.equal( - !!result[1].isVerified, - true, - 'matches secondEmail isVerified' - ); - - const thirdEmailRecord = fake.newUserDataHex().email; - r = await client.postThen( - '/account/' + user.accountId + '/emails', - thirdEmailRecord - ); - respOkEmpty(r); - - r = await client.getThen('/account/' + user.accountId + '/emails'); - respOk(r); - - result = r.obj; - assert.lengthOf(result, 3); - // Secondary emails are not returned in a deterministic order; normalize. - if (result[2].email !== thirdEmailRecord.email) { - assert.equal( - result[2].email, - secondEmailRecord.email, - 'second email record was returned third' - ); - [result[1], result[2]] = [result[2], result[1]]; - } - - assert.equal( - result[2].email, - thirdEmailRecord.email, - 'matches thirdEmailRecord email' - ); - assert.equal( - !!result[2].isPrimary, - false, - 'isPrimary is false on thirdEmailRecord email' - ); - assert.equal( - !!result[2].isVerified, - false, - 'matches secondEmail thirdEmailRecord' - ); - - r = await client.delThen( - '/account/' + - user.accountId + - '/emails/' + - emailToHex(secondEmailRecord.email) - ); - respOkEmpty(r); - - r = await client.getThen('/account/' + user.accountId + '/emails'); - respOk(r); - - result = r.obj; - assert.lengthOf(result, 2); - assert.equal( - result[0].email, - user.account.email, - 'matches account email' - ); - assert.equal( - !!result[0].isPrimary, - true, - 'isPrimary is true on account email' - ); - assert.equal( - !!result[0].isVerified, - !!user.account.emailVerified, - 'matches account emailVerified' - ); - - r = await client.getThen('/email/' + emailToHex(thirdEmailRecord.email)); - respOk(r); - - result = r.obj; - assert.equal(result.email, thirdEmailRecord.email, 'matches email'); - assert.equal(!!result.isPrimary, false, 'isPrimary is false on email'); - assert.equal( - !!result.isVerified, - !!thirdEmailRecord.emailVerified, - 'matches emailVerified' - ); - }); - - it('add account, check password, retrieve it, delete it', () => { - var user = fake.newUserDataHex(); - return client - .putThen('/account/' + user.accountId, user.account) - .then(function (r) { - respOkEmpty(r); - var randomPassword = Buffer.from(crypto.randomBytes(32)).toString( - 'hex' - ); - return client - .postThen('/account/' + user.accountId + '/checkPassword', { - verifyHash: randomPassword, - }) - .then( - function (r) { - assert(false, "should not be here, password isn't valid"); - }, - function (err) { - assert(err, 'incorrect password produces an error'); - return client.postThen( - '/account/' + user.accountId + '/checkPassword', - { verifyHash: user.account.verifyHash } - ); - } - ); - }) - .then(function (r) { - respOk(r); - var account = r.obj; - assert.equal(account.uid, user.accountId); - return client.getThen('/account/' + user.accountId); - }) - .then(function (r) { - respOk(r); - - var account = r.obj; - var fields = 'accountId,email,emailCode,kA,verifierVersion,authSalt'.split( - ',' - ); - fields.forEach(function (f) { - assert.equal( - user.account[f], - account[f], - 'Both Fields ' + f + ' are the same' - ); - }); - assert.equal( - user.account.emailVerified, - !!account.emailVerified, - 'Both fields emailVerified are the same' - ); - assert(!account.verifyHash, 'verifyHash field should be absent'); - }) - .then(function () { - return client.headThen( - '/emailRecord/' + emailToHex(user.account.email) - ); - }) - .then(function (r) { - respOkEmpty(r); - return client.getThen( - '/emailRecord/' + emailToHex(user.account.email) - ); - }) - .then(function (r) { - respOk(r); - var account = r.obj; - var fields = 'accountId,email,emailCode,kA,verifierVersion,authSalt'.split( - ',' - ); - fields.forEach(function (f) { - assert.equal( - user.account[f], - account[f], - 'Both Fields ' + f + ' are the same' - ); - }); - assert.equal( - user.account.emailVerified, - !!account.emailVerified, - 'Both fields emailVerified are the same' - ); - assert(!account.verifyHash, 'verifyHash field should be absent'); - }) - .then(function () { - return client.delThen('/account/' + user.accountId); - }) - .then(function (r) { - respOk(r); - // now make sure this record no longer exists - return client - .headThen('/emailRecord/' + emailToHex(user.account.email)) - .then( - function (r) { - assert( - false, - 'Should not be here, since this account no longer exists' - ); - }, - function (err) { - assert.equal( - err.toString(), - 'NotFoundError', - 'Account not found (no body due to being a HEAD request' - ); - assert.deepEqual( - err.body, - {}, - 'Body contains nothing since this is a HEAD request' - ); - assert.deepEqual(err.statusCode, 404, 'Status Code is 404'); - } - ); - }); - }); - - it('session token handling', () => { - var user = fake.newUserDataHex(); - var verifiedUser = fake.newUserDataHex(); - delete verifiedUser.sessionToken.tokenVerificationId; - - // Fetch all of the session tokens for the account - return client - .getThen('/account/' + user.accountId + '/sessions') - .then(function (r) { - respOk(r); - assert.isArray(r.obj); - assert.lengthOf(r.obj, 0); - - // Create accounts - return P.all([ - client.putThen('/account/' + user.accountId, user.account), - client.putThen( - '/account/' + verifiedUser.accountId, - verifiedUser.account - ), - ]); - }) - .then(function () { - // Fetch all of the session tokens for the account - return client.getThen('/account/' + user.accountId + '/sessions'); - }) - .then(function (r) { - assert.lengthOf(r.obj, 0); - - // Attempt to fetch a non-existent session token - return client.getThen('/sessionToken/' + user.sessionTokenId).then( - function () { - assert( - false, - 'A non-existent session token should not have returned anything' - ); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function () { - // Create a session token - return client.putThen( - '/sessionToken/' + user.sessionTokenId, - user.sessionToken - ); - }) - .then(function (r) { - respOk(r); - - // Fetch all of the session tokens for the account - return client.getThen('/account/' + user.accountId + '/sessions'); - }) - .then(function (r) { - respOk(r); - var sessions = r.obj; - assert.lengthOf(sessions, 1); - assert.equal( - Object.keys(sessions[0]).length, - 20, - 'session has correct properties' - ); - assert.equal( - sessions[0].tokenId, - user.sessionTokenId, - 'tokenId is correct' - ); - assert.equal(sessions[0].uid, user.accountId, 'uid is correct'); - assert.equal( - sessions[0].createdAt, - user.sessionToken.createdAt, - 'createdAt is correct' - ); - assert.equal( - sessions[0].uaBrowser, - user.sessionToken.uaBrowser, - 'uaBrowser is correct' - ); - assert.equal( - sessions[0].uaBrowserVersion, - user.sessionToken.uaBrowserVersion, - 'uaBrowserVersion is correct' - ); - assert.equal( - sessions[0].uaOS, - user.sessionToken.uaOS, - 'uaOS is correct' - ); - assert.equal( - sessions[0].uaOSVersion, - user.sessionToken.uaOSVersion, - 'uaOSVersion is correct' - ); - assert.equal( - sessions[0].uaDeviceType, - user.sessionToken.uaDeviceType, - 'uaDeviceType is correct' - ); - assert.equal( - sessions[0].uaFormFactor, - user.sessionToken.uaFormFactor, - 'uaFormFactor is correct' - ); - assert.equal( - sessions[0].lastAccessTime, - user.sessionToken.createdAt, - 'lastAccessTime is correct' - ); - assert.equal( - sessions[0].authAt, - user.sessionToken.createdAt, - 'authAt is correct' - ); - - // Fetch the session token - return client.getThen('/sessionToken/' + user.sessionTokenId); - }) - .then(function (r) { - var token = r.obj; - - assert.deepEqual( - token.tokenData, - user.sessionToken.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - user.accountId, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - user.sessionToken.createdAt, - 'createdAt matches' - ); - assert.equal( - token.uaBrowser, - user.sessionToken.uaBrowser, - 'uaBrowser matches' - ); - assert.equal( - token.uaBrowserVersion, - user.sessionToken.uaBrowserVersion, - 'uaBrowserVersion matches' - ); - assert.equal(token.uaOS, user.sessionToken.uaOS, 'uaOS matches'); - assert.equal( - token.uaOSVersion, - user.sessionToken.uaOSVersion, - 'uaOSVersion matches' - ); - assert.equal( - token.uaDeviceType, - user.sessionToken.uaDeviceType, - 'uaDeviceType matches' - ); - assert.equal( - token.uaFormFactor, - user.sessionToken.uaFormFactor, - 'uaFormFactor matches' - ); - assert.equal( - token.lastAccessTime, - token.createdAt, - 'lastAccessTime was set' - ); - assert.equal( - token.authAt, - token.createdAt, - 'authAt was set to default' - ); - assert.equal( - !!token.emailVerified, - user.account.emailVerified, - 'emailVerified same as account emailVerified' - ); - assert.equal( - token.email, - user.account.email, - 'token.email same as account email' - ); - assert.deepEqual( - token.emailCode, - user.account.emailCode, - 'token emailCode same as account emailCode' - ); - assert(token.verifierSetAt, 'verifierSetAt is set to a truthy value'); - assert.isAbove(token.accountCreatedAt, 0); - assert.equal( - token.mustVerify, - user.sessionToken.mustVerify, - 'mustVerify is correct' - ); - assert.equal( - token.tokenVerificationId, - user.sessionToken.tokenVerificationId, - 'tokenVerificationId is correct' - ); - - // Create a verified session token - return client.putThen( - '/sessionToken/' + verifiedUser.sessionTokenId, - verifiedUser.sessionToken - ); - }) - .then(function (r) { - respOk(r); - - // Fetch the verified session token - return client.getThen('/sessionToken/' + verifiedUser.sessionTokenId); - }) - .then(function (r) { - var token = r.obj; - - assert.deepEqual( - token.tokenData, - verifiedUser.sessionToken.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - verifiedUser.accountId, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - verifiedUser.sessionToken.createdAt, - 'createdAt matches' - ); - assert.equal( - token.uaBrowser, - verifiedUser.sessionToken.uaBrowser, - 'uaBrowser matches' - ); - assert.equal( - token.uaBrowserVersion, - verifiedUser.sessionToken.uaBrowserVersion, - 'uaBrowserVersion matches' - ); - assert.equal( - token.uaOS, - verifiedUser.sessionToken.uaOS, - 'uaOS matches' - ); - assert.equal( - token.uaOSVersion, - verifiedUser.sessionToken.uaOSVersion, - 'uaOSVersion matches' - ); - assert.equal( - token.uaDeviceType, - verifiedUser.sessionToken.uaDeviceType, - 'uaDeviceType matches' - ); - assert.equal( - token.uaFormFactor, - verifiedUser.sessionToken.uaFormFactor, - 'uaFormFactor matches' - ); - assert.equal( - token.lastAccessTime, - token.createdAt, - 'lastAccessTime was set' - ); - assert.equal( - token.authAt, - token.createdAt, - 'authAt was set to default' - ); - assert.equal( - !!token.emailVerified, - verifiedUser.account.emailVerified, - 'emailVerified same as account emailVerified' - ); - assert.equal( - token.email, - verifiedUser.account.email, - 'token.email same as account email' - ); - assert.deepEqual( - token.emailCode, - verifiedUser.account.emailCode, - 'token emailCode same as account emailCode' - ); - assert(token.verifierSetAt, 'verifierSetAt is set to a truthy value'); - assert.isAbove(token.accountCreatedAt, 0); - assert.isTrue(!!token.mustVerify); - assert.isNull(token.tokenVerificationId); - - // Attempt to verify a non-existent session token - return client - .postThen( - '/tokens/' + crypto.randomBytes(16).toString('hex') + '/verify', - { - uid: user.accountId, - } - ) - .then( - function () { - assert(false, 'Verifying a non-existent token should fail'); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function () { - // Attempt to verify a session token with the wrong uid - return client - .postThen( - '/tokens/' + user.sessionToken.tokenVerificationId + '/verify', - { - uid: crypto.randomBytes(16).toString('hex'), - } - ) - .then( - function () { - assert(false, 'Verifying a non-existent token should fail'); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function () { - // Verify the unverified session token - return client.postThen( - '/tokens/' + user.sessionToken.tokenVerificationId + '/verify', - { - uid: user.accountId, - } - ); - }) - .then(function () { - // Fetch the newly verified session token - return client.getThen('/sessionToken/' + user.sessionTokenId); - }) - .then(function (r) { - assert.equal(!!r.obj.mustVerify, true, 'mustVerify is true'); - assert.isNull(r.obj.tokenVerificationId); - - // Attempt to verify the session token again - return client - .postThen( - '/tokens/' + user.sessionToken.tokenVerificationId + '/verify', - { - uid: user.accountId, - } - ) - .then( - function () { - assert(false, 'Verifying a verified token should have failed'); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function () { - // Update the newly verified session token - return client.postThen( - '/sessionToken/' + user.sessionTokenId + '/update', - { - uaBrowser: 'different browser', - uaBrowserVersion: 'different browser version', - uaOS: 'different OS', - uaOSVersion: 'different OS version', - uaDeviceType: 'different device type', - lastAccessTime: 42, - authAt: 1234567, - } - ); - }) - .then(function (r) { - respOk(r); - - // Fetch all of the session tokens for the account - return client.getThen('/account/' + user.accountId + '/sessions'); - }) - .then(function (r) { - respOk(r); - var sessions = r.obj; - assert.lengthOf(sessions, 1); - assert.equal( - sessions[0].tokenId, - user.sessionTokenId, - 'tokenId is correct' - ); - assert.equal(sessions[0].uid, user.accountId, 'uid is correct'); - assert.equal( - sessions[0].createdAt, - user.sessionToken.createdAt, - 'createdAt is correct' - ); - assert.equal( - sessions[0].uaBrowser, - 'different browser', - 'uaBrowser is correct' - ); - assert.equal( - sessions[0].uaBrowserVersion, - 'different browser version', - 'uaBrowserVersion is correct' - ); - assert.equal(sessions[0].uaOS, 'different OS', 'uaOS is correct'); - assert.equal( - sessions[0].uaOSVersion, - 'different OS version', - 'uaOSVersion is correct' - ); - assert.equal( - sessions[0].uaDeviceType, - 'different device type', - 'uaDeviceType is correct' - ); - assert.equal( - sessions[0].lastAccessTime, - 42, - 'lastAccessTime is correct' - ); - assert.equal(sessions[0].authAt, 1234567, 'authAt is correct'); - - // Fetch the newly verified session token - return client.getThen('/sessionToken/' + user.sessionTokenId); - }) - .then(function (r) { - var token = r.obj; - - assert.deepEqual( - token.tokenData, - user.sessionToken.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - user.accountId, - 'token belongs to this account' - ); - assert.equal( - token.createdAt, - user.sessionToken.createdAt, - 'createdAt was not updated' - ); - assert.equal( - token.uaBrowser, - 'different browser', - 'uaBrowser was updated' - ); - assert.equal( - token.uaBrowserVersion, - 'different browser version', - 'uaBrowserVersion was updated' - ); - assert.equal(token.uaOS, 'different OS', 'uaOS was updated'); - assert.equal( - token.uaOSVersion, - 'different OS version', - 'uaOSVersion was updated' - ); - assert.equal( - token.uaDeviceType, - 'different device type', - 'uaDeviceType was updated' - ); - assert.equal(token.lastAccessTime, 42, 'lastAccessTime was updated'); - assert.equal(token.authAt, 1234567, 'authAt was updated'); - - // Create a device - return client.putThen( - '/account/' + user.accountId + '/device/' + user.deviceId, - user.device - ); - }) - .then(function (r) { - respOk(r); - - // Fetch devices for the account - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - respOk(r); - assert.lengthOf(r.obj, 1); - - // Fetch the session again to make sure device info is included - return client.getThen('/account/' + user.accountId + '/sessions'); - }) - .then(function (r) { - respOk(r); - var sessions = r.obj; - assert.lengthOf(sessions, 1); - var s = sessions[0]; - assert(s.createdAt); - assert.equal(s.deviceCallbackAuthKey.length, 22); - assert(s.deviceCallbackPublicKey); - assert.equal(s.deviceCallbackURL, 'fake callback URL'); - assert.equal(s.deviceCallbackIsExpired, false); - assert(s.deviceCreatedAt); - assert(s.deviceId); - assert.equal(s.deviceName, 'fake device name'); - assert.equal(s.deviceType, 'fake device type'); - assert(s.lastAccessTime); - assert(s.tokenId); - assert(s.uaBrowser); - assert(s.uaBrowserVersion); - assert(s.uaDeviceType); - assert(s.uaFormFactor); - assert(s.uaOS); - assert(s.uaOSVersion); - assert(s.uid); - // Delete both session tokens - return P.all([ - client.delThen('/sessionToken/' + user.sessionTokenId), - client.delThen('/sessionToken/' + verifiedUser.sessionTokenId), - ]); - }) - .then(function (results) { - assert.lengthOf(results, 2); - results.forEach(function (result) { - respOk(result); - }); - - // Fetch devices for the account - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - respOk(r); - assert.lengthOf(r.obj, 0); - - // Fetch all of the session tokens for the account - return client.getThen('/account/' + user.accountId + '/sessions'); - }) - .then(function (r) { - respOk(r); - assert.lengthOf(r.obj, 0); - - // Attempt to fetch a deleted session token - return client.getThen('/sessionToken/' + user.sessionTokenId).then( - () => - assert( - false, - 'Fetching the non-existant sessionToken should have failed' - ), - (err) => testNotFound(err) - ); - }); - }); - - it('device handling', () => { - const user = fake.newUserDataHex(); - const zombieUser = fake.newUserDataHex(); - return client - .getThen('/account/' + user.accountId + '/devices') - .then(function (r) { - respOk(r); - assert.isArray(r.obj); - assert.lengthOf(r.obj, 0); - return client.putThen('/account/' + user.accountId, user.account); - }) - .then(function () { - return client.putThen( - '/sessionToken/' + user.sessionTokenId, - user.sessionToken - ); - }) - .then(function () { - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - assert.lengthOf(r.obj, 0); - return client.putThen( - '/account/' + user.accountId + '/device/' + user.deviceId, - user.device - ); - }) - .then(function (r) { - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - respOk(r); - var devices = r.obj; - assert.lengthOf(devices, 1); - assert.lengthOf(Object.keys(devices[0]), 19); - assert.equal(devices[0].uid, user.accountId, 'uid is correct'); - assert.equal(devices[0].id, user.deviceId, 'id is correct'); - assert.equal( - devices[0].sessionTokenId, - user.sessionTokenId, - 'sessionTokenId is correct' - ); - assert.equal( - devices[0].refreshTokenId, - null, - 'refreshTokenId is correct' - ); - assert.equal( - devices[0].createdAt, - user.device.createdAt, - 'createdAt is correct' - ); - assert.equal(devices[0].name, user.device.name, 'name is correct'); - assert.equal(devices[0].type, user.device.type, 'type is correct'); - assert.equal( - devices[0].callbackURL, - user.device.callbackURL, - 'callbackURL is correct' - ); - assert.equal( - devices[0].callbackPublicKey, - user.device.callbackPublicKey, - 'callbackPublicKey is correct' - ); - assert.equal( - devices[0].callbackAuthKey, - user.device.callbackAuthKey, - 'callbackAuthKey is correct' - ); - assert.equal( - devices[0].callbackIsExpired, - user.device.callbackIsExpired, - 'callbackIsExpired is correct' - ); - assert.deepEqual( - devices[0].availableCommands, - {}, - 'availableCommands is correct' - ); - assert.equal( - devices[0].uaBrowser, - user.sessionToken.uaBrowser, - 'uaBrowser is correct' - ); - assert.equal( - devices[0].uaBrowserVersion, - user.sessionToken.uaBrowserVersion, - 'uaBrowserVersion is correct' - ); - assert.equal( - devices[0].uaOS, - user.sessionToken.uaOS, - 'uaOS is correct' - ); - assert.equal( - devices[0].uaOSVersion, - user.sessionToken.uaOSVersion, - 'uaOSVersion is correct' - ); - assert.equal( - devices[0].uaDeviceType, - user.sessionToken.uaDeviceType, - 'uaDeviceType is correct' - ); - assert.equal( - devices[0].uaFormFactor, - user.sessionToken.uaFormFactor, - 'uaFormFactor is correct' - ); - assert.equal( - devices[0].lastAccessTime, - user.sessionToken.createdAt, - 'lastAccessTime is correct' - ); - }) - .then(function () { - return client.getThen( - '/account/' + user.accountId + '/device/' + user.deviceId - ); - }) - .then(function (r) { - respOk(r); - var device = r.obj; - assert.equal(device.id, user.deviceId, 'id is correct'); - assert.equal( - device.createdAt, - user.device.createdAt, - 'createdAt is correct' - ); - assert.equal(device.name, user.device.name, 'name is correct'); - assert.equal(device.type, user.device.type, 'type is correct'); - assert.equal( - device.callbackURL, - user.device.callbackURL, - 'callbackURL is correct' - ); - assert.equal( - device.callbackPublicKey, - user.device.callbackPublicKey, - 'callbackPublicKey is correct' - ); - assert.equal( - device.callbackAuthKey, - user.device.callbackAuthKey, - 'callbackAuthKey is correct' - ); - assert.equal( - device.callbackIsExpired, - user.device.callbackIsExpired, - 'callbackIsExpired is correct' - ); - assert.deepEqual( - device.availableCommands, - {}, - 'availableCommands is correct' - ); - }) - .then(function () { - return client.getThen( - '/account/' + - user.accountId + - '/tokens/' + - user.sessionToken.tokenVerificationId + - '/device' - ); - }) - .then(function (r) { - respOk(r); - var device = r.obj; - assert.equal(device.id, user.deviceId, 'id is correct'); - assert.equal( - device.createdAt, - user.device.createdAt, - 'createdAt is correct' - ); - assert.equal(device.name, user.device.name, 'name is correct'); - assert.equal(device.type, user.device.type, 'type is correct'); - assert.equal( - device.callbackURL, - user.device.callbackURL, - 'callbackURL is correct' - ); - assert.equal( - device.callbackPublicKey, - user.device.callbackPublicKey, - 'callbackPublicKey is correct' - ); - assert.equal( - device.callbackAuthKey, - user.device.callbackAuthKey, - 'callbackAuthKey is correct' - ); - assert.equal( - device.callbackIsExpired, - user.device.callbackIsExpired, - 'callbackIsExpired is correct' - ); - assert.deepEqual( - device.availableCommands, - {}, - 'availableCommands is correct' - ); - - return client.postThen( - '/account/' + - user.accountId + - '/device/' + - user.deviceId + - '/update', - { - name: 'wibble', - type: 'mobile', - callbackURL: '', - callbackPublicKey: null, - callbackAuthKey: null, - callbackIsExpired: null, - availableCommands: {}, - } - ); - }) - .then(function (r) { - respOk(r); - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - respOk(r); - var devices = r.obj; - assert.lengthOf(devices, 1); - assert.equal(devices[0].uid, user.accountId, 'uid is correct'); - assert.equal(devices[0].id, user.deviceId, 'id is correct'); - assert.equal( - devices[0].sessionTokenId, - user.sessionTokenId, - 'sessionTokenId is correct' - ); - assert.equal( - devices[0].createdAt, - user.device.createdAt, - 'createdAt is correct' - ); - assert.equal(devices[0].name, 'wibble', 'name is correct'); - assert.equal(devices[0].type, 'mobile', 'type is correct'); - assert.equal(devices[0].callbackURL, '', 'callbackURL is correct'); - assert.equal( - devices[0].callbackPublicKey, - user.device.callbackPublicKey, - 'callbackPublicKey is correct' - ); - assert.equal( - devices[0].callbackAuthKey, - user.device.callbackAuthKey, - 'callbackAuthKey is correct' - ); - assert.equal( - devices[0].callbackIsExpired, - false, - 'callbackIsExpired is correct' - ); - assert.deepEqual( - devices[0].availableCommands, - {}, - 'availableCommands is correct' - ); - assert.equal( - devices[0].uaBrowser, - user.sessionToken.uaBrowser, - 'uaBrowser is correct' - ); - assert.equal( - devices[0].uaBrowserVersion, - user.sessionToken.uaBrowserVersion, - 'uaBrowserVersion is correct' - ); - assert.equal( - devices[0].uaOS, - user.sessionToken.uaOS, - 'uaOS is correct' - ); - assert.equal( - devices[0].uaOSVersion, - user.sessionToken.uaOSVersion, - 'uaOSVersion is correct' - ); - assert.equal( - devices[0].uaDeviceType, - user.sessionToken.uaDeviceType, - 'uaDeviceType is correct' - ); - assert.equal( - devices[0].uaFormFactor, - user.sessionToken.uaFormFactor, - 'uaFormFactor is correct' - ); - assert.equal( - devices[0].lastAccessTime, - user.sessionToken.createdAt, - 'lastAccessTime is correct' - ); - - return client.postThen( - '/account/' + - user.accountId + - '/device/' + - user.deviceId + - '/update', - { - sessionTokenId: zombieUser.sessionTokenId, - } - ); - }) - .then(function (r) { - respOk(r); - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - respOk(r); - var devices = r.obj; - assert.lengthOf(devices, 0); - - return client.postThen( - '/account/' + - user.accountId + - '/device/' + - user.deviceId + - '/update', - { - sessionTokenId: user.sessionTokenId, - } - ); - }) - .then(function (r) { - respOk(r); - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - respOk(r); - var devices = r.obj; - assert.lengthOf(devices, 1); - - return client.postThen( - '/account/' + - user.accountId + - '/device/' + - user.deviceId + - '/update', - { - name: '4a6f686e', - } - ); - }) - .then(function (r) { - respOk(r); - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - respOk(r); - var devices = r.obj; - assert.lengthOf(devices, 1); - assert.equal( - devices[0].name, - '4a6f686e', - 'name was not automagically bufferized' - ); - - return client.putThen( - '/account/' + user.accountId + '/device/' + user.oauthDeviceId, - user.oauthDevice - ); - }) - .then(function (r) { - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - respOk(r); - var devices = r.obj; - assert.lengthOf(devices, 2); - const sessionDevice = devices.find((d) => d.sessionTokenId); - const oauthDevice = devices.find((d) => d.refreshTokenId); - - assert.equal(sessionDevice.uid, user.accountId, 'uid is correct'); - assert.equal( - sessionDevice.sessionTokenId, - user.sessionTokenId, - 'sessionTokenId is correct' - ); - assert.equal( - sessionDevice.refreshTokenId, - null, - 'refreshTokenId is correct' - ); - - assert.lengthOf(Object.keys(oauthDevice), 19); - assert.equal(oauthDevice.uid, user.accountId, 'uid is correct'); - assert.equal(oauthDevice.id, user.oauthDeviceId, 'id is correct'); - assert.equal( - oauthDevice.sessionTokenId, - null, - 'sessionTokenId is correct' - ); - assert.equal( - oauthDevice.refreshTokenId, - user.refreshTokenId, - 'refreshTokenId is correct' - ); - assert.equal( - oauthDevice.createdAt, - user.oauthDevice.createdAt, - 'createdAt is correct' - ); - assert.equal( - oauthDevice.name, - user.oauthDevice.name, - 'name is correct' - ); - assert.equal( - oauthDevice.type, - user.oauthDevice.type, - 'type is correct' - ); - assert.equal( - oauthDevice.callbackURL, - user.oauthDevice.callbackURL, - 'callbackURL is correct' - ); - assert.equal( - oauthDevice.callbackPublicKey, - user.oauthDevice.callbackPublicKey, - 'callbackPublicKey is correct' - ); - assert.equal( - oauthDevice.callbackAuthKey, - user.oauthDevice.callbackAuthKey, - 'callbackAuthKey is correct' - ); - assert.equal( - oauthDevice.callbackIsExpired, - user.oauthDevice.callbackIsExpired, - 'callbackIsExpired is correct' - ); - assert.deepEqual( - oauthDevice.availableCommands, - {}, - 'availableCommands is correct' - ); - assert.isNull(oauthDevice.uaBrowser); - assert.isNull(oauthDevice.uaBrowserVersion); - assert.isNull(oauthDevice.uaOS); - assert.isNull(oauthDevice.uaOSVersion); - assert.isNull(oauthDevice.uaDeviceType); - assert.isNull(oauthDevice.uaFormFactor); - assert.isNull(oauthDevice.lastAccessTime); - - return client.postThen( - '/account/' + - user.accountId + - '/device/' + - oauthDevice.id + - '/update', - { - name: 'a new device name', - } - ); - }) - .then(function (r) { - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - respOk(r); - var devices = r.obj; - assert.lengthOf(devices, 2); - const sessionDevice = devices.find((d) => d.sessionTokenId); - const oauthDevice = devices.find((d) => d.refreshTokenId); - - assert.equal( - sessionDevice.sessionTokenId, - user.sessionTokenId, - 'sessionTokenId is correct' - ); - assert.isNull(sessionDevice.refreshTokenId); - - assert.isNull(oauthDevice.sessionTokenId); - assert.equal( - oauthDevice.refreshTokenId, - oauthDevice.refreshTokenId, - 'refreshTokenId is correct' - ); - assert.equal( - oauthDevice.name, - 'a new device name', - 'name is correct' - ); - - return client.delThen( - '/account/' + user.accountId + '/device/' + user.oauthDeviceId - ); - }) - .then(function (r) { - respOk(r); - assert.deepEqual(r.obj, { - sessionTokenId: null, - refreshTokenId: user.refreshTokenId, - }); - - return client.delThen( - '/account/' + user.accountId + '/device/' + user.deviceId - ); - }) - .then(function (r) { - respOk(r); - assert.deepEqual(r.obj, { - sessionTokenId: user.sessionTokenId, - refreshTokenId: null, - }); - return client.getThen('/account/' + user.accountId + '/devices'); - }) - .then(function (r) { - respOk(r); - assert.lengthOf(r.obj, 0); - - return client - .getThen('/account/' + user.accountId + '/device' + user.deviceId) - .then( - function () { - assert( - false, - 'A non-existent deviceId should not have returned anything' - ); - }, - function (err) { - testNotFound(err); - } - ); - }); - }); - - it('key fetch token handling', () => { - var user = fake.newUserDataHex(); - user.sessionToken.tokenVerificationId = - user.keyFetchToken.tokenVerificationId; - var verifiedUser = fake.newUserDataHex(); - delete verifiedUser.keyFetchToken.tokenVerificationId; - - // Create accounts - return P.all([ - client.putThen('/account/' + user.accountId, user.account), - client.putThen( - '/account/' + verifiedUser.accountId, - verifiedUser.account - ), - ]) - .then(function () { - // Attempt to fetch a non-existent key fetch token - return client.getThen('/keyFetchToken/' + user.keyFetchTokenId).then( - function () { - assert( - false, - 'A non-existent keyFetchToken should not have returned anything' - ); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function () { - // Attempt to fetch a non-existent key fetch token with its verification state - return client - .getThen('/keyFetchToken/' + user.keyFetchTokenId + '/verified') - .then( - function () { - assert( - false, - 'A non-existent keyFetchToken should not have returned anything' - ); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function () { - // Create a session token and a key fetch token - return P.all([ - client.putThen( - '/sessionToken/' + user.sessionTokenId, - user.sessionToken - ), - client.putThen( - '/keyFetchToken/' + user.keyFetchTokenId, - user.keyFetchToken - ), - ]); - }) - .then(function (r) { - respOk(r[0]); - respOk(r[1]); - - // Fetch the key fetch token - return client.getThen('/keyFetchToken/' + user.keyFetchTokenId); - }) - .then(function (r) { - var token = r.obj; - - // tokenId is not returned from db.keyFetchToken() - assert.deepEqual( - token.uid, - user.accountId, - 'token belongs to this account' - ); - assert.deepEqual( - token.authKey, - user.keyFetchToken.authKey, - 'authKey matches' - ); - assert.deepEqual( - token.keyBundle, - user.keyFetchToken.keyBundle, - 'keyBundle matches' - ); - assert(token.createdAt, 'Got a createdAt'); - assert.equal(!!token.emailVerified, user.account.emailVerified); - assert(token.verifierSetAt, 'verifierSetAt is set to a truthy value'); - assert.isUndefined(token.tokenVerificationId); - - // Fetch the key fetch token with its verification state - return client.getThen( - '/keyFetchToken/' + user.keyFetchTokenId + '/verified' - ); - }) - .then(function (r) { - var token = r.obj; - - assert.deepEqual( - token.uid, - user.accountId, - 'token belongs to this account' - ); - assert.deepEqual( - token.authKey, - user.keyFetchToken.authKey, - 'authKey matches' - ); - assert.deepEqual( - token.keyBundle, - user.keyFetchToken.keyBundle, - 'keyBundle matches' - ); - assert(token.createdAt, 'Got a createdAt'); - assert.equal(!!token.emailVerified, user.account.emailVerified); - assert(token.verifierSetAt, 'verifierSetAt is set to a truthy value'); - assert.equal( - token.tokenVerificationId, - user.keyFetchToken.tokenVerificationId, - 'tokenVerificationId is correct' - ); - - // Fetch the session token with its verification state - return client.getThen('/sessionToken/' + user.sessionTokenId); - }) - .then(function (r) { - assert.equal( - r.obj.tokenVerificationId, - user.sessionToken.tokenVerificationId, - 'tokenVerificationId is correct' - ); - - // Attempt to verify a non-existent key fetch token - return client - .postThen( - '/tokens/' + crypto.randomBytes(16).toString('hex') + '/verify', - { - uid: user.accountId, - } - ) - .then( - function () { - assert(false, 'Verifying a non-existent token should fail'); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function (r) { - // Attempt to verify a key fetch token with the wrong uid - return client - .postThen( - '/tokens/' + user.keyFetchToken.tokenVerificationId + '/verify', - { - uid: crypto.randomBytes(16).toString('hex'), - } - ) - .then( - function () { - assert(false, 'Verifying a non-existent token should fail'); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function (r) { - // Verify the key fetch token - return client.postThen( - '/tokens/' + user.keyFetchToken.tokenVerificationId + '/verify', - { - uid: user.accountId, - } - ); - }) - .then(function () { - // Fetch the key fetch token - return client.getThen('/keyFetchToken/' + user.keyFetchTokenId); - }) - .then(function (r) { - assert.isUndefined(r.obj.tokenVerificationId); - - // Fetch the key fetch token with its verification state - return client.getThen( - '/keyFetchToken/' + user.keyFetchTokenId + '/verified' - ); - }) - .then(function (r) { - assert.isNull(r.obj.tokenVerificationId); - - // Fetch the session token with its verification state - return client.getThen('/sessionToken/' + user.sessionTokenId); - }) - .then(function (r) { - assert.isNull(r.obj.tokenVerificationId); - - // Attempt to verify the key fetch token again - return client - .postThen( - '/tokens/' + user.keyFetchToken.tokenVerificationId + '/verify', - { - uid: user.accountId, - } - ) - .then( - function () { - assert(false, 'Verifying a verified token should have failed'); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function () { - // Create a verified key fetch token - return client.putThen( - '/keyFetchToken/' + verifiedUser.keyFetchTokenId, - verifiedUser.keyFetchToken - ); - }) - .then(function () { - // Fetch the verified key fetch token - return client.getThen( - '/keyFetchToken/' + verifiedUser.keyFetchTokenId - ); - }) - .then(function (r) { - assert.isUndefined(r.obj.tokenVerificationId); - - // Fetch the verified key fetch token with its verification state - return client.getThen( - '/keyFetchToken/' + verifiedUser.keyFetchTokenId + '/verified' - ); - }) - .then(function (r) { - assert.isNull(r.obj.tokenVerificationId); - // Delete both key fetch tokens - return P.all([ - client.delThen('/keyFetchToken/' + user.keyFetchTokenId), - client.delThen('/keyFetchToken/' + verifiedUser.keyFetchTokenId), - ]); - }) - .then(function (results) { - assert.lengthOf(results, 2); - results.forEach(function (result) { - respOk(result); - }); - - // Attempt to fetch a deleted key fetch token - return client.getThen('/keyFetchToken/' + user.keyFetchTokenId).then( - function () { - assert( - false, - 'Fetching the non-existant keyFetchToken should have failed' - ); - }, - function (err) { - testNotFound(err); - } - ); - }); - }); - - it('account reset token handling', () => { - var user = fake.newUserDataHex(); - return client - .putThen('/account/' + user.accountId, user.account) - .then(function () { - return client - .getThen('/accountResetToken/' + user.accountResetTokenId) - .then( - function () { - assert( - false, - 'A non-existant session token should not have returned anything' - ); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function () { - return client.putThen( - '/passwordForgotToken/' + user.passwordForgotTokenId, - user.passwordForgotToken - ); - }) - .then(function (r) { - respOk(r); - // now, verify the password (which inserts the accountResetToken) - return client.postThen( - '/passwordForgotToken/' + user.passwordForgotTokenId + '/verified', - user.accountResetToken - ); - }) - .then(function (r) { - respOk(r); - // check the accountResetToken exists - return client.getThen( - '/accountResetToken/' + user.accountResetTokenId - ); - }) - .then(function (r) { - respOk(r); - return client.getThen( - '/accountResetToken/' + user.accountResetTokenId - ); - }) - .then(function (r) { - var token = r.obj; - - // tokenId is not returned from db.accountResetToken() - assert.deepEqual( - token.uid, - user.accountId, - 'token belongs to this account' - ); - assert.deepEqual( - token.tokenData, - user.accountResetToken.data, - 'token data matches' - ); - assert(token.createdAt, 'Got a createdAt'); - assert(token.verifierSetAt, 'verifierSetAt is set to a truthy value'); - - // now delete it - return client.delThen( - '/accountResetToken/' + user.accountResetTokenId - ); - }) - .then(function (r) { - respOk(r); - // now make sure the token no longer exists - return client - .getThen('/accountResetToken/' + user.accountResetTokenId) - .then( - function () { - assert( - false, - 'Fetching the non-existant accountResetToken should have failed' - ); - }, - function (err) { - testNotFound(err); - } - ); - }); - }); - - it('password change token handling', () => { - var user = fake.newUserDataHex(); - return client - .putThen('/account/' + user.accountId, user.account) - .then(function () { - return client - .getThen('/passwordChangeToken/' + user.passwordChangeTokenId) - .then( - function () { - assert( - false, - 'A non-existant session token should not have returned anything' - ); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function (r) { - return client.putThen( - '/passwordChangeToken/' + user.passwordChangeTokenId, - user.passwordChangeToken - ); - }) - .then(function (r) { - respOk(r); - return client.getThen( - '/passwordChangeToken/' + user.passwordChangeTokenId - ); - }) - .then(function (r) { - var token = r.obj; - - // tokenId is not returned from db.passwordChangeToken() - assert.deepEqual( - token.tokenData, - user.passwordChangeToken.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - user.accountId, - 'token belongs to this account' - ); - assert(token.createdAt, 'Got a createdAt'); - assert(token.verifierSetAt, 'verifierSetAt is set to a truthy value'); - - // now delete it - return client.delThen( - '/passwordChangeToken/' + user.passwordChangeTokenId - ); - }) - .then(function (r) { - respOk(r); - // now make sure the token no longer exists - return client - .getThen('/passwordChangeToken/' + user.passwordChangeTokenId) - .then( - function () { - assert( - false, - 'Fetching the non-existant passwordChangeToken should have failed' - ); - }, - function (err) { - testNotFound(err); - } - ); - }); - }); - - it('password forgot token handling', () => { - var user = fake.newUserDataHex(); - return client - .putThen('/account/' + user.accountId, user.account) - .then(function () { - return client - .getThen('/passwordForgotToken/' + user.passwordForgotTokenId) - .then( - function () { - assert( - false, - 'A non-existant session token should not have returned anything' - ); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function () { - return client.putThen( - '/passwordForgotToken/' + user.passwordForgotTokenId, - user.passwordForgotToken - ); - }) - .then(function (r) { - respOk(r); - return client.getThen( - '/passwordForgotToken/' + user.passwordForgotTokenId - ); - }) - .then(function (r) { - var token = r.obj; - - // tokenId is not returned from db.passwordForgotToken() - assert.deepEqual( - token.tokenData, - user.passwordForgotToken.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - user.accountId, - 'token belongs to this account' - ); - assert(token.createdAt, 'Got a createdAt'); - assert.deepEqual(token.passCode, user.passwordForgotToken.passCode); - assert.equal( - token.tries, - user.passwordForgotToken.tries, - 'Tries is correct' - ); - assert.equal(token.email, user.account.email); - assert(token.verifierSetAt, 'verifierSetAt is set to a truthy value'); - - // now update this token (with extra tries) - user.passwordForgotToken.tries += 1; - return client.postThen( - '/passwordForgotToken/' + user.passwordForgotTokenId + '/update', - user.passwordForgotToken - ); - }) - .then(function (r) { - respOk(r); - - // re-fetch this token - return client.getThen( - '/passwordForgotToken/' + user.passwordForgotTokenId - ); - }) - .then(function (r) { - var token = r.obj; - - // tokenId is not returned from db.passwordForgotToken() - assert.deepEqual( - token.tokenData, - user.passwordForgotToken.data, - 'token data matches' - ); - assert.deepEqual( - token.uid, - user.accountId, - 'token belongs to this account' - ); - assert(token.createdAt, 'Got a createdAt'); - assert.deepEqual(token.passCode, user.passwordForgotToken.passCode); - assert.equal( - token.tries, - user.passwordForgotToken.tries, - 'Tries is correct (now incremented)' - ); - assert.equal(token.email, user.account.email); - assert(token.verifierSetAt, 'verifierSetAt is set to a truthy value'); - - // now delete it - return client.delThen( - '/passwordForgotToken/' + user.passwordForgotTokenId - ); - }) - .then(function (r) { - respOk(r); - // now make sure the token no longer exists - return client - .getThen('/passwordForgotToken/' + user.passwordForgotTokenId) - .then( - function () { - assert( - false, - 'Fetching the non-existant passwordForgotToken should have failed' - ); - }, - function (err) { - testNotFound(err); - } - ); - }); - }); - - it('password forgot token verified', () => { - var user = fake.newUserDataHex(); - return client - .putThen('/account/' + user.accountId, user.account) - .then(function (r) { - respOk(r); - return client.putThen( - '/passwordForgotToken/' + user.passwordForgotTokenId, - user.passwordForgotToken - ); - }) - .then(function (r) { - respOk(r); - // now, verify the password (which inserts the accountResetToken) - return client.postThen( - '/passwordForgotToken/' + user.passwordForgotTokenId + '/verified', - user.accountResetToken - ); - }) - .then(function (r) { - respOk(r); - // check the accountResetToken exists - return client.getThen( - '/accountResetToken/' + user.accountResetTokenId - ); - }) - .then(function (r) { - var token = r.obj; - - // tokenId is not returned from db.accountResetToken() - assert.deepEqual( - token.uid, - user.accountId, - 'token belongs to this account' - ); - assert.deepEqual( - token.tokenData, - user.accountResetToken.data, - 'token data matches' - ); - assert(token.createdAt, 'Got a createdAt'); - assert(token.verifierSetAt, 'verifierSetAt is set to a truthy value'); - - // make sure then passwordForgotToken no longer exists - return client - .getThen('/passwordForgotToken/' + user.passwordForgotTokenId) - .then( - function () { - assert( - false, - 'Fetching the non-existant passwordForgotToken should have failed' - ); - }, - function (err) { - testNotFound(err); - } - ); - }) - .then(function () { - //check that the account has been verified - return client.getThen( - '/emailRecord/' + emailToHex(user.account.email) - ); - }) - .then(function (r) { - respOk(r); - var account = r.obj; - assert.equal( - true, - !!account.emailVerified, - 'emailVerified is now true' - ); - }); - }); - - it('locale', () => { - var user = fake.newUserDataHex(); - return client - .putThen('/account/' + user.accountId, user.account) - .then(function (r) { - respOk(r); - return client.putThen( - '/sessionToken/' + user.sessionTokenId, - user.sessionToken - ); - }) - .then(function (r) { - respOk(r); - return client.postThen('/account/' + user.accountId + '/locale', { - locale: 'en-US', - }); - }) - .then(function (r) { - respOk(r); - return client.getThen('/sessionToken/' + user.sessionTokenId); - }) - .then(function (r) { - respOk(r); - assert.equal('en-US', r.obj.locale, 'locale was set properly'); - }); - }); - - it('unblock codes', () => { - var user = fake.newUserDataHex(); - var uid = user.accountId; - var unblockCode = crypto.randomBytes(4).toString('hex'); - return client - .putThen('/account/' + uid + '/unblock/' + unblockCode) - .then(function (r) { - respOkEmpty(r); - return client.delThen('/account/' + uid + '/unblock/' + unblockCode); - }) - .then(function (r) { - respOk(r); - assert.isAtMost(r.obj.createdAt, Date.now()); - return client - .delThen('/account/' + uid + '/unblock/' + unblockCode) - .then( - function (r) { - assert( - false, - 'This request should have failed (instead it suceeded)' - ); - }, - function (err) { - testNotFound(err); - } - ); - }); - }); - - it('email bounces', () => { - var email = Math.random() + '@email.bounces'; - return client - .postThen('/emailBounces', { - email: email, - bounceType: 'Permanent', - bounceSubType: 'NoEmail', - }) - .then(function (r) { - respOkEmpty(r); - return client.getThen( - '/emailBounces/' + Buffer.from(email).toString('hex') - ); - }) - .then(function (r) { - respOk(r); - assert.lengthOf(r.obj, 1); - assert.equal(r.obj[0].email, email); - assert.isAtMost(r.obj[0].createdAt, Date.now()); - }); - }); - - it('sign-in codes', () => { - const user = fake.newUserDataHex(); - const now = Date.now(); - const signinCode = crypto.randomBytes(6).toString('hex'); - const flowId = crypto.randomBytes(32).toString('hex'); - const goodTimestamp = now - 1; - const badTimestamp = now - cfg.signinCodesMaxAge - 1; - - // Create an account - return client - .putThen(`/account/${user.accountId}`, user.account) - .then(() => { - // Create a sign-in code - return client.putThen(`/signinCodes/${signinCode}`, { - uid: user.accountId, - createdAt: goodTimestamp, - flowId, - }); - }) - .then((r) => { - respOkEmpty(r); - - // Attempt to create a duplicate sign-in code - return client - .putThen(`/signinCodes/${signinCode}`, { - uid: user.accountId, - createdAt: goodTimestamp, - flowId: crypto.randomBytes(32).toString('hex'), - }) - .then( - () => - assert(false, 'creating a duplicate sign-in code should fail'), - (err) => testConflict(err) - ); - }) - .then(() => { - // Consume the sign-in code - return client.postThen(`/signinCodes/${signinCode}/consume`); - }) - .then((r) => { - respOk(r); - assert.deepEqual( - r.obj, - { - email: user.account.email, - flowId, - }, - 'consuming a sign-in code should return the email address and flowId' - ); - - // Attempt to consume the sign-in code again - return client.postThen(`/signinCodes/${signinCode}/consume`).then( - () => - assert(false, 'consuming a consumed sign-in code should fail'), - (err) => testNotFound(err) - ); - }) - .then(() => { - // Create an expired sign-in code - return client.putThen(`/signinCodes/${signinCode}`, { - uid: user.accountId, - createdAt: badTimestamp, - }); - }) - .then((r) => { - respOkEmpty(r); - - // Attempt to consume the expired sign-in code - return client.postThen(`/signinCodes/${signinCode}/consume`).then( - () => - assert(false, 'consuming an expired sign-in code should fail'), - (err) => testNotFound(err) - ); - }); - }); - - describe('reset account tokens', () => { - let user; - before(() => { - user = fake.newUserDataHex(); - return client.putThen(`/account/${user.accountId}`, user.account); - }); - - it('should remove password forgot token', () => { - return client - .putThen( - '/passwordForgotToken/' + user.passwordForgotTokenId, - user.passwordForgotToken - ) - .then((r) => { - respOk(r); - return client.postThen(`/account/${user.accountId}/resetTokens`); - }) - .then((r) => { - respOk(r); - return client - .getThen('/passwordForgotToken/' + user.passwordForgotTokenId) - .then( - () => { - assert( - false, - 'This request should have failed (instead it succeeded)' - ); - }, - (err) => { - testNotFound(err); - } - ); - }); - }); - - it('should remove password change token', () => { - return client - .putThen( - '/passwordChangeToken/' + user.passwordChangeTokenId, - user.passwordChangeToken - ) - .then((r) => { - respOk(r); - return client.postThen(`/account/${user.accountId}/resetTokens`); - }) - .then((r) => { - respOk(r); - return client - .getThen('/passwordChangeToken/' + user.passwordChangeTokenId) - .then( - () => { - assert( - false, - 'This request should have failed (instead it succeeded)' - ); - }, - (err) => { - testNotFound(err); - } - ); - }); - }); - - it('should remove account reset token', () => { - return client - .putThen( - '/passwordForgotToken/' + user.passwordForgotTokenId, - user.passwordForgotToken - ) - .then((r) => { - respOk(r); - // now, verify the password (which inserts the accountResetToken) - return client.postThen( - '/passwordForgotToken/' + - user.passwordForgotTokenId + - '/verified', - user.accountResetToken - ); - }) - .then(function (r) { - respOk(r); - // check the accountResetToken exists - return client.getThen( - '/accountResetToken/' + user.accountResetTokenId - ); - }) - .then((r) => { - respOk(r); - return client.postThen(`/account/${user.accountId}/resetTokens`); - }) - .then(function (r) { - respOk(r); - // check the accountResetToken exists - return client - .getThen('/accountResetToken/' + user.accountResetTokenId) - .then( - () => { - assert( - false, - 'This request should have failed (instead it succeeded)' - ); - }, - (err) => { - testNotFound(err); - } - ); - }); - }); - }); - - it('GET an unknown path', () => { - var p = captureFailureEvent(server); - return client - .getThen('/foo') - .then( - function (r) { - assert( - false, - 'This request should have failed (instead it suceeded)' - ); - }, - function (err) { - testNotFound(err); - return p; - } - ) - .then(function () { - assert('server emitted a failure event'); - }); - }); - - it('PUT an unknown path', () => { - var p = captureFailureEvent(server); - return client - .putThen('/bar', {}) - .then( - function (r) { - assert( - false, - 'This request should have failed (instead it suceeded)' - ); - }, - function (err) { - testNotFound(err); - return p; - } - ) - .then(function () { - assert('server emitted a failure event'); - }); - }); - - it('POST an unknown path', () => { - var p = captureFailureEvent(server); - return client.postThen('/baz', {}).then( - function (r) { - assert( - false, - 'This request should have failed (instead it suceeded)' - ); - }, - function (err) { - testNotFound(err); - return p; - } - ); - }); - - it('DELETE an unknown path', () => { - var p = captureFailureEvent(server); - return client.delThen('/qux').then( - function (r) { - assert( - false, - 'This request should have failed (instead it suceeded)' - ); - }, - function (err) { - testNotFound(err); - return p; - } - ); - }); - - it('HEAD an unknown path', () => { - var p = captureFailureEvent(server); - return client.headThen('/wibble').then( - function (r) { - assert( - false, - 'This request should have failed (instead it suceeded)' - ); - }, - function (err) { - assert.deepEqual( - err.body, - {}, - 'Body is empty since this is a HEAD request' - ); - return p; - } - ); - }); - - it('rejection of invalid hex data', () => { - var user = fake.newUserDataHex(); - user.account.kA = 'invalid-hex-data'; - return client.putThen('/account/' + user.accountId, user.account).then( - function () { - assert(false, 'Invalid hex data should cause the request to fail'); - }, - function (err) { - assert.equal(err.statusCode, 400, 'returns a 400'); - } - ); - }); - - describe('add account, add email, change email', () => { - let user, secondEmailRecord; - - before(() => { - user = fake.newUserDataHex(); - secondEmailRecord = user.email; - - // Create account - return client - .putThen('/account/' + user.accountId, user.account) - .then(function (r) { - respOkEmpty(r); - // Create secondary email - return client.postThen( - '/account/' + user.accountId + '/emails', - user.email - ); - }) - .then(function (r) { - respOk(r); - const emailCodeHex = secondEmailRecord.emailCode.toString('hex'); - // Verify secondary email - return client.postThen( - '/account/' + user.accountId + '/verifyEmail/' + emailCodeHex - ); - }) - .then(function (r) { - respOkEmpty(r); - return client.getThen('/account/' + user.accountId + '/emails'); - }) - .then(function (r) { - respOk(r); - const result = r.obj; - assert.equal( - result[0].email, - user.account.email, - 'matches account email' - ); - assert.equal( - !!result[0].isPrimary, - true, - 'isPrimary is true on account email' - ); - assert.equal( - !!result[0].isVerified, - !!user.account.emailVerified, - 'matches account emailVerified' - ); - - assert.equal( - result[1].email, - secondEmailRecord.email, - 'matches secondEmail email' - ); - assert.equal( - !!result[1].isPrimary, - false, - 'isPrimary is false on secondEmail email' - ); - assert.equal( - !!result[1].isVerified, - true, - 'matches secondEmail isVerified' - ); - }); - }); - - it('should change email', () => { - return client - .postThen( - '/email/' + - emailToHex(secondEmailRecord.email) + - '/account/' + - user.accountId - ) - .then((r) => { - respOkEmpty(r); - return client.getThen('/account/' + user.accountId + '/emails'); - }) - .then(function (r) { - respOk(r); - const result = r.obj; - - assert.equal( - result[0].email, - secondEmailRecord.email, - 'matches secondEmail email' - ); - assert.equal( - !!result[0].isPrimary, - true, - 'isPrimary is true on secondEmail email' - ); - assert.equal( - !!result[0].isVerified, - true, - 'matches secondEmail isVerified' - ); - - assert.equal( - result[1].email, - user.account.email, - 'matches account email' - ); - assert.equal( - !!result[1].isPrimary, - false, - 'isPrimary is false on account email' - ); - assert.equal( - !!result[1].isVerified, - !!user.account.emailVerified, - 'matches account emailVerified' - ); - }); - }); - }); - - describe('add account, verify session and keyfetch with tokenVerificationCode', () => { - let user; - - before(() => { - user = fake.newUserDataHex(); - - return client - .putThen('/account/' + user.accountId, user.account) - .then(function (r) { - respOkEmpty(r); - }); - }); - - it('should verify session and keyfetch with tokenVerificationCode', () => { - return P.all([ - client.putThen( - '/sessionToken/' + user.sessionTokenId, - user.sessionToken - ), - client.putThen( - '/keyFetchToken/' + user.keyFetchTokenId, - user.keyFetchToken - ), - ]) - .spread((sessionToken, keyFetchToken) => { - respOkEmpty(sessionToken); - respOkEmpty(keyFetchToken); - return client.getThen('/sessionToken/' + user.sessionTokenId); - }) - .then((r) => { - respOk(r); - return client.postThen( - '/tokens/' + - user.sessionToken.tokenVerificationCode + - '/verifyCode', - { - uid: user.accountId, - } - ); - }) - .then((r) => { - respOk(r); - return P.all([ - client.getThen('/sessionToken/' + user.sessionTokenId), - client.getThen( - '/keyFetchToken/' + user.keyFetchTokenId + '/verified' - ), - ]); - }) - .spread((sessionTokenResp, keyFetchTokenResp) => { - respOk(sessionTokenResp); - respOk(keyFetchTokenResp); - const sessionToken = sessionTokenResp.obj; - const keyFetchToken = keyFetchTokenResp.obj; - assert.isNull(sessionToken.tokenVerificationId); - assert.notOk(sessionToken.tokenVerificationCodeHash); - assert.notOk(sessionToken.tokenVerificationCodeExpiresAt); - assert.isNull(keyFetchToken.tokenVerificationId); - }); - }); - }); - - describe('totp tokens', () => { - let user; - beforeEach(() => { - user = fake.newUserDataHex(); - return client - .putThen('/account/' + user.accountId, user.account) - .then((r) => { - respOkEmpty(r); - return client.putThen('/totp/' + user.accountId, user.totp); - }) - .then((r) => respOkEmpty(r)); - }); - - it('should get totp token', () => { - return client.getThen('/totp/' + user.accountId).then((r) => { - const result = r.obj; - assert.equal( - result.sharedSecret, - user.totp.sharedSecret, - 'sharedSecret set' - ); - assert.equal(result.epoch, user.totp.epoch, 'epoch set'); - assert.equal(result.verified, user.totp.verified, 'verified set'); - assert.equal(result.enabled, user.totp.enabled, 'enabled set'); - }); - }); - - it('should delete totp token', () => { - return client.delThen('/totp/' + user.accountId).then((r) => { - respOkEmpty(r); - return client - .getThen('/totp/' + user.accountId) - .then(assert.fail, (err) => testNotFound(err)); - }); - }); - - it('should update totp token', () => { - const totpOptions = { - verified: true, - enabled: true, - }; - return client - .postThen('/totp/' + user.accountId + '/update', totpOptions) - .then((r) => { - respOkEmpty(r); - return client.getThen('/totp/' + user.accountId).then((r) => { - const result = r.obj; - assert.equal( - result.sharedSecret, - user.totp.sharedSecret, - 'sharedSecret set' - ); - assert.equal(result.epoch, user.totp.epoch, 'epoch set'); - assert.equal( - result.verified, - totpOptions.verified, - 'verified set' - ); - assert.equal(result.enabled, user.totp.enabled, 'enable set'); - }); - }); - }); - }); - - describe('should set session verification method', () => { - let user; - beforeEach(() => { - user = fake.newUserDataHex(); - return client - .putThen('/account/' + user.accountId, user.account) - .then((r) => { - respOkEmpty(r); - return client.putThen('/totp/' + user.accountId, user.totp); - }) - .then((r) => respOkEmpty(r)) - .then(() => - client.putThen( - '/sessionToken/' + user.sessionTokenId, - user.sessionToken - ) - ) - .then((res) => respOkEmpty(res)); - }); - - it('set session verification method - totp-2fa', () => { - const verifyOptions = { - verificationMethod: 'totp-2fa', - }; - return client - .postThen( - '/tokens/' + user.sessionTokenId + '/verifyWithMethod', - verifyOptions - ) - .then((res) => { - respOkEmpty(res); - return client.getThen('/sessionToken/' + user.sessionTokenId); - }) - .then((sessionToken) => { - sessionToken = sessionToken.obj; - assert.equal( - sessionToken.verificationMethod, - 2, - 'verificationMethod set' - ); - assert.ok(sessionToken.verifiedAt, 'verifiedAt set'); - }); - }); - - it('set session verification method - recovery-code', () => { - const verifyOptions = { - verificationMethod: 'recovery-code', - }; - return client - .postThen( - '/tokens/' + user.sessionTokenId + '/verifyWithMethod', - verifyOptions - ) - .then((res) => { - respOkEmpty(res); - return client.getThen('/sessionToken/' + user.sessionTokenId); - }) - .then((sessionToken) => { - sessionToken = sessionToken.obj; - assert.equal( - sessionToken.verificationMethod, - 3, - 'verificationMethod set' - ); - assert.ok(sessionToken.verifiedAt, 'verifiedAt set'); - }); - }); - }); - - describe('recovery codes', function () { - // Consuming recovery codes is more time intensive since the scrypt hashes need - // to be compared. Let set timeout higher than 2s default. - this.timeout(12000); - - let user; - beforeEach(() => { - user = fake.newUserDataHex(); - return client - .putThen('/account/' + user.accountId, user.account) - .then((r) => { - respOkEmpty(r); - }); - }); - - it('should generate new recovery codes', () => { - return client - .postThen('/account/' + user.accountId + '/recoveryCodes', { - count: 8, - }) - .then((res) => { - const codes = res.obj; - assert.lengthOf(codes, 8); - }); - }); - - it('should fail to consume unknown recovery code', () => { - return client - .postThen( - '/account/' + user.accountId + '/recoveryCodes/' + '12345678' - ) - .then(assert.fail, (err) => { - testNotFound(err); - }); - }); - - it('should consume recovery code', () => { - return client - .postThen('/account/' + user.accountId + '/recoveryCodes', { - count: 8, - }) - .then((res) => { - const codes = res.obj; - assert.lengthOf(codes, 8); - return client.postThen( - '/account/' + user.accountId + '/recoveryCodes/' + codes[0] - ); - }) - .then((res) => { - const result = res.obj; - assert.equal( - result.remaining, - 7, - 'correct number of remaining codes' - ); - }); - }); - }); - - describe('recovery key', function () { - let user, recoveryKey; - beforeEach(() => { - user = fake.newUserDataHex(); - return client - .putThen('/account/' + user.accountId, user.account) - .then((r) => { - respOkEmpty(r); - recoveryKey = { - recoveryKeyId: crypto.randomBytes(16).toString('hex'), - recoveryData: crypto.randomBytes(64).toString('hex'), - enabled: true, - }; - return client.postThen( - '/account/' + user.accountId + '/recoveryKey', - recoveryKey - ); - }) - .then((r) => { - respOkEmpty(r); - }); - }); - - it('should create a recovery key', () => { - assert.ok(recoveryKey); - }); - - it('should get a recovery key', () => { - return client - .getThen( - '/account/' + - user.accountId + - '/recoveryKey/' + - recoveryKey.recoveryKeyId - ) - .then((res) => { - const recoveryKeyResult = res.obj; - assert.equal( - recoveryKeyResult.recoveryData, - recoveryKey.recoveryData, - 'recoveryData match' - ); - assert.equal(recoveryKeyResult.enabled, true); - assert.ok(recoveryKeyResult.createdAt); - }); - }); - - it('should delete a recovery key', () => { - return client - .delThen('/account/' + user.accountId + '/recoveryKey') - .then((r) => { - respOkEmpty(r); - }); - }); - - it('should check if recovery key exists', () => { - return client - .getThen('/account/' + user.accountId + '/recoveryKey') - .then((res) => { - const result = res.obj; - assert.isTrue(result.exists); - }); - }); - - it('should create disabled key and then enable it', async () => { - user = fake.newUserDataHex(); - let res = await client.putThen( - '/account/' + user.accountId, - user.account - ); - respOkEmpty(res); - recoveryKey = { - recoveryKeyId: crypto.randomBytes(16).toString('hex'), - recoveryData: crypto.randomBytes(64).toString('hex'), - enabled: false, - }; - res = await client.postThen( - '/account/' + user.accountId + '/recoveryKey', - recoveryKey - ); - respOkEmpty(res); - - res = await client.getThen( - '/account/' + - user.accountId + - '/recoveryKey/' + - recoveryKey.recoveryKeyId - ); - let result = res.obj; - assert.equal( - result.recoveryData, - recoveryKey.recoveryData, - 'recoveryData match' - ); - assert.equal(result.enabled, false); - assert.ok(result.createdAt); - - const updatedKey = Object.assign({}, recoveryKey, { - enabled: true, - verifiedAt: Date.now(), - }); - res = await client.postThen( - '/account/' + user.accountId + '/recoveryKey/update', - updatedKey - ); - respOkEmpty(res); - - res = await client.getThen( - '/account/' + - user.accountId + - '/recoveryKey/' + - recoveryKey.recoveryKeyId - ); - result = res.obj; - assert.equal( - result.recoveryData, - recoveryKey.recoveryData, - 'recoveryData match' - ); - assert.equal(result.enabled, true); - assert.ok(result.verifiedAt); - }); - }); - - describe('ecosystem anon id', async () => { - let user; - const ecosystemAnonId = 'eyJhbGciOiJFQ0RILUVTIiwia'; - - beforeEach(async () => { - user = fake.newUserDataHex(); - const r = await client.putThen( - '/account/' + user.accountId, - user.account - ); - respOkEmpty(r); - }); - - it('should update ecosystem anon id', async () => { - let r = await client.putThen( - '/account/' + user.accountId + '/ecosystemAnonId', - { - ecosystemAnonId, - } - ); - respOkEmpty(r); - - r = await client.getThen('/account/' + user.accountId); - const account = r.obj; - assert.equal(account.ecosystemAnonId, ecosystemAnonId); - }); - }); - - after(() => server.close()); - }); -}; diff --git a/packages/fxa-auth-db-mysql/db-server/test/client-then.js b/packages/fxa-auth-db-mysql/db-server/test/client-then.js deleted file mode 100644 index 6512ffabf90..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/test/client-then.js +++ /dev/null @@ -1,33 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -const clients = require('restify-clients'); -var P = require('../../lib/promise'); - -var ops = ['head', 'get', 'post', 'put', 'del']; - -module.exports = function createClient(cfg) { - cfg.agent = false; - cfg.headers = { - connection: 'close', - }; - const client = clients.createJsonClient(cfg); - - // create a thenable version of each operation - ops.forEach(function (name) { - client[name + 'Then'] = function () { - var p = P.defer(); - var args = Array.prototype.slice.call(arguments, 0); - args.push(function (err, req, res, obj) { - if (err) { - return p.reject(err); - } - p.resolve({ req: req, res: res, obj: obj }); - }); - client[name].apply(this, args); - return p.promise; - }; - }); - - return client; -}; diff --git a/packages/fxa-auth-db-mysql/db-server/test/fake.js b/packages/fxa-auth-db-mysql/db-server/test/fake.js deleted file mode 100644 index 67c007ebb10..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/test/fake.js +++ /dev/null @@ -1,238 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -var crypto = require('crypto'); -var base64url = require('base64url'); -const { normalizeEmail } = require('fxa-shared').email.helpers; - -function hex(len) { - return crypto.randomBytes(len).toString('hex'); -} -function hex16() { - return hex(16); -} -function hex32() { - return hex(32); -} -// function hex64() { return hex(64) } -function hex96() { - return hex(96); -} - -function base64(len) { - return base64url(crypto.randomBytes(len)); -} - -function base64_16() { - return base64(16); -} -function base64_65() { - return base64(65); -} - -function buf(len) { - return Buffer.from(crypto.randomBytes(len)); -} -function buf16() { - return buf(16); -} -function buf32() { - return buf(32); -} -// function buf64() { return buf(64) } -function buf96() { - return buf(96); -} - -module.exports.newUserDataHex = function () { - var data = {}; - - // account - data.accountId = hex16(); - data.account = { - email: hex16() + '@example.com', - emailCode: hex16(), - emailVerified: false, - verifierVersion: 1, - verifyHash: hex32(), - authSalt: hex32(), - kA: hex32(), - wrapWrapKb: hex32(), - verifierSetAt: Date.now(), - createdAt: Date.now(), - ecosystemAnonId: 'initialEcosystemAnonId', - }; - data.account.normalizedEmail = normalizeEmail(data.account.email); - - // sessionToken - data.sessionTokenId = hex32(); - data.sessionToken = { - data: hex32(), - uid: data.accountId, - createdAt: Date.now(), - uaBrowser: 'fake browser', - uaBrowserVersion: 'fake browser version', - uaOS: 'fake OS', - uaOSVersion: 'fake OS version', - uaDeviceType: 'fake device type', - uaFormFactor: 'fake form factor', - mustVerify: true, - tokenVerificationId: hex16(), - tokenVerificationCode: crypto.randomBytes(4).toString('hex'), - tokenVerificationCodeExpiresAt: Date.now() + 20000, - }; - - // device - data.deviceId = hex16(); - data.device = { - uid: data.accountId, - sessionTokenId: data.sessionTokenId, - refreshTokenId: null, - createdAt: Date.now(), - name: 'fake device name', - type: 'fake device type', - callbackURL: 'fake callback URL', - callbackPublicKey: base64_65(), - callbackAuthKey: base64_16(), - callbackIsExpired: false, - }; - - // oauth device - data.refreshTokenId = hex32(); - data.oauthDeviceId = hex16(); - data.oauthDevice = { - uid: data.accountId, - sessionTokenId: null, - refreshTokenId: data.refreshTokenId, - createdAt: Date.now(), - name: 'fake oauth device name', - type: 'oauth device', - callbackURL: 'fake oauth callback URL', - callbackPublicKey: base64_65(), - callbackAuthKey: base64_16(), - callbackIsExpired: false, - }; - - // keyFetchToken - data.keyFetchTokenId = hex32(); - data.keyFetchToken = { - authKey: hex32(), - uid: data.accountId, - keyBundle: hex96(), - createdAt: Date.now(), - tokenVerificationId: data.sessionToken.tokenVerificationId, - }; - - // accountResetToken - data.accountResetTokenId = hex32(); - data.accountResetToken = { - tokenId: data.accountResetTokenId, - data: hex32(), - uid: data.accountId, - createdAt: Date.now(), - }; - - // passwordChangeToken - data.passwordChangeTokenId = hex32(); - data.passwordChangeToken = { - data: hex32(), - uid: data.accountId, - createdAt: Date.now(), - }; - - // passwordForgotToken - data.passwordForgotTokenId = hex32(); - data.passwordForgotToken = { - data: hex32(), - uid: data.accountId, - passCode: hex16(), - tries: 1, - createdAt: Date.now(), - }; - - // email - data.email = { - email: hex16() + '@example.com', - emailCode: hex16(), - isVerified: 0, - isPrimary: 0, - verifiedAt: undefined, - createdAt: Date.now(), - }; - data.email.normalizedEmail = normalizeEmail(data.email.email); - - data.totp = { - sharedSecret: hex(10), - epoch: 0, - verified: false, - enabled: true, - }; - - return data; -}; - -module.exports.newUserDataBuffer = function () { - var data = {}; - - // account - data.accountId = buf16(); - data.account = { - email: hex16() + '@example.com', - emailCode: buf16(), - emailVerified: false, - verifierVersion: 1, - verifyHash: buf32(), - authSalt: buf32(), - kA: buf32(), - wrapWrapKb: buf32(), - verifierSetAt: Date.now(), - createdAt: Date.now(), - }; - data.account.normalizedEmail = normalizeEmail(data.account.email); - - // sessionToken - data.sessionTokenId = buf32(); - data.sessionToken = { - data: buf32(), - uid: data.accountId, - createdAt: Date.now(), - }; - - // keyFetchToken - data.keyFetchTokenId = buf32(); - data.keyFetchToken = { - authKey: buf32(), - uid: data.accountId, - keyBundle: buf96(), - createdAt: Date.now(), - }; - - // accountResetToken - data.accountResetTokenId = buf32(); - data.accountResetToken = { - tokenId: data.accountResetTokenId, - data: buf32(), - uid: data.accountId, - createdAt: Date.now(), - }; - - // passwordChangeToken - data.passwordChangeTokenId = buf32(); - data.passwordChangeToken = { - data: buf32(), - uid: data.accountId, - createdAt: Date.now(), - }; - - // passwordForgotToken - data.passwordForgotTokenId = buf32(); - data.passwordForgotToken = { - data: buf32(), - uid: data.accountId, - passCode: buf16(), - tries: 1, - createdAt: Date.now(), - }; - - return data; -}; diff --git a/packages/fxa-auth-db-mysql/db-server/test/local/bufferize.js b/packages/fxa-auth-db-mysql/db-server/test/local/bufferize.js deleted file mode 100644 index 6381d871890..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/test/local/bufferize.js +++ /dev/null @@ -1,228 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -'use strict'; - -const { assert } = require('chai'); -const sinon = require('sinon'); - -describe('bufferize', () => { - it('bufferize module', () => { - var bufferize = require('../../lib/bufferize'); - assert.isObject(bufferize); - assert.lengthOf(Object.keys(bufferize), 4); - assert.isFunction(bufferize.unbuffer); - assert.isFunction(bufferize.bufferize); - assert.isFunction(bufferize.bufferizeRequest); - assert.isFunction(bufferize.hexToUtf8); - - var result = bufferize.unbuffer({ - foo: Buffer.from('42', 'hex'), - bar: '42', - }); - assert.isObject(result); - assert.lengthOf(Object.keys(result), 2); - assert.equal(result.foo, '42', 'bufferize.unbuffer unbuffered correctly'); - assert.equal(result.foo, '42', 'bufferize.unbuffer preserved string'); - - result = bufferize.bufferize({ - foo: '00', - bar: 'ffff', - }); - - assert.isObject(result); - assert.lengthOf(Object.keys(result), 2); - assert( - Buffer.isBuffer(result.foo), - 'bufferize.bufferize returned buffer for 00' - ); - assert.lengthOf(result.foo, 1); - assert.equal( - result.foo[0], - 0x00, - 'bufferize.bufferize returned correct data for 00' - ); - assert( - Buffer.isBuffer(result.bar), - 'bufferize.bufferize returned buffer for ffff' - ); - assert.lengthOf(result.bar, 2); - assert.equal( - result.bar[0], - 0xff, - 'bufferize.bufferize returned correct first byte for ffff' - ); - assert.equal( - result.bar[1], - 0xff, - 'bufferize.bufferize returned correct second byte for ffff' - ); - - result = bufferize.bufferize( - { - foo: '00', - bar: 'ffff', - wibble: '00', - }, - new Set(['foo', 'bar']) - ); - - assert.isObject(result); - assert.lengthOf(Object.keys(result), 3); - assert( - Buffer.isBuffer(result.foo), - 'bufferize.bufferize returned buffer for 00' - ); - assert.lengthOf(result.foo, 1); - assert.equal( - result.foo[0], - 0x00, - 'bufferize.bufferize returned correct data for 00' - ); - assert( - Buffer.isBuffer(result.bar), - 'bufferize.bufferize returned buffer for ffff' - ); - assert.lengthOf(result.bar, 2); - assert.equal( - result.bar[0], - 0xff, - 'bufferize.bufferize returned correct first byte for ffff' - ); - assert.equal( - result.bar[1], - 0xff, - 'bufferize.bufferize returned correct second byte for ffff' - ); - assert.equal( - result.wibble, - '00', - 'bufferize.bufferize ignored property not in match list' - ); - - result = bufferize.bufferize( - { - foo: '00', - bar: null, - baz: undefined, - }, - new Set(['foo', 'bar', 'baz']) - ); - - assert.isObject(result); - assert.lengthOf(Object.keys(result), 3); - assert( - Buffer.isBuffer(result.foo), - 'bufferize.bufferize returned buffer for 00' - ); - assert.lengthOf(result.foo, 1); - assert.equal( - result.foo[0], - 0x00, - 'bufferize.bufferize returned correct data for 00' - ); - assert.isNull(result.bar); - assert.isUndefined(result.baz); - - var request = { - body: { - no: 'badf00d', - nope: 'f00d', - yes: 'f00d', - }, - params: { - y: 'deadbeef', - n: 'deadbeef', - }, - }; - var next = sinon.spy(); - var keys = new Set(['yes', 'y']); - bufferize.bufferizeRequest(keys, request, {}, next); - - assert.lengthOf(Object.keys(request), 2); - - assert.lengthOf(Object.keys(request.body), 3); - assert.equal( - request.body.no, - 'badf00d', - 'bufferize.bufferizeRequest preserved body string badf00d' - ); - assert.equal( - request.body.nope, - 'f00d', - 'bufferize.bufferizeRequest ignored body property not in matchlist' - ); - assert( - Buffer.isBuffer(request.body.yes), - 'bufferize.bufferizeRequest returned buffer for body f00d' - ); - assert.lengthOf(request.body.yes, 2); - assert.equal( - request.body.yes[0], - 0xf0, - 'bufferize.bufferizeRequest returned correct first byte for body f00d' - ); - assert.equal( - request.body.yes[1], - 0x0d, - 'bufferize.bufferizeRequest returned correct second byte for body f00d' - ); - - assert.lengthOf(Object.keys(request.params), 2); - assert( - Buffer.isBuffer(request.params.y), - 'bufferize.bufferizeRequest returned buffer for params deadbeef' - ); - assert.lengthOf(request.params.y, 4); - assert.equal( - request.params.y[0], - 0xde, - 'bufferize.bufferizeRequest returned correct first byte for params deadbeef' - ); - assert.equal( - request.params.y[1], - 0xad, - 'bufferize.bufferizeRequest returned correct second byte for params deadbeef' - ); - assert.equal( - request.params.y[2], - 0xbe, - 'bufferize.bufferizeRequest returned correct third byte for params deadbeef' - ); - assert.equal( - request.params.y[3], - 0xef, - 'bufferize.bufferizeRequest returned correct fourth byte for params deadbeef' - ); - assert.equal( - request.params.n, - 'deadbeef', - 'bufferize.bufferizeRequest ignored params not in matchlist' - ); - assert(next.calledOnce, 'bufferize.bufferizeRequest called next'); - assert.lengthOf(next.getCall(0).args, 0); - - request = { - body: { - buf: 'invalid', - }, - }; - next = sinon.spy(); - bufferize.bufferizeRequest(null, request, {}, next); - - assert.lengthOf(Object.keys(request), 1); - assert.lengthOf(Object.keys(request.body), 1); - assert.equal( - request.body.buf, - 'invalid', - 'bufferize.bufferizeRequest did not overwrite invalid field in body' - ); - assert(next.calledOnce, 'bufferize.bufferizeRequest called next'); - assert.lengthOf(next.getCall(0).args, 1); - assert.equal( - next.getCall(0).args[0].statusCode, - 400, - 'bufferize.bufferizeRequest called next with a 400 error' - ); - }); -}); diff --git a/packages/fxa-auth-db-mysql/db-server/test/local/error.js b/packages/fxa-auth-db-mysql/db-server/test/local/error.js deleted file mode 100644 index 9de7eaec7e4..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/test/local/error.js +++ /dev/null @@ -1,43 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -'use strict'; - -const { assert } = require('chai'); - -describe('error', () => { - it('error module', () => { - const error = require('../../lib/error'); - assert.isFunction(error); - - const duplicate = error.duplicate(); - assert.isObject(duplicate); - assert(duplicate instanceof error, 'is an instance of error'); - assert.equal(duplicate.code, 409); - assert.equal(duplicate.errno, 101); - assert.equal(duplicate.message, 'Record already exists'); - assert.equal(duplicate.error, 'Conflict'); - assert.equal(duplicate.toString(), 'Error: Record already exists'); - - const notFound = error.notFound(); - assert.isObject(notFound); - assert(notFound instanceof error, 'is an instance of error'); - assert.equal(notFound.code, 404); - assert.equal(notFound.errno, 116); - assert.equal(notFound.message, 'Not Found'); - assert.equal(notFound.error, 'Not Found'); - assert.equal(notFound.toString(), 'Error: Not Found'); - - const err = new Error('Something broke.'); - err.code = 'ER_QUERY_INTERRUPTED'; - err.errno = 1317; - const wrap = error.wrap(err); - assert.isObject(wrap); - assert(wrap instanceof error, 'is an instance of error'); - assert.equal(wrap.code, 500); - assert.equal(wrap.errno, 1317); - assert.equal(wrap.message, 'ER_QUERY_INTERRUPTED'); - assert.equal(wrap.error, 'Internal Server Error'); - assert.equal(wrap.toString(), 'Error: ER_QUERY_INTERRUPTED'); - }); -}); diff --git a/packages/fxa-auth-db-mysql/db-server/test/local/safeJsonFormatter.js b/packages/fxa-auth-db-mysql/db-server/test/local/safeJsonFormatter.js deleted file mode 100644 index d3ff5156ea4..00000000000 --- a/packages/fxa-auth-db-mysql/db-server/test/local/safeJsonFormatter.js +++ /dev/null @@ -1,24 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -'use strict'; - -const { assert } = require('chai'); -const safeJsonFormatter = require('../../lib/safeJsonFormatter'); - -describe('safeJsonFormatter module', () => { - it('safeJsonFormatter function exported', () => { - assert.isFunction(safeJsonFormatter); - }); - - it('escapes input', () => { - const req = {}; - const res = { - setHeader: () => {}, - }; - const body = { foo: '