From 4d109a05a70a3669848ea7b0c6810be5a81914cc Mon Sep 17 00:00:00 2001 From: Vijay Budhram Date: Tue, 17 Jul 2018 15:20:40 -0400 Subject: [PATCH] feat(recovery): update delete recovery key and get recovery key endpoints (#2518), r=@rfk --- docs/api.md | 72 ++++++++--- docs/recovery_keys.md | 4 +- lib/db.js | 22 ++-- lib/error.js | 21 ++++ lib/routes/account.js | 2 +- lib/routes/index.js | 4 +- lib/routes/recovery-key.js | 190 +++++++++++++++++++++++++++++ lib/routes/recovery-keys.js | 87 ------------- lib/senders/email.js | 1 + npm-shrinkwrap.json | 180 +++++++++++++++------------ test/client/api.js | 36 +++++- test/client/index.js | 12 ++ test/local/routes/account.js | 3 +- test/local/routes/recovery-keys.js | 155 +++++++++++++++++++++-- test/mocks.js | 2 + test/remote/recovery_key_tests.js | 64 ++++++++++ test/routes_helpers.js | 10 +- 17 files changed, 657 insertions(+), 208 deletions(-) create mode 100644 lib/routes/recovery-key.js delete mode 100644 lib/routes/recovery-keys.js diff --git a/docs/api.md b/docs/api.md index 5c434165..b466386b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -58,9 +58,11 @@ see [`mozilla/fxa-js-client`](https://github.com/mozilla/fxa-js-client). * [Recovery codes](#recovery-codes) * [GET /recoveryCodes (:lock: sessionToken)](#get-recoverycodes) * [POST /session/verify/recoveryCode (:lock: sessionToken)](#post-sessionverifyrecoverycode) - * [Recovery keys](#recovery-keys) - * [POST /recoveryKeys (:lock: sessionToken)](#post-recoverykeys) - * [GET /recoveryKeys/{recoveryKeyId} (:lock: accountResetToken)](#get-recoverykeysrecoverykeyid) + * [Recovery key](#recovery-key) + * [POST /recoveryKey (:lock: sessionToken)](#post-recoverykey) + * [GET /recoveryKey/:recoveryKeyId (:lock: accountResetToken)](#get-recoverykey) + * [POST /recoveryKey/exists (:lock: sessionToken)](#post-recoverykeyexists) + * [DELETE /recoveryKey (:lock: sessionToken)](#delete-recoverykey) * [Session](#session) * [POST /session/destroy (:lock: sessionToken)](#post-sessiondestroy) * [POST /session/reauth (:lock: sessionToken)](#post-sessionreauth) @@ -285,6 +287,10 @@ for `code` and `errno` are: Recovery code not found. * `code: 400, errno: 157`: Unavailable device command. +* `code: 400, errno: 158`: + Recovery key not found. +* `code: 400, errno: 159`: + Recovery key is not valid. * `code: 503, errno: 201`: Service unavailable * `code: 503, errno: 202`: @@ -2346,12 +2352,12 @@ Verify a session using a recovery code. -### Recovery keys +### Recovery key -#### POST /recoveryKeys +#### POST /recoveryKey :lock: HAWK-authenticated with session token - + Creates a new recovery key for a user. Recovery keys are one-time-use tokens @@ -2359,30 +2365,68 @@ that can be used to recover the user's kB if they forget their password. For more details, see the [recovery keys](recovery_keys.md) docs. - + ##### Request body * `recoveryKeyId`: *validators.recoveryKeyId* - + A unique identifier for this recovery key, derived from the key via HKDF. - + * `recoveryData`: *validators.recoveryData* - + An encrypted bundle containing the user's kB. - + -#### GET /recoveryKeys/{recoveryKeyId} +#### GET /recoveryKey/:recoveryKeyId :lock: HAWK-authenticated with account reset token - + Retrieve the account recovery data associated with the given recovery key. - + +##### Response body + +* `recoveryData`: *string* + + + + + +#### POST /recoveryKey/exists +:lock: HAWK-authenticated with session token + +This route checks to see if given user has setup an account recovery key. +When used during the password reset flow, an email can be provided (instead +of a sessionToken) to check for the status. However, when +using an email, the request is rate limited. + + +##### Request body + +* `email`: *validators.email.required* + + + + + +##### Response body + +* `status`: *boolean, required* + + + + + +#### DELETE /recoveryKey + +This route remove an account's recovery key. When the key is +removed, it can no longer be used to restore an account's kB. + ### Session diff --git a/docs/recovery_keys.md b/docs/recovery_keys.md index 98ce8577..dc45fbf1 100644 --- a/docs/recovery_keys.md +++ b/docs/recovery_keys.md @@ -21,7 +21,7 @@ Creating a new recovery key involves the following steps: * recover-data = JWE(recover-enc, {"alg": "dir", "enc": "A256GCM", "kid": recover-kid}, kB) * FxA web-content submits recovery data to FxA server for storage, associating it with the fingerprint (recover-kid) - * `POST /recoveryKeys`, providing `recoveryKeyId` and `recoveryData` in the request body. + * `POST /recoveryKey`, providing `recoveryKeyId` and `recoveryData` in the request body. This scheme ensures someone in posession of the recovery key, can request the encrypted recovery data @@ -42,7 +42,7 @@ as follows: * FxA web-content uses the recovery code to derive the fingerprint and encryption key (recover-kid and recover-enc as defined above). * FxA web-content requests recover-data from FxA server, providing recover-kid. - * `GET /recoveryKeys/:recoveryKeyId`, authenticated with `accountResetToken`. + * `GET /recoveryKey/:recoveryKeyId`, authenticated with `accountResetToken`. * Providing the `:recoveryKeyId` here proves that the user posesses the recovery key, while the `accountResetToken` proves that they control the email address of the account. diff --git a/lib/db.js b/lib/db.js index 52db6c19..265354e1 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1288,7 +1288,7 @@ module.exports = ( } SAFE_URLS.createRecoveryKey = new SafeUrl( - '/account/:uid/recoveryKeys', + '/account/:uid/recoveryKey', 'db.createRecoveryKey' ) DB.prototype.createRecoveryKey = function (uid, recoveryKeyId, recoveryData) { @@ -1298,23 +1298,29 @@ module.exports = ( } SAFE_URLS.getRecoveryKey = new SafeUrl( - '/account/:uid/recoveryKeys/:recoveryKeyId', + '/account/:uid/recoveryKey', 'db.getRecoveryKey' ) - DB.prototype.getRecoveryKey = function (uid, recoveryKeyId) { + DB.prototype.getRecoveryKey = function (uid) { log.trace({op: 'DB.getRecoveryKey', uid}) - return this.pool.get(SAFE_URLS.getRecoveryKey, { uid, recoveryKeyId }) + return this.pool.get(SAFE_URLS.getRecoveryKey, {uid}) + .catch(err => { + if (isNotFoundError(err)) { + throw error.recoveryKeyNotFound() + } + throw err + }) } SAFE_URLS.deleteRecoveryKey = new SafeUrl( - '/account/:uid/recoveryKeys/:recoveryKeyId', - 'db.createRecoveryKey' + '/account/:uid/recoveryKey', + 'db.deleteRecoveryKey' ) - DB.prototype.deleteRecoveryKey = function (uid, recoveryKeyId) { + DB.prototype.deleteRecoveryKey = function (uid) { log.trace({op: 'DB.deleteRecoveryKey', uid}) - return this.pool.del(SAFE_URLS.deleteRecoveryKey, { uid, recoveryKeyId }) + return this.pool.del(SAFE_URLS.deleteRecoveryKey, { uid }) } diff --git a/lib/error.js b/lib/error.js index 9d7cd6d6..a5b71c20 100644 --- a/lib/error.js +++ b/lib/error.js @@ -70,6 +70,9 @@ var ERRNO = { RECOVERY_CODE_NOT_FOUND: 156, DEVICE_COMMAND_UNAVAILABLE: 157, + RECOVERY_KEY_NOT_FOUND: 158, + RECOVERY_KEY_INVALID: 159, + SERVER_BUSY: 201, FEATURE_NOT_ENABLED: 202, BACKEND_SERVICE_FAILURE: 203, @@ -794,6 +797,24 @@ AppError.unavailableDeviceCommand = () => { }) } +AppError.recoveryKeyNotFound = () => { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_KEY_NOT_FOUND, + message: 'Recovery key not found.' + }) +} + +AppError.recoveryKeyInvalid = () => { + return new AppError({ + code: 400, + error: 'Bad Request', + errno: ERRNO.RECOVERY_KEY_INVALID, + message: 'Recovery key is not valid.' + }) +} + AppError.backendServiceFailure = (service, operation) => { return new AppError({ code: 500, diff --git a/lib/routes/account.js b/lib/routes/account.js index 5e1f63a7..795bcd2c 100644 --- a/lib/routes/account.js +++ b/lib/routes/account.js @@ -1058,7 +1058,7 @@ module.exports = (log, db, mailer, Password, config, customs, signinUtils, push) function deleteRecoveryKey() { if (recoveryKeyId) { - return db.deleteRecoveryKey(account.uid, recoveryKeyId) + return db.deleteRecoveryKey(account.uid) } return P.resolve() diff --git a/lib/routes/index.js b/lib/routes/index.js index e9357e5b..1073cc6f 100644 --- a/lib/routes/index.js +++ b/lib/routes/index.js @@ -55,7 +55,7 @@ module.exports = function ( const unblockCodes = require('./unblock-codes')(log, db, mailer, config.signinUnblock, customs) const totp = require('./totp')(log, db, mailer, customs, config.totp) const recoveryCodes = require('./recovery-codes')(log, db, config.totp, customs, mailer) - const recoveryKeys = require('./recovery-keys')(log, db, Password, config.verifierVersion, customs) + const recoveryKey = require('./recovery-key')(log, db, Password, config.verifierVersion, customs) const util = require('./util')( log, config, @@ -79,7 +79,7 @@ module.exports = function ( totp, unblockCodes, util, - recoveryKeys + recoveryKey ) v1Routes.forEach(r => { r.path = basePath + '/v1' + r.path }) defaults.forEach(r => { r.path = basePath + r.path }) diff --git a/lib/routes/recovery-key.js b/lib/routes/recovery-key.js new file mode 100644 index 00000000..27ef648c --- /dev/null +++ b/lib/routes/recovery-key.js @@ -0,0 +1,190 @@ +/* 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' + +const errors = require('../error') +const validators = require('./validators') +const isA = require('joi') +const butil = require('../crypto/butil') + +module.exports = (log, db, Password, verifierVersion, customs) => { + return [ + { + method: 'POST', + path: '/recoveryKey', + options: { + auth: { + strategy: 'sessionToken' + }, + validate: { + payload: { + recoveryKeyId: validators.recoveryKeyId, + recoveryData: validators.recoveryData + } + } + }, + handler: async function (request) { + log.begin('createRecoveryKey', request) + + const uid = request.auth.credentials.uid + const sessionToken = request.auth.credentials + const {recoveryKeyId, recoveryData} = request.payload + + return createRecoveryKey() + .then(emitMetrics) + .then(() => { return {} }) + + function createRecoveryKey() { + if (sessionToken.tokenVerificationId) { + throw errors.unverifiedSession() + } + + return db.createRecoveryKey(uid, recoveryKeyId, recoveryData) + } + + function emitMetrics() { + log.info({ + op: 'account.recoveryKey.created', + uid + }) + + return request.emitMetricsEvent('recoveryKey.created', {uid}) + } + } + }, + { + method: 'GET', + path: '/recoveryKey/{recoveryKeyId}', + options: { + auth: { + strategy: 'accountResetToken' + }, + validate: { + params: { + recoveryKeyId: validators.recoveryKeyId + } + } + }, + handler: async function (request) { + log.begin('getRecoveryKey', request) + + const uid = request.auth.credentials.uid + const ip = request.app.clientAddress + const recoveryKeyId = request.params.recoveryKeyId + let recoveryData + + return customs.checkAuthenticated('getRecoveryKey', ip, uid) + .then(getRecoveryKey) + .then(() => { return {recoveryData} }) + + function getRecoveryKey() { + return db.getRecoveryKey(uid) + .then((res) => { + // `db.getRecoveryKey` doesn't require recoveryKeyId to retrieve + // the recovery bundle, however, we should perform a security + // check to ensure that the returned bundle contains the recoveryKeyId. + if (! butil.buffersAreEqual(res.recoveryKeyId, recoveryKeyId)) { + throw errors.recoveryKeyInvalid() + } + recoveryData = res.recoveryData + }) + } + } + }, + { + method: 'POST', + path: '/recoveryKey/exists', + options: { + auth: { + mode: 'optional', + strategy: 'sessionToken' + }, + validate: { + payload: { + email: validators.email().optional() + } + }, + response: { + schema: { + exists: isA.boolean().required() + } + } + }, + handler(request) { + log.begin('recoveryKeyExists', request) + + const email = request.payload.email + let exists = false, uid + + if (request.auth.credentials) { + uid = request.auth.credentials.uid + } + + return Promise.resolve() + .then(() => { + if (! uid) { + // If not using a sessionToken, an email is required to check + // for a recovery key. This occurs when checking from the + // password reset page and allows us to redirect the user to either + // the regular password reset or account recovery password reset. + if (! email) { + throw errors.missingRequestParameter('email') + } + + return customs.check(request, email, 'recoveryKeyExists') + .then(() => db.accountRecord(email)) + .then((result) => uid = result.uid) + } + + // When checking from `/settings` a sessionToken is required and the + // request is not rate limited. + }) + .then(() => { + return db.getRecoveryKey(uid) + .then((recoveryKey) => { + if (recoveryKey) { + exists = true + } + }, (err) => { + if (err.errno === errors.ERRNO.RECOVERY_KEY_NOT_FOUND) { + exists = false + return + } + throw err + }) + }) + .then(() => { + return {exists} + }) + } + }, + { + method: 'DELETE', + path: '/recoveryKey', + options: { + auth: { + strategy: 'sessionToken' + } + }, + handler(request) { + log.begin('recoveryKeyDelete', request) + + return Promise.resolve() + .then(() => { + const sessionToken = request.auth.credentials + + if (sessionToken.tokenVerificationId) { + throw errors.unverifiedSession() + } + + return db.deleteRecoveryKey(sessionToken.uid) + .then(() => { + return {} + }) + }) + } + } + ] +} diff --git a/lib/routes/recovery-keys.js b/lib/routes/recovery-keys.js deleted file mode 100644 index 48c96205..00000000 --- a/lib/routes/recovery-keys.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/. */ - -'use strict' - -const errors = require('../error') -const validators = require('./validators') - -module.exports = (log, db, Password, verifierVersion, customs) => { - return [ - { - method: 'POST', - path: '/recoveryKeys', - options: { - auth: { - strategy: 'sessionToken' - }, - validate: { - payload: { - recoveryKeyId: validators.recoveryKeyId, - recoveryData: validators.recoveryData - } - } - }, - handler: async function (request) { - log.begin('createRecoveryKey', request) - - const uid = request.auth.credentials.uid - const sessionToken = request.auth.credentials - const {recoveryKeyId, recoveryData} = request.payload - - return createRecoveryKey() - .then(emitMetrics) - .then(() => { return {} }) - - function createRecoveryKey() { - if (sessionToken.tokenVerificationId) { - throw errors.unverifiedSession() - } - - return db.createRecoveryKey(uid, recoveryKeyId, recoveryData) - } - - function emitMetrics() { - log.info({ - op: 'account.recoveryKey.created', - uid - }) - - return request.emitMetricsEvent('recoveryKey.created', {uid}) - } - } - }, - { - method: 'GET', - path: '/recoveryKeys/{recoveryKeyId}', - options: { - auth: { - strategy: 'accountResetToken' - }, - validate: { - params: { - recoveryKeyId: validators.recoveryKeyId - } - } - }, - handler: async function (request) { - log.begin('getRecoveryKey', request) - - const uid = request.auth.credentials.uid - const ip = request.app.clientAddress - const recoveryKeyId = request.params.recoveryKeyId - let recoveryData - - return customs.checkAuthenticated('getRecoveryKey', ip, uid) - .then(getRecoveryKey) - .then(() => { return {recoveryData} }) - - function getRecoveryKey() { - return db.getRecoveryKey(uid, recoveryKeyId) - .then((res) => recoveryData = res.recoveryData) - } - } - } - ] -} diff --git a/lib/senders/email.js b/lib/senders/email.js index 4344dded..2f625824 100644 --- a/lib/senders/email.js +++ b/lib/senders/email.js @@ -610,6 +610,7 @@ module.exports = function (log, config) { Mailer.prototype.recoveryEmail = function (message) { var templateName = 'recoveryEmail' var query = { + uid: message.uid, token: message.token, code: message.code, email: message.email diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 43a6de93..31ec23ae 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2727,7 +2727,7 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "fxa-auth-db-mysql": { - "version": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#b5c0f0e34b41c9464528f01ddd4bc8a28ed44571", + "version": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#29b9b4bc65784659ff103d77a390dc043e41992c", "from": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#master", "dev": true, "requires": { @@ -2772,9 +2772,9 @@ "dev": true }, "JSONStream": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", - "integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", + "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", "requires": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" @@ -2786,9 +2786,9 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "acorn": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", - "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", + "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==" }, "acorn-jsx": { "version": "3.0.1", @@ -2811,9 +2811,9 @@ "integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=" }, "agent-base": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", - "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", "dev": true, "requires": { "es6-promisify": "^5.0.0" @@ -2840,6 +2840,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -2978,9 +2979,9 @@ "dev": true }, "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "dev": true, "optional": true, "requires": { @@ -3036,9 +3037,9 @@ "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=" }, "buffer-from": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", - "integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==" }, "builtin-modules": { "version": "1.1.1", @@ -3539,12 +3540,12 @@ } }, "dtrace-provider": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz", - "integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.7.tgz", + "integrity": "sha1-3JObTT4GIM/gwc2APQ0tftBP/QQ=", "optional": true, "requires": { - "nan": "^2.3.3" + "nan": "^2.10.0" } }, "ecc-jsbn": { @@ -3567,17 +3568,17 @@ } }, "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "requires": { "is-arrayish": "^0.2.1" } }, "es5-ext": { - "version": "0.10.42", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.42.tgz", - "integrity": "sha512-AJxO1rmPe1bDEfSR6TJ/FgMFYuTBhR5R57KW58iCkYACMyFbrkqVyzXSurYoScDGvgyMpk7uRF/lPUPPTmsRSA==", + "version": "0.10.45", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz", + "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==", "requires": { "es6-iterator": "~2.0.3", "es6-symbol": "~3.1.1", @@ -3741,7 +3742,7 @@ }, "eslint-plugin-fxa": { "version": "git+https://github.com/mozilla/eslint-plugin-fxa.git#e082927b4c6dc17d21414e35f4c94312adbaba92", - "from": "eslint-plugin-fxa@git+https://github.com/mozilla/eslint-plugin-fxa.git#e082927b4c6dc17d21414e35f4c94312adbaba92" + "from": "git+https://github.com/mozilla/eslint-plugin-fxa.git#master" }, "espree": { "version": "3.5.4", @@ -3886,12 +3887,14 @@ } }, "find-my-way": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-1.12.0.tgz", - "integrity": "sha512-d7wZ0IeijAZDA/gvHjCNxxRTDCn5j9hnugcgEbNzYhofbDfogGhyRu93mtcJoAxeB1zemWTz9JB2JzNOar/qbA==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-1.15.1.tgz", + "integrity": "sha512-cwR1IxkB1JIIGxWpX3TQC1U/51htT4dps536rno7fkszeSSevvZGkl1dpIANRNq+X6/VDSF/S4JAuDPSTepHBA==", "dev": true, "requires": { - "fast-decode-uri-component": "^1.0.0" + "fast-decode-uri-component": "^1.0.0", + "safe-regex": "^1.1.0", + "semver-store": "^0.3.0" } }, "find-up": { @@ -4476,9 +4479,9 @@ "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=" }, "hosted-git-info": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==" + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" }, "hpack.js": { "version": "2.1.6", @@ -4528,9 +4531,9 @@ } }, "ignore": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz", - "integrity": "sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg==" + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==" }, "imurmurhash": { "version": "0.1.4", @@ -4633,7 +4636,8 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true }, "is-builtin-module": { "version": "1.0.0", @@ -4862,6 +4866,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -5126,7 +5131,8 @@ "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "optional": true }, "loud-rejection": { "version": "1.6.0", @@ -5435,12 +5441,12 @@ }, "dependencies": { "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", "dev": true, "requires": { - "lodash": "^4.14.0" + "lodash": "^4.17.10" } } } @@ -7029,9 +7035,9 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "p-limit": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", - "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "requires": { "p-try": "^1.0.0" } @@ -7292,7 +7298,8 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "optional": true }, "repeating": { "version": "2.0.1", @@ -7333,9 +7340,9 @@ }, "dependencies": { "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "dev": true } } @@ -7405,9 +7412,9 @@ }, "dependencies": { "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "dev": true } } @@ -7439,9 +7446,9 @@ "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==" }, "restify-errors": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/restify-errors/-/restify-errors-6.0.0.tgz", - "integrity": "sha512-Ytrpbf0KQ2h7TSrcCqmtA8dybaLv/+H9GHfayBiewwpNuzTIcomvLOfRzR0e4u2VdUsqggbWBrNogwKum0uQIQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/restify-errors/-/restify-errors-6.1.1.tgz", + "integrity": "sha512-QSwjp1b0pHB8QQQwqaPJu+VroGHAGX+HeHqz50awIb8334SAENCKeCI1VAhN099n4h0UVNupJ99ozx0pkHdqew==", "requires": { "assert-plus": "^1.0.0", "lodash": "^4.17.4", @@ -7450,9 +7457,9 @@ } }, "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" } } }, @@ -7477,6 +7484,12 @@ "onetime": "^1.0.0" } }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", @@ -7527,11 +7540,20 @@ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "safe-json-stringify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz", - "integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", "optional": true }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7571,6 +7593,12 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, + "semver-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz", + "integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==", + "dev": true + }, "shelljs": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", @@ -7721,9 +7749,9 @@ "dev": true }, "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", "dev": true, "requires": { "asn1": "~0.2.3", @@ -7733,6 +7761,7 @@ "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" } }, @@ -7773,9 +7802,9 @@ } }, "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", "dev": true }, "strip-ansi": { @@ -8012,18 +8041,11 @@ "dev": true }, "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.0.tgz", + "integrity": "sha512-5n12uMzKCjvB2HPFHnbQSjaqAa98L5iIXmHrZCLavuZVe0qe/SJGbDGWlpaHk5lnBkWRDO+dRu1/PgmUYKPPTw==", "requires": { - "inherits": "2.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - } + "inherits": "2.0.3" } }, "util-deprecate": { diff --git a/test/client/api.js b/test/client/api.js index e1d722d0..01c2b277 100644 --- a/test/client/api.js +++ b/test/client/api.js @@ -866,7 +866,7 @@ module.exports = config => { .then((token) => { return this.doRequest( 'POST', - this.baseURL + '/recoveryKeys', + this.baseURL + '/recoveryKey', token, { recoveryKeyId, @@ -881,7 +881,39 @@ module.exports = config => { .then((token) => { return this.doRequest( 'GET', - `${this.baseURL}/recoveryKeys/${recoveryKeyId}`, + `${this.baseURL}/recoveryKey/${recoveryKeyId}`, + token + ) + }) + } + + ClientApi.prototype.getRecoveryKeyExistsWithSession = function (sessionTokenHex) { + return tokens.SessionToken.fromHex(sessionTokenHex) + .then((token) => { + return this.doRequest( + 'POST', + `${this.baseURL}/recoveryKey/exists`, + token, + {} + ) + }) + } + + ClientApi.prototype.getRecoveryKeyExistsWithEmail = function (email) { + return this.doRequest( + 'POST', + `${this.baseURL}/recoveryKey/exists`, + undefined, + {email} + ) + } + + ClientApi.prototype.deleteRecoveryKey = function (sessionTokenHex) { + return tokens.SessionToken.fromHex(sessionTokenHex) + .then((token) => { + return this.doRequest( + 'DELETE', + `${this.baseURL}/recoveryKey`, token ) }) diff --git a/test/client/index.js b/test/client/index.js index f997c0b3..6f2a7e97 100644 --- a/test/client/index.js +++ b/test/client/index.js @@ -558,6 +558,18 @@ module.exports = config => { return this.api.getRecoveryKey(this.accountResetToken, recoveryKeyId) } + Client.prototype.getRecoveryKeyExists = function (email) { + if (! email) { + return this.api.getRecoveryKeyExistsWithSession(this.sessionToken) + } else { + return this.api.getRecoveryKeyExistsWithEmail(email) + } + } + + Client.prototype.deleteRecoveryKey = function () { + return this.api.deleteRecoveryKey(this.sessionToken) + } + Client.prototype.resetAccountWithRecoveryKey = function (newPassword, kB, recoveryKeyId, headers, options = {}) { if (! this.accountResetToken) { throw new Error('call verifyPasswordResetCode before calling resetAccountWithRecoveryKey') diff --git a/test/local/routes/account.js b/test/local/routes/account.js index acec708e..a22fd8d3 100644 --- a/test/local/routes/account.js +++ b/test/local/routes/account.js @@ -165,9 +165,8 @@ describe('/account/reset', function () { it('should have deleted recovery key', () => { assert.equal(mockDB.deleteRecoveryKey.callCount, 1) const args = mockDB.deleteRecoveryKey.args[0] - assert.equal(args.length, 2, 'db.deleteRecoveryKey passed correct number of args') + assert.equal(args.length, 1, 'db.deleteRecoveryKey passed correct number of args') assert.equal(args[0], uid, 'uid passed') - assert.equal(args[1], mockRequest.payload.recoveryKeyId, 'recoveryKeyId passed') }) it('should have reset custom server', () => { diff --git a/test/local/routes/recovery-keys.js b/test/local/routes/recovery-keys.js index 9589dcb9..cd3a7d32 100644 --- a/test/local/routes/recovery-keys.js +++ b/test/local/routes/recovery-keys.js @@ -9,6 +9,7 @@ const getRoute = require('../../routes_helpers').getRoute const mocks = require('../../mocks') const P = require('../../../lib/promise') const sinon = require('sinon') +const errors = require('../../../lib/error') let log, db, customs, routes, route, request, response const email = 'test@email.com' @@ -16,7 +17,7 @@ const recoveryKeyId = '000000' const recoveryData = '11111111111' const uid = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' -describe('POST /recoveryKeys', () => { +describe('POST /recoveryKey', () => { describe('should create recovery key', () => { beforeEach(() => { const requestOptions = { @@ -24,7 +25,7 @@ describe('POST /recoveryKeys', () => { log, payload: {recoveryKeyId, recoveryData} } - return setup({db: {}}, {}, '/recoveryKeys', requestOptions).then(r => response = r) + return setup({db: {}}, {}, '/recoveryKey', requestOptions).then(r => response = r) }) it('returned the correct response', () => { @@ -68,15 +69,15 @@ describe('POST /recoveryKeys', () => { const requestOptions = { credentials: {uid, email, tokenVerificationId: '1232311'}, } - return setup({db: {}}, {}, '/recoveryKeys', requestOptions) + return setup({db: {}}, {}, '/recoveryKey', requestOptions) .then(assert.fail, (err) => { - assert.deepEqual(err.errno, 138, 'returns unverified session error') + assert.deepEqual(err.errno, errors.ERRNO.SESSION_UNVERIFIED, 'returns unverified session error') }) }) }) }) -describe('GET /recoveryKeys/{recoveryKeyId}', () => { +describe('GET /recoveryKey/{recoveryKeyId}', () => { describe('should get recovery key', () => { beforeEach(() => { const requestOptions = { @@ -84,7 +85,7 @@ describe('GET /recoveryKeys/{recoveryKeyId}', () => { params: {recoveryKeyId}, log } - return setup({db: {recoveryData}}, {}, '/recoveryKeys/{recoveryKeyId}', requestOptions) + return setup({db: {recoveryData, recoveryKeyId}}, {}, '/recoveryKey/{recoveryKeyId}', requestOptions) .then(r => response = r) }) @@ -112,9 +113,143 @@ describe('GET /recoveryKeys/{recoveryKeyId}', () => { it('called db.getRecoveryKey correctly', () => { assert.equal(db.getRecoveryKey.callCount, 1) const args = db.getRecoveryKey.args[0] - assert.equal(args.length, 2) + assert.equal(args.length, 1) assert.equal(args[0], uid) - assert.equal(args[1], recoveryKeyId) + }) + }) + + describe('fails to return recovery data with recoveryKeyId mismatch', () => { + beforeEach(() => { + const requestOptions = { + credentials: {uid, email}, + params: {recoveryKeyId}, + log + } + return setup({db: {recoveryData, recoveryKeyId: '11111'}}, {}, '/recoveryKey/{recoveryKeyId}', requestOptions) + .then(assert.fail, (err) => response = err) + }) + + it('returned the correct response', () => { + assert.deepEqual(response.errno, errors.ERRNO.RECOVERY_KEY_INVALID, 'correct invalid recovery key errno') + }) + }) +}) + +describe('POST /recoveryKey/exists', () => { + describe('should check if recovery key exists using sessionToken', () => { + beforeEach(() => { + const requestOptions = { + credentials: {uid, email}, + log + } + return setup({db: {recoveryData, }}, {}, '/recoveryKey/exists', requestOptions) + .then(r => response = r) + }) + + it('returned the correct response', () => { + assert.deepEqual(response.exists, true, 'exists ') + }) + + it('called log.begin correctly', () => { + assert.equal(log.begin.callCount, 1) + const args = log.begin.args[0] + assert.equal(args.length, 2) + assert.equal(args[0], 'recoveryKeyExists') + assert.equal(args[1], request) + }) + + it('called db.getRecoveryKey correctly', () => { + assert.equal(db.getRecoveryKey.callCount, 1) + const args = db.getRecoveryKey.args[0] + assert.equal(args.length, 1) + assert.equal(args[0], uid) + }) + }) + + describe('should check if recovery key exists using email', () => { + beforeEach(() => { + const requestOptions = { + payload: {email}, + log + } + return setup({db: {uid, email, recoveryData}}, {}, '/recoveryKey/exists', requestOptions) + .then(r => response = r) + }) + + it('returned the correct response', () => { + assert.deepEqual(response.exists, true, 'exists ') + }) + + it('called log.begin correctly', () => { + assert.equal(log.begin.callCount, 1) + const args = log.begin.args[0] + assert.equal(args.length, 2) + assert.equal(args[0], 'recoveryKeyExists') + assert.equal(args[1], request) + }) + + it('called customs.check correctly', () => { + assert.equal(customs.check.callCount, 1) + const args = customs.check.args[0] + assert.equal(args.length, 3) + assert.equal(args[1], email) + assert.equal(args[2], 'recoveryKeyExists') + }) + + it('called db.getRecoveryKey correctly', () => { + assert.equal(db.getRecoveryKey.callCount, 1) + const args = db.getRecoveryKey.args[0] + assert.equal(args.length, 1) + assert.equal(args[0], uid) + }) + }) +}) + +describe('DELETE /recoveryKey', () => { + describe('should delete recovery key', () => { + beforeEach(() => { + const requestOptions = { + method: 'DELETE', + credentials: {uid, email}, + log + } + return setup({db: {recoveryData}}, {}, '/recoveryKey', requestOptions) + .then(r => response = r) + }) + + it('returned the correct response', () => { + assert.ok(response, 'empty response ') + }) + + it('called log.begin correctly', () => { + assert.equal(log.begin.callCount, 1) + const args = log.begin.args[0] + assert.equal(args.length, 2) + assert.equal(args[0], 'recoveryKeyDelete') + assert.equal(args[1], request) + }) + + it('called db.deleteRecoveryKey correctly', () => { + assert.equal(db.deleteRecoveryKey.callCount, 1) + const args = db.deleteRecoveryKey.args[0] + assert.equal(args.length, 1) + assert.equal(args[0], uid) + }) + }) + + describe('should fail for unverified session', () => { + beforeEach(() => { + const requestOptions = { + method: 'DELETE', + credentials: {uid, email, tokenVerificationId: 'unverified'}, + log + } + return setup({db: {recoveryData}}, {}, '/recoveryKey', requestOptions) + .then(assert.fail, (err) => response = err) + }) + + it('returned the correct response', () => { + assert.equal(response.errno, errors.ERRNO.SESSION_UNVERIFIED, 'unverified session') }) }) }) @@ -127,7 +262,7 @@ function setup(results, errors, path, requestOptions) { db = mocks.mockDB(results.db, errors.db) customs = mocks.mockCustoms(errors.customs) routes = makeRoutes({log, db, customs}) - route = getRoute(routes, path) + route = getRoute(routes, path, requestOptions.method) request = mocks.mockRequest(requestOptions) request.emitMetricsEvent = sinon.spy(() => P.resolve({})) return runTest(route, request) @@ -139,7 +274,7 @@ function makeRoutes(options = {}) { const customs = options.customs || mocks.mockCustoms() const config = options.config || {signinConfirmation: {}} const Password = require('../../../lib/crypto/password')(log, config) - return require('../../../lib/routes/recovery-keys')(log, db, Password, config.verifierVersion, customs) + return require('../../../lib/routes/recovery-key')(log, db, Password, config.verifierVersion, customs) } function runTest(route, request) { diff --git a/test/mocks.js b/test/mocks.js index c27f34dc..546a3ea7 100644 --- a/test/mocks.js +++ b/test/mocks.js @@ -345,6 +345,7 @@ function mockDB (data, errors) { }), getRecoveryKey: sinon.spy(() => { return P.resolve({ + recoveryKeyId: data.recoveryKeyId, recoveryData: data.recoveryData }) }), @@ -553,6 +554,7 @@ function mockRequest (data, errors) { }, path: data.path, params: data.params || {}, + method: data.method || undefined, payload: data.payload || {}, query: data.query || {}, setMetricsFlowCompleteSignal: metricsContext.setFlowCompleteSignal, diff --git a/test/remote/recovery_key_tests.js b/test/remote/recovery_key_tests.js index 846be768..7e95e243 100644 --- a/test/remote/recovery_key_tests.js +++ b/test/remote/recovery_key_tests.js @@ -75,6 +75,14 @@ describe('remote recovery keys', function () { }) }) + it('should fail to get unknown recovery key', () => { + return getAccountResetToken(client, server, email) + .then(() => client.getRecoveryKey('abce1234567890')) + .then(assert.fail, (err) => { + assert.equal(err.errno, 159, 'recovery key is not valid') + }) + }) + it('should fail if recoveryKeyId is missing', () => { return getAccountResetToken(client, server, email) .then(() => client.getRecoveryKey(recoveryKeyId)) @@ -122,6 +130,62 @@ describe('remote recovery keys', function () { }) }) + it('should delete recovery key', () => { + return client.deleteRecoveryKey() + .then((res) => { + assert.ok(res, 'empty response') + return client.getRecoveryKeyExists() + .then((result) => { + assert.equal(result.exists, false, 'recovery key deleted') + }) + }) + }) + + describe('check recovery key status', () => { + describe('with sessionToken', () => { + it('should return true if recovery key exists', () => { + return client.getRecoveryKeyExists() + .then((res) => { + assert.equal(res.exists, true, 'recovery key exists') + }) + }) + + it('should return false if recovery key doesn\'t exist', () => { + email = server.uniqueEmail() + return Client.createAndVerify(config.publicUrl, email, password, server.mailbox, {keys: true}) + .then((c) => { + client = c + return client.getRecoveryKeyExists() + }) + .then((res) => { + assert.equal(res.exists, false, 'recovery key doesnt exists') + }) + }) + }) + + describe('with email', () => { + it('should return true if recovery key exists', () => { + return client.getRecoveryKeyExists(email) + .then((res) => { + assert.equal(res.exists, true, 'recovery key exists') + }) + }) + + it('should return false if recovery key doesn\'t exist', () => { + email = server.uniqueEmail() + return Client.createAndVerify(config.publicUrl, email, password, server.mailbox, {keys: true}) + .then((c) => { + client = c + return client.getRecoveryKeyExists(email) + }) + .then((res) => { + assert.equal(res.exists, false, 'recovery key doesn\'t exist') + }) + }) + }) + + }) + after(() => { return TestServer.stop(server) }) diff --git a/test/routes_helpers.js b/test/routes_helpers.js index 91d53f41..3d8672df 100644 --- a/test/routes_helpers.js +++ b/test/routes_helpers.js @@ -4,12 +4,20 @@ 'use strict' -exports.getRoute = function (routes, path) { +exports.getRoute = function (routes, path, method) { var route = null routes.some(function (r) { if (r.path === path) { route = r + + if (method) { + if (r.method === method) { + return true + } + return false + } + return true } })