summaryrefslogtreecommitdiff
path: root/services/fxaccounts/FxAccountsClient.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'services/fxaccounts/FxAccountsClient.jsm')
-rw-r--r--services/fxaccounts/FxAccountsClient.jsm623
1 files changed, 0 insertions, 623 deletions
diff --git a/services/fxaccounts/FxAccountsClient.jsm b/services/fxaccounts/FxAccountsClient.jsm
deleted file mode 100644
index fbe8da2fe4..0000000000
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ /dev/null
@@ -1,623 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-this.EXPORTED_SYMBOLS = ["FxAccountsClient"];
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/Log.jsm");
-Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://services-common/utils.js");
-Cu.import("resource://services-common/hawkclient.js");
-Cu.import("resource://services-common/hawkrequest.js");
-Cu.import("resource://services-crypto/utils.js");
-Cu.import("resource://gre/modules/FxAccountsCommon.js");
-Cu.import("resource://gre/modules/Credentials.jsm");
-
-const HOST_PREF = "identity.fxaccounts.auth.uri";
-
-const SIGNIN = "/account/login";
-const SIGNUP = "/account/create";
-
-this.FxAccountsClient = function(host = Services.prefs.getCharPref(HOST_PREF)) {
- this.host = host;
-
- // The FxA auth server expects requests to certain endpoints to be authorized
- // using Hawk.
- this.hawk = new HawkClient(host);
- this.hawk.observerPrefix = "FxA:hawk";
-
- // Manage server backoff state. C.f.
- // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#backoff-protocol
- this.backoffError = null;
-};
-
-this.FxAccountsClient.prototype = {
-
- /**
- * Return client clock offset, in milliseconds, as determined by hawk client.
- * Provided because callers should not have to know about hawk
- * implementation.
- *
- * The offset is the number of milliseconds that must be added to the client
- * clock to make it equal to the server clock. For example, if the client is
- * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
- */
- get localtimeOffsetMsec() {
- return this.hawk.localtimeOffsetMsec;
- },
-
- /*
- * Return current time in milliseconds
- *
- * Not used by this module, but made available to the FxAccounts.jsm
- * that uses this client.
- */
- now: function() {
- return this.hawk.now();
- },
-
- /**
- * Common code from signIn and signUp.
- *
- * @param path
- * Request URL path. Can be /account/create or /account/login
- * @param email
- * The email address for the account (utf8)
- * @param password
- * The user's password
- * @param [getKeys=false]
- * If set to true the keyFetchToken will be retrieved
- * @param [retryOK=true]
- * If capitalization of the email is wrong and retryOK is set to true,
- * we will retry with the suggested capitalization from the server
- * @return Promise
- * Returns a promise that resolves to an object:
- * {
- * authAt: authentication time for the session (seconds since epoch)
- * email: the primary email for this account
- * keyFetchToken: a key fetch token (hex)
- * sessionToken: a session token (hex)
- * uid: the user's unique ID (hex)
- * unwrapBKey: used to unwrap kB, derived locally from the
- * password (not revealed to the FxA server)
- * verified (optional): flag indicating verification status of the
- * email
- * }
- */
- _createSession: function(path, email, password, getKeys=false,
- retryOK=true) {
- return Credentials.setup(email, password).then((creds) => {
- let data = {
- authPW: CommonUtils.bytesAsHex(creds.authPW),
- email: email,
- };
- let keys = getKeys ? "?keys=true" : "";
-
- return this._request(path + keys, "POST", null, data).then(
- // Include the canonical capitalization of the email in the response so
- // the caller can set its signed-in user state accordingly.
- result => {
- result.email = data.email;
- result.unwrapBKey = CommonUtils.bytesAsHex(creds.unwrapBKey);
-
- return result;
- },
- error => {
- log.debug("Session creation failed", error);
- // If the user entered an email with different capitalization from
- // what's stored in the database (e.g., Greta.Garbo@gmail.COM as
- // opposed to greta.garbo@gmail.com), the server will respond with a
- // errno 120 (code 400) and the expected capitalization of the email.
- // We retry with this email exactly once. If successful, we use the
- // server's version of the email as the signed-in-user's email. This
- // is necessary because the email also serves as salt; so we must be
- // in agreement with the server on capitalization.
- //
- // API reference:
- // https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md
- if (ERRNO_INCORRECT_EMAIL_CASE === error.errno && retryOK) {
- if (!error.email) {
- log.error("Server returned errno 120 but did not provide email");
- throw error;
- }
- return this._createSession(path, error.email, password, getKeys,
- false);
- }
- throw error;
- }
- );
- });
- },
-
- /**
- * Create a new Firefox Account and authenticate
- *
- * @param email
- * The email address for the account (utf8)
- * @param password
- * The user's password
- * @param [getKeys=false]
- * If set to true the keyFetchToken will be retrieved
- * @return Promise
- * Returns a promise that resolves to an object:
- * {
- * uid: the user's unique ID (hex)
- * sessionToken: a session token (hex)
- * keyFetchToken: a key fetch token (hex),
- * unwrapBKey: used to unwrap kB, derived locally from the
- * password (not revealed to the FxA server)
- * }
- */
- signUp: function(email, password, getKeys=false) {
- return this._createSession(SIGNUP, email, password, getKeys,
- false /* no retry */);
- },
-
- /**
- * Authenticate and create a new session with the Firefox Account API server
- *
- * @param email
- * The email address for the account (utf8)
- * @param password
- * The user's password
- * @param [getKeys=false]
- * If set to true the keyFetchToken will be retrieved
- * @return Promise
- * Returns a promise that resolves to an object:
- * {
- * authAt: authentication time for the session (seconds since epoch)
- * email: the primary email for this account
- * keyFetchToken: a key fetch token (hex)
- * sessionToken: a session token (hex)
- * uid: the user's unique ID (hex)
- * unwrapBKey: used to unwrap kB, derived locally from the
- * password (not revealed to the FxA server)
- * verified: flag indicating verification status of the email
- * }
- */
- signIn: function signIn(email, password, getKeys=false) {
- return this._createSession(SIGNIN, email, password, getKeys,
- true /* retry */);
- },
-
- /**
- * Check the status of a session given a session token
- *
- * @param sessionTokenHex
- * The session token encoded in hex
- * @return Promise
- * Resolves with a boolean indicating if the session is still valid
- */
- sessionStatus: function (sessionTokenHex) {
- return this._request("/session/status", "GET",
- deriveHawkCredentials(sessionTokenHex, "sessionToken")).then(
- () => Promise.resolve(true),
- error => {
- if (isInvalidTokenError(error)) {
- return Promise.resolve(false);
- }
- throw error;
- }
- );
- },
-
- /**
- * Destroy the current session with the Firefox Account API server
- *
- * @param sessionTokenHex
- * The session token encoded in hex
- * @return Promise
- */
- signOut: function (sessionTokenHex, options = {}) {
- let path = "/session/destroy";
- if (options.service) {
- path += "?service=" + encodeURIComponent(options.service);
- }
- return this._request(path, "POST",
- deriveHawkCredentials(sessionTokenHex, "sessionToken"));
- },
-
- /**
- * Check the verification status of the user's FxA email address
- *
- * @param sessionTokenHex
- * The current session token encoded in hex
- * @return Promise
- */
- recoveryEmailStatus: function (sessionTokenHex, options = {}) {
- let path = "/recovery_email/status";
- if (options.reason) {
- path += "?reason=" + encodeURIComponent(options.reason);
- }
-
- return this._request(path, "GET",
- deriveHawkCredentials(sessionTokenHex, "sessionToken"));
- },
-
- /**
- * Resend the verification email for the user
- *
- * @param sessionTokenHex
- * The current token encoded in hex
- * @return Promise
- */
- resendVerificationEmail: function(sessionTokenHex) {
- return this._request("/recovery_email/resend_code", "POST",
- deriveHawkCredentials(sessionTokenHex, "sessionToken"));
- },
-
- /**
- * Retrieve encryption keys
- *
- * @param keyFetchTokenHex
- * A one-time use key fetch token encoded in hex
- * @return Promise
- * Returns a promise that resolves to an object:
- * {
- * kA: an encryption key for recevorable data (bytes)
- * wrapKB: an encryption key that requires knowledge of the
- * user's password (bytes)
- * }
- */
- accountKeys: function (keyFetchTokenHex) {
- let creds = deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
- let keyRequestKey = creds.extra.slice(0, 32);
- let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
- Credentials.keyWord("account/keys"), 3 * 32);
- let respHMACKey = morecreds.slice(0, 32);
- let respXORKey = morecreds.slice(32, 96);
-
- return this._request("/account/keys", "GET", creds).then(resp => {
- if (!resp.bundle) {
- throw new Error("failed to retrieve keys");
- }
-
- let bundle = CommonUtils.hexToBytes(resp.bundle);
- let mac = bundle.slice(-32);
-
- let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
- CryptoUtils.makeHMACKey(respHMACKey));
-
- let bundleMAC = CryptoUtils.digestBytes(bundle.slice(0, -32), hasher);
- if (mac !== bundleMAC) {
- throw new Error("error unbundling encryption keys");
- }
-
- let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64));
-
- return {
- kA: keyAWrapB.slice(0, 32),
- wrapKB: keyAWrapB.slice(32)
- };
- });
- },
-
- /**
- * Sends a public key to the FxA API server and returns a signed certificate
- *
- * @param sessionTokenHex
- * The current session token encoded in hex
- * @param serializedPublicKey
- * A public key (usually generated by jwcrypto)
- * @param lifetime
- * The lifetime of the certificate
- * @return Promise
- * Returns a promise that resolves to the signed certificate.
- * The certificate can be used to generate a Persona assertion.
- * @throws a new Error
- * wrapping any of these HTTP code/errno pairs:
- * https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#response-12
- */
- signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) {
- let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
-
- let body = { publicKey: serializedPublicKey,
- duration: lifetime };
- return Promise.resolve()
- .then(_ => this._request("/certificate/sign", "POST", creds, body))
- .then(resp => resp.cert,
- err => {
- log.error("HAWK.signCertificate error: " + JSON.stringify(err));
- throw err;
- });
- },
-
- /**
- * Determine if an account exists
- *
- * @param email
- * The email address to check
- * @return Promise
- * The promise resolves to true if the account exists, or false
- * if it doesn't. The promise is rejected on other errors.
- */
- accountExists: function (email) {
- return this.signIn(email, "").then(
- (cantHappen) => {
- throw new Error("How did I sign in with an empty password?");
- },
- (expectedError) => {
- switch (expectedError.errno) {
- case ERRNO_ACCOUNT_DOES_NOT_EXIST:
- return false;
- break;
- case ERRNO_INCORRECT_PASSWORD:
- return true;
- break;
- default:
- // not so expected, any more ...
- throw expectedError;
- break;
- }
- }
- );
- },
-
- /**
- * Given the uid of an existing account (not an arbitrary email), ask
- * the server if it still exists via /account/status.
- *
- * Used for differentiating between password change and account deletion.
- */
- accountStatus: function(uid) {
- return this._request("/account/status?uid="+uid, "GET").then(
- (result) => {
- return result.exists;
- },
- (error) => {
- log.error("accountStatus failed with: " + error);
- return Promise.reject(error);
- }
- );
- },
-
- /**
- * Register a new device
- *
- * @method registerDevice
- * @param sessionTokenHex
- * Session token obtained from signIn
- * @param name
- * Device name
- * @param type
- * Device type (mobile|desktop)
- * @param [options]
- * Extra device options
- * @param [options.pushCallback]
- * `pushCallback` push endpoint callback
- * @param [options.pushPublicKey]
- * `pushPublicKey` push public key (URLSafe Base64 string)
- * @param [options.pushAuthKey]
- * `pushAuthKey` push auth secret (URLSafe Base64 string)
- * @return Promise
- * Resolves to an object:
- * {
- * id: Device identifier
- * createdAt: Creation time (milliseconds since epoch)
- * name: Name of device
- * type: Type of device (mobile|desktop)
- * }
- */
- registerDevice(sessionTokenHex, name, type, options = {}) {
- let path = "/account/device";
-
- let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
- let body = { name, type };
-
- if (options.pushCallback) {
- body.pushCallback = options.pushCallback;
- }
- if (options.pushPublicKey && options.pushAuthKey) {
- body.pushPublicKey = options.pushPublicKey;
- body.pushAuthKey = options.pushAuthKey;
- }
-
- return this._request(path, "POST", creds, body);
- },
-
- /**
- * Sends a message to other devices. Must conform with the push payload schema:
- * https://github.com/mozilla/fxa-auth-server/blob/master/docs/pushpayloads.schema.json
- *
- * @method notifyDevice
- * @param sessionTokenHex
- * Session token obtained from signIn
- * @param deviceIds
- * Devices to send the message to
- * @param payload
- * Data to send with the message
- * @return Promise
- * Resolves to an empty object:
- * {}
- */
- notifyDevices(sessionTokenHex, deviceIds, payload, TTL = 0) {
- const body = {
- to: deviceIds,
- payload,
- TTL
- };
- return this._request("/account/devices/notify", "POST",
- deriveHawkCredentials(sessionTokenHex, "sessionToken"), body);
- },
-
- /**
- * Update the session or name for an existing device
- *
- * @method updateDevice
- * @param sessionTokenHex
- * Session token obtained from signIn
- * @param id
- * Device identifier
- * @param name
- * Device name
- * @param [options]
- * Extra device options
- * @param [options.pushCallback]
- * `pushCallback` push endpoint callback
- * @param [options.pushPublicKey]
- * `pushPublicKey` push public key (URLSafe Base64 string)
- * @param [options.pushAuthKey]
- * `pushAuthKey` push auth secret (URLSafe Base64 string)
- * @return Promise
- * Resolves to an object:
- * {
- * id: Device identifier
- * name: Device name
- * }
- */
- updateDevice(sessionTokenHex, id, name, options = {}) {
- let path = "/account/device";
-
- let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
- let body = { id, name };
- if (options.pushCallback) {
- body.pushCallback = options.pushCallback;
- }
- if (options.pushPublicKey && options.pushAuthKey) {
- body.pushPublicKey = options.pushPublicKey;
- body.pushAuthKey = options.pushAuthKey;
- }
-
- return this._request(path, "POST", creds, body);
- },
-
- /**
- * Delete a device and its associated session token, signing the user
- * out of the server.
- *
- * @method signOutAndDestroyDevice
- * @param sessionTokenHex
- * Session token obtained from signIn
- * @param id
- * Device identifier
- * @param [options]
- * Options object
- * @param [options.service]
- * `service` query parameter
- * @return Promise
- * Resolves to an empty object:
- * {}
- */
- signOutAndDestroyDevice(sessionTokenHex, id, options = {}) {
- let path = "/account/device/destroy";
-
- if (options.service) {
- path += "?service=" + encodeURIComponent(options.service);
- }
-
- let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
- let body = { id };
-
- return this._request(path, "POST", creds, body);
- },
-
- /**
- * Get a list of currently registered devices
- *
- * @method getDeviceList
- * @param sessionTokenHex
- * Session token obtained from signIn
- * @return Promise
- * Resolves to an array of objects:
- * [
- * {
- * id: Device id
- * isCurrentDevice: Boolean indicating whether the item
- * represents the current device
- * name: Device name
- * type: Device type (mobile|desktop)
- * },
- * ...
- * ]
- */
- getDeviceList(sessionTokenHex) {
- let path = "/account/devices";
- let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
-
- return this._request(path, "GET", creds, {});
- },
-
- _clearBackoff: function() {
- this.backoffError = null;
- },
-
- /**
- * A general method for sending raw API calls to the FxA auth server.
- * All request bodies and responses are JSON.
- *
- * @param path
- * API endpoint path
- * @param method
- * The HTTP request method
- * @param credentials
- * Hawk credentials
- * @param jsonPayload
- * A JSON payload
- * @return Promise
- * Returns a promise that resolves to the JSON response of the API call,
- * or is rejected with an error. Error responses have the following properties:
- * {
- * "code": 400, // matches the HTTP status code
- * "errno": 107, // stable application-level error number
- * "error": "Bad Request", // string description of the error type
- * "message": "the value of salt is not allowed to be undefined",
- * "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error
- * }
- */
- _request: function hawkRequest(path, method, credentials, jsonPayload) {
- let deferred = Promise.defer();
-
- // We were asked to back off.
- if (this.backoffError) {
- log.debug("Received new request during backoff, re-rejecting.");
- deferred.reject(this.backoffError);
- return deferred.promise;
- }
-
- this.hawk.request(path, method, credentials, jsonPayload).then(
- (response) => {
- try {
- let responseObj = JSON.parse(response.body);
- deferred.resolve(responseObj);
- } catch (err) {
- log.error("json parse error on response: " + response.body);
- deferred.reject({error: err});
- }
- },
-
- (error) => {
- log.error("error " + method + "ing " + path + ": " + JSON.stringify(error));
- if (error.retryAfter) {
- log.debug("Received backoff response; caching error as flag.");
- this.backoffError = error;
- // Schedule clearing of cached-error-as-flag.
- CommonUtils.namedTimer(
- this._clearBackoff,
- error.retryAfter * 1000,
- this,
- "fxaBackoffTimer"
- );
- }
- deferred.reject(error);
- }
- );
-
- return deferred.promise;
- },
-};
-
-function isInvalidTokenError(error) {
- if (error.code != 401) {
- return false;
- }
- switch (error.errno) {
- case ERRNO_INVALID_AUTH_TOKEN:
- case ERRNO_INVALID_AUTH_TIMESTAMP:
- case ERRNO_INVALID_AUTH_NONCE:
- return true;
- }
- return false;
-}