From ac60fca10ce70e94190ce654daec75123f857d6a Mon Sep 17 00:00:00 2001 From: Edouard Oger Date: Thu, 14 May 2020 10:49:55 -0400 Subject: [PATCH 1/4] wip --- browser/app/profile/firefox.js | 3 + services/fxaccounts/FxAccounts.jsm | 169 ++++++++++++++++-- services/fxaccounts/FxAccountsKeys.jsm | 109 +++++++---- services/fxaccounts/RustFxAccount.js | 133 ++++++++++---- .../tests/xpcshell/test_accounts.js | 2 +- services/sync/modules/browserid_identity.js | 9 +- .../tests/unit/test_browserid_identity.js | 4 +- 7 files changed, 334 insertions(+), 95 deletions(-) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 65d447802bfb47..31e9c09e0fc12b 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1418,6 +1418,9 @@ pref("browser.uiCustomization.state", ""); // A restart is mandatory after flipping that preference. pref("identity.fxaccounts.enabled", true); +// Use of the experimental Rust backend. +pref("identity.fxaccounts.useExperimentalRustClient", true /* TODO FALSE! */); + // The remote FxA root content URL. Must use HTTPS. pref("identity.fxaccounts.remote.root", "https://accounts.firefox.com/"); diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm index 61854ac7840b16..6f8ea36a680d0e 100644 --- a/services/fxaccounts/FxAccounts.jsm +++ b/services/fxaccounts/FxAccounts.jsm @@ -3,6 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); const { PromiseUtils } = ChromeUtils.import( "resource://gre/modules/PromiseUtils.jsm" ); @@ -105,6 +108,12 @@ ChromeUtils.defineModuleGetter( "resource://gre/modules/FxAccountsTelemetry.jsm" ); +ChromeUtils.defineModuleGetter( + this, + "RustFxAccount", + "resource://gre/modules/RustFxAccount.js" +); + XPCOMUtils.defineLazyModuleGetters(this, { Preferences: "resource://gre/modules/Preferences.jsm", }); @@ -116,6 +125,15 @@ XPCOMUtils.defineLazyPreferenceGetter( true ); +XPCOMUtils.defineLazyPreferenceGetter( + this, + "USE_RUST", + "identity.fxaccounts.useExperimentalRustClient", + null, + null, + val => (AppConstants.NIGHTLY_BUILD ? val : false) // On non-nightly builds the pref shouldn't even work. +); + // An AccountState object holds all state related to one specific account. // It is considered "private" to the FxAccounts modules. // Only one AccountState is ever "current" in the FxAccountsInternal object - @@ -426,6 +444,17 @@ class FxAccounts { return this._internal.telemetry; } + // Help VSCode help us with some nice autocompletions :) + /** + * @returns {RustFxAccount} + */ + get _rustFxa() { + if (USE_RUST) { + return this._internal.rustFxa; + } + return false; + } + _withCurrentAccountState(func) { return this._internal.withCurrentAccountState(func); } @@ -454,21 +483,25 @@ class FxAccounts { // We expose last accessed times in 'days ago' const ONE_DAY = 24 * 60 * 60 * 1000; - return this._withSessionToken(async sessionToken => { - const attachedClients = await this._internal.fxAccountsClient.attachedClients( - sessionToken - ); - // We should use the server timestamp here - bug 1595635 - let now = Date.now(); - return attachedClients.map(client => { - const daysAgo = client.lastAccessTime - ? Math.max(Math.floor((now - client.lastAccessTime) / ONE_DAY), 0) - : null; - return { - id: client.clientId, - lastAccessedDaysAgo: daysAgo, - }; + let attachedClients; + + if (this._rustFxa) { + attachedClients = await this._rustFxa.getAttachedClients(); + } else { + attachedClients = await this._withSessionToken(async sessionToken => { + return this._internal.fxAccountsClient.attachedClients(sessionToken); }); + } + // We should use the server timestamp here - bug 1595635 + let now = Date.now(); + return attachedClients.map(client => { + const daysAgo = client.lastAccessTime + ? Math.max(Math.floor((now - client.lastAccessTime) / ONE_DAY), 0) + : null; + return { + id: client.clientId, + lastAccessedDaysAgo: daysAgo, + }; }); } @@ -486,8 +519,10 @@ class FxAccounts { * @returns {Promise} Object containing "code" and "state" properties. */ authorizeOAuthCode(options) { - return this._withVerifiedAccountState(async state => { - const { sessionToken } = await state.getUserAccountData(["sessionToken"]); + // TODO: here we'd just grab the session token from fxa because we can't do all rust: + // https://github.com/mozilla/application-services/issues/3122 + + const oauthAuthorize = async sessionToken => { const params = { ...options }; if (params.keys_jwk) { const jwk = JSON.parse( @@ -510,6 +545,16 @@ class FxAccounts { } catch (err) { throw this._internal._errorToErrorClass(err); } + }; + + if (this._rustFxa) { + const sessionToken = this._rustFxa.getSessionToken(); + return oauthAuthorize(sessionToken); + } + + return this._withVerifiedAccountState(async state => { + const { sessionToken } = await state.getUserAccountData(["sessionToken"]); + return oauthAuthorize(sessionToken); }); } @@ -536,6 +581,15 @@ class FxAccounts { * UNKNOWN_ERROR */ async getOAuthToken(options = {}) { + if (this._rustFxa) { + if (Array.isArray(options.scope)) { + throw new Error( + "The Rust backend does not support requesting multiple scopes at once." + ); + } + const accessToken = await this._rustFxa.getAccessToken(options.scope); + return accessToken.token; + } try { return await this._internal.getOAuthToken(options); } catch (err) { @@ -598,6 +652,10 @@ class FxAccounts { * an unknown token is passed. */ removeCachedOAuthToken(options) { + if (this._rustFxa) { + // Well we don't have a method clears tokens for 1 scope, so... + return this._rustFxa.clearAccessTokenCache(); + } return this._internal.removeCachedOAuthToken(options); } @@ -622,6 +680,37 @@ class FxAccounts { * in pathological cases (eg, file-system errors, etc) */ getSignedInUser() { + if (this._rustFxa) { + return (async () => { + let sessionToken; + try { + sessionToken = await this._rustFxa.getSessionToken(); + } catch { + // TODO: Throwing if sessionToken is None is a bit... savage, + // but maybe in M5 we won't need to answer "do we have a session token". + } + + if (!sessionToken) { + // This likely means we're logged-out. + return null; + } + const { + email, + uid, + avatarDefault, + avatar, + displayName, + } = await this._rustFxa.getProfile(); + return { + email, + uid, + verified: true /* there's no such thing as "unverified state" in the OAuth world */, + displayName, + avatar, + avatarDefault, + }; + })(); + } // Note we don't return the session token, but use it to see if we // should fetch the profile. const ACCT_DATA_FIELDS = ["email", "uid", "verified", "sessionToken"]; @@ -685,6 +774,10 @@ class FxAccounts { * you saw an auth related exception from a remote service.) */ checkAccountStatus() { + if (this._rustFxa) { + // Noop, coz that function is a bit complex, sorry. + return Promise.resolve(true); + } // Note that we don't use _withCurrentAccountState here because that will // cause an exception to be thrown if we end up signing out due to the // account not existing, which isn't what we want here. @@ -706,7 +799,14 @@ class FxAccounts { * considered the canonical, albiet expensive, way to determine the * status of the account. */ - hasLocalSession() { + async hasLocalSession() { + if (this._rustFxa) { + try { + return !!(await this._rustFxa.getSessionToken()); + } catch { + return false; + } + } return this._withCurrentAccountState(async state => { let data = await state.getUserAccountData(["sessionToken"]); return !!(data && data.sessionToken); @@ -731,6 +831,9 @@ class FxAccounts { // "an object"), but to be useful across devices, the payload really needs // formalizing. We should try and do something better here. notifyDevices(deviceIds, excludedIds, payload, TTL) { + if (this._rustFxa) { + return Promise.resolve(true); // noop, will get removed anyway. + } return this._internal.notifyDevices(deviceIds, excludedIds, payload, TTL); } @@ -739,6 +842,9 @@ class FxAccounts { * */ resendVerificationEmail() { + if (this._rustFxa) { + return Promise.resolve(true); + } return this._withSessionToken((token, currentState) => { this._internal.startPollEmailStatus(currentState, token, "start"); return this._internal.fxAccountsClient.resendVerificationEmail(token); @@ -746,6 +852,9 @@ class FxAccounts { } async signOut(localOnly) { + if (this._rustFxa) { + return this._rustFxa.disconnect(); + } // Note that we do not use _withCurrentAccountState here, otherwise we // end up with an exception due to the user signing out before the call is // complete - but that's the entire point of this method :) @@ -756,6 +865,10 @@ class FxAccounts { // so that sync can change it when it notices the device name being changed, // and that could probably be replaced with a pref observer. updateDeviceRegistration() { + if (this._rustFxa) { + // Short-circuit the thing. + return this.device.updateDeviceRegistration(); + } return this._withCurrentAccountState(_ => { return this._internal.updateDeviceRegistration(); }); @@ -763,6 +876,9 @@ class FxAccounts { // we should try and kill this too. whenVerified(data) { + if (this._rustFxa) { + return Promise.resolve(true); + } return this._withCurrentAccountState(_ => { return this._internal.whenVerified(data); }); @@ -793,6 +909,25 @@ FxAccountsInternal.prototype = { // All significant initialization should be done in this initialize() method // to help with our mocking story. initialize() { + if (USE_RUST) { + let logins = Services.logins.findLogins( + "chrome://fxarust", + null, + "FxA Rust state" + ); + if (logins.length == 1) { + let stateJSON = logins[0]; + this.rustFxa = new RustFxAccount(stateJSON.password); + } else { + this.rustFxa = new RustFxAccount({ + fxaServer: "https://accounts.firefox.com", + clientId: "3c49430b43dfba77", + redirectUri: + "https://accounts.firefox.com/oauth/success/3c49430b43dfba77", + }); + } + } + XPCOMUtils.defineLazyGetter(this, "fxaPushService", function() { return Cc["@mozilla.org/fxaccounts/push;1"].getService( Ci.nsISupports diff --git a/services/fxaccounts/FxAccountsKeys.jsm b/services/fxaccounts/FxAccountsKeys.jsm index c37aec1539e6d1..357af212aea0ee 100644 --- a/services/fxaccounts/FxAccountsKeys.jsm +++ b/services/fxaccounts/FxAccountsKeys.jsm @@ -27,8 +27,16 @@ class FxAccountsKeys { * Checks if we currently have encryption keys or if we have enough to * be able to successfully fetch them for the signed-in-user. */ - canGetKeys() { - return this._fxia.withCurrentAccountState(async currentState => { + async canGetKeys() { + if (this._fxia.rustFxa) { + try { + const token = await this._fxia.rustFxa.getAccessToken(SCOPE_OLD_SYNC); + return !!token.key; + } catch (e) { + return false; + } + } + return this._fxia.withCurrentAccountState(async (currentState) => { let userData = await currentState.getUserAccountData(); if (!userData) { throw new Error("Can't possibly get keys; User is not signed in"); @@ -43,7 +51,7 @@ class FxAccountsKeys { return ( userData && (userData.keyFetchToken || - DERIVED_KEYS_NAMES.every(k => userData[k]) || + DERIVED_KEYS_NAMES.every((k) => userData[k]) || userData.kB) ); }); @@ -71,7 +79,31 @@ class FxAccountsKeys { * or null if no user is signed in */ async getKeys() { - return this._fxia.withCurrentAccountState(async currentState => { + if (this._fxia.rustFxa) { + let { + key: { kid, k: kSync }, + } = await this._fxia.rustFxa.getAccessToken(SCOPE_OLD_SYNC); + let kXCS = kid.split("-")[1]; + + // Base64 -> hex. + kSync = CommonUtils.bufferToHex( + new Uint8Array( + ChromeUtils.base64URLDecode(kSync, { padding: "ignore" }) + ) + ); + kXCS = CommonUtils.bufferToHex( + new Uint8Array(ChromeUtils.base64URLDecode(kXCS, { padding: "ignore" })) + ); + + // In practice the users of getKeys() only use the `k...` keys. + return { + kSync, + kXCS, + kExtSync: null, // huuuuh + kExtKbHash: null, // yeah huuh. + }; + } + return this._fxia.withCurrentAccountState(async (currentState) => { try { let userData = await currentState.getUserAccountData(); if (!userData) { @@ -89,17 +121,17 @@ class FxAccountsKeys { }); userData = await currentState.getUserAccountData(); } - if (DERIVED_KEYS_NAMES.every(k => !!userData[k])) { + if (DERIVED_KEYS_NAMES.every((k) => !!userData[k])) { return userData; } if (!currentState.whenKeysReadyDeferred) { currentState.whenKeysReadyDeferred = PromiseUtils.defer(); if (userData.keyFetchToken) { - this.fetchAndUnwrapKeys(userData.keyFetchToken).then( - dataWithKeys => { - if (DERIVED_KEYS_NAMES.some(k => !dataWithKeys[k])) { + this._fetchAndUnwrapKeys(userData.keyFetchToken).then( + (dataWithKeys) => { + if (DERIVED_KEYS_NAMES.some((k) => !dataWithKeys[k])) { const missing = DERIVED_KEYS_NAMES.filter( - k => !dataWithKeys[k] + (k) => !dataWithKeys[k] ); currentState.whenKeysReadyDeferred.reject( new Error(`user data missing: ${missing.join(", ")}`) @@ -108,7 +140,7 @@ class FxAccountsKeys { } currentState.whenKeysReadyDeferred.resolve(dataWithKeys); }, - err => { + (err) => { currentState.whenKeysReadyDeferred.reject(err); } ); @@ -126,7 +158,7 @@ class FxAccountsKeys { /** * Once the user's email is verified, we can request the keys */ - fetchKeys(keyFetchToken) { + _fetchKeys(keyFetchToken) { let client = this._fxia.fxAccountsClient; log.debug( `Fetching keys with token ${!!keyFetchToken} from ${client.host}` @@ -137,8 +169,8 @@ class FxAccountsKeys { return client.accountKeys(keyFetchToken); } - fetchAndUnwrapKeys(keyFetchToken) { - return this._fxia.withCurrentAccountState(async currentState => { + _fetchAndUnwrapKeys(keyFetchToken) { + return this._fxia.withCurrentAccountState(async (currentState) => { if (logPII) { log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken); } @@ -150,7 +182,7 @@ class FxAccountsKeys { return null; } - let { wrapKB } = await this.fetchKeys(keyFetchToken); + let { wrapKB } = await this._fetchKeys(keyFetchToken); let data = await currentState.getUserAccountData(); @@ -176,12 +208,12 @@ class FxAccountsKeys { log.debug( "Keys Obtained:" + - DERIVED_KEYS_NAMES.map(k => `${k}=${!!updateData[k]}`).join(", ") + DERIVED_KEYS_NAMES.map((k) => `${k}=${!!updateData[k]}`).join(", ") ); if (logPII) { log.debug( "Keys Obtained:" + - DERIVED_KEYS_NAMES.map(k => `${k}=${updateData[k]}`).join(", ") + DERIVED_KEYS_NAMES.map((k) => `${k}=${updateData[k]}`).join(", ") ); } @@ -191,10 +223,35 @@ class FxAccountsKeys { }); } + /** + * @param {String} scopes Space separated requested scopes + * @param {String} clientId oauth client id + */ + async getScopedKeys(scopes, clientId) { + const scopedKeys = {}; + if (this._fxia.rustFxa) { + for (const scope of scopes.split(" ")) { + const { key } = await this._fxia.rustFxa.getAccessToken(scope); + scopedKeys[scope] = key; + } + return scopedKeys; + } + const { sessionToken } = await this._fxia._getVerifiedAccountOrReject(); + const keyData = await this._fxia.fxAccountsClient.getScopedKeyData( + sessionToken, + clientId, + scopes + ); + for (const [scope, data] of Object.entries(keyData)) { + scopedKeys[scope] = await this._getKeyForScope(scope, data); + } + return scopedKeys; + } + /** * @param {String} scope Single key bearing scope */ - async getKeyForScope(scope, { keyRotationTimestamp }) { + async _getKeyForScope(scope, { keyRotationTimestamp }) { if (scope !== SCOPE_OLD_SYNC) { throw new Error(`Unavailable key material for ${scope}`); } @@ -217,24 +274,6 @@ class FxAccountsKeys { }; } - /** - * @param {String} scopes Space separated requested scopes - * @param {String} clientId oauth client id - */ - async getScopedKeys(scopes, clientId) { - const { sessionToken } = await this._fxia._getVerifiedAccountOrReject(); - const keyData = await this._fxia.fxAccountsClient.getScopedKeyData( - sessionToken, - clientId, - scopes - ); - const scopedKeys = {}; - for (const [scope, data] of Object.entries(keyData)) { - scopedKeys[scope] = await this.getKeyForScope(scope, data); - } - return scopedKeys; - } - async _deriveKeys(uid, kBbytes) { return { kSync: CommonUtils.bytesAsHex(await this._deriveSyncKey(kBbytes)), diff --git a/services/fxaccounts/RustFxAccount.js b/services/fxaccounts/RustFxAccount.js index 340d0b1ad62062..2a4702bc99bffb 100644 --- a/services/fxaccounts/RustFxAccount.js +++ b/services/fxaccounts/RustFxAccount.js @@ -4,6 +4,8 @@ const EXPORTED_SYMBOLS = ["RustFxAccount"]; +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + /** * This class is a low-level JS wrapper around the `mozIFirefoxAccountsBridge` * interface. @@ -64,7 +66,7 @@ class RustFxAccount { * @returns {Promise} The JSON representation of the state. */ async stateJSON() { - return promisify(this.bridge.stateJSON); + return this._promisify(this.bridge.stateJSON); } /** * Request a OAuth token by starting a new OAuth flow. @@ -77,7 +79,7 @@ class RustFxAccount { * @returns {Promise} a URL string that the caller should navigate to. */ async beginOAuthFlow(scopes) { - return promisify(this.bridge.beginOAuthFlow, scopes); + return this._promisify(this.bridge.beginOAuthFlow, scopes); } /** * Complete an OAuth flow initiated by `beginOAuthFlow(...)`. @@ -87,7 +89,7 @@ class RustFxAccount { * @throws if there was an error during the login flow. */ async completeOAuthFlow(code, state) { - return promisify(this.bridge.completeOAuthFlow, code, state); + return this._promisify(this.bridge.completeOAuthFlow, code, state); } /** * Try to get an OAuth access token. @@ -112,7 +114,9 @@ class RustFxAccount { * the desired scope. */ async getAccessToken(scope, ttl) { - return JSON.parse(await promisify(this.bridge.getAccessToken, scope, ttl)); + return JSON.parse( + await this._promisify(this.bridge.getAccessToken, scope, ttl) + ); } /** * Get the session token if held. @@ -121,7 +125,7 @@ class RustFxAccount { * @throws if a session token is not being held. */ async getSessionToken() { - return promisify(this.bridge.getSessionToken); + return this._promisify(this.bridge.getSessionToken); } /** * Returns the list of OAuth attached clients. @@ -144,7 +148,7 @@ class RustFxAccount { * @throws if a session token is not being held. */ async getAttachedClients() { - return JSON.parse(await promisify(this.bridge.getAttachedClients)); + return JSON.parse(await this._promisify(this.bridge.getAttachedClients)); } /** * Check whether the currently held refresh token is active. @@ -155,7 +159,9 @@ class RustFxAccount { * @returns {Promise} */ async checkAuthorizationStatus() { - return JSON.parse(await promisify(this.bridge.checkAuthorizationStatus)); + return JSON.parse( + await this._promisify(this.bridge.checkAuthorizationStatus) + ); } /* * This method should be called when a request made with @@ -165,14 +171,14 @@ class RustFxAccount { * again. */ async clearAccessTokenCache() { - return promisify(this.bridge.clearAccessTokenCache); + return this._promisify(this.bridge.clearAccessTokenCache); } /* * Disconnect from the account and optionaly destroy our device record. * `beginOAuthFlow(...)` will need to be called to reconnect. */ async disconnect() { - return promisify(this.bridge.disconnect); + return this._promisify(this.bridge.disconnect); } /** * Gets the logged-in user profile. @@ -191,7 +197,9 @@ class RustFxAccount { * at least the `profile` scope. */ async getProfile(ignoreCache) { - return JSON.parse(await promisify(this.bridge.getProfile, ignoreCache)); + return JSON.parse( + await this._promisify(this.bridge.getProfile, ignoreCache) + ); } /** * Start a migration process from a session-token-based authenticated account. @@ -212,7 +220,7 @@ class RustFxAccount { copySessionToken = false ) { return JSON.parse( - await promisify( + await this._promisify( this.bridge.migrateFromSessionToken, sessionToken, kSync, @@ -228,7 +236,7 @@ class RustFxAccount { */ async retryMigrateFromSessionToken() { return JSON.parse( - await promisify(this.bridge.retryMigrateFromSessionToken) + await this._promisify(this.bridge.retryMigrateFromSessionToken) ); } /** @@ -238,7 +246,7 @@ class RustFxAccount { * @returns {Promise} true if a migration flow can be resumed. */ async isInMigrationState() { - return promisify(this.bridge.isInMigrationState); + return this._promisify(this.bridge.isInMigrationState); } /** * Called after a password change was done through webchannel. @@ -246,7 +254,7 @@ class RustFxAccount { * @param {string} sessionToken */ async handleSessionTokenChange(sessionToken) { - return promisify(this.bridge.handleSessionTokenChange, sessionToken); + return this._promisify(this.bridge.handleSessionTokenChange, sessionToken); } /** * Get the token server URL with `1.0/sync/1.5` appended at the end. @@ -254,28 +262,28 @@ class RustFxAccount { * @returns {Promise} */ async getTokenServerEndpointURL() { - let url = await promisify(this.bridge.getTokenServerEndpointURL); + let url = await this._promisify(this.bridge.getTokenServerEndpointURL); return `${url}${url.endsWith("/") ? "" : "/"}1.0/sync/1.5`; } /** * @returns {Promise} */ async getConnectionSuccessURL() { - return promisify(this.bridge.getConnectionSuccessURL); + return this._promisify(this.bridge.getConnectionSuccessURL); } /** * @param {string} entrypoint * @returns {Promise} */ async getManageAccountURL(entrypoint) { - return promisify(this.bridge.getManageAccountURL, entrypoint); + return this._promisify(this.bridge.getManageAccountURL, entrypoint); } /** * @param {string} entrypoint * @returns {Promise} */ async getManageDevicesURL(entrypoint) { - return promisify(this.bridge.getManageDevicesURL, entrypoint); + return this._promisify(this.bridge.getManageDevicesURL, entrypoint); } /** * Fetch the devices in the account. @@ -302,7 +310,9 @@ class RustFxAccount { * @returns {Promise<[Device]>} */ async fetchDevices(ignoreCache) { - return JSON.parse(await promisify(this.bridge.fetchDevices, ignoreCache)); + return JSON.parse( + await this._promisify(this.bridge.fetchDevices, ignoreCache) + ); } /** * Rename the local device @@ -310,7 +320,7 @@ class RustFxAccount { * @param {string} name */ async setDeviceDisplayName(name) { - return promisify(this.bridge.setDeviceDisplayName, name); + return this._promisify(this.bridge.setDeviceDisplayName, name); } /** * Handle an incoming Push message payload. @@ -326,7 +336,9 @@ class RustFxAccount { * @return {Promise<[TabReceivedCommand|DeviceConnectedEvent|DeviceDisconnectedEvent]>} */ async handlePushMessage(payload) { - return JSON.parse(await promisify(this.bridge.handlePushMessage, payload)); + return JSON.parse( + await this._promisify(this.bridge.handlePushMessage, payload) + ); } /** * Fetch for device commands we didn't receive through Push. @@ -342,7 +354,7 @@ class RustFxAccount { * @returns {Promise<[TabReceivedCommand]>} */ async pollDeviceCommands() { - return JSON.parse(await promisify(this.bridge.pollDeviceCommands)); + return JSON.parse(await this._promisify(this.bridge.pollDeviceCommands)); } /** * Send a tab to a device identified by its ID. @@ -352,7 +364,7 @@ class RustFxAccount { * @param {string} url */ async sendSingleTab(targetId, title, url) { - return promisify(this.bridge.sendSingleTab, targetId, title, url); + return this._promisify(this.bridge.sendSingleTab, targetId, title, url); } /** * Update our FxA push subscription. @@ -362,7 +374,7 @@ class RustFxAccount { * @param {string} authKey */ async setDevicePushSubscription(endpoint, publicKey, authKey) { - return promisify( + return this._promisify( this.bridge.setDevicePushSubscription, endpoint, publicKey, @@ -377,7 +389,7 @@ class RustFxAccount { * @param {[DeviceCapability]} supportedCapabilities */ async initializeDevice(name, deviceType, supportedCapabilities) { - return promisify( + return this._promisify( this.bridge.initializeDevice, name, deviceType, @@ -390,23 +402,66 @@ class RustFxAccount { * @param {[DeviceCapability]} supportedCapabilities */ async ensureCapabilities(supportedCapabilities) { - return promisify(this.bridge.ensureCapabilities, supportedCapabilities); + return this._promisify( + this.bridge.ensureCapabilities, + supportedCapabilities + ); } -} -function promisify(func, ...params) { - return new Promise((resolve, reject) => { - func(...params, { - // This object implicitly implements - // `mozIFirefoxAccountsBridgeCallback`. - handleSuccess: resolve, - handleError(code, message) { - let error = new Error(message); - error.result = code; - reject(error); - }, + _promisify(func, ...params) { + const fxa = this; + return new Promise((resolve, reject) => { + func(...params, { + // This object implicitly implements + // `mozIFirefoxAccountsBridgeCallback`. + handleSuccess(res) { + // Get and write the FxA state. + // Very much prototype quality code. + // There's a high chance the JS event loop will mess up everything, + // such as doing OP1 "write-state" AFTER OP2 "write-state". + fxa.bridge.stateJSON({ + // Who doesn't like recursion? + handleSuccess(state) { + let loginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + Ci.nsILoginInfo, + "init" + ); + let login = new loginInfo( + "chrome://fxarust", + null, // aFormActionOrigin, + "FxA Rust state", // aHttpRealm, + "fxarust", // aUsername + state, // aPassword + "", // aUsernameField + "" + ); // aPasswordField + + let existingLogins = Services.logins.findLogins( + "chrome://fxarust", + null, + "FxA Rust state" + ); + if (existingLogins.length) { + Services.logins.modifyLogin(existingLogins[0], login); + } else { + Services.logins.addLogin(login); + } + }, + handleError(code, message) { + // Something went wrong. + }, + }); + resolve(res); + }, + handleError(code, message) { + let error = new Error(message); + error.result = code; + reject(error); + }, + }); }); - }); + } } /** diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js index ea83cf11031679..ff1d59b29a74cc 100644 --- a/services/fxaccounts/tests/xpcshell/test_accounts.js +++ b/services/fxaccounts/tests/xpcshell/test_accounts.js @@ -1031,7 +1031,7 @@ add_test(function test_fetchAndUnwrapKeys_no_token() { fxa .setSignedInUser(user) .then(user2 => { - return fxa.keys.fetchAndUnwrapKeys(); + return fxa.keys._fetchAndUnwrapKeys(); }) .catch(error => { log.info("setSignedInUser correctly rejected"); diff --git a/services/sync/modules/browserid_identity.js b/services/sync/modules/browserid_identity.js index 829265716b9e8a..28416657780d48 100644 --- a/services/sync/modules/browserid_identity.js +++ b/services/sync/modules/browserid_identity.js @@ -65,7 +65,14 @@ XPCOMUtils.defineLazyPreferenceGetter( XPCOMUtils.defineLazyPreferenceGetter( this, "USE_OAUTH_FOR_SYNC_TOKEN", - "identity.sync.useOAuthForSyncToken" + "identity.sync.useOAuthForSyncToken", + null, + null, + // No matter what, activating the Rust backend should activate the OAuth fetching for keys. + val => + Services.prefs.getBoolPref("identity.fxaccounts.useExperimentalRustClient") + ? true + : val ); // FxAccountsCommon.js doesn't use a "namespace", so create one here. diff --git a/services/sync/tests/unit/test_browserid_identity.js b/services/sync/tests/unit/test_browserid_identity.js index a9f83dd0cba63a..399bf051bdd4fd 100644 --- a/services/sync/tests/unit/test_browserid_identity.js +++ b/services/sync/tests/unit/test_browserid_identity.js @@ -812,7 +812,7 @@ add_task(async function test_getKeysMissing() { }, // And the keys object with a mock that returns no keys. keys: { - fetchAndUnwrapKeys() { + _fetchAndUnwrapKeys() { return Promise.resolve({}); }, }, @@ -855,7 +855,7 @@ add_task(async function test_signedInUserMissing() { configureFxAccountIdentity(browseridManager, globalIdentityConfig); let fxa = new FxAccounts({ - fetchAndUnwrapKeys() { + _fetchAndUnwrapKeys() { return Promise.resolve({}); }, fxAccountsClient: new MockFxAccountsClient(), From 35c76eb7d0373190b81cd9731387475403e61b9f Mon Sep 17 00:00:00 2001 From: Vlad Filippov Date: Thu, 7 May 2020 13:56:42 -0400 Subject: [PATCH 2/4] Bug 1630291 - adjust rust backend pref and basic fxa login --- browser/app/profile/firefox.js | 4 +-- browser/base/content/browser-sync.js | 13 ++++++++- services/fxaccounts/FxAccounts.jsm | 18 ++++++++++++- services/fxaccounts/FxAccountsCommon.js | 4 +++ services/fxaccounts/FxAccountsConfig.jsm | 6 ++++- services/fxaccounts/FxAccountsWebChannel.jsm | 27 +++++++++++++++++++ .../tests/xpcshell/test_rust_account.js | 24 +++++++++++++++++ .../fxaccounts/tests/xpcshell/xpcshell.ini | 1 + 8 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 services/fxaccounts/tests/xpcshell/test_rust_account.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 31e9c09e0fc12b..145a549f3aad20 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1418,8 +1418,8 @@ pref("browser.uiCustomization.state", ""); // A restart is mandatory after flipping that preference. pref("identity.fxaccounts.enabled", true); -// Use of the experimental Rust backend. -pref("identity.fxaccounts.useExperimentalRustClient", true /* TODO FALSE! */); +// Use of the Rust backend vs JS backend +pref("identity.fxaccounts.useRustBackend", false); // The remote FxA root content URL. Must use HTTPS. pref("identity.fxaccounts.remote.root", "https://accounts.firefox.com/"); diff --git a/browser/base/content/browser-sync.js b/browser/base/content/browser-sync.js index 7b34c21a75f99b..5cb574917536f4 100644 --- a/browser/base/content/browser-sync.js +++ b/browser/base/content/browser-sync.js @@ -23,6 +23,12 @@ ChromeUtils.defineModuleGetter( "resource://services-sync/main.js" ); +XPCOMUtils.defineLazyPreferenceGetter( + this, + "RUST_BACKEND", + "identity.fxaccounts.useRustBackend" +); + const MIN_STATUS_ANIMATION_DURATION = 1600; var gSync = { @@ -735,7 +741,12 @@ var gSync = { }, async openFxAEmailFirstPage(entryPoint) { - const url = await FxAccounts.config.promiseConnectAccountURI(entryPoint); + let url; + if (RUST_BACKEND) { + url = await FxAccounts.config.promiseConnectAccountOAuthURI(); + } else { + url = await FxAccounts.config.promiseConnectAccountURI(entryPoint); + } switchToTabHavingURI(url, true, { replaceQueryString: true }); }, diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm index 6f8ea36a680d0e..947492cf3d0f78 100644 --- a/services/fxaccounts/FxAccounts.jsm +++ b/services/fxaccounts/FxAccounts.jsm @@ -40,6 +40,7 @@ const { FXA_PWDMGR_REAUTH_WHITELIST, FXA_PWDMGR_SECURE_FIELDS, FX_OAUTH_CLIENT_ID, + FX_OAUTH_WEBCHANNEL_REDIRECT, KEY_LIFETIME, ON_ACCOUNT_STATE_CHANGE_NOTIFICATION, ONLOGIN_NOTIFICATION, @@ -125,10 +126,16 @@ XPCOMUtils.defineLazyPreferenceGetter( true ); +XPCOMUtils.defineLazyPreferenceGetter( + this, + "ROOT_URL", + "identity.fxaccounts.remote.root" +); + XPCOMUtils.defineLazyPreferenceGetter( this, "USE_RUST", - "identity.fxaccounts.useExperimentalRustClient", + "identity.fxaccounts.useRustBackend", null, null, val => (AppConstants.NIGHTLY_BUILD ? val : false) // On non-nightly builds the pref shouldn't even work. @@ -390,6 +397,15 @@ function copyObjectProperties(from, to, thisObj, keys) { class FxAccounts { constructor(mocks = null) { this._internal = new FxAccountsInternal(); + if (RUST_BACKEND) { + // FxAccounts functionality with the Rust backend. + this._rustFxAccount = new RustFxAccount({ + fxaServer: ROOT_URL, + clientId: FX_OAUTH_CLIENT_ID, + redirectUri: FX_OAUTH_WEBCHANNEL_REDIRECT + }); + } + if (mocks) { // it's slightly unfortunate that we need to mock the main "internal" object // before calling initialize, primarily so a mock `newAccountState` is in diff --git a/services/fxaccounts/FxAccountsCommon.js b/services/fxaccounts/FxAccountsCommon.js index 77cb8eaea18909..9999af717f46a9 100644 --- a/services/fxaccounts/FxAccountsCommon.js +++ b/services/fxaccounts/FxAccountsCommon.js @@ -88,8 +88,11 @@ exports.COMMAND_SENDTAB = exports.COMMAND_PREFIX + exports.COMMAND_SENDTAB_TAIL; // OAuth exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776"; +exports.FX_OAUTH_WEBCHANNEL_REDIRECT = + "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel"; exports.SCOPE_PROFILE = "profile"; exports.SCOPE_OLD_SYNC = "https://identity.mozilla.com/apps/oldsync"; +exports.SCOPE_SESSION_TOKEN = "https://identity.mozilla.com/tokens/session"; // OAuth metadata for other Firefox-related services that we might need to know about // in order to provide an enhanced user experience. @@ -112,6 +115,7 @@ exports.COMMAND_PAIR_COMPLETE = "fxaccounts:pair_complete"; exports.COMMAND_PROFILE_CHANGE = "profile:change"; exports.COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account"; exports.COMMAND_LOGIN = "fxaccounts:login"; +exports.COMMAND_OAUTH_LOGIN = "fxaccounts:oauth_login"; exports.COMMAND_LOGOUT = "fxaccounts:logout"; exports.COMMAND_DELETE = "fxaccounts:delete"; exports.COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences"; diff --git a/services/fxaccounts/FxAccountsConfig.jsm b/services/fxaccounts/FxAccountsConfig.jsm index f980fcd4d2995f..e026fb45da4186 100644 --- a/services/fxaccounts/FxAccountsConfig.jsm +++ b/services/fxaccounts/FxAccountsConfig.jsm @@ -7,7 +7,7 @@ var EXPORTED_SYMBOLS = ["FxAccountsConfig"]; const { RESTRequest } = ChromeUtils.import( "resource://services-common/rest.js" ); -const { log } = ChromeUtils.import( +const { SCOPE_PROFILE, SCOPE_OLD_SYNC, SCOPE_SESSION_TOKEN, log } = ChromeUtils.import( "resource://gre/modules/FxAccountsCommon.js" ); const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); @@ -74,6 +74,10 @@ var FxAccountsConfig = { }); }, + async promiseConnectAccountOAuthURI() { + return fxAccounts._rustFxAccount.beginOAuthFlow([SCOPE_PROFILE, SCOPE_OLD_SYNC, SCOPE_SESSION_TOKEN]); + }, + async promiseForceSigninURI(entrypoint, extraParams = {}) { return this._buildURL("force_auth", { extraParams: { entrypoint, service: SYNC_PARAM, ...extraParams }, diff --git a/services/fxaccounts/FxAccountsWebChannel.jsm b/services/fxaccounts/FxAccountsWebChannel.jsm index e07db560a47a24..0909c8bf65cae8 100644 --- a/services/fxaccounts/FxAccountsWebChannel.jsm +++ b/services/fxaccounts/FxAccountsWebChannel.jsm @@ -18,6 +18,7 @@ const { XPCOMUtils } = ChromeUtils.import( const { COMMAND_PROFILE_CHANGE, COMMAND_LOGIN, + COMMAND_OAUTH_LOGIN, COMMAND_LOGOUT, COMMAND_DELETE, COMMAND_CAN_LINK_ACCOUNT, @@ -244,6 +245,32 @@ FxAccountsWebChannel.prototype = { .login(data) .catch(error => this._sendError(error, message, sendingContext)); break; + case COMMAND_OAUTH_LOGIN: + const rustFxAccount = fxAccounts._rustFxAccount; + rustFxAccount + .completeOAuthFlow(data.code, data.state) + .then(async () => { + const profile = await rustFxAccount.getProfile(); + let state = await rustFxAccount.stateJSON(); + state = JSON.parse(state); + const data = { + email: profile.email, + services: {}, + sessionToken: state.session_token, + uid: profile.uid, + verified: true, + } + + return this._helpers.login(data) + }) + .then(() => { + // redirect to the sms page to connect more devices + browser.loadURI(`${accountServer.spec}sms`, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }) + }) + .catch(error => this._sendError(error, message, sendingContext)); + break; case COMMAND_LOGOUT: case COMMAND_DELETE: this._helpers diff --git a/services/fxaccounts/tests/xpcshell/test_rust_account.js b/services/fxaccounts/tests/xpcshell/test_rust_account.js new file mode 100644 index 00000000000000..d6fb14768f7e7b --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_rust_account.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ROOT_URL = "https://accounts.firefox.com"; +const FX_OAUTH_CLIENT_ID = "5882386c6d801776"; +const FX_OAUTH_WEBCHANNEL_REDIRECT = + "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel"; +const PROFILE_SCOPE = "profile"; + +const { RustFxAccount } = ChromeUtils.import( + "resource://gre/modules/RustFxAccount.js" +); + +add_task(async function test_begin_oauth_flow() { + let bridge = new RustFxAccount({ + fxaServer: ROOT_URL, + clientId: FX_OAUTH_CLIENT_ID, + redirectUri: FX_OAUTH_WEBCHANNEL_REDIRECT, + }); + let flow = await bridge.beginOAuthFlow([PROFILE_SCOPE]); + Assert.ok(flow); +}); diff --git a/services/fxaccounts/tests/xpcshell/xpcshell.ini b/services/fxaccounts/tests/xpcshell/xpcshell.ini index ea0e7f2c4135d3..9f6b6ea8e8a457 100644 --- a/services/fxaccounts/tests/xpcshell/xpcshell.ini +++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini @@ -22,6 +22,7 @@ support-files = [test_rust_fxaccount.js] [test_push_service.js] [test_telemetry.js] +[test_rust_account.js] [test_web_channel.js] [test_profile.js] [test_storage_manager.js] From 4bf03543807c13bcb14b4900d9094470e3b38ca6 Mon Sep 17 00:00:00 2001 From: Vlad Filippov Date: Wed, 27 May 2020 15:53:31 -0400 Subject: [PATCH 3/4] remove extra fxa instance --- browser/app/profile/firefox.js | 2 +- services/fxaccounts/FxAccounts.jsm | 15 +++------------ services/fxaccounts/FxAccountsConfig.jsm | 2 +- services/fxaccounts/FxAccountsWebChannel.jsm | 2 +- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 145a549f3aad20..8f6f6a83be7931 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1419,7 +1419,7 @@ pref("browser.uiCustomization.state", ""); pref("identity.fxaccounts.enabled", true); // Use of the Rust backend vs JS backend -pref("identity.fxaccounts.useRustBackend", false); +pref("identity.fxaccounts.useRustBackend", true /* TODO: make false later */); // The remote FxA root content URL. Must use HTTPS. pref("identity.fxaccounts.remote.root", "https://accounts.firefox.com/"); diff --git a/services/fxaccounts/FxAccounts.jsm b/services/fxaccounts/FxAccounts.jsm index 947492cf3d0f78..6e5786fb10d46b 100644 --- a/services/fxaccounts/FxAccounts.jsm +++ b/services/fxaccounts/FxAccounts.jsm @@ -134,7 +134,7 @@ XPCOMUtils.defineLazyPreferenceGetter( XPCOMUtils.defineLazyPreferenceGetter( this, - "USE_RUST", + "RUST_BACKEND", "identity.fxaccounts.useRustBackend", null, null, @@ -397,15 +397,6 @@ function copyObjectProperties(from, to, thisObj, keys) { class FxAccounts { constructor(mocks = null) { this._internal = new FxAccountsInternal(); - if (RUST_BACKEND) { - // FxAccounts functionality with the Rust backend. - this._rustFxAccount = new RustFxAccount({ - fxaServer: ROOT_URL, - clientId: FX_OAUTH_CLIENT_ID, - redirectUri: FX_OAUTH_WEBCHANNEL_REDIRECT - }); - } - if (mocks) { // it's slightly unfortunate that we need to mock the main "internal" object // before calling initialize, primarily so a mock `newAccountState` is in @@ -465,7 +456,7 @@ class FxAccounts { * @returns {RustFxAccount} */ get _rustFxa() { - if (USE_RUST) { + if (RUST_BACKEND) { return this._internal.rustFxa; } return false; @@ -925,7 +916,7 @@ FxAccountsInternal.prototype = { // All significant initialization should be done in this initialize() method // to help with our mocking story. initialize() { - if (USE_RUST) { + if (RUST_BACKEND) { let logins = Services.logins.findLogins( "chrome://fxarust", null, diff --git a/services/fxaccounts/FxAccountsConfig.jsm b/services/fxaccounts/FxAccountsConfig.jsm index e026fb45da4186..f0423785df0c84 100644 --- a/services/fxaccounts/FxAccountsConfig.jsm +++ b/services/fxaccounts/FxAccountsConfig.jsm @@ -75,7 +75,7 @@ var FxAccountsConfig = { }, async promiseConnectAccountOAuthURI() { - return fxAccounts._rustFxAccount.beginOAuthFlow([SCOPE_PROFILE, SCOPE_OLD_SYNC, SCOPE_SESSION_TOKEN]); + return fxAccounts._internal.rustFxa.beginOAuthFlow([SCOPE_PROFILE, SCOPE_OLD_SYNC, SCOPE_SESSION_TOKEN]); }, async promiseForceSigninURI(entrypoint, extraParams = {}) { diff --git a/services/fxaccounts/FxAccountsWebChannel.jsm b/services/fxaccounts/FxAccountsWebChannel.jsm index 0909c8bf65cae8..31b24a955ce8be 100644 --- a/services/fxaccounts/FxAccountsWebChannel.jsm +++ b/services/fxaccounts/FxAccountsWebChannel.jsm @@ -246,7 +246,7 @@ FxAccountsWebChannel.prototype = { .catch(error => this._sendError(error, message, sendingContext)); break; case COMMAND_OAUTH_LOGIN: - const rustFxAccount = fxAccounts._rustFxAccount; + const rustFxAccount = fxAccounts._internal.rustFxa; rustFxAccount .completeOAuthFlow(data.code, data.state) .then(async () => { From 81d50947d19a927e50a4cda65aea9c92babcc17c Mon Sep 17 00:00:00 2001 From: Edouard Oger Date: Thu, 28 May 2020 17:07:50 -0400 Subject: [PATCH 4/4] Move state persistence to Rust --- services/fxaccounts/RustFxAccount.js | 133 +++++------------- .../firefox-accounts-bridge/src/punt/task.rs | 66 ++++++++- xpcom/build/Services.py | 3 + 3 files changed, 104 insertions(+), 98 deletions(-) diff --git a/services/fxaccounts/RustFxAccount.js b/services/fxaccounts/RustFxAccount.js index 2a4702bc99bffb..340d0b1ad62062 100644 --- a/services/fxaccounts/RustFxAccount.js +++ b/services/fxaccounts/RustFxAccount.js @@ -4,8 +4,6 @@ const EXPORTED_SYMBOLS = ["RustFxAccount"]; -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - /** * This class is a low-level JS wrapper around the `mozIFirefoxAccountsBridge` * interface. @@ -66,7 +64,7 @@ class RustFxAccount { * @returns {Promise} The JSON representation of the state. */ async stateJSON() { - return this._promisify(this.bridge.stateJSON); + return promisify(this.bridge.stateJSON); } /** * Request a OAuth token by starting a new OAuth flow. @@ -79,7 +77,7 @@ class RustFxAccount { * @returns {Promise} a URL string that the caller should navigate to. */ async beginOAuthFlow(scopes) { - return this._promisify(this.bridge.beginOAuthFlow, scopes); + return promisify(this.bridge.beginOAuthFlow, scopes); } /** * Complete an OAuth flow initiated by `beginOAuthFlow(...)`. @@ -89,7 +87,7 @@ class RustFxAccount { * @throws if there was an error during the login flow. */ async completeOAuthFlow(code, state) { - return this._promisify(this.bridge.completeOAuthFlow, code, state); + return promisify(this.bridge.completeOAuthFlow, code, state); } /** * Try to get an OAuth access token. @@ -114,9 +112,7 @@ class RustFxAccount { * the desired scope. */ async getAccessToken(scope, ttl) { - return JSON.parse( - await this._promisify(this.bridge.getAccessToken, scope, ttl) - ); + return JSON.parse(await promisify(this.bridge.getAccessToken, scope, ttl)); } /** * Get the session token if held. @@ -125,7 +121,7 @@ class RustFxAccount { * @throws if a session token is not being held. */ async getSessionToken() { - return this._promisify(this.bridge.getSessionToken); + return promisify(this.bridge.getSessionToken); } /** * Returns the list of OAuth attached clients. @@ -148,7 +144,7 @@ class RustFxAccount { * @throws if a session token is not being held. */ async getAttachedClients() { - return JSON.parse(await this._promisify(this.bridge.getAttachedClients)); + return JSON.parse(await promisify(this.bridge.getAttachedClients)); } /** * Check whether the currently held refresh token is active. @@ -159,9 +155,7 @@ class RustFxAccount { * @returns {Promise} */ async checkAuthorizationStatus() { - return JSON.parse( - await this._promisify(this.bridge.checkAuthorizationStatus) - ); + return JSON.parse(await promisify(this.bridge.checkAuthorizationStatus)); } /* * This method should be called when a request made with @@ -171,14 +165,14 @@ class RustFxAccount { * again. */ async clearAccessTokenCache() { - return this._promisify(this.bridge.clearAccessTokenCache); + return promisify(this.bridge.clearAccessTokenCache); } /* * Disconnect from the account and optionaly destroy our device record. * `beginOAuthFlow(...)` will need to be called to reconnect. */ async disconnect() { - return this._promisify(this.bridge.disconnect); + return promisify(this.bridge.disconnect); } /** * Gets the logged-in user profile. @@ -197,9 +191,7 @@ class RustFxAccount { * at least the `profile` scope. */ async getProfile(ignoreCache) { - return JSON.parse( - await this._promisify(this.bridge.getProfile, ignoreCache) - ); + return JSON.parse(await promisify(this.bridge.getProfile, ignoreCache)); } /** * Start a migration process from a session-token-based authenticated account. @@ -220,7 +212,7 @@ class RustFxAccount { copySessionToken = false ) { return JSON.parse( - await this._promisify( + await promisify( this.bridge.migrateFromSessionToken, sessionToken, kSync, @@ -236,7 +228,7 @@ class RustFxAccount { */ async retryMigrateFromSessionToken() { return JSON.parse( - await this._promisify(this.bridge.retryMigrateFromSessionToken) + await promisify(this.bridge.retryMigrateFromSessionToken) ); } /** @@ -246,7 +238,7 @@ class RustFxAccount { * @returns {Promise} true if a migration flow can be resumed. */ async isInMigrationState() { - return this._promisify(this.bridge.isInMigrationState); + return promisify(this.bridge.isInMigrationState); } /** * Called after a password change was done through webchannel. @@ -254,7 +246,7 @@ class RustFxAccount { * @param {string} sessionToken */ async handleSessionTokenChange(sessionToken) { - return this._promisify(this.bridge.handleSessionTokenChange, sessionToken); + return promisify(this.bridge.handleSessionTokenChange, sessionToken); } /** * Get the token server URL with `1.0/sync/1.5` appended at the end. @@ -262,28 +254,28 @@ class RustFxAccount { * @returns {Promise} */ async getTokenServerEndpointURL() { - let url = await this._promisify(this.bridge.getTokenServerEndpointURL); + let url = await promisify(this.bridge.getTokenServerEndpointURL); return `${url}${url.endsWith("/") ? "" : "/"}1.0/sync/1.5`; } /** * @returns {Promise} */ async getConnectionSuccessURL() { - return this._promisify(this.bridge.getConnectionSuccessURL); + return promisify(this.bridge.getConnectionSuccessURL); } /** * @param {string} entrypoint * @returns {Promise} */ async getManageAccountURL(entrypoint) { - return this._promisify(this.bridge.getManageAccountURL, entrypoint); + return promisify(this.bridge.getManageAccountURL, entrypoint); } /** * @param {string} entrypoint * @returns {Promise} */ async getManageDevicesURL(entrypoint) { - return this._promisify(this.bridge.getManageDevicesURL, entrypoint); + return promisify(this.bridge.getManageDevicesURL, entrypoint); } /** * Fetch the devices in the account. @@ -310,9 +302,7 @@ class RustFxAccount { * @returns {Promise<[Device]>} */ async fetchDevices(ignoreCache) { - return JSON.parse( - await this._promisify(this.bridge.fetchDevices, ignoreCache) - ); + return JSON.parse(await promisify(this.bridge.fetchDevices, ignoreCache)); } /** * Rename the local device @@ -320,7 +310,7 @@ class RustFxAccount { * @param {string} name */ async setDeviceDisplayName(name) { - return this._promisify(this.bridge.setDeviceDisplayName, name); + return promisify(this.bridge.setDeviceDisplayName, name); } /** * Handle an incoming Push message payload. @@ -336,9 +326,7 @@ class RustFxAccount { * @return {Promise<[TabReceivedCommand|DeviceConnectedEvent|DeviceDisconnectedEvent]>} */ async handlePushMessage(payload) { - return JSON.parse( - await this._promisify(this.bridge.handlePushMessage, payload) - ); + return JSON.parse(await promisify(this.bridge.handlePushMessage, payload)); } /** * Fetch for device commands we didn't receive through Push. @@ -354,7 +342,7 @@ class RustFxAccount { * @returns {Promise<[TabReceivedCommand]>} */ async pollDeviceCommands() { - return JSON.parse(await this._promisify(this.bridge.pollDeviceCommands)); + return JSON.parse(await promisify(this.bridge.pollDeviceCommands)); } /** * Send a tab to a device identified by its ID. @@ -364,7 +352,7 @@ class RustFxAccount { * @param {string} url */ async sendSingleTab(targetId, title, url) { - return this._promisify(this.bridge.sendSingleTab, targetId, title, url); + return promisify(this.bridge.sendSingleTab, targetId, title, url); } /** * Update our FxA push subscription. @@ -374,7 +362,7 @@ class RustFxAccount { * @param {string} authKey */ async setDevicePushSubscription(endpoint, publicKey, authKey) { - return this._promisify( + return promisify( this.bridge.setDevicePushSubscription, endpoint, publicKey, @@ -389,7 +377,7 @@ class RustFxAccount { * @param {[DeviceCapability]} supportedCapabilities */ async initializeDevice(name, deviceType, supportedCapabilities) { - return this._promisify( + return promisify( this.bridge.initializeDevice, name, deviceType, @@ -402,66 +390,23 @@ class RustFxAccount { * @param {[DeviceCapability]} supportedCapabilities */ async ensureCapabilities(supportedCapabilities) { - return this._promisify( - this.bridge.ensureCapabilities, - supportedCapabilities - ); + return promisify(this.bridge.ensureCapabilities, supportedCapabilities); } +} - _promisify(func, ...params) { - const fxa = this; - return new Promise((resolve, reject) => { - func(...params, { - // This object implicitly implements - // `mozIFirefoxAccountsBridgeCallback`. - handleSuccess(res) { - // Get and write the FxA state. - // Very much prototype quality code. - // There's a high chance the JS event loop will mess up everything, - // such as doing OP1 "write-state" AFTER OP2 "write-state". - fxa.bridge.stateJSON({ - // Who doesn't like recursion? - handleSuccess(state) { - let loginInfo = new Components.Constructor( - "@mozilla.org/login-manager/loginInfo;1", - Ci.nsILoginInfo, - "init" - ); - let login = new loginInfo( - "chrome://fxarust", - null, // aFormActionOrigin, - "FxA Rust state", // aHttpRealm, - "fxarust", // aUsername - state, // aPassword - "", // aUsernameField - "" - ); // aPasswordField - - let existingLogins = Services.logins.findLogins( - "chrome://fxarust", - null, - "FxA Rust state" - ); - if (existingLogins.length) { - Services.logins.modifyLogin(existingLogins[0], login); - } else { - Services.logins.addLogin(login); - } - }, - handleError(code, message) { - // Something went wrong. - }, - }); - resolve(res); - }, - handleError(code, message) { - let error = new Error(message); - error.result = code; - reject(error); - }, - }); +function promisify(func, ...params) { + return new Promise((resolve, reject) => { + func(...params, { + // This object implicitly implements + // `mozIFirefoxAccountsBridgeCallback`. + handleSuccess: resolve, + handleError(code, message) { + let error = new Error(message); + error.result = code; + reject(error); + }, }); - } + }); } /** diff --git a/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/task.rs b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/task.rs index ff103e5ddfb990..448a744aacc065 100644 --- a/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/task.rs +++ b/services/fxaccounts/rust-bridge/firefox-accounts-bridge/src/punt/task.rs @@ -15,15 +15,17 @@ use fxa_client::{ FirefoxAccount, }; use moz_task::{DispatchOptions, Task, TaskRunnable, ThreadPtrHandle, ThreadPtrHolder}; -use nserror::nsresult; -use nsstring::{nsACString, nsCString}; +use nserror::{nsresult, NS_ERROR_NOT_AVAILABLE}; +use nsstring::{nsACString, nsAString, nsCString, nsString}; use std::{ fmt::Write, mem, str, sync::{Arc, Mutex, Weak}, }; +use thin_vec::thin_vec; use xpcom::{ - interfaces::{mozIFirefoxAccountsBridgeCallback, nsIEventTarget}, + getter_addrefs, + interfaces::{mozIFirefoxAccountsBridgeCallback, nsIEventTarget, nsILoginInfo, nsISupports}, RefPtr, }; @@ -458,6 +460,57 @@ impl PuntTask { .map(|_| PuntResult::Null), }?) } + + fn persist_state(&self) -> Result<(), Error> { + let fxa = self.fxa.upgrade().ok_or_else(|| Error::AlreadyTornDown)?; + let fxa = fxa.lock()?; + let state = fxa.to_json()?; + let login_info = + xpcom::create_instance::(cstr!("@mozilla.org/login-manager/loginInfo;1")) + .ok_or_else(|| Error::Nsresult(NS_ERROR_NOT_AVAILABLE))?; + let origin = nsString::from("chrome://fxarust"); + let http_realm = nsString::from("FxA Rust state"); + let username = nsString::from("fxarust"); + let password = nsString::from(&state); + let empty = nsString::from(""); + unsafe { + login_info + .Init( + &*origin as *const nsAString, + std::ptr::null(), /* form_action_origin */ + &*http_realm as *const nsAString, + &*username as *const nsAString, + &*password as *const nsAString, + &*empty as *const nsAString, /* username_field */ + &*empty as *const nsAString, /* password_field */ + ) + .to_result()?; + } + + let logins_service = + xpcom::services::get_Logins().ok_or_else(|| Error::Nsresult(NS_ERROR_NOT_AVAILABLE))?; + let mut ret = thin_vec![]; + unsafe { + logins_service + .FindLogins( + &*origin as *const nsAString, + std::ptr::null(), + &*http_realm as *const nsAString, + &mut ret, + ) + .to_result()?; + } + if ret.is_empty() { + getter_addrefs(|p| unsafe { logins_service.AddLogin(&*login_info, p) })?; + } else { + unsafe { + logins_service + .ModifyLogin(&*ret[0], &*login_info.coerce::()) + .to_result()?; + } + } + Ok(()) + } } fn to_capabilities(capabilities: &[nsCString]) -> error::Result> { @@ -502,7 +555,12 @@ impl Task for PuntTask { &mut *self.result.borrow_mut(), Err(Error::AlreadyRan(self.name)), ) { - Ok(result) => unsafe { callback.HandleSuccess(result.into_variant().coerce()) }, + Ok(result) => { + if let Err(_) = self.persist_state() { + // TODO we should log here once we can log to about:sync-log. + } + unsafe { callback.HandleSuccess(result.into_variant().coerce()) } + } Err(err) => { let mut message = nsCString::new(); write!(message, "{}", err).unwrap(); diff --git a/xpcom/build/Services.py b/xpcom/build/Services.py index 16a453aaa2a5e4..eb96438cf08168 100644 --- a/xpcom/build/Services.py +++ b/xpcom/build/Services.py @@ -58,6 +58,8 @@ def service(name, iface, contractid): # NB: this should also expose nsIXULAppInfo, as does Services.jsm. service('AppInfoService', 'nsIXULRuntime', "@mozilla.org/xre/app-info;1") +service('Logins', 'nsILoginManager', + "@mozilla.org/login-manager;1") if buildconfig.substs.get('ENABLE_REMOTE_AGENT'): service('RemoteAgent', 'nsIRemoteAgent', @@ -96,6 +98,7 @@ def service(name, iface, contractid): #include "nsIURIFixup.h" #include "nsIBits.h" #include "nsIXULRuntime.h" +#include "nsILoginManager.h" """ if buildconfig.substs.get('ENABLE_REMOTE_AGENT'):