/* 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 { IPPAuthProvider } from "moz-src:///toolkit/components/ipprotection/IPPAuthProvider.sys.mjs"; import { GUARDIAN_EXPERIMENT_TYPE } from "moz-src:///toolkit/components/ipprotection/GuardianClient.sys.mjs"; const lazy = {}; ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => ChromeUtils.importESModule( "resource://gre/modules/FxAccounts.sys.mjs" ).getFxAccountsSingleton() ); ChromeUtils.defineESModuleGetters(lazy, { IPPEnrollAndEntitleManager: "moz-src:///toolkit/components/ipprotection/fxa/IPPEnrollAndEntitleManager.sys.mjs", IPPSignInWatcher: "moz-src:///toolkit/components/ipprotection/fxa/IPPSignInWatcher.sys.mjs", IPProtectionService: "moz-src:///toolkit/components/ipprotection/IPProtectionService.sys.mjs", }); const CLIENT_ID_MAP = { "http://localhost:3000": "6089c54fdc970aed", "https://guardian-dev.herokuapp.com": "64ef9b544a31bca8", "https://dev.vpn.nonprod.webservices.mozgcp.net": "64ef9b544a31bca8", "https://stage.guardian.nonprod.cloudops.mozgcp.net": "e6eb0d1e856335fc", "https://stage.vpn.nonprod.webservices.mozgcp.net": "e6eb0d1e856335fc", "https://fpn.firefox.com": "e6eb0d1e856335fc", "https://vpn.mozilla.org": "e6eb0d1e856335fc", }; const GUARDIAN_ENDPOINT_PREF = "browser.ipProtection.guardian.endpoint"; const GUARDIAN_ENDPOINT_DEFAULT = "https://vpn.mozilla.com"; /** * FxA implementation of IPPAuthProvider. Handles OAuth token retrieval, * enrollment via Guardian, and FxA-specific proxy bypass rules. */ class IPPFxaAuthProviderSingleton extends IPPAuthProvider { #signInWatcher = null; #enrollAndEntitleFn = null; /** * @param {object} [signInWatcher] - Custom sign-in watcher. Defaults to IPPSignInWatcher. * @param {Function} [enrollAndEntitleFn] - Custom enroll function. Defaults to the FxA hidden-window flow. */ constructor(signInWatcher = null, enrollAndEntitleFn = null) { super(); this.#signInWatcher = signInWatcher; this.#enrollAndEntitleFn = enrollAndEntitleFn ?? IPPFxaAuthProviderSingleton.#defaultEnrollAndEntitle; } get signInWatcher() { return this.#signInWatcher ?? lazy.IPPSignInWatcher; } /** * @param {AbortSignal} [abortSignal] * @returns {Promise<{isEnrolledAndEntitled: boolean, entitlement?: object, error?: string}>} */ async enrollAndEntitle(abortSignal) { return this.#enrollAndEntitleFn(abortSignal); } static async #defaultEnrollAndEntitle(abortSignal = null) { try { const result = await lazy.IPProtectionService.guardian.enrollWithFxa( GUARDIAN_EXPERIMENT_TYPE, abortSignal ); if (!result?.ok) { return { isEnrolledAndEntitled: false, error: result?.error }; } } catch (error) { return { isEnrolledAndEntitled: false, error: error?.message }; } const { entitlement, error } = await IPPFxaAuthProviderSingleton.#fetchEntitlement(); if (error || !entitlement) { return { isEnrolledAndEntitled: false, error }; } return { isEnrolledAndEntitled: true, entitlement }; } /** * @param {boolean} [forceRefetch=false] * @returns {Promise<{entitlement?: object, error?: string}>} */ async getEntitlement(forceRefetch = false) { const isLinked = await this.#isLinkedToGuardian(!forceRefetch); if (!isLinked) { return {}; } return IPPFxaAuthProviderSingleton.#fetchEntitlement(); } async #isLinkedToGuardian(useCache = true) { try { const endpoint = Services.prefs.getCharPref( GUARDIAN_ENDPOINT_PREF, GUARDIAN_ENDPOINT_DEFAULT ); const clientId = CLIENT_ID_MAP[new URL(endpoint).origin]; if (!clientId) { return false; } const cached = await lazy.fxAccounts.listAttachedOAuthClients(); if (cached.some(c => c.id === clientId)) { return true; } if (useCache) { return false; } const refreshed = await lazy.fxAccounts.listAttachedOAuthClients(true); return refreshed.some(c => c.id === clientId); } catch (_) { return false; } } static async #fetchEntitlement() { try { const { status, entitlement, error } = await lazy.IPProtectionService.guardian.fetchUserInfo(); if (error || !entitlement || status != 200) { return { error: error || `Status: ${status}` }; } return { entitlement }; } catch (error) { return { error: error.message }; } } get helpers() { return [this.signInWatcher, lazy.IPPEnrollAndEntitleManager]; } get isReady() { // For non authenticated users, we don't know yet their enroll state so the UI // is shown and they have to login. if (!this.signInWatcher.isSignedIn) { return false; } // If the current account is not enrolled and entitled, the UI is shown and // they have to opt-in. // If they are currently enrolling, they have already opted-in. if ( !lazy.IPPEnrollAndEntitleManager.isEnrolledAndEntitled && !lazy.IPPEnrollAndEntitleManager.isEnrolling ) { return false; } return true; } /** * Retrieves an FxA OAuth token and returns a disposable handle that revokes * it on disposal. * * @param {AbortSignal} [abortSignal] * @returns {Promise<{token: string} & Disposable>} */ async getToken(abortSignal = null) { let tasks = [ lazy.fxAccounts.getOAuthToken({ scope: ["profile", "https://identity.mozilla.com/apps/vpn"], }), ]; if (abortSignal) { abortSignal.throwIfAborted(); tasks.push( new Promise((_, rej) => { abortSignal?.addEventListener("abort", rej, { once: true }); }) ); } const token = await Promise.race(tasks); if (!token) { return null; } return { token, [Symbol.dispose]: () => { lazy.fxAccounts.removeCachedOAuthToken({ token }); }, }; } async aboutToStart() { let result; if (lazy.IPPEnrollAndEntitleManager.isEnrolling) { result = await lazy.IPPEnrollAndEntitleManager.waitForEnrollment(); } if (!lazy.IPPEnrollAndEntitleManager.isEnrolledAndEntitled) { return { error: result?.error }; } return null; } get excludedUrlPrefs() { return [ "identity.fxaccounts.remote.profile.uri", "identity.fxaccounts.auth.uri", ]; } } const IPPFxaAuthProvider = new IPPFxaAuthProviderSingleton(); export { IPPFxaAuthProvider, IPPFxaAuthProviderSingleton };