packages/fxa-content-server/app/scripts/lib/fxa-client.js (602 lines of code) (raw):
/* 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/. */
// a very light wrapper around the real FxaClient to reduce boilerplate code
// and to allow us to develop to features that are not yet present in the real
// client.
import _ from 'underscore';
import $ from 'jquery';
import AuthErrors from './auth-errors';
import Constants from './constants';
import RecoveryKey from './crypto/recovery-keys';
import Session from './session';
import SignInReasons from './sign-in-reasons';
import VerificationReasons from './verification-reasons';
import VerificationMethods from './verification-methods';
import AuthClient from 'fxa-auth-client/browser';
import ExperimentMixin from '../views/mixins/experiment-mixin';
function trim(str) {
return $.trim(str);
}
const CONTEXTS_REQUIRE_KEYS = [
// allow fx_desktop_v1, many users signed up using
// the old context and are now using a newer version
// of Firefox that accepts WebChannel messages.
Constants.FX_DESKTOP_V1_CONTEXT,
Constants.FX_DESKTOP_V2_CONTEXT,
Constants.FX_DESKTOP_V3_CONTEXT,
// ios uses the old CustomEvents and cannot accept WebChannel messages
];
/**
* Check if keys should be requested
* @param {Object} relier - relier being signed in to.
* @param {String} sessionTokenContext - context of the current session
* token.
* @returns {Boolean}
*/
function wantsKeys(relier, sessionTokenContext) {
return (
relier.wantsKeys() || _.contains(CONTEXTS_REQUIRE_KEYS, sessionTokenContext)
);
}
// errors from the FxaJSClient must be normalized so that they
// are translated and reported to metrics correctly.
function wrapClientToNormalizeErrors(client) {
const proto = Object.getPrototypeOf(client);
const wrappedClient = Object.create(proto);
for (var key of Object.getOwnPropertyNames(proto)) {
if (typeof client[key] === 'function') {
wrappedClient[key] = function (key, ...args) {
const retval = this[key].apply(this, args);
// make no assumptions about the client returning a promise.
// If the return value is not a promise, just return the value.
if (!retval.then) {
return retval;
}
// a promise was returned, ensure any errors are normalized.
return retval.then(null, function (err) {
throw AuthErrors.toError(err);
});
}.bind(client, key);
}
}
return wrappedClient;
}
// Class method decorator to get an fxa-auth-client instance and pass
// it as the first argument to the method.
function withClient(callback) {
return function (...args) {
return this._getClient().then((client) =>
callback.apply(this, [client, ...args])
);
};
}
/**
* Create a delegate method to the fxa-auth-client.
*
* @param {String} method to delegate to.
* @returns {Function}
*/
function createClientDelegate(method) {
return function (...args) {
return this._getClient().then((client) => {
if (!_.isFunction(client[method])) {
throw new Error(`Invalid method on fxa-auth-client: ${method}`);
}
return client[method](...args);
});
};
}
function getUpdatedSessionData(email, relier, accountData, options = {}) {
var sessionTokenContext = options.sessionTokenContext;
if (!sessionTokenContext && relier.isSync()) {
sessionTokenContext = Constants.SESSION_TOKEN_USED_FOR_SYNC;
}
var updatedSessionData = {
email: email,
sessionToken: accountData.sessionToken,
sessionTokenContext: sessionTokenContext,
uid: accountData.uid,
verificationMethod: accountData.verificationMethod,
verificationReason: accountData.verificationReason,
verified: accountData.verified ?? false,
metricsEnabled: accountData.metricsEnabled ?? true,
providerUid: accountData.providerUid,
};
if (wantsKeys(relier, sessionTokenContext)) {
updatedSessionData.unwrapBKey = accountData.unwrapBKey;
updatedSessionData.keyFetchToken = accountData.keyFetchToken;
}
return updatedSessionData;
}
function FxaClientWrapper(options = {}) {
if (options.client) {
this._client = wrapClientToNormalizeErrors(options.client);
} else if (options.authServerUrl) {
this._authServerUrl = options.authServerUrl;
}
}
function determineKeyStretchVersion() {
const params = new URLSearchParams(window.location.search);
if (params.get('stretch') === '2') {
return 2;
}
if (ExperimentMixin.isInExperimentGroup('keyStretchV2', 'v2')) {
return 2;
}
return 1;
}
FxaClientWrapper.prototype = {
_getClient() {
if (this._client) {
return Promise.resolve(this._client);
}
const options = {
keyStretchVersion: determineKeyStretchVersion(),
};
return AuthClient.create(this._authServerUrl, options).then((client) => {
this._client = wrapClientToNormalizeErrors(client);
return this._client;
});
},
/**
* Fetch some entropy from the server
*
* @returns {Promise}
*/
getRandomBytes: createClientDelegate('getRandomBytes'),
/**
* Check the user's current password without affecting session state.
*
* @param {String} email
* @param {String} password
* @param {String} sessionToken
* An optional existing sessionToken for the user's account, which
* can be used to check the password without creating a new session.
* @returns {Promise}
*/
checkPassword: withClient((client, email, password, sessionToken) => {
if (sessionToken) {
return client.sessionReauth(sessionToken, email, password, {
reason: SignInReasons.PASSWORD_CHECK,
});
} else {
return client
.signIn(email, password, {
reason: SignInReasons.PASSWORD_CHECK,
})
.then(function (sessionInfo) {
// a session was created on the backend to check the user's
// password. Delete the newly created session immediately
// so that the session token is not left in the database.
if (sessionInfo && sessionInfo.sessionToken) {
return client.sessionDestroy(sessionInfo.sessionToken);
}
});
}
}),
/**
* Check whether an account exists for the given uid.
*
* @param {String} uid
* @returns {Promise}
*/
checkAccountExists: withClient((client, uid) => {
return client.accountStatus(uid).then(function (status) {
return status.exists;
});
}),
/**
* Check whether an account exists for the given email.
*
* @param {String} email
*
* @returns {Promise}
*/
checkAccountExistsByEmail: withClient((client, email) => {
return client.accountStatusByEmail(email).then(function (status) {
return status.exists;
});
}),
/**
* Check if the account's email is registered and retrieve third-party auth related values.
* @param {String} email
* @returns {Promise<{
* exists: boolean,
* hasLinkedAccount: boolean,
* hasPassword: boolean
* }>}
*/
checkAccountStatus: withClient((client, email) => {
return client
.accountStatusByEmail(email, { thirdPartyAuthStatus: true })
.then(function ({ exists, hasLinkedAccount, hasPassword }) {
return {
exists,
hasLinkedAccount,
hasPassword,
};
});
}),
/**
* Authenticate a user.
*
* @method signIn
* @param {String} originalEmail
* @param {String} password
* @param {Relier} relier
* @param {Object} [options]
* @param {String} [options.metricsContext] - context metadata for use in
* flow events
* @param {String} [options.reason] - Reason for the sign in. See definitions
* in sign-in-reasons.js. Defaults to SIGN_IN_REASONS.SIGN_IN.
* @param {String} [options.resume] - Resume token, passed in the
* verification link if the user must verify their email.
* @param {String} [options.sessionTokenContext] - The context for which
* the session token is being created. Defaults to the
* relier's context.
* @param {Boolean} [options.skipCaseError] - if set to true, INCORRECT_EMAIL_CASE
* errors will be returned to be handled locally instead of automatically
* being retried in the fxa-auth-client.
* @param {String} [options.unblockCode] - Unblock code.
* @returns {Promise}
*/
signIn: withClient(
(client, originalEmail, password, relier, options = {}) => {
var email = trim(originalEmail);
var signInOptions = {
keys: wantsKeys(relier),
reason: options.reason || SignInReasons.SIGN_IN,
};
// `service` is sent on signIn to notify users when a new service
// has been attached to their account.
if (relier.has('service')) {
signInOptions.service = relier.get('service');
}
if (relier.has('redirectTo')) {
signInOptions.redirectTo = relier.get('redirectTo');
}
if (options.unblockCode) {
signInOptions.unblockCode = options.unblockCode;
}
if (options.resume) {
signInOptions.resume = options.resume;
}
if (options.skipCaseError) {
signInOptions.skipCaseError = options.skipCaseError;
}
if (options.originalLoginEmail) {
signInOptions.originalLoginEmail = options.originalLoginEmail;
}
if (options.verificationMethod) {
signInOptions.verificationMethod = options.verificationMethod;
}
setMetricsContext(signInOptions, options);
return client
.signIn(email, password, signInOptions)
.then(function (accountData) {
if (
!accountData.verified &&
// eslint-disable-next-line no-prototype-builtins
!accountData.hasOwnProperty('verificationReason')
) {
// Set a default verificationReason to `SIGN_UP` to allow
// staged rollouts of servers. To handle calls to the
// legacy /account/login that lacks a verificationReason,
// assume SIGN_UP if the account is not verified.
accountData.verificationReason = VerificationReasons.SIGN_UP;
if (signInOptions.verificationMethod) {
accountData.verificationMethod = signInOptions.verificationMethod;
} else {
accountData.verificationMethod = VerificationMethods.EMAIL;
}
}
// The `originalLoginEmail` is a users current primary email, ensure
// the account model uses this email and updates local storage with it
if (signInOptions.originalLoginEmail) {
email = signInOptions.originalLoginEmail;
}
return getUpdatedSessionData(email, relier, accountData, options);
});
}
),
/**
* Re-authenticate a user.
*
* @method sessionReauth
* @param {String} sessionToken
* @param {String} originalEmail
* @param {String} password
* @param {Relier} relier
* @param {Object} [options]
* @param {String} [options.metricsContext] - context metadata for use in
* flow events
* @param {String} [options.reason] - Reason for the sign in. See definitions
* in sign-in-reasons.js. Defaults to SIGN_IN_REASONS.SIGN_IN.
* @param {String} [options.resume] - Resume token, passed in the
* verification link if the user must verify their email.
* @param {Boolean} [options.skipCaseError] - if set to true, INCORRECT_EMAIL_CASE
* errors will be returned to be handled locally instead of automatically
* being retried in the fxa-auth-client.
* @param {String} [options.unblockCode] - Unblock code.
* @param {String} [options.originalLoginEmail] - the original email address as entered
* by the user, if different from the one used for login.
* @param {String} [options.verificationMethod] - the method to use to verify the
* session, if it is not already verified.
* @returns {Promise}
*/
sessionReauth: withClient(
(client, sessionToken, originalEmail, password, relier, options = {}) => {
const email = trim(originalEmail);
const reauthOptions = {
keys: wantsKeys(relier),
reason: options.reason || SignInReasons.SIGN_IN,
};
if (relier.has('service')) {
reauthOptions.service = relier.get('service');
}
if (relier.has('redirectTo')) {
reauthOptions.redirectTo = relier.get('redirectTo');
}
if (options.unblockCode) {
reauthOptions.unblockCode = options.unblockCode;
}
if (options.resume) {
reauthOptions.resume = options.resume;
}
if (options.skipCaseError) {
reauthOptions.skipCaseError = options.skipCaseError;
}
if (options.originalLoginEmail) {
reauthOptions.originalLoginEmail = options.originalLoginEmail;
}
if (options.verificationMethod) {
reauthOptions.verificationMethod = options.verificationMethod;
}
setMetricsContext(reauthOptions, options);
return client
.sessionReauth(sessionToken, email, password, reauthOptions)
.then((accountData) => {
accountData.sessionToken = sessionToken;
return getUpdatedSessionData(email, relier, accountData, options);
});
}
),
/**
* Sign up a user
*
* @method signUp
* @param {String} originalEmail
* @param {String} password
* @param {Relier} relier
* @param {Object} [options]
* @param {String} [options.metricsContext] - Metrics context metadata
* @param {Boolean} [options.preVerified] - is the user preVerified
* @param {String} [options.resume] - Resume token, passed in the
* verification link if the user must verify
* their email.
* @param {String} [options.sessionTokenContext] - The context for
* which the session token is being created.
* Defaults to the relier's context.
* @param {String} [options.style] - Specify the style for emails
* @returns {Promise}
*/
signUp: withClient(function (
client,
originalEmail,
password,
relier,
options = {}
) {
var email = trim(originalEmail);
var signUpOptions = {
keys: wantsKeys(relier),
};
if (relier.has('service')) {
signUpOptions.service = relier.get('service');
}
if (relier.has('redirectTo')) {
signUpOptions.redirectTo = relier.get('redirectTo');
}
if (options.preVerified) {
signUpOptions.preVerified = true;
}
if (options.resume) {
signUpOptions.resume = options.resume;
}
if (options.verificationMethod) {
signUpOptions.verificationMethod = options.verificationMethod;
}
if (relier.has('style')) {
signUpOptions.style = relier.get('style');
}
setMetricsContext(signUpOptions, options);
return client
.signUp(email, password, signUpOptions)
.then((accountData) =>
getUpdatedSessionData(email, relier, accountData, options)
);
}),
/**
* Re-sends a verification code to the account's recovery email address.
*
* @param {Object} relier being signed into.
* @param {String} sessionToken sessionToken obtained from signIn
* @param {Object} [options={}] Options
* @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.style]
* Specify the style for the email
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
signUpResend: withClient((client, relier, sessionToken, options = {}) => {
var clientOptions = {
redirectTo: relier.get('redirectTo'),
service: relier.get('service'),
};
if (options.resume) {
clientOptions.resume = options.resume;
}
if (relier.has('style')) {
clientOptions.style = relier.get('style');
}
return client.recoveryEmailResendCode(sessionToken, clientOptions);
}),
/**
* Sends a verification code to the account's recovery email address
* that will verify the current session.
*
* @param {String} sessionToken sessionToken obtained from signIn
* @return {Promise} A promise that will be fulfilled with JSON `xhr.responseText` of the request
*/
sessionVerifyResend: withClient((client, sessionToken, options = {}) => {
const clientOptions = {
type: 'upgradeSession',
};
if (options.redirectTo) {
clientOptions.redirectTo = options.redirectTo;
}
return client.recoveryEmailResendCode(sessionToken, clientOptions);
}),
/**
* Destroy the user's current or custom session
*
* @param {String} sessionToken
* @param {Object} [options]
* @param {String} [options.customSessionToken] - if provided, deletes a custom session token
* @returns {Promise}
*
*/
sessionDestroy: createClientDelegate('sessionDestroy'),
/**
* Verify a signup code
*
* @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 {String} [options.style]
* Specify the style of confirmation email to be sent
* @return {Promise} resolves when complete
*/
verifyCode: createClientDelegate('verifyCode'),
/**
* Initiate a password reset
*
* @method passwordReset
* @param {String} originalEmail
* @param {Relier} relier
* @param {Object} [options]
* @param {String} [options.metricsContext] - context metadata for use in
* flow events
* @param {String} [options.resume] - Resume token, passed in the
* verification link if the user must verify their email.
* @return {Promise} resolves when complete
*/
passwordReset: withClient((client, originalEmail, relier, options = {}) => {
var email = trim(originalEmail);
var clientOptions = {
redirectTo: relier.get('redirectTo'),
service: relier.get('service'),
};
if (options.resume) {
clientOptions.resume = options.resume;
}
setMetricsContext(clientOptions, options);
return client
.passwordForgotSendCode(email, clientOptions)
.then(function (result) {
Session.clear();
return result;
});
}),
/**
* Re-sends a password reset verification code to the account's recovery email address.
*
* @param {String} originalEmail
* @param {String} passwordForgotToken
* @param {Object} relier
* @param {Object} [options={}] Options
* @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.
* @return {Promise} resolves when complete
*/
passwordResetResend: withClient(
(client, originalEmail, passwordForgotToken, relier, options = {}) => {
var email = trim(originalEmail);
// the linters complain if this is defined in the call to
// passwordForgotResendCode
var clientOptions = {
redirectTo: relier.get('redirectTo'),
service: relier.get('service'),
};
if (options.resume) {
clientOptions.resume = options.resume;
}
return client.passwordForgotResendCode(
email,
passwordForgotToken,
clientOptions
);
}
),
/**
* Submits the verification token to the server.
* The API returns accountResetToken to the client.
*
* @method passwordForgotVerifyCode
* @param {String} originalEmail
* @param {String} newPassword
* @param {String} token
* @param {String} code
* @param {Object} relier
* @param {Object} [options={}]
* @param {String} [options.emailToHashWith]
* If specified, the password is hashed with this email address, otherwise
* the user's password is hashed with their current email.
* @return {Promise} resolves when complete
*/
completePasswordReset: withClient(
(client, originalEmail, newPassword, token, code, relier, options = {}) => {
const email = trim(originalEmail);
var accountResetOptions = {
keys: wantsKeys(relier),
sessionToken: true,
};
return Promise.resolve()
.then(() => {
if (options.accountResetToken) {
return { accountResetToken: options.accountResetToken };
}
return client.passwordForgotVerifyCode(code, token, {});
})
.then((result) => {
let emailToHashWith = email;
// The `emailToHashWith` option is returned by the auth-server to let the content-server
// know what to hash the new password with. This is important in the scenario where a user
// has changed their primary email address. In this case, they must still hash with the
// account's original email because this will maintain backwards compatibility with
// how account password hashing works previously.
if (options.emailToHashWith) {
emailToHashWith = trim(options.emailToHashWith);
}
return client.accountReset(
emailToHashWith,
newPassword,
result.accountResetToken,
accountResetOptions
);
})
.then((accountData) => {
return getUpdatedSessionData(email, relier, accountData);
});
}
),
/**
* Check if the password reset is complete
*
* @param {String} token to check
* @returns {Promise} resolves to true if password reset has completed, false otw.
*/
isPasswordResetComplete: withClient((client, token) => {
return client.passwordForgotStatus(token).then(
function () {
// if the request succeeds, the password reset hasn't completed
return false;
},
function (err) {
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
return true;
}
throw err;
}
);
}),
/**
* Change the user's password
*
* @param {String} originalEmail
* @param {String} oldPassword
* @param {String} newPassword
* @param {String} sessionToken
* @param {String} sessionTokenContext
* @param {Relier} relier
* @returns {Promise} resolves with new session information on success.
*/
changePassword: withClient(
(
client,
originalEmail,
oldPassword,
newPassword,
sessionToken,
sessionTokenContext,
relier
) => {
var email = trim(originalEmail);
return client
.passwordChange(email, oldPassword, newPassword, sessionToken, {
keys: wantsKeys(relier, sessionTokenContext),
})
.then((accountData = {}) => {
return getUpdatedSessionData(email, relier, accountData, {
sessionTokenContext: sessionTokenContext,
});
});
}
),
/**
* Deletes the account.
*
* @param {String} originalEmail Email input
* @param {String} password Password input
* @param {String} sessionToken User session token
* @return {Promise} resolves when complete
*/
deleteAccount: withClient((client, originalEmail, password, sessionToken) => {
var email = trim(originalEmail);
return client.accountDestroy(email, password, {}, sessionToken);
}),
/**
* Responds successfully if the session status is valid, requires the sessionToken.
*
* @param {String} sessionToken User session token
* @return {Promise} resolves when complete
*/
sessionStatus: createClientDelegate('sessionStatus'),
/**
* Verify an account and a session using a otp based code.
*
* @param {String} sessionToken User session token
* @param {String} code Code to verify account and session
* @return {Promise} resolves when complete
*/
sessionVerifyCode: createClientDelegate('sessionVerifyCode'),
/**
* Resend the verify code based on otp.
*
* @param {String} sessionToken User session token
* @return {Promise} resolves when complete
*/
sessionResendVerifyCode: createClientDelegate('sessionResendVerifyCode'),
/**
* Check if `sessionToken` is valid
*
* @param {String} sessionToken
* @returns {Promise} resolves to true if valid, false otw.
*/
isSignedIn(sessionToken) {
// Check if the user is signed in.
if (!sessionToken) {
return Promise.resolve(false);
}
// Validate session token
return this.sessionStatus(sessionToken).then(
function () {
return true;
},
function (err) {
// the only error that we expect is INVALID_TOKEN,
// rethrow all others.
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
return false;
}
throw err;
}
);
},
/**
* Check the status of the sessionToken. Status information
* includes whether the session is verified, and if not, the reason
* it must be verified and by which method.
*
* @param {String} sessionToken
* @returns {Promise} resolves with the account's current session
* information if session is valid. Rejects with an INVALID_TOKEN error
* if session is invalid.
*
* Session information:
* {
* verified: <boolean>
* verificationMethod: <see lib/verification-methods.js>
* verificationReason: <see lib/verification-reasons.js>
* }
*/
recoveryEmailStatus: withClient(function (client, sessionToken) {
return client.recoveryEmailStatus(sessionToken).then(function (response) {
if (!response.verified) {
// This is a little bit unnatural. /recovery_email/status
// returns two fields, `emailVerified` and
// `sessionVerified`. The client side depends on a reason
// to show the correct UI. Convert `emailVerified` to
// a `verificationReason`.
var verificationReason = response.emailVerified
? VerificationReasons.SIGN_IN
: VerificationReasons.SIGN_UP;
return {
email: response.email,
verificationReason: verificationReason,
verified: false,
};
}
// /recovery_email/status returns `emailVerified` and
// `sessionVerified`, we don't want those.
return _.pick(response, 'email', 'verified');
});
}),
/**
* This function gets the status of the user's sessionToken.
* It differs from `recoveryEmailStatus` because it also returns
* `sessionVerified`, which gives the true state of the sessionToken.
*
* Note that a session is considered verified if it has gone through
* an email verification loop.
*
* @param {String} sessionToken
* @returns {Promise} resolves with response when complete.
*/
sessionVerificationStatus: createClientDelegate('recoveryEmailStatus'),
/**
* Get the base16 bundle of encrypted kA|wrapKb.
*
* @param {String} keyFetchToken
* @param {String} oldUnwrapBKey
* @return {Promise} resolves when complete
*/
accountKeys: createClientDelegate('accountKeys'),
/**
* Get the account profile from the auth server.
*
* @param {String} sessionToken
* @return {Promise} resolves when complete
*/
accountProfile: createClientDelegate('accountProfile'),
/**
* Get the account details from the auth server.
*
* @param {String} sessionToken
* @return {Promise}
*/
account: createClientDelegate('account'),
/**
* Get a list of all devices for a user
*
* @param {String} sessionToken sessionToken obtained from signIn
* @return {Promise} resolves when complete
*/
deviceList: createClientDelegate('deviceList'),
/**
* Get user's sessions
*
* @param {String} sessionToken
* @returns {Promise} resolves with response when complete.
*/
sessions: createClientDelegate('sessions'),
/**
* Get user's security events
*
* @param {String} sessionToken
* @returns {Promise} resolves with response when complete.
*/
securityEvents: createClientDelegate('securityEvents'),
/**
* Get user's attached clients
*
* @param {String} sessionToken
* @returns {Promise} resolves with response when complete.
*/
attachedClients: createClientDelegate('attachedClients'),
/**
* Unregister an existing device
*
* @param {String} sessionToken Session token obtained from signIn
* @param {String} deviceId User-unique identifier of device
* @return {Promise} resolves when complete
*/
deviceDestroy: createClientDelegate('deviceDestroy'),
/**
* Get user's attached clients
*
* @param {String} sessionToken
* @returns {Promise} resolves with response when complete.
*/
attachedClientDestroy: createClientDelegate('attachedClientDestroy'),
/**
* Send an unblock email.
*
* @param {String} email - destination email address
* @param {Object} [options] - options
* @param {String} [options.metricsContext] - context metadata for use in
* flow events
* @returns {Promise} resolves with response when complete.
*/
sendUnblockEmail: withClient((client, email, options = {}) => {
const sendUnblockCodeOptions = {};
setMetricsContext(sendUnblockCodeOptions, options);
return client.sendUnblockCode(email, sendUnblockCodeOptions);
}),
/**
* Reject an unblock code.
*
* @param {String} uid - user id
* @param {String} unblockCode - unblock code
* @returns {Promise} resolves when complete.
*/
rejectUnblockCode: createClientDelegate('rejectUnblockCode'),
/**
* Get the recovery emails associated with the signed in account.
*
* @param {String} sessionToken SessionToken obtained from signIn
* @returns {Promise} resolves to the list of recovery emails when complete
*/
recoveryEmails: createClientDelegate('recoveryEmails'),
/**
* Create a new recovery email for the signed in account.
*
* @param {String} sessionToken SessionToken obtained from signIn
* @param {String} email new email to be added
* @param {Object} options options
* @returns {Promise} resolves when complete
*/
recoveryEmailCreate: createClientDelegate('recoveryEmailCreate'),
/**
* Remove the recovery email for the signed in account.
*
* @param {String} sessionToken SessionToken obtained from signIn
* @param {String} email email to be removed
* @returns {Promise} resolves when complete
*/
recoveryEmailDestroy: createClientDelegate('recoveryEmailDestroy'),
/**
* Re-sends a verification code to the account's recovery email address.
*
* @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.lang]
* set the language for the 'Accept-Language' header
* @return {Promise} resolves when complete
*/
resendEmailCode: createClientDelegate('recoveryEmailResendCode'),
deleteEmail: createClientDelegate('deleteEmail'),
/**
* Set the new primary email address for a user.
*
* @param {String} sessionToken User session token
* @param {String} email The new primary email address
* @return {Promise} resolves when complete
*/
recoveryEmailSetPrimaryEmail: createClientDelegate(
'recoveryEmailSetPrimaryEmail'
),
/**
* Verify secondary email via a code.
*
* @param {String} sessionToken User session token
* @param {String} email The email address
* @param {Number} code Code to verify address with
* @return {Promise} resolves when complete
*/
recoveryEmailSecondaryVerifyCode: createClientDelegate(
'recoveryEmailSecondaryVerifyCode'
),
/**
* Resend secondary email verification code.
*
* @param {String} sessionToken User session token
* @param {String} email Email to resend verification code
*/
recoveryEmailSecondaryResendCode: createClientDelegate(
'recoveryEmailSecondaryResendCode'
),
/**
* Creates a new TOTP token for the current user.
*
* @param {String} sessionToken SessionToken obtained from signIn
* @returns {Promise} resolves when complete
*/
createTotpToken: createClientDelegate('createTotpToken'),
/**
* Deletes the current user's TOTP token.
*
* @param {String} sessionToken SessionToken obtained from signIn
* @returns {Promise} resolves when complete
*/
deleteTotpToken: createClientDelegate('deleteTotpToken'),
/**
* Checks to see if the current user has a TOTP token.
*
* @param {String} sessionToken SessionToken obtained from signIn
* @returns {Promise} resolves when complete
*/
checkTotpTokenExists: createClientDelegate('checkTotpTokenExists'),
/**
* Checks to see if the TOTP code is valid and verifies the TOTP token
* if it is.
*
* @param {String} sessionToken SessionToken obtained from signIn
* @param {String} code TOTP code
* @param {Object} [options={}] Options
* @param {String} [options.service] - service used
* @returns {Promise} resolves when complete
*/
verifyTotpCode: createClientDelegate('verifyTotpCode'),
/**
* Consume and verifies a session with a backup authentication code.
*
* @param {String} sessionToken SessionToken obtained from signIn
* @param {String} code Backup authentication code
* @returns {Promise} resolves when complete
*/
consumeRecoveryCode: createClientDelegate('consumeRecoveryCode'),
/**
* Replace all user backup authentication codes.
*
* @param {String} sessionToken SessionToken obtained from signIn
* @returns {Promise} resolves when complete
*/
replaceRecoveryCodes: createClientDelegate('replaceRecoveryCodes'),
/**
* Creates a new account recovery key bundle for the current user. To
* create an account recovery key first a session re-auth is performed,
* then the account keys are fetched and finally the recovery
* bundle stores an encrypted copy of the original user's `kB`
*
* @param {String} email - email address
* @param {String} password - current password of the user
* @param {String} sessionToken - SessionToken obtained from signIn
* @param {String} uid - current uid of the user
* @returns {Promise} resolves with response when complete.
*/
createRecoveryBundle: withClient(
(client, email, password, sessionToken, uid, enabled = true) => {
let recoveryKey, keys, recoveryJwk;
return client
.sessionReauth(sessionToken, email, password, {
keys: true,
reason: VerificationReasons.RECOVERY_KEY,
})
.then((res) => client.accountKeys(res.keyFetchToken, res.unwrapBKey))
.then((result) => {
keys = result;
return RecoveryKey.generateRecoveryKey(
Constants.RECOVERY_KEY_LENGTH
).then((result) => {
recoveryKey = result;
return RecoveryKey.getRecoveryJwk(uid, recoveryKey);
});
})
.then((result) => {
recoveryJwk = result;
return RecoveryKey.bundleRecoveryData(recoveryJwk, keys);
})
.then((bundle) =>
client.createRecoveryKey(
sessionToken,
recoveryJwk.kid,
bundle,
enabled
)
)
.then(() => {
return { recoveryKey, recoveryKeyId: recoveryJwk.kid };
});
}
),
/**
* Deletes the account recovery key associated with this user.
*
* @param sessionToken
*/
deleteRecoveryKey: createClientDelegate('deleteRecoveryKey'),
/**
* Verify the account recovery key associated with this user.
*
* @param sessionToken
* @param recoveryKeyId
*/
verifyRecoveryKey: createClientDelegate('verifyRecoveryKey'),
/**
* This checks to see if an account recovery key exists for a user.
*
* @param sessionToken
* @param {String} email User's email
* @returns {Promise} resolves with response when complete.
*/
recoveryKeyExists: createClientDelegate('recoveryKeyExists'),
/**
* Verify passwordForgotCode which returns an `accountResetToken`.
*
* @param {String} passwordForgotCode - password forgot code
* @param {String} passwordForgotToken - password forgot token
* @param {Object} [options={}] Options
* @param {String} [options.accountResetWithRecoveryKey] - perform account reset with account recovery key
* @returns {Promise} resolves with response when complete.
*/
passwordForgotVerifyCode: withClient(
(client, passwordForgotCode, passwordForgotToken, options = {}) => {
return client.passwordForgotVerifyCode(
passwordForgotCode,
passwordForgotToken,
options
);
}
),
/**
* Gets account recovery key bundle for the current user.
*
* @param {String} accountResetToken
* @param {String} uid - Uid of user
* @param {String} recoveryKey - User's account recovery key
* @returns {Promise} resolves with response when complete.
*/
getRecoveryBundle: withClient(
(client, accountResetToken, uid, recoveryKey) => {
return RecoveryKey.getRecoveryJwk(uid, recoveryKey).then(
(recoveryJwk) => {
return client
.getRecoveryKey(accountResetToken, recoveryJwk.kid)
.then((bundle) =>
RecoveryKey.unbundleRecoveryData(recoveryJwk, bundle.recoveryData)
)
.then((data) => {
return {
keys: data,
recoveryKeyId: recoveryJwk.kid,
};
});
}
);
}
),
/**
* Reset an account using an account recovery key. This maintains a user's original encryption keys.
*
* @param {String} accountResetToken
* @param {String} email - Email of user
* @param {String} newPassword - New password for user
* @param {String} recoveryKeyId - The recoveryKeyId that mapped to original account recovery key
* @param {String} kB - Wrap new password with this kB
* @param {String} relier - Relier to sign-in
* @returns {Promise} resolves with response when complete.
*/
resetPasswordWithRecoveryKey: withClient(
(
client,
accountResetToken,
email,
newPassword,
recoveryKeyId,
kB,
relier
) => {
const keys = { kB };
return client
.resetPasswordWithRecoveryKey(
accountResetToken,
email,
newPassword,
recoveryKeyId,
keys,
{ keys: true, sessionToken: true }
)
.then((accountData) => {
return getUpdatedSessionData(email, relier, accountData);
});
}
),
/**
* Create an OAuth code using sessionToken
*
* @param {String} sessionToken
* @param {String} clientId
* @param {String} state
* @param {Object} [options={}]
* @param {String} [options.access_type=online] if `access_type=offline`, a refresh token
* will be issued when trading the code for an access token.
* @param {String} [options.acr_values] allowed ACR values
* @param {String} [options.keys_jwe] Keys used to encrypt
* @param {String} [options.redirect_uri] registered redirect URI to return to
* @param {String} [options.response_type=code] response type
* @param {String} [options.scope] requested scopes
* @param {String} [options.code_challenge_method] PKCE code challenge method
* @param {String} [options.code_challenge] PKCE code challenge
* @returns {Promise} A promise that will be fulfilled with:
* - `redirect` - redirect URI
* - `code` - authorization code
* - `state` - state token
*/
createOAuthCode: createClientDelegate('createOAuthCode'),
/**
* Create an OAuth token using `sessionToken`
*
* @param {String} sessionToken
* @param {String} clientId
* @param {Object} [options={}] Options
* @param {String} [options.access_type=online] if `access_type=offline`, a refresh token
* will be issued when trading the code for an access token.
* @param {String} [options.scope] requested scopes
* @param {Number} [options.ttl] time to live, in seconds
* @returns {Promise} A promise that will be fulfilled with:
* - `access_token` - The access token
* - `refresh_token` - A refresh token, if `options.access_type=offline`
* - `id_token` - an OIDC ID token, returned if `scope` includes `openid`
* - `scope` - Requested scopes
* - `auth_at` - Time the user authenticated
* - `token_type` - The string `bearer`
* - `expires_in` - Time at which the token expires
*/
createOAuthToken: createClientDelegate('createOAuthToken'),
/**
* Use `sessionToken` to get scoped key data for the RP associated with `client_id`
*
* @param {String} sessionToken
* @param {String} clientId
* @param {String} scope
* @returns {Promise} A promise that will be fulfilled with:
* - `identifier`
* - `keyRotationSecret`
* - `keyRotationTimestamp`
*/
getOAuthScopedKeyData: createClientDelegate('getOAuthScopedKeyData'),
/**
* Get a list of subscription plans with an OAuth access token.
*
* @param {String} token An access token from the OAuth server.
* @returns {Promise} A promise that will be fulfilled with a list of
* subscription plans from SubHub.
*/
getSubscriptionPlans: createClientDelegate('getSubscriptionPlans'),
/**
* Get a list of active subscriptions with an OAuth access token.
*
* @param {String} token A token from the OAuth server.
* @returns {Promise} A promise that will be fulfilled with a list of active
* subscriptions.
*/
getActiveSubscriptions: createClientDelegate('getActiveSubscriptions'),
/**
* Create a support ticket.
*
* @param {String} token A token from the OAuth server.
* @param {Object} [supportTicket={}]
* @param {String} [supportTicket.topic]
* @param {String} [supportTicket.subject] Optional subject
* @param {String} [supportTicket.message]
* @returns {Promise} A promise that will be fulfilled with:
* - `success`
* - `ticket` OR `error`
*/
createSupportTicket: createClientDelegate('createSupportTicket'),
/**
* Update a user newsletters subscription.
*
* @param {String[]} [newsletters]
* @returns {Promise} - resolves with empty response
*/
updateNewsletters: createClientDelegate('updateNewsletters'),
/**
* Verify an ID Token.
*
* @param {String} idToken An ID Token supplied as an id_token_hint by an RP
* @returns {Promise} resolves with response when complete.
*/
verifyIdToken: createClientDelegate('verifyIdToken'),
/**
* Create a one-time use sign-in code.
*
* @returns {Promise} resolves with response when complete.
*/
createSigninCode: createClientDelegate('createSigninCode'),
/**
* Queues up a reminder to CAD to be delivered at a later time.
*
* @returns {Promise} resolves with response when complete.
*/
createCadReminder: createClientDelegate('createCadReminder'),
/**
* Sends a push notification to compatible devices that can verify a login
* request
*
* @returns {Promise} resolves with response when complete.
*/
sendPushLoginRequest: createClientDelegate('sendPushLoginRequest'),
finishSetup: withClient((client, relier, token, email, password) => {
return client.finishSetup(token, email, password).then((accountData) => {
return getUpdatedSessionData(email, relier, accountData);
});
}),
verifyAccountThirdParty: withClient(
(client, relier, token, provider, metricsContext) => {
return client
.verifyAccountThirdParty(
token,
provider,
relier.get('service'),
metricsContext
)
.then((accountData) => {
return getUpdatedSessionData(accountData.email, relier, accountData);
});
}
),
createPassword: withClient((client, token, email, password) => {
return client.createPassword(token, email, password);
}),
};
export default FxaClientWrapper;
function setMetricsContext(serverOptions, options) {
if (options.metricsContext) {
serverOptions.metricsContext = options.metricsContext;
}
}