diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 65d447802bfb47..8f6f6a83be7931 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 Rust backend vs JS backend +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/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 61854ac7840b16..6e5786fb10d46b 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" ); @@ -37,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, @@ -105,6 +109,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 +126,21 @@ XPCOMUtils.defineLazyPreferenceGetter( true ); +XPCOMUtils.defineLazyPreferenceGetter( + this, + "ROOT_URL", + "identity.fxaccounts.remote.root" +); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "RUST_BACKEND", + "identity.fxaccounts.useRustBackend", + 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 +451,17 @@ class FxAccounts { return this._internal.telemetry; } + // Help VSCode help us with some nice autocompletions :) + /** + * @returns {RustFxAccount} + */ + get _rustFxa() { + if (RUST_BACKEND) { + return this._internal.rustFxa; + } + return false; + } + _withCurrentAccountState(func) { return this._internal.withCurrentAccountState(func); } @@ -454,21 +490,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 +526,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 +552,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 +588,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 +659,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 +687,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 +781,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 +806,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 +838,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 +849,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 +859,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 +872,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 +883,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 +916,25 @@ FxAccountsInternal.prototype = { // All significant initialization should be done in this initialize() method // to help with our mocking story. initialize() { + if (RUST_BACKEND) { + 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/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..f0423785df0c84 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._internal.rustFxa.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/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/FxAccountsWebChannel.jsm b/services/fxaccounts/FxAccountsWebChannel.jsm index e07db560a47a24..31b24a955ce8be 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._internal.rustFxa; + 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/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/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/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] 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(), 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'):