feat(recovery): update delete recovery key and get recovery key endpoints (#2518), r=@rfk

This commit is contained in:
Vijay Budhram 2018-07-17 15:20:40 -04:00 коммит произвёл GitHub
Родитель b6908b9fb0
Коммит 4d109a05a7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
17 изменённых файлов: 657 добавлений и 208 удалений

Просмотреть файл

@ -58,9 +58,11 @@ see [`mozilla/fxa-js-client`](https://github.com/mozilla/fxa-js-client).
* [Recovery codes](#recovery-codes) * [Recovery codes](#recovery-codes)
* [GET /recoveryCodes (:lock: sessionToken)](#get-recoverycodes) * [GET /recoveryCodes (:lock: sessionToken)](#get-recoverycodes)
* [POST /session/verify/recoveryCode (:lock: sessionToken)](#post-sessionverifyrecoverycode) * [POST /session/verify/recoveryCode (:lock: sessionToken)](#post-sessionverifyrecoverycode)
* [Recovery keys](#recovery-keys) * [Recovery key](#recovery-key)
* [POST /recoveryKeys (:lock: sessionToken)](#post-recoverykeys) * [POST /recoveryKey (:lock: sessionToken)](#post-recoverykey)
* [GET /recoveryKeys/{recoveryKeyId} (:lock: accountResetToken)](#get-recoverykeysrecoverykeyid) * [GET /recoveryKey/:recoveryKeyId (:lock: accountResetToken)](#get-recoverykey)
* [POST /recoveryKey/exists (:lock: sessionToken)](#post-recoverykeyexists)
* [DELETE /recoveryKey (:lock: sessionToken)](#delete-recoverykey)
* [Session](#session) * [Session](#session)
* [POST /session/destroy (:lock: sessionToken)](#post-sessiondestroy) * [POST /session/destroy (:lock: sessionToken)](#post-sessiondestroy)
* [POST /session/reauth (:lock: sessionToken)](#post-sessionreauth) * [POST /session/reauth (:lock: sessionToken)](#post-sessionreauth)
@ -285,6 +287,10 @@ for `code` and `errno` are:
Recovery code not found. Recovery code not found.
* `code: 400, errno: 157`: * `code: 400, errno: 157`:
Unavailable device command. Unavailable device command.
* `code: 400, errno: 158`:
Recovery key not found.
* `code: 400, errno: 159`:
Recovery key is not valid.
* `code: 503, errno: 201`: * `code: 503, errno: 201`:
Service unavailable Service unavailable
* `code: 503, errno: 202`: * `code: 503, errno: 202`:
@ -2346,12 +2352,12 @@ Verify a session using a recovery code.
<!--end-response-body-post-sessionverifyrecoverycode-remaining--> <!--end-response-body-post-sessionverifyrecoverycode-remaining-->
### Recovery keys ### Recovery key
#### POST /recoveryKeys #### POST /recoveryKey
:lock: HAWK-authenticated with session token :lock: HAWK-authenticated with session token
<!--begin-route-post-recoverykeys--> <!--begin-route-post-recoverykey-->
Creates a new recovery key for a user. Creates a new recovery key for a user.
Recovery keys are one-time-use tokens 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. if they forget their password.
For more details, see the For more details, see the
[recovery keys](recovery_keys.md) docs. [recovery keys](recovery_keys.md) docs.
<!--end-route-post-recoverykeys--> <!--end-route-post-recoverykey-->
##### Request body ##### Request body
* `recoveryKeyId`: *validators.recoveryKeyId* * `recoveryKeyId`: *validators.recoveryKeyId*
<!--begin-request-body-post-recoverykeys-recoveryKeyId--> <!--begin-request-body-post-recoverykey-recoveryKeyId-->
A unique identifier for this recovery key, derived from the key via HKDF. A unique identifier for this recovery key, derived from the key via HKDF.
<!--end-request-body-post-recoverykeys-recoveryKeyId--> <!--end-request-body-post-recoverykey-recoveryKeyId-->
* `recoveryData`: *validators.recoveryData* * `recoveryData`: *validators.recoveryData*
<!--begin-request-body-post-recoverykeys-recoveryData--> <!--begin-request-body-post-recoverykey-recoveryData-->
An encrypted bundle containing the user's kB. An encrypted bundle containing the user's kB.
<!--end-request-body-post-recoverykeys-recoveryData--> <!--end-request-body-post-recoverykey-recoveryData-->
#### GET /recoveryKeys/{recoveryKeyId} #### GET /recoveryKey/:recoveryKeyId
:lock: HAWK-authenticated with account reset token :lock: HAWK-authenticated with account reset token
<!--begin-route-get-recoverykeysrecoverykeyid--> <!--begin-route-get-recoverykeyrecoverykeyid-->
Retrieve the account recovery data associated with the given recovery key. Retrieve the account recovery data associated with the given recovery key.
<!--end-route-get-recoverykeysrecoverykeyid--> <!--end-route-get-recoverykeyrecoverykeyid-->
##### Response body
* `recoveryData`: *string*
<!--begin-response-body-post-recoverykeyrecoverykeyid-recoverydata-->
<!--end-response-body-post-recoverykeyrecoverykeyid-recoverydata-->
#### POST /recoveryKey/exists
:lock: HAWK-authenticated with session token
<!--begin-route-post-recoverykeyexists-->
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.
<!--end-route-post-recoverykeyexists-->
##### Request body
* `email`: *validators.email.required*
<!--begin-request-body-post-recoverykeyexists-email-->
<!--end-request-body-post-recoverykeyexists-email-->
##### Response body
* `status`: *boolean, required*
<!--begin-response-body-post-recoverykeyexists-email-->
<!--end-response-body-post-recoverykeyexists-email-->
#### DELETE /recoveryKey
<!--begin-route-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.
<!--end-route-delete-recoverykey-->
### Session ### Session

Просмотреть файл

@ -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) * recover-data = JWE(recover-enc, {"alg": "dir", "enc": "A256GCM", "kid": recover-kid}, kB)
* FxA web-content submits recovery data to FxA server for storage, * FxA web-content submits recovery data to FxA server for storage,
associating it with the fingerprint (recover-kid) 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, This scheme ensures someone in posession of the recovery key,
can request the encrypted recovery data can request the encrypted recovery data
@ -42,7 +42,7 @@ as follows:
* FxA web-content uses the recovery code to derive the fingerprint * FxA web-content uses the recovery code to derive the fingerprint
and encryption key (recover-kid and recover-enc as defined above). and encryption key (recover-kid and recover-enc as defined above).
* FxA web-content requests recover-data from FxA server, providing recover-kid. * 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, * Providing the `:recoveryKeyId` here proves that the user posesses the recovery key,
while the `accountResetToken` proves that they control the email address while the `accountResetToken` proves that they control the email address
of the account. of the account.

Просмотреть файл

@ -1288,7 +1288,7 @@ module.exports = (
} }
SAFE_URLS.createRecoveryKey = new SafeUrl( SAFE_URLS.createRecoveryKey = new SafeUrl(
'/account/:uid/recoveryKeys', '/account/:uid/recoveryKey',
'db.createRecoveryKey' 'db.createRecoveryKey'
) )
DB.prototype.createRecoveryKey = function (uid, recoveryKeyId, recoveryData) { DB.prototype.createRecoveryKey = function (uid, recoveryKeyId, recoveryData) {
@ -1298,23 +1298,29 @@ module.exports = (
} }
SAFE_URLS.getRecoveryKey = new SafeUrl( SAFE_URLS.getRecoveryKey = new SafeUrl(
'/account/:uid/recoveryKeys/:recoveryKeyId', '/account/:uid/recoveryKey',
'db.getRecoveryKey' 'db.getRecoveryKey'
) )
DB.prototype.getRecoveryKey = function (uid, recoveryKeyId) { DB.prototype.getRecoveryKey = function (uid) {
log.trace({op: 'DB.getRecoveryKey', 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( SAFE_URLS.deleteRecoveryKey = new SafeUrl(
'/account/:uid/recoveryKeys/:recoveryKeyId', '/account/:uid/recoveryKey',
'db.createRecoveryKey' 'db.deleteRecoveryKey'
) )
DB.prototype.deleteRecoveryKey = function (uid, recoveryKeyId) { DB.prototype.deleteRecoveryKey = function (uid) {
log.trace({op: 'DB.deleteRecoveryKey', uid}) log.trace({op: 'DB.deleteRecoveryKey', uid})
return this.pool.del(SAFE_URLS.deleteRecoveryKey, { uid, recoveryKeyId }) return this.pool.del(SAFE_URLS.deleteRecoveryKey, { uid })
} }

Просмотреть файл

@ -70,6 +70,9 @@ var ERRNO = {
RECOVERY_CODE_NOT_FOUND: 156, RECOVERY_CODE_NOT_FOUND: 156,
DEVICE_COMMAND_UNAVAILABLE: 157, DEVICE_COMMAND_UNAVAILABLE: 157,
RECOVERY_KEY_NOT_FOUND: 158,
RECOVERY_KEY_INVALID: 159,
SERVER_BUSY: 201, SERVER_BUSY: 201,
FEATURE_NOT_ENABLED: 202, FEATURE_NOT_ENABLED: 202,
BACKEND_SERVICE_FAILURE: 203, 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) => { AppError.backendServiceFailure = (service, operation) => {
return new AppError({ return new AppError({
code: 500, code: 500,

Просмотреть файл

@ -1058,7 +1058,7 @@ module.exports = (log, db, mailer, Password, config, customs, signinUtils, push)
function deleteRecoveryKey() { function deleteRecoveryKey() {
if (recoveryKeyId) { if (recoveryKeyId) {
return db.deleteRecoveryKey(account.uid, recoveryKeyId) return db.deleteRecoveryKey(account.uid)
} }
return P.resolve() return P.resolve()

Просмотреть файл

@ -55,7 +55,7 @@ module.exports = function (
const unblockCodes = require('./unblock-codes')(log, db, mailer, config.signinUnblock, customs) const unblockCodes = require('./unblock-codes')(log, db, mailer, config.signinUnblock, customs)
const totp = require('./totp')(log, db, mailer, customs, config.totp) const totp = require('./totp')(log, db, mailer, customs, config.totp)
const recoveryCodes = require('./recovery-codes')(log, db, config.totp, customs, mailer) 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')( const util = require('./util')(
log, log,
config, config,
@ -79,7 +79,7 @@ module.exports = function (
totp, totp,
unblockCodes, unblockCodes,
util, util,
recoveryKeys recoveryKey
) )
v1Routes.forEach(r => { r.path = basePath + '/v1' + r.path }) v1Routes.forEach(r => { r.path = basePath + '/v1' + r.path })
defaults.forEach(r => { r.path = basePath + r.path }) defaults.forEach(r => { r.path = basePath + r.path })

190
lib/routes/recovery-key.js Normal file
Просмотреть файл

@ -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 {}
})
})
}
}
]
}

Просмотреть файл

@ -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)
}
}
}
]
}

Просмотреть файл

@ -610,6 +610,7 @@ module.exports = function (log, config) {
Mailer.prototype.recoveryEmail = function (message) { Mailer.prototype.recoveryEmail = function (message) {
var templateName = 'recoveryEmail' var templateName = 'recoveryEmail'
var query = { var query = {
uid: message.uid,
token: message.token, token: message.token,
code: message.code, code: message.code,
email: message.email email: message.email

180
npm-shrinkwrap.json сгенерированный
Просмотреть файл

@ -2727,7 +2727,7 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
}, },
"fxa-auth-db-mysql": { "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", "from": "git+https://github.com/mozilla/fxa-auth-db-mysql.git#master",
"dev": true, "dev": true,
"requires": { "requires": {
@ -2772,9 +2772,9 @@
"dev": true "dev": true
}, },
"JSONStream": { "JSONStream": {
"version": "1.3.2", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz",
"integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=", "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==",
"requires": { "requires": {
"jsonparse": "^1.2.0", "jsonparse": "^1.2.0",
"through": ">=2.2.7 <3" "through": ">=2.2.7 <3"
@ -2786,9 +2786,9 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
}, },
"acorn": { "acorn": {
"version": "5.5.3", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz",
"integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==" "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ=="
}, },
"acorn-jsx": { "acorn-jsx": {
"version": "3.0.1", "version": "3.0.1",
@ -2811,9 +2811,9 @@
"integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=" "integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo="
}, },
"agent-base": { "agent-base": {
"version": "4.2.0", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
"integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
"dev": true, "dev": true,
"requires": { "requires": {
"es6-promisify": "^5.0.0" "es6-promisify": "^5.0.0"
@ -2840,6 +2840,7 @@
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
"optional": true,
"requires": { "requires": {
"kind-of": "^3.0.2", "kind-of": "^3.0.2",
"longest": "^1.0.1", "longest": "^1.0.1",
@ -2978,9 +2979,9 @@
"dev": true "dev": true
}, },
"bcrypt-pbkdf": { "bcrypt-pbkdf": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
@ -3036,9 +3037,9 @@
"integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=" "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8="
}, },
"buffer-from": { "buffer-from": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz",
"integrity": "sha512-83apNb8KK0Se60UE1+4Ukbe3HbfELJ6UlI4ldtOGs7So4KD26orJM8hIY9lxdzP+UpItH1Yh/Y8GUvNFWFFRxA==" "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ=="
}, },
"builtin-modules": { "builtin-modules": {
"version": "1.1.1", "version": "1.1.1",
@ -3539,12 +3540,12 @@
} }
}, },
"dtrace-provider": { "dtrace-provider": {
"version": "0.8.6", "version": "0.8.7",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.7.tgz",
"integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=", "integrity": "sha1-3JObTT4GIM/gwc2APQ0tftBP/QQ=",
"optional": true, "optional": true,
"requires": { "requires": {
"nan": "^2.3.3" "nan": "^2.10.0"
} }
}, },
"ecc-jsbn": { "ecc-jsbn": {
@ -3567,17 +3568,17 @@
} }
}, },
"error-ex": { "error-ex": {
"version": "1.3.1", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"requires": { "requires": {
"is-arrayish": "^0.2.1" "is-arrayish": "^0.2.1"
} }
}, },
"es5-ext": { "es5-ext": {
"version": "0.10.42", "version": "0.10.45",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.42.tgz", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz",
"integrity": "sha512-AJxO1rmPe1bDEfSR6TJ/FgMFYuTBhR5R57KW58iCkYACMyFbrkqVyzXSurYoScDGvgyMpk7uRF/lPUPPTmsRSA==", "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==",
"requires": { "requires": {
"es6-iterator": "~2.0.3", "es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.1", "es6-symbol": "~3.1.1",
@ -3741,7 +3742,7 @@
}, },
"eslint-plugin-fxa": { "eslint-plugin-fxa": {
"version": "git+https://github.com/mozilla/eslint-plugin-fxa.git#e082927b4c6dc17d21414e35f4c94312adbaba92", "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": { "espree": {
"version": "3.5.4", "version": "3.5.4",
@ -3886,12 +3887,14 @@
} }
}, },
"find-my-way": { "find-my-way": {
"version": "1.12.0", "version": "1.15.1",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-1.12.0.tgz", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-1.15.1.tgz",
"integrity": "sha512-d7wZ0IeijAZDA/gvHjCNxxRTDCn5j9hnugcgEbNzYhofbDfogGhyRu93mtcJoAxeB1zemWTz9JB2JzNOar/qbA==", "integrity": "sha512-cwR1IxkB1JIIGxWpX3TQC1U/51htT4dps536rno7fkszeSSevvZGkl1dpIANRNq+X6/VDSF/S4JAuDPSTepHBA==",
"dev": true, "dev": true,
"requires": { "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": { "find-up": {
@ -4476,9 +4479,9 @@
"integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=" "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk="
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "2.6.0", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
"integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==" "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w=="
}, },
"hpack.js": { "hpack.js": {
"version": "2.1.6", "version": "2.1.6",
@ -4528,9 +4531,9 @@
} }
}, },
"ignore": { "ignore": {
"version": "3.3.8", "version": "3.3.10",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
"integrity": "sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg==" "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug=="
}, },
"imurmurhash": { "imurmurhash": {
"version": "0.1.4", "version": "0.1.4",
@ -4633,7 +4636,8 @@
"is-buffer": { "is-buffer": {
"version": "1.1.6", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "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": { "is-builtin-module": {
"version": "1.0.0", "version": "1.0.0",
@ -4862,6 +4866,7 @@
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"optional": true,
"requires": { "requires": {
"is-buffer": "^1.1.5" "is-buffer": "^1.1.5"
} }
@ -5126,7 +5131,8 @@
"longest": { "longest": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
"optional": true
}, },
"loud-rejection": { "loud-rejection": {
"version": "1.6.0", "version": "1.6.0",
@ -5435,12 +5441,12 @@
}, },
"dependencies": { "dependencies": {
"async": { "async": {
"version": "2.6.0", "version": "2.6.1",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
"integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"lodash": "^4.14.0" "lodash": "^4.17.10"
} }
} }
} }
@ -7029,9 +7035,9 @@
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
}, },
"p-limit": { "p-limit": {
"version": "1.2.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
"integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
"requires": { "requires": {
"p-try": "^1.0.0" "p-try": "^1.0.0"
} }
@ -7292,7 +7298,8 @@
"repeat-string": { "repeat-string": {
"version": "1.6.1", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
"optional": true
}, },
"repeating": { "repeating": {
"version": "2.0.1", "version": "2.0.1",
@ -7333,9 +7340,9 @@
}, },
"dependencies": { "dependencies": {
"uuid": { "uuid": {
"version": "3.2.1", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
"dev": true "dev": true
} }
} }
@ -7405,9 +7412,9 @@
}, },
"dependencies": { "dependencies": {
"uuid": { "uuid": {
"version": "3.2.1", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
"dev": true "dev": true
} }
} }
@ -7439,9 +7446,9 @@
"integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==" "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg=="
}, },
"restify-errors": { "restify-errors": {
"version": "6.0.0", "version": "6.1.1",
"resolved": "https://registry.npmjs.org/restify-errors/-/restify-errors-6.0.0.tgz", "resolved": "https://registry.npmjs.org/restify-errors/-/restify-errors-6.1.1.tgz",
"integrity": "sha512-Ytrpbf0KQ2h7TSrcCqmtA8dybaLv/+H9GHfayBiewwpNuzTIcomvLOfRzR0e4u2VdUsqggbWBrNogwKum0uQIQ==", "integrity": "sha512-QSwjp1b0pHB8QQQwqaPJu+VroGHAGX+HeHqz50awIb8334SAENCKeCI1VAhN099n4h0UVNupJ99ozx0pkHdqew==",
"requires": { "requires": {
"assert-plus": "^1.0.0", "assert-plus": "^1.0.0",
"lodash": "^4.17.4", "lodash": "^4.17.4",
@ -7450,9 +7457,9 @@
} }
}, },
"uuid": { "uuid": {
"version": "3.2.1", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
} }
} }
}, },
@ -7477,6 +7484,12 @@
"onetime": "^1.0.0" "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": { "right-align": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
@ -7527,11 +7540,20 @@
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
}, },
"safe-json-stringify": { "safe-json-stringify": {
"version": "1.1.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz",
"integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==", "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==",
"optional": true "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": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "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", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" "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": { "shelljs": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz",
@ -7721,9 +7749,9 @@
"dev": true "dev": true
}, },
"sshpk": { "sshpk": {
"version": "1.14.1", "version": "1.14.2",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
"integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
"dev": true, "dev": true,
"requires": { "requires": {
"asn1": "~0.2.3", "asn1": "~0.2.3",
@ -7733,6 +7761,7 @@
"ecc-jsbn": "~0.1.1", "ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1", "getpass": "^0.1.1",
"jsbn": "~0.1.0", "jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0" "tweetnacl": "~0.14.0"
} }
}, },
@ -7773,9 +7802,9 @@
} }
}, },
"stringstream": { "stringstream": {
"version": "0.0.5", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz",
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==",
"dev": true "dev": true
}, },
"strip-ansi": { "strip-ansi": {
@ -8012,18 +8041,11 @@
"dev": true "dev": true
}, },
"util": { "util": {
"version": "0.10.3", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.11.0.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "integrity": "sha512-5n12uMzKCjvB2HPFHnbQSjaqAa98L5iIXmHrZCLavuZVe0qe/SJGbDGWlpaHk5lnBkWRDO+dRu1/PgmUYKPPTw==",
"requires": { "requires": {
"inherits": "2.0.1" "inherits": "2.0.3"
},
"dependencies": {
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
}
} }
}, },
"util-deprecate": { "util-deprecate": {

Просмотреть файл

@ -866,7 +866,7 @@ module.exports = config => {
.then((token) => { .then((token) => {
return this.doRequest( return this.doRequest(
'POST', 'POST',
this.baseURL + '/recoveryKeys', this.baseURL + '/recoveryKey',
token, token,
{ {
recoveryKeyId, recoveryKeyId,
@ -881,7 +881,39 @@ module.exports = config => {
.then((token) => { .then((token) => {
return this.doRequest( return this.doRequest(
'GET', '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 token
) )
}) })

Просмотреть файл

@ -558,6 +558,18 @@ module.exports = config => {
return this.api.getRecoveryKey(this.accountResetToken, recoveryKeyId) 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 = {}) { Client.prototype.resetAccountWithRecoveryKey = function (newPassword, kB, recoveryKeyId, headers, options = {}) {
if (! this.accountResetToken) { if (! this.accountResetToken) {
throw new Error('call verifyPasswordResetCode before calling resetAccountWithRecoveryKey') throw new Error('call verifyPasswordResetCode before calling resetAccountWithRecoveryKey')

Просмотреть файл

@ -165,9 +165,8 @@ describe('/account/reset', function () {
it('should have deleted recovery key', () => { it('should have deleted recovery key', () => {
assert.equal(mockDB.deleteRecoveryKey.callCount, 1) assert.equal(mockDB.deleteRecoveryKey.callCount, 1)
const args = mockDB.deleteRecoveryKey.args[0] 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[0], uid, 'uid passed')
assert.equal(args[1], mockRequest.payload.recoveryKeyId, 'recoveryKeyId passed')
}) })
it('should have reset custom server', () => { it('should have reset custom server', () => {

Просмотреть файл

@ -9,6 +9,7 @@ const getRoute = require('../../routes_helpers').getRoute
const mocks = require('../../mocks') const mocks = require('../../mocks')
const P = require('../../../lib/promise') const P = require('../../../lib/promise')
const sinon = require('sinon') const sinon = require('sinon')
const errors = require('../../../lib/error')
let log, db, customs, routes, route, request, response let log, db, customs, routes, route, request, response
const email = 'test@email.com' const email = 'test@email.com'
@ -16,7 +17,7 @@ const recoveryKeyId = '000000'
const recoveryData = '11111111111' const recoveryData = '11111111111'
const uid = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' const uid = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
describe('POST /recoveryKeys', () => { describe('POST /recoveryKey', () => {
describe('should create recovery key', () => { describe('should create recovery key', () => {
beforeEach(() => { beforeEach(() => {
const requestOptions = { const requestOptions = {
@ -24,7 +25,7 @@ describe('POST /recoveryKeys', () => {
log, log,
payload: {recoveryKeyId, recoveryData} 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', () => { it('returned the correct response', () => {
@ -68,15 +69,15 @@ describe('POST /recoveryKeys', () => {
const requestOptions = { const requestOptions = {
credentials: {uid, email, tokenVerificationId: '1232311'}, credentials: {uid, email, tokenVerificationId: '1232311'},
} }
return setup({db: {}}, {}, '/recoveryKeys', requestOptions) return setup({db: {}}, {}, '/recoveryKey', requestOptions)
.then(assert.fail, (err) => { .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', () => { describe('should get recovery key', () => {
beforeEach(() => { beforeEach(() => {
const requestOptions = { const requestOptions = {
@ -84,7 +85,7 @@ describe('GET /recoveryKeys/{recoveryKeyId}', () => {
params: {recoveryKeyId}, params: {recoveryKeyId},
log log
} }
return setup({db: {recoveryData}}, {}, '/recoveryKeys/{recoveryKeyId}', requestOptions) return setup({db: {recoveryData, recoveryKeyId}}, {}, '/recoveryKey/{recoveryKeyId}', requestOptions)
.then(r => response = r) .then(r => response = r)
}) })
@ -112,9 +113,143 @@ describe('GET /recoveryKeys/{recoveryKeyId}', () => {
it('called db.getRecoveryKey correctly', () => { it('called db.getRecoveryKey correctly', () => {
assert.equal(db.getRecoveryKey.callCount, 1) assert.equal(db.getRecoveryKey.callCount, 1)
const args = db.getRecoveryKey.args[0] const args = db.getRecoveryKey.args[0]
assert.equal(args.length, 2) assert.equal(args.length, 1)
assert.equal(args[0], uid) 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) db = mocks.mockDB(results.db, errors.db)
customs = mocks.mockCustoms(errors.customs) customs = mocks.mockCustoms(errors.customs)
routes = makeRoutes({log, db, customs}) routes = makeRoutes({log, db, customs})
route = getRoute(routes, path) route = getRoute(routes, path, requestOptions.method)
request = mocks.mockRequest(requestOptions) request = mocks.mockRequest(requestOptions)
request.emitMetricsEvent = sinon.spy(() => P.resolve({})) request.emitMetricsEvent = sinon.spy(() => P.resolve({}))
return runTest(route, request) return runTest(route, request)
@ -139,7 +274,7 @@ function makeRoutes(options = {}) {
const customs = options.customs || mocks.mockCustoms() const customs = options.customs || mocks.mockCustoms()
const config = options.config || {signinConfirmation: {}} const config = options.config || {signinConfirmation: {}}
const Password = require('../../../lib/crypto/password')(log, config) 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) { function runTest(route, request) {

Просмотреть файл

@ -345,6 +345,7 @@ function mockDB (data, errors) {
}), }),
getRecoveryKey: sinon.spy(() => { getRecoveryKey: sinon.spy(() => {
return P.resolve({ return P.resolve({
recoveryKeyId: data.recoveryKeyId,
recoveryData: data.recoveryData recoveryData: data.recoveryData
}) })
}), }),
@ -553,6 +554,7 @@ function mockRequest (data, errors) {
}, },
path: data.path, path: data.path,
params: data.params || {}, params: data.params || {},
method: data.method || undefined,
payload: data.payload || {}, payload: data.payload || {},
query: data.query || {}, query: data.query || {},
setMetricsFlowCompleteSignal: metricsContext.setFlowCompleteSignal, setMetricsFlowCompleteSignal: metricsContext.setFlowCompleteSignal,

Просмотреть файл

@ -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', () => { it('should fail if recoveryKeyId is missing', () => {
return getAccountResetToken(client, server, email) return getAccountResetToken(client, server, email)
.then(() => client.getRecoveryKey(recoveryKeyId)) .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(() => { after(() => {
return TestServer.stop(server) return TestServer.stop(server)
}) })

Просмотреть файл

@ -4,12 +4,20 @@
'use strict' 'use strict'
exports.getRoute = function (routes, path) { exports.getRoute = function (routes, path, method) {
var route = null var route = null
routes.some(function (r) { routes.some(function (r) {
if (r.path === path) { if (r.path === path) {
route = r route = r
if (method) {
if (r.method === method) {
return true
}
return false
}
return true return true
} }
}) })