fxa-js-client/client/FxAccountClient.js

1561 строка
51 KiB
JavaScript

/* 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/. */
define([
'sjcl',
'p',
'./lib/credentials',
'./lib/errors',
'./lib/hawkCredentials',
'./lib/metricsContext',
'./lib/request',
], function (sjcl, P, credentials, ERRORS, hawkCredentials, metricsContext, Request) {
'use strict';
var VERSION = 'v1';
var uriVersionRegExp = new RegExp('/' + VERSION + '$');
var HKDF_SIZE = 2 * 32;
function isUndefined(val) {
return typeof val === 'undefined';
}
function isNull(val) {
return val === null;
}
function isEmptyObject(val) {
return Object.prototype.toString.call(val) === '[object Object]' && ! Object.keys(val).length;
}
function isEmptyString(val) {
return val === '';
}
function required(val, name) {
if (isUndefined(val) ||
isNull(val) ||
isEmptyObject(val) ||
isEmptyString(val)) {
throw new Error('Missing ' + name);
}
}
/**
* @class FxAccountClient
* @constructor
* @param {String} uri Auth Server URI
* @param {Object} config Configuration
*/
function FxAccountClient(uri, config) {
if (! uri && ! config) {
throw new Error('Firefox Accounts auth server endpoint or configuration object required.');
}
if (typeof uri !== 'string') {
config = uri || {};
uri = config.uri;
}
if (typeof config === 'undefined') {
config = {};
}
if (! uri) {
throw new Error('FxA auth server uri not set.');
}
if (!uriVersionRegExp.test(uri)) {
uri = uri + '/' + VERSION;
}
this.request = new Request(uri, config.xhr, { localtimeOffsetMsec: config.localtimeOffsetMsec });
}
FxAccountClient.VERSION = VERSION;
/**
* @method signUp
* @param {String} email Email input
* @param {String} password Password input
* @param {Object} [options={}] Options
* @param {Boolean} [options.keys]
* If `true`, calls the API with `?keys=true` to get the keyFetchToken
* @param {String} [options.service]
* Opaque alphanumeric token to be included in verification links
* @param {String} [options.redirectTo]
* a URL that the client should be redirected to after handling the request
* @param {String} [options.preVerified]
* set email to be verified if possible
* @param {String} [options.resume]
* Opaque url-encoded string that will be included in the verification link
* as a querystring parameter, useful for continuing an OAuth flow for
* example.
* @param {String} [options.lang]
* set the language for the 'Accept-Language' header
* @param {Object} [options.metricsContext={}] Metrics context metadata
* @param {String} options.metricsContext.deviceId identifier for the current device
* @param {String} options.metricsContext.flowId identifier for the current event flow
* @param {Number} options.metricsContext.flowBeginTime flow.begin event time
* @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
* @param {Number} options.metricsContext.utmContent content identifier
* @param {Number} options.metricsContext.utmMedium acquisition medium
* @param {Number} options.metricsContext.utmSource traffic source
* @param {Number} options.metricsContext.utmTerm search terms
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.signUp = function (email, password, options) {
var self = this;
return P()
.then(function () {
required(email, 'email');
required(password, 'password');
return credentials.setup(email, password);
})
.then(
function (result) {
/*eslint complexity: [2, 13] */
var endpoint = '/account/create';
var data = {
email: result.emailUTF8,
authPW: sjcl.codec.hex.fromBits(result.authPW)
};
var requestOpts = {};
if (options) {
if (options.service) {
data.service = options.service;
}
if (options.redirectTo) {
data.redirectTo = options.redirectTo;
}
// preVerified is used for unit/functional testing
if (options.preVerified) {
data.preVerified = options.preVerified;
}
if (options.resume) {
data.resume = options.resume;
}
if (options.keys) {
endpoint += '?keys=true';
}
if (options.lang) {
requestOpts.headers = {
'Accept-Language': options.lang
};
}
if (options.metricsContext) {
data.metricsContext = metricsContext.marshall(options.metricsContext);
}
}
return self.request.send(endpoint, 'POST', null, data, requestOpts)
.then(
function(accountData) {
if (options && options.keys) {
accountData.unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey);
}
return accountData;
}
);
}
);
};
/**
* @method signIn
* @param {String} email Email input
* @param {String} password Password input
* @param {Object} [options={}] Options
* @param {Boolean} [options.keys]
* If `true`, calls the API with `?keys=true` to get the keyFetchToken
* @param {Boolean} [options.skipCaseError]
* If `true`, the request will skip the incorrect case error
* @param {String} [options.service]
* Service being signed into
* @param {String} [options.reason]
* Reason for sign in. Can be one of: `signin`, `password_check`,
* `password_change`, `password_reset`
* @param {String} [options.redirectTo]
* a URL that the client should be redirected to after handling the request
* @param {String} [options.resume]
* Opaque url-encoded string that will be included in the verification link
* as a querystring parameter, useful for continuing an OAuth flow for
* example.
* @param {Object} [options.metricsContext={}] Metrics context metadata
* @param {String} options.metricsContext.deviceId identifier for the current device
* @param {String} options.metricsContext.flowId identifier for the current event flow
* @param {Number} options.metricsContext.flowBeginTime flow.begin event time
* @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
* @param {Number} options.metricsContext.utmContent content identifier
* @param {Number} options.metricsContext.utmMedium acquisition medium
* @param {Number} options.metricsContext.utmSource traffic source
* @param {Number} options.metricsContext.utmTerm search terms
* @param {String} [options.unblockCode]
* Login unblock code.
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.signIn = function (email, password, options) {
var self = this;
options = options || {};
return P()
.then(function () {
required(email, 'email');
required(password, 'password');
return credentials.setup(email, password);
})
.then(
function (result) {
var endpoint = '/account/login';
if (options.keys) {
endpoint += '?keys=true';
}
var data = {
email: result.emailUTF8,
authPW: sjcl.codec.hex.fromBits(result.authPW)
};
if (options.metricsContext) {
data.metricsContext = metricsContext.marshall(options.metricsContext);
}
if (options.reason) {
data.reason = options.reason;
}
if (options.redirectTo) {
data.redirectTo = options.redirectTo;
}
if (options.resume) {
data.resume = options.resume;
}
if (options.service) {
data.service = options.service;
}
if (options.unblockCode) {
data.unblockCode = options.unblockCode;
}
if (options.originalLoginEmail) {
data.originalLoginEmail = options.originalLoginEmail;
}
return self.request.send(endpoint, 'POST', null, data)
.then(
function(accountData) {
if (options.keys) {
accountData.unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey);
}
return accountData;
},
function(error) {
if (error && error.email && error.errno === ERRORS.INCORRECT_EMAIL_CASE && !options.skipCaseError) {
options.skipCaseError = true;
options.originalLoginEmail = email;
return self.signIn(error.email, password, options);
} else {
throw error;
}
}
);
}
);
};
/**
* @method verifyCode
* @param {String} uid Account ID
* @param {String} code Verification code
* @param {Object} [options={}] Options
* @param {String} [options.service]
* Service being signed into
* @param {String} [options.reminder]
* Reminder that was used to verify the account
* @param {String} [options.type]
* Type of code being verified, only supports `secondary` otherwise will verify account/sign-in
* @param {Boolean} [options.marketingOptIn]
* If `true`, notifies marketing of opt-in intent.
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.verifyCode = function(uid, code, options) {
var self = this;
return P()
.then(function () {
required(uid, 'uid');
required(code, 'verify code');
var data = {
uid: uid,
code: code
};
if (options) {
if (options.service) {
data.service = options.service;
}
if (options.reminder) {
data.reminder = options.reminder;
}
if (options.type) {
data.type = options.type;
}
if (options.marketingOptIn) {
data.marketingOptIn = true;
}
}
return self.request.send('/recovery_email/verify_code', 'POST', null, data);
});
};
/**
* @method recoveryEmailStatus
* @param {String} sessionToken sessionToken obtained from signIn
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.recoveryEmailStatus = function(sessionToken) {
var self = this;
return P()
.then(function () {
required(sessionToken, 'sessionToken');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
return self.request.send('/recovery_email/status', 'GET', creds);
});
};
/**
* Re-sends a verification code to the account's recovery email address.
*
* @method recoveryEmailResendCode
* @param {String} sessionToken sessionToken obtained from signIn
* @param {Object} [options={}] Options
* @param {String} [options.email]
* Code will be resent to this email, only used for secondary email codes
* @param {String} [options.service]
* Opaque alphanumeric token to be included in verification links
* @param {String} [options.redirectTo]
* a URL that the client should be redirected to after handling the request
* @param {String} [options.resume]
* Opaque url-encoded string that will be included in the verification link
* as a querystring parameter, useful for continuing an OAuth flow for
* example.
* @param {String} [options.type]
* Specifies the type of code to send, currently only supported type is
* `upgradeSession`.
* @param {String} [options.lang]
* set the language for the 'Accept-Language' header
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.recoveryEmailResendCode = function(sessionToken, options) {
var self = this;
var data = {};
var requestOpts = {};
return P()
.then(function () {
required(sessionToken, 'sessionToken');
if (options) {
if (options.email) {
data.email = options.email;
}
if (options.service) {
data.service = options.service;
}
if (options.redirectTo) {
data.redirectTo = options.redirectTo;
}
if (options.resume) {
data.resume = options.resume;
}
if (options.type) {
data.type = options.type;
}
if (options.lang) {
requestOpts.headers = {
'Accept-Language': options.lang
};
}
}
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
return self.request.send('/recovery_email/resend_code', 'POST', creds, data, requestOpts);
});
};
/**
* Used to ask the server to send a recovery code.
* The API returns passwordForgotToken to the client.
*
* @method passwordForgotSendCode
* @param {String} email
* @param {Object} [options={}] Options
* @param {String} [options.service]
* Opaque alphanumeric token to be included in verification links
* @param {String} [options.redirectTo]
* a URL that the client should be redirected to after handling the request
* @param {String} [options.resume]
* Opaque url-encoded string that will be included in the verification link
* as a querystring parameter, useful for continuing an OAuth flow for
* example.
* @param {String} [options.lang]
* set the language for the 'Accept-Language' header
* @param {Object} [options.metricsContext={}] Metrics context metadata
* @param {String} options.metricsContext.deviceId identifier for the current device
* @param {String} options.metricsContext.flowId identifier for the current event flow
* @param {Number} options.metricsContext.flowBeginTime flow.begin event time
* @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
* @param {Number} options.metricsContext.utmContent content identifier
* @param {Number} options.metricsContext.utmMedium acquisition medium
* @param {Number} options.metricsContext.utmSource traffic source
* @param {Number} options.metricsContext.utmTerm search terms
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.passwordForgotSendCode = function(email, options) {
var self = this;
var data = {
email: email
};
var requestOpts = {};
return P()
.then(function () {
required(email, 'email');
if (options) {
if (options.service) {
data.service = options.service;
}
if (options.redirectTo) {
data.redirectTo = options.redirectTo;
}
if (options.resume) {
data.resume = options.resume;
}
if (options.lang) {
requestOpts.headers = {
'Accept-Language': options.lang
};
}
if (options.metricsContext) {
data.metricsContext = metricsContext.marshall(options.metricsContext);
}
}
return self.request.send('/password/forgot/send_code', 'POST', null, data, requestOpts);
});
};
/**
* Re-sends a verification code to the account's recovery email address.
* HAWK-authenticated with the passwordForgotToken.
*
* @method passwordForgotResendCode
* @param {String} email
* @param {String} passwordForgotToken
* @param {Object} [options={}] Options
* @param {String} [options.service]
* Opaque alphanumeric token to be included in verification links
* @param {String} [options.redirectTo]
* a URL that the client should be redirected to after handling the request
* @param {String} [options.resume]
* Opaque url-encoded string that will be included in the verification link
* as a querystring parameter, useful for continuing an OAuth flow for
* example.
* @param {String} [options.lang]
* set the language for the 'Accept-Language' header
* @param {Object} [options.metricsContext={}] Metrics context metadata
* @param {String} options.metricsContext.deviceId identifier for the current device
* @param {String} options.metricsContext.flowId identifier for the current event flow
* @param {Number} options.metricsContext.flowBeginTime flow.begin event time
* @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
* @param {Number} options.metricsContext.utmContent content identifier
* @param {Number} options.metricsContext.utmMedium acquisition medium
* @param {Number} options.metricsContext.utmSource traffic source
* @param {Number} options.metricsContext.utmTerm search terms
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.passwordForgotResendCode = function(email, passwordForgotToken, options) {
var self = this;
var data = {
email: email
};
var requestOpts = {};
return P()
.then(function () {
required(email, 'email');
required(passwordForgotToken, 'passwordForgotToken');
if (options) {
if (options.service) {
data.service = options.service;
}
if (options.redirectTo) {
data.redirectTo = options.redirectTo;
}
if (options.resume) {
data.resume = options.resume;
}
if (options.lang) {
requestOpts.headers = {
'Accept-Language': options.lang
};
}
if (options.metricsContext) {
data.metricsContext = metricsContext.marshall(options.metricsContext);
}
}
return hawkCredentials(passwordForgotToken, 'passwordForgotToken', HKDF_SIZE);
})
.then(function(creds) {
return self.request.send('/password/forgot/resend_code', 'POST', creds, data, requestOpts);
});
};
/**
* Submits the verification token to the server.
* The API returns accountResetToken to the client.
* HAWK-authenticated with the passwordForgotToken.
*
* @method passwordForgotVerifyCode
* @param {String} code
* @param {String} passwordForgotToken
* @param {Object} [options.metricsContext={}] Metrics context metadata
* @param {String} options.metricsContext.deviceId identifier for the current device
* @param {String} options.metricsContext.flowId identifier for the current event flow
* @param {Number} options.metricsContext.flowBeginTime flow.begin event time
* @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
* @param {Number} options.metricsContext.utmContent content identifier
* @param {Number} options.metricsContext.utmMedium acquisition medium
* @param {Number} options.metricsContext.utmSource traffic source
* @param {Number} options.metricsContext.utmTerm search terms
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.passwordForgotVerifyCode = function(code, passwordForgotToken, options) {
var self = this;
return P()
.then(function () {
required(code, 'reset code');
required(passwordForgotToken, 'passwordForgotToken');
return hawkCredentials(passwordForgotToken, 'passwordForgotToken', HKDF_SIZE);
})
.then(function(creds) {
var data = {
code: code
};
if (options && options.metricsContext) {
data.metricsContext = metricsContext.marshall(options.metricsContext);
}
return self.request.send('/password/forgot/verify_code', 'POST', creds, data);
});
};
/**
* Returns the status for the passwordForgotToken.
* If the request returns a success response, the token has not yet been consumed.
* @method passwordForgotStatus
* @param {String} passwordForgotToken
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.passwordForgotStatus = function(passwordForgotToken) {
var self = this;
return P()
.then(function () {
required(passwordForgotToken, 'passwordForgotToken');
return hawkCredentials(passwordForgotToken, 'passwordForgotToken', HKDF_SIZE);
})
.then(function(creds) {
return self.request.send('/password/forgot/status', 'GET', creds);
});
};
/**
* The API returns reset result to the client.
* HAWK-authenticated with accountResetToken
*
* @method accountReset
* @param {String} email
* @param {String} newPassword
* @param {String} accountResetToken
* @param {Object} [options={}] Options
* @param {Boolean} [options.keys]
* If `true`, a new `keyFetchToken` is provisioned. `options.sessionToken`
* is required if `options.keys` is true.
* @param {Boolean} [options.sessionToken]
* If `true`, a new `sessionToken` is provisioned.
* @param {Object} [options.metricsContext={}] Metrics context metadata
* @param {String} options.metricsContext.deviceId identifier for the current device
* @param {String} options.metricsContext.flowId identifier for the current event flow
* @param {Number} options.metricsContext.flowBeginTime flow.begin event time
* @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
* @param {Number} options.metricsContext.utmContent content identifier
* @param {Number} options.metricsContext.utmMedium acquisition medium
* @param {Number} options.metricsContext.utmSource traffic source
* @param {Number} options.metricsContext.utmTerm search terms
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.accountReset = function(email, newPassword, accountResetToken, options) {
var self = this;
var data = {};
var unwrapBKey;
options = options || {};
if (options.sessionToken) {
data.sessionToken = options.sessionToken;
}
if (options.metricsContext) {
data.metricsContext = metricsContext.marshall(options.metricsContext);
}
return P()
.then(function () {
required(email, 'email');
required(newPassword, 'new password');
required(accountResetToken, 'accountResetToken');
if (options.keys) {
required(options.sessionToken, 'sessionToken');
}
return credentials.setup(email, newPassword);
})
.then(
function (result) {
if (options.keys) {
unwrapBKey = sjcl.codec.hex.fromBits(result.unwrapBKey);
}
data.authPW = sjcl.codec.hex.fromBits(result.authPW);
return hawkCredentials(accountResetToken, 'accountResetToken', HKDF_SIZE);
}
).then(
function (creds) {
var queryParams = '';
if (options.keys) {
queryParams = '?keys=true';
}
var endpoint = '/account/reset' + queryParams;
return self.request.send(endpoint, 'POST', creds, data)
.then(
function(accountData) {
if (options.keys && accountData.keyFetchToken) {
accountData.unwrapBKey = unwrapBKey;
}
return accountData;
}
);
}
);
};
/**
* Get the base16 bundle of encrypted kA|wrapKb.
*
* @method accountKeys
* @param {String} keyFetchToken
* @param {String} oldUnwrapBKey
* @return {Promise} A promise that will be fulfilled with JSON of {kA, kB} of the key bundle
*/
FxAccountClient.prototype.accountKeys = function(keyFetchToken, oldUnwrapBKey) {
var self = this;
return P()
.then(function () {
required(keyFetchToken, 'keyFetchToken');
required(oldUnwrapBKey, 'oldUnwrapBKey');
return hawkCredentials(keyFetchToken, 'keyFetchToken', 3 * 32);
})
.then(function(creds) {
var bundleKey = sjcl.codec.hex.fromBits(creds.bundleKey);
return self.request.send('/account/keys', 'GET', creds)
.then(
function(payload) {
return credentials.unbundleKeyFetchResponse(bundleKey, payload.bundle);
});
})
.then(function(keys) {
return {
kB: sjcl.codec.hex.fromBits(
credentials.xor(
sjcl.codec.hex.toBits(keys.wrapKB),
sjcl.codec.hex.toBits(oldUnwrapBKey)
)
),
kA: keys.kA
};
});
};
/**
* This deletes the account completely. All stored data is erased.
*
* @method accountDestroy
* @param {String} email Email input
* @param {String} password Password input
* @param {Object} [options={}] Options
* @param {Boolean} [options.skipCaseError]
* If `true`, the request will skip the incorrect case error
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.accountDestroy = function(email, password, options) {
var self = this;
options = options || {};
return P()
.then(function () {
required(email, 'email');
required(password, 'password');
return credentials.setup(email, password);
})
.then(
function (result) {
var data = {
email: result.emailUTF8,
authPW: sjcl.codec.hex.fromBits(result.authPW)
};
return self.request.send('/account/destroy', 'POST', null, data)
.then(
function(response) {
return response;
},
function(error) {
// if incorrect email case error
if (error && error.email && error.errno === ERRORS.INCORRECT_EMAIL_CASE && !options.skipCaseError) {
options.skipCaseError = true;
return self.accountDestroy(error.email, password, options);
} else {
throw error;
}
}
);
}
);
};
/**
* Gets the status of an account by uid.
*
* @method accountStatus
* @param {String} uid User account id
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.accountStatus = function(uid) {
var self = this;
return P()
.then(function () {
required(uid, 'uid');
return self.request.send('/account/status?uid=' + uid, 'GET');
});
};
/**
* Gets the status of an account by email.
*
* @method accountStatusByEmail
* @param {String} email User account email
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.accountStatusByEmail = function(email) {
var self = this;
return P()
.then(function () {
required(email, 'email');
return self.request.send('/account/status', 'POST', null, {email: email});
});
};
/**
* Destroys this session, by invalidating the sessionToken.
*
* @method sessionDestroy
* @param {String} sessionToken User session token
* @param {Object} [options={}] Options
* @param {String} [options.customSessionToken] Override which session token to destroy for this same user
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.sessionDestroy = function(sessionToken, options) {
var self = this;
var data = {};
options = options || {};
if (options.customSessionToken) {
data.customSessionToken = options.customSessionToken;
}
return P()
.then(function () {
required(sessionToken, 'sessionToken');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
return self.request.send('/session/destroy', 'POST', creds, data);
});
};
/**
* Responds successfully if the session status is valid, requires the sessionToken.
*
* @method sessionStatus
* @param {String} sessionToken User session token
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.sessionStatus = function(sessionToken) {
var self = this;
return P()
.then(function () {
required(sessionToken, 'sessionToken');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
return self.request.send('/session/status', 'GET', creds);
});
};
/**
* Sign a BrowserID public key
*
* @method certificateSign
* @param {String} sessionToken User session token
* @param {Object} publicKey The key to sign
* @param {int} duration Time interval from now when the certificate will expire in milliseconds
* @param {Object} [options={}] Options
* @param {String} [service=''] The requesting service, sent via the query string
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.certificateSign = function(sessionToken, publicKey, duration, options) {
var self = this;
var data = {
publicKey: publicKey,
duration: duration
};
return P()
.then(function () {
required(sessionToken, 'sessionToken');
required(publicKey, 'publicKey');
required(duration, 'duration');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
options = options || {};
var queryString = '';
if (options.service) {
queryString = '?service=' + encodeURIComponent(options.service);
}
return self.request.send('/certificate/sign' + queryString, 'POST', creds, data);
});
};
/**
* Change the password from one known value to another.
*
* @method passwordChange
* @param {String} email
* @param {String} oldPassword
* @param {String} newPassword
* @param {Object} [options={}] Options
* @param {Boolean} [options.keys]
* If `true`, calls the API with `?keys=true` to get a new keyFetchToken
* @param {String} [options.sessionToken]
* If a `sessionToken` is passed, a new sessionToken will be returned
* with the same `verified` status as the existing sessionToken.
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.passwordChange = function(email, oldPassword, newPassword, options) {
var self = this;
options = options || {};
return P()
.then(function () {
required(email, 'email');
required(oldPassword, 'old password');
required(newPassword, 'new password');
return self._passwordChangeStart(email, oldPassword);
})
.then(function (credentials) {
var oldCreds = credentials;
var emailToHashWith = credentials.emailToHashWith || email;
return self._passwordChangeKeys(oldCreds)
.then(function (keys) {
return self._passwordChangeFinish(emailToHashWith, newPassword, oldCreds, keys, options);
});
});
};
/**
* First step to change the password.
*
* @method passwordChangeStart
* @private
* @param {String} email
* @param {String} oldPassword
* @param {Object} [options={}] Options
* @param {Boolean} [options.skipCaseError]
* If `true`, the request will skip the incorrect case error
* @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText` and `oldUnwrapBKey`
*/
FxAccountClient.prototype._passwordChangeStart = function(email, oldPassword, options) {
var self = this;
options = options || {};
return P()
.then(function () {
required(email, 'email');
required(oldPassword, 'old password');
return credentials.setup(email, oldPassword);
})
.then(function (oldCreds) {
var data = {
email: oldCreds.emailUTF8,
oldAuthPW: sjcl.codec.hex.fromBits(oldCreds.authPW)
};
return self.request.send('/password/change/start', 'POST', null, data)
.then(
function(passwordData) {
passwordData.oldUnwrapBKey = sjcl.codec.hex.fromBits(oldCreds.unwrapBKey);
// Similar to password reset, this keeps the contract that we always
// hash passwords with the original account email.
passwordData.emailToHashWith = email;
return passwordData;
},
function(error) {
// if incorrect email case error
if (error && error.email && error.errno === ERRORS.INCORRECT_EMAIL_CASE && !options.skipCaseError) {
options.skipCaseError = true;
return self._passwordChangeStart(error.email, oldPassword, options);
} else {
throw error;
}
}
);
});
};
function checkCreds(creds) {
required(creds, 'credentials');
required(creds.oldUnwrapBKey, 'credentials.oldUnwrapBKey');
required(creds.keyFetchToken, 'credentials.keyFetchToken');
required(creds.passwordChangeToken, 'credentials.passwordChangeToken');
}
/**
* Second step to change the password.
*
* @method _passwordChangeKeys
* @private
* @param {Object} oldCreds This object should consists of `oldUnwrapBKey`, `keyFetchToken` and `passwordChangeToken`.
* @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText`
*/
FxAccountClient.prototype._passwordChangeKeys = function(oldCreds) {
var self = this;
return P()
.then(function () {
checkCreds(oldCreds);
})
.then(function () {
return self.accountKeys(oldCreds.keyFetchToken, oldCreds.oldUnwrapBKey);
});
};
/**
* Third step to change the password.
*
* @method _passwordChangeFinish
* @private
* @param {String} email
* @param {String} newPassword
* @param {Object} oldCreds This object should consists of `oldUnwrapBKey`, `keyFetchToken` and `passwordChangeToken`.
* @param {Object} keys This object should contain the unbundled keys
* @param {Object} [options={}] Options
* @param {Boolean} [options.keys]
* If `true`, calls the API with `?keys=true` to get the keyFetchToken
* @param {String} [options.sessionToken]
* If a `sessionToken` is passed, a new sessionToken will be returned
* with the same `verified` status as the existing sessionToken.
* @return {Promise} A promise that will be fulfilled with JSON of `xhr.responseText`
*/
FxAccountClient.prototype._passwordChangeFinish = function(email, newPassword, oldCreds, keys, options) {
options = options || {};
var self = this;
return P()
.then(function () {
required(email, 'email');
required(newPassword, 'new password');
checkCreds(oldCreds);
required(keys, 'keys');
required(keys.kB, 'keys.kB');
var defers = [];
defers.push(credentials.setup(email, newPassword));
defers.push(hawkCredentials(oldCreds.passwordChangeToken, 'passwordChangeToken', HKDF_SIZE));
if (options.sessionToken) {
// Unbundle session data to get session id
defers.push(hawkCredentials(options.sessionToken, 'sessionToken', HKDF_SIZE));
}
return P.all(defers);
})
.spread(function (newCreds, hawkCreds, sessionData) {
var newWrapKb = sjcl.codec.hex.fromBits(
credentials.xor(
sjcl.codec.hex.toBits(keys.kB),
newCreds.unwrapBKey
)
);
var queryParams = '';
if (options.keys) {
queryParams = '?keys=true';
}
var sessionTokenId;
if (sessionData && sessionData.id) {
sessionTokenId = sessionData.id;
}
return self.request.send('/password/change/finish' + queryParams, 'POST', hawkCreds, {
wrapKb: newWrapKb,
authPW: sjcl.codec.hex.fromBits(newCreds.authPW),
sessionToken: sessionTokenId
})
.then(function (accountData) {
if (options.keys && accountData.keyFetchToken) {
accountData.unwrapBKey = sjcl.codec.hex.fromBits(newCreds.unwrapBKey);
}
return accountData;
});
});
};
/**
* Get 32 bytes of random data. This should be combined with locally-sourced entropy when creating salts, etc.
*
* @method getRandomBytes
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.getRandomBytes = function() {
return this.request.send('/get_random_bytes', 'POST');
};
/**
* Add a new device
*
* @method deviceRegister
* @param {String} sessionToken User session token
* @param {String} deviceName Name of device
* @param {String} deviceType Type of device (mobile|desktop)
* @param {Object} [options={}] Options
* @param {string} [options.deviceCallback] Device's push endpoint.
* @param {string} [options.devicePublicKey] Public key used to encrypt push messages.
* @param {string} [options.deviceAuthKey] Authentication secret used to encrypt push messages.
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.deviceRegister = function (sessionToken, deviceName, deviceType, options) {
var request = this.request;
options = options || {};
return P()
.then(function () {
required(sessionToken, 'sessionToken');
required(deviceName, 'deviceName');
required(deviceType, 'deviceType');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
var data = {
name: deviceName,
type: deviceType
};
if (options.deviceCallback) {
data.pushCallback = options.deviceCallback;
}
if (options.devicePublicKey && options.deviceAuthKey) {
data.pushPublicKey = options.devicePublicKey;
data.pushAuthKey = options.deviceAuthKey;
}
return request.send('/account/device', 'POST', creds, data);
});
};
/**
* Update the name of an existing device
*
* @method deviceUpdate
* @param {String} sessionToken User session token
* @param {String} deviceId User-unique identifier of device
* @param {String} deviceName Name of device
* @param {Object} [options={}] Options
* @param {string} [options.deviceCallback] Device's push endpoint.
* @param {string} [options.devicePublicKey] Public key used to encrypt push messages.
* @param {string} [options.deviceAuthKey] Authentication secret used to encrypt push messages.
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.deviceUpdate = function (sessionToken, deviceId, deviceName, options) {
var request = this.request;
options = options || {};
return P()
.then(function () {
required(sessionToken, 'sessionToken');
required(deviceId, 'deviceId');
required(deviceName, 'deviceName');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
var data = {
id: deviceId,
name: deviceName
};
if (options.deviceCallback) {
data.pushCallback = options.deviceCallback;
}
if (options.devicePublicKey && options.deviceAuthKey) {
data.pushPublicKey = options.devicePublicKey;
data.pushAuthKey = options.deviceAuthKey;
}
return request.send('/account/device', 'POST', creds, data);
});
};
/**
* Unregister an existing device
*
* @method deviceDestroy
* @param {String} sessionToken Session token obtained from signIn
* @param {String} deviceId User-unique identifier of device
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.deviceDestroy = function (sessionToken, deviceId) {
var request = this.request;
return P()
.then(function () {
required(sessionToken, 'sessionToken');
required(deviceId, 'deviceId');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
var data = {
id: deviceId
};
return request.send('/account/device/destroy', 'POST', creds, data);
});
};
/**
* Get a list of all devices for a user
*
* @method deviceList
* @param {String} sessionToken sessionToken obtained from signIn
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.deviceList = function (sessionToken) {
var request = this.request;
return P()
.then(function () {
required(sessionToken, 'sessionToken');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
return request.send('/account/devices', 'GET', creds);
});
};
/**
* Get a list of user's sessions
*
* @method sessions
* @param {String} sessionToken sessionToken obtained from signIn
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.sessions = function (sessionToken) {
var request = this.request;
return P()
.then(function () {
required(sessionToken, 'sessionToken');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
return request.send('/account/sessions', 'GET', creds);
});
};
/**
* Send an unblock code
*
* @method sendUnblockCode
* @param {String} email email where to send the login authorization code
* @param {Object} [options={}] Options
* @param {Object} [options.metricsContext={}] Metrics context metadata
* @param {String} options.metricsContext.deviceId identifier for the current device
* @param {String} options.metricsContext.flowId identifier for the current event flow
* @param {Number} options.metricsContext.flowBeginTime flow.begin event time
* @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
* @param {Number} options.metricsContext.utmContent content identifier
* @param {Number} options.metricsContext.utmMedium acquisition medium
* @param {Number} options.metricsContext.utmSource traffic source
* @param {Number} options.metricsContext.utmTerm search terms
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.sendUnblockCode = function (email, options) {
var self = this;
return P()
.then(function () {
required(email, 'email');
var data = {
email: email
};
if (options && options.metricsContext) {
data.metricsContext = metricsContext.marshall(options.metricsContext);
}
return self.request.send('/account/login/send_unblock_code', 'POST', null, data);
});
};
/**
* Reject a login unblock code. Code will be deleted from the server
* and will not be able to be used again.
*
* @method rejectLoginAuthorizationCode
* @param {String} uid Account ID
* @param {String} unblockCode unblock code
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
FxAccountClient.prototype.rejectUnblockCode = function (uid, unblockCode) {
var self = this;
return P()
.then(function () {
required(uid, 'uid');
required(unblockCode, 'unblockCode');
var data = {
uid: uid,
unblockCode: unblockCode
};
return self.request.send('/account/login/reject_unblock_code', 'POST', null, data);
});
};
/**
* Send an sms.
*
* @method sendSms
* @param {String} sessionToken SessionToken obtained from signIn
* @param {String} phoneNumber Phone number sms will be sent to
* @param {String} messageId Corresponding message id that will be sent
* @param {Object} [options={}] Options
* @param {String} [options.lang] Language that sms will be sent in
* @param {Array} [options.features] Array of features to be enabled for the request
* @param {Object} [options.metricsContext={}] Metrics context metadata
* @param {String} options.metricsContext.deviceId identifier for the current device
* @param {String} options.metricsContext.flowId identifier for the current event flow
* @param {Number} options.metricsContext.flowBeginTime flow.begin event time
* @param {Number} options.metricsContext.utmCampaign marketing campaign identifier
* @param {Number} options.metricsContext.utmContent content identifier
* @param {Number} options.metricsContext.utmMedium acquisition medium
* @param {Number} options.metricsContext.utmSource traffic source
* @param {Number} options.metricsContext.utmTerm search terms
*/
FxAccountClient.prototype.sendSms = function (sessionToken, phoneNumber, messageId, options) {
var request = this.request;
return P()
.then(function () {
required(sessionToken, 'sessionToken');
required(phoneNumber, 'phoneNumber');
required(messageId, 'messageId');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
var data = {
phoneNumber: phoneNumber,
messageId: messageId
};
var requestOpts = {};
if (options) {
if (options.lang) {
requestOpts.headers = {
'Accept-Language': options.lang
};
}
if (options.features) {
data.features = options.features;
}
if (options.metricsContext) {
data.metricsContext = metricsContext.marshall(options.metricsContext);
}
}
return request.send('/sms', 'POST', creds, data, requestOpts);
});
};
/**
* Get SMS status for the current user.
*
* @method smsStatus
* @param {String} sessionToken SessionToken obtained from signIn
* @param {Object} [options={}] Options
* @param {String} [options.country] country Country to force for testing.
*/
FxAccountClient.prototype.smsStatus = function (sessionToken, options) {
var request = this.request;
options = options || {};
return P()
.then(function () {
required(sessionToken, 'sessionToken');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function (creds) {
var url = '/sms/status';
if (options.country) {
url += '?country=' + encodeURIComponent(options.country);
}
return request.send(url, 'GET', creds);
});
};
/**
* Consume a signinCode.
*
* @method consumeSigninCode
* @param {String} code The signinCode entered by the user
* @param {String} flowId Identifier for the current event flow
* @param {Number} flowBeginTime Timestamp for the flow.begin event
* @param {String} [deviceId] Identifier for the current device
*/
FxAccountClient.prototype.consumeSigninCode = function (code, flowId, flowBeginTime, deviceId) {
var self = this;
return P()
.then(function () {
required(code, 'code');
required(flowId, 'flowId');
required(flowBeginTime, 'flowBeginTime');
return self.request.send('/signinCodes/consume', 'POST', null, {
code: code,
metricsContext: {
deviceId: deviceId,
flowId: flowId,
flowBeginTime: flowBeginTime
}
});
});
};
/**
* Get the recovery emails associated with the signed in account.
*
* @method recoveryEmails
* @param {String} sessionToken SessionToken obtained from signIn
*/
FxAccountClient.prototype.recoveryEmails = function (sessionToken) {
var request = this.request;
return P()
.then(function () {
required(sessionToken, 'sessionToken');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
return request.send('/recovery_emails', 'GET', creds);
});
};
/**
* Create a new recovery email for the signed in account.
*
* @method recoveryEmailCreate
* @param {String} sessionToken SessionToken obtained from signIn
* @param {String} email new email to be added
*/
FxAccountClient.prototype.recoveryEmailCreate = function (sessionToken, email) {
var request = this.request;
return P()
.then(function () {
required(sessionToken, 'sessionToken');
required(sessionToken, 'email');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
var data = {
email: email
};
return request.send('/recovery_email', 'POST', creds, data);
});
};
/**
* Remove the recovery email for the signed in account.
*
* @method recoveryEmailDestroy
* @param {String} sessionToken SessionToken obtained from signIn
* @param {String} email email to be removed
*/
FxAccountClient.prototype.recoveryEmailDestroy = function (sessionToken, email) {
var request = this.request;
return P()
.then(function () {
required(sessionToken, 'sessionToken');
required(sessionToken, 'email');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
var data = {
email: email
};
return request.send('/recovery_email/destroy', 'POST', creds, data);
});
};
/**
* Changes user's primary email address.
*
* @method recoveryEmailSetPrimaryEmail
* @param {String} sessionToken SessionToken obtained from signIn
* @param {String} email Email that will be the new primary email for user
*/
FxAccountClient.prototype.recoveryEmailSetPrimaryEmail = function (sessionToken, email) {
var request = this.request;
return P()
.then(function () {
required(sessionToken, 'sessionToken');
return hawkCredentials(sessionToken, 'sessionToken', HKDF_SIZE);
})
.then(function(creds) {
var data = {
email: email
};
return request.send('/recovery_email/set_primary', 'POST', creds, data);
});
};
/**
* Check for a required argument. Exposed for unit testing.
*
* @param {Value} val - value to check
* @param {String} name - name of value
* @throws {Error} if argument is falsey, or an empty object
*/
FxAccountClient.prototype._required = required;
return FxAccountClient;
});