зеркало из https://github.com/mozilla/gecko-dev.git
840 строки
26 KiB
JavaScript
840 строки
26 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/. */
|
|
|
|
import { CommonUtils } from "resource://services-common/utils.sys.mjs";
|
|
|
|
import { HawkClient } from "resource://services-common/hawkclient.sys.mjs";
|
|
import { deriveHawkCredentials } from "resource://services-common/hawkrequest.sys.mjs";
|
|
import { CryptoUtils } from "resource://services-crypto/utils.sys.mjs";
|
|
|
|
import {
|
|
ERRNO_ACCOUNT_DOES_NOT_EXIST,
|
|
ERRNO_INCORRECT_EMAIL_CASE,
|
|
ERRNO_INCORRECT_PASSWORD,
|
|
ERRNO_INVALID_AUTH_NONCE,
|
|
ERRNO_INVALID_AUTH_TIMESTAMP,
|
|
ERRNO_INVALID_AUTH_TOKEN,
|
|
log,
|
|
} from "resource://gre/modules/FxAccountsCommon.sys.mjs";
|
|
|
|
import { Credentials } from "resource://gre/modules/Credentials.sys.mjs";
|
|
|
|
const HOST_PREF = "identity.fxaccounts.auth.uri";
|
|
|
|
const SIGNIN = "/account/login";
|
|
const SIGNUP = "/account/create";
|
|
// Devices older than this many days will not appear in the devices list
|
|
const DEVICES_FILTER_DAYS = 21;
|
|
|
|
export var FxAccountsClient = function (
|
|
host = Services.prefs.getStringPref(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;
|
|
};
|
|
|
|
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.sys.mjs
|
|
* that uses this client.
|
|
*/
|
|
now() {
|
|
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(path, email, password, getKeys = false, retryOK = true) {
|
|
return Credentials.setup(email, password).then(creds => {
|
|
let data = {
|
|
authPW: CommonUtils.bytesAsHex(creds.authPW),
|
|
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(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
|
|
*/
|
|
async sessionStatus(sessionTokenHex) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
return this._request("/session/status", "GET", credentials).then(
|
|
() => Promise.resolve(true),
|
|
error => {
|
|
if (isInvalidTokenError(error)) {
|
|
return Promise.resolve(false);
|
|
}
|
|
throw error;
|
|
}
|
|
);
|
|
},
|
|
|
|
/**
|
|
* List all the clients connected to the authenticated user's account,
|
|
* including devices, OAuth clients, and web sessions.
|
|
*
|
|
* @param sessionTokenHex
|
|
* The session token encoded in hex
|
|
* @return Promise
|
|
*/
|
|
async attachedClients(sessionTokenHex) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
return this._requestWithHeaders(
|
|
"/account/attached_clients",
|
|
"GET",
|
|
credentials
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Retrieves an OAuth authorization code.
|
|
*
|
|
* @param String sessionTokenHex
|
|
* The session token encoded in hex
|
|
* @param {Object} options
|
|
* @param options.client_id
|
|
* @param options.state
|
|
* @param options.scope
|
|
* @param options.access_type
|
|
* @param options.code_challenge_method
|
|
* @param options.code_challenge
|
|
* @param [options.keys_jwe]
|
|
* @returns {Promise<Object>} Object containing `code` and `state`.
|
|
*/
|
|
async oauthAuthorize(sessionTokenHex, options) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
const body = {
|
|
client_id: options.client_id,
|
|
response_type: "code",
|
|
state: options.state,
|
|
scope: options.scope,
|
|
access_type: options.access_type,
|
|
code_challenge: options.code_challenge,
|
|
code_challenge_method: options.code_challenge_method,
|
|
};
|
|
if (options.keys_jwe) {
|
|
body.keys_jwe = options.keys_jwe;
|
|
}
|
|
return this._request("/oauth/authorization", "POST", credentials, body);
|
|
},
|
|
/**
|
|
* Exchanges an OAuth authorization code with a refresh token, access tokens and an optional JWE representing scoped keys
|
|
* Takes in the sessionToken to tie the device record associated with the session, with the device record associated with the refreshToken
|
|
*
|
|
* @param string sessionTokenHex: The session token encoded in hex
|
|
* @param String code: OAuth authorization code
|
|
* @param String verifier: OAuth PKCE verifier
|
|
* @param String clientId: OAuth client ID
|
|
*
|
|
* @returns { Object } object containing `refresh_token`, `access_token` and `keys_jwe`
|
|
**/
|
|
async oauthToken(sessionTokenHex, code, verifier, clientId) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
const body = {
|
|
grant_type: "authorization_code",
|
|
code,
|
|
client_id: clientId,
|
|
code_verifier: verifier,
|
|
};
|
|
return this._request("/oauth/token", "POST", credentials, body);
|
|
},
|
|
/**
|
|
* Destroy an OAuth access token or refresh token.
|
|
*
|
|
* @param String clientId
|
|
* @param String token The token to be revoked.
|
|
*/
|
|
async oauthDestroy(clientId, token) {
|
|
const body = {
|
|
client_id: clientId,
|
|
token,
|
|
};
|
|
return this._request("/oauth/destroy", "POST", null, body);
|
|
},
|
|
|
|
/**
|
|
* Query for the information required to derive
|
|
* scoped encryption keys requested by the specified OAuth client.
|
|
*
|
|
* @param sessionTokenHex
|
|
* The session token encoded in hex
|
|
* @param clientId
|
|
* @param scope
|
|
* Space separated list of scopes
|
|
* @return Promise
|
|
*/
|
|
async getScopedKeyData(sessionTokenHex, clientId, scope) {
|
|
if (!clientId) {
|
|
throw new Error("Missing 'clientId' parameter");
|
|
}
|
|
if (!scope) {
|
|
throw new Error("Missing 'scope' parameter");
|
|
}
|
|
const params = {
|
|
client_id: clientId,
|
|
scope,
|
|
};
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
return this._request(
|
|
"/account/scoped-key-data",
|
|
"POST",
|
|
credentials,
|
|
params
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Destroy the current session with the Firefox Account API server and its
|
|
* associated device.
|
|
*
|
|
* @param sessionTokenHex
|
|
* The session token encoded in hex
|
|
* @return Promise
|
|
*/
|
|
async signOut(sessionTokenHex, options = {}) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
let path = "/session/destroy";
|
|
if (options.service) {
|
|
path += "?service=" + encodeURIComponent(options.service);
|
|
}
|
|
return this._request(path, "POST", credentials);
|
|
},
|
|
|
|
/**
|
|
* Check the verification status of the user's FxA email address
|
|
*
|
|
* @param sessionTokenHex
|
|
* The current session token encoded in hex
|
|
* @return Promise
|
|
*/
|
|
async recoveryEmailStatus(sessionTokenHex, options = {}) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
let path = "/recovery_email/status";
|
|
if (options.reason) {
|
|
path += "?reason=" + encodeURIComponent(options.reason);
|
|
}
|
|
|
|
return this._request(path, "GET", credentials);
|
|
},
|
|
|
|
/**
|
|
* Resend the verification email for the user
|
|
*
|
|
* @param sessionTokenHex
|
|
* The current token encoded in hex
|
|
* @return Promise
|
|
*/
|
|
async resendVerificationEmail(sessionTokenHex) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
return this._request("/recovery_email/resend_code", "POST", credentials);
|
|
},
|
|
|
|
/**
|
|
* 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)
|
|
* }
|
|
*/
|
|
async accountKeys(keyFetchTokenHex) {
|
|
let creds = await deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
|
|
let keyRequestKey = creds.extra.slice(0, 32);
|
|
let morecreds = await CryptoUtils.hkdfLegacy(
|
|
keyRequestKey,
|
|
undefined,
|
|
Credentials.keyWord("account/keys"),
|
|
3 * 32
|
|
);
|
|
let respHMACKey = morecreds.slice(0, 32);
|
|
let respXORKey = morecreds.slice(32, 96);
|
|
|
|
const resp = await this._request("/account/keys", "GET", creds);
|
|
if (!resp.bundle) {
|
|
throw new Error("failed to retrieve keys");
|
|
}
|
|
|
|
let bundle = CommonUtils.hexToBytes(resp.bundle);
|
|
let mac = bundle.slice(-32);
|
|
let key = CommonUtils.byteStringToArrayBuffer(respHMACKey);
|
|
// CryptoUtils.hmac takes ArrayBuffers as inputs for the key and data and
|
|
// returns an ArrayBuffer.
|
|
let bundleMAC = await CryptoUtils.hmac(
|
|
"SHA-256",
|
|
key,
|
|
CommonUtils.byteStringToArrayBuffer(bundle.slice(0, -32))
|
|
);
|
|
if (mac !== CommonUtils.arrayBufferToByteString(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),
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Obtain an OAuth access token by authenticating using a session token.
|
|
*
|
|
* @param {String} sessionTokenHex
|
|
* The session token encoded in hex
|
|
* @param {String} clientId
|
|
* @param {String} scope
|
|
* List of space-separated scopes.
|
|
* @param {Number} ttl
|
|
* Token time to live.
|
|
* @return {Promise<Object>} Object containing an `access_token`.
|
|
*/
|
|
async accessTokenWithSessionToken(sessionTokenHex, clientId, scope, ttl) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
const body = {
|
|
client_id: clientId,
|
|
grant_type: "fxa-credentials",
|
|
scope,
|
|
ttl,
|
|
};
|
|
return this._request("/oauth/token", "POST", credentials, body);
|
|
},
|
|
|
|
/**
|
|
* 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(email) {
|
|
return this.signIn(email, "").then(
|
|
() => {
|
|
throw new Error("How did I sign in with an empty password?");
|
|
},
|
|
expectedError => {
|
|
switch (expectedError.errno) {
|
|
case ERRNO_ACCOUNT_DOES_NOT_EXIST:
|
|
return false;
|
|
case ERRNO_INCORRECT_PASSWORD:
|
|
return true;
|
|
default:
|
|
// not so expected, any more ...
|
|
throw expectedError;
|
|
}
|
|
}
|
|
);
|
|
},
|
|
|
|
/**
|
|
* 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(uid) {
|
|
return this._request("/account/status?uid=" + uid, "GET").then(
|
|
result => {
|
|
return result.exists;
|
|
},
|
|
error => {
|
|
log.error("accountStatus failed", 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.availableCommands]
|
|
* Available commands for this device
|
|
* @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)
|
|
* }
|
|
*/
|
|
async registerDevice(sessionTokenHex, name, type, options = {}) {
|
|
let path = "/account/device";
|
|
|
|
let creds = await 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;
|
|
}
|
|
body.availableCommands = options.availableCommands;
|
|
|
|
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. If null, will be sent to all devices.
|
|
* @param excludedIds
|
|
* Devices to exclude when sending to all devices (deviceIds must be null).
|
|
* @param payload
|
|
* Data to send with the message
|
|
* @return Promise
|
|
* Resolves to an empty object:
|
|
* {}
|
|
*/
|
|
async notifyDevices(
|
|
sessionTokenHex,
|
|
deviceIds,
|
|
excludedIds,
|
|
payload,
|
|
TTL = 0
|
|
) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
if (deviceIds && excludedIds) {
|
|
throw new Error(
|
|
"You cannot specify excluded devices if deviceIds is set."
|
|
);
|
|
}
|
|
const body = {
|
|
to: deviceIds || "all",
|
|
payload,
|
|
TTL,
|
|
};
|
|
if (excludedIds) {
|
|
body.excluded = excludedIds;
|
|
}
|
|
return this._request("/account/devices/notify", "POST", credentials, body);
|
|
},
|
|
|
|
/**
|
|
* Retrieves pending commands for our device.
|
|
*
|
|
* @method getCommands
|
|
* @param sessionTokenHex - Session token obtained from signIn
|
|
* @param [index] - If specified, only messages received after the one who
|
|
* had that index will be retrieved.
|
|
* @param [limit] - Maximum number of messages to retrieve.
|
|
*/
|
|
async getCommands(sessionTokenHex, { index, limit }) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
const params = new URLSearchParams();
|
|
if (index != undefined) {
|
|
params.set("index", index);
|
|
}
|
|
if (limit != undefined) {
|
|
params.set("limit", limit);
|
|
}
|
|
const path = `/account/device/commands?${params.toString()}`;
|
|
return this._request(path, "GET", credentials);
|
|
},
|
|
|
|
/**
|
|
* Invokes a command on another device.
|
|
*
|
|
* @method invokeCommand
|
|
* @param sessionTokenHex - Session token obtained from signIn
|
|
* @param command - Name of the command to invoke
|
|
* @param target - Recipient device ID.
|
|
* @param payload
|
|
* @return Promise
|
|
* Resolves to the request's response, (which should be an empty object)
|
|
*/
|
|
async invokeCommand(sessionTokenHex, command, target, payload) {
|
|
const credentials = await deriveHawkCredentials(
|
|
sessionTokenHex,
|
|
"sessionToken"
|
|
);
|
|
const body = {
|
|
command,
|
|
target,
|
|
payload,
|
|
};
|
|
return this._request(
|
|
"/account/devices/invoke_command",
|
|
"POST",
|
|
credentials,
|
|
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.availableCommands]
|
|
* Available commands for this device
|
|
* @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
|
|
* }
|
|
*/
|
|
async updateDevice(sessionTokenHex, id, name, options = {}) {
|
|
let path = "/account/device";
|
|
|
|
let creds = await 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;
|
|
}
|
|
body.availableCommands = options.availableCommands;
|
|
|
|
return this._request(path, "POST", creds, body);
|
|
},
|
|
|
|
/**
|
|
* Get a list of currently registered devices that have been accessed
|
|
* in the last `DEVICES_FILTER_DAYS` days
|
|
*
|
|
* @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)
|
|
* },
|
|
* ...
|
|
* ]
|
|
*/
|
|
async getDeviceList(sessionTokenHex) {
|
|
let timestamp = Date.now() - 1000 * 60 * 60 * 24 * DEVICES_FILTER_DAYS;
|
|
let path = `/account/devices?filterIdleDevicesTimestamp=${timestamp}`;
|
|
let creds = await deriveHawkCredentials(sessionTokenHex, "sessionToken");
|
|
return this._request(path, "GET", creds, {});
|
|
},
|
|
|
|
_clearBackoff() {
|
|
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
|
|
* }
|
|
*/
|
|
async _requestWithHeaders(path, method, credentials, jsonPayload) {
|
|
// We were asked to back off.
|
|
if (this.backoffError) {
|
|
log.debug("Received new request during backoff, re-rejecting.");
|
|
throw this.backoffError;
|
|
}
|
|
let response;
|
|
try {
|
|
response = await this.hawk.request(
|
|
path,
|
|
method,
|
|
credentials,
|
|
jsonPayload
|
|
);
|
|
} catch (error) {
|
|
log.error(`error ${method}ing ${path}`, 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"
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
try {
|
|
return { body: JSON.parse(response.body), headers: response.headers };
|
|
} catch (error) {
|
|
log.error("json parse error on response: " + response.body);
|
|
// eslint-disable-next-line no-throw-literal
|
|
throw { error };
|
|
}
|
|
},
|
|
|
|
async _request(path, method, credentials, jsonPayload) {
|
|
const response = await this._requestWithHeaders(
|
|
path,
|
|
method,
|
|
credentials,
|
|
jsonPayload
|
|
);
|
|
return response.body;
|
|
},
|
|
};
|
|
|
|
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;
|
|
}
|