From 9728aab2dcc1a90aa08f69e764d43a86d3f4d608 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Wed, 22 Nov 2023 21:22:06 +0200 Subject: [PATCH] Backed out changeset 0fa8c456f37b (bug 1863372) for causing bc failures on browser/base/content/test/static/browser_all_files_referenced.js CLOSED TREE --- services/fxaccounts/FxAccountsOAuth.sys.mjs | 143 ------------------ services/fxaccounts/moz.build | 1 - .../tests/xpcshell/test_oauth_flow.js | 88 ----------- .../fxaccounts/tests/xpcshell/xpcshell.ini | 1 - 4 files changed, 233 deletions(-) delete mode 100644 services/fxaccounts/FxAccountsOAuth.sys.mjs delete mode 100644 services/fxaccounts/tests/xpcshell/test_oauth_flow.js diff --git a/services/fxaccounts/FxAccountsOAuth.sys.mjs b/services/fxaccounts/FxAccountsOAuth.sys.mjs deleted file mode 100644 index ec5189101ee4..000000000000 --- a/services/fxaccounts/FxAccountsOAuth.sys.mjs +++ /dev/null @@ -1,143 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { - FX_OAUTH_CLIENT_ID, - SCOPE_PROFILE, - SCOPE_PROFILE_WRITE, - SCOPE_OLD_SYNC, -} from "resource://gre/modules/FxAccountsCommon.sys.mjs"; - -const VALID_SCOPES = [SCOPE_PROFILE, SCOPE_PROFILE_WRITE, SCOPE_OLD_SYNC]; - -/** - * Handles all logic and state related to initializing, and completing OAuth flows - * with FxA - * It's possible to start multiple OAuth flow, but only one can be completed, and once one flow is completed - * all the other in-flight flows will be concluded, and attempting to complete those flows will result in errors. - */ -export class FxAccountsOAuth { - #flow; - constructor() { - this.#flow = {}; - } - - /** - * Stores a flow in-memory - * @param { string } state: A base-64 URL-safe string represnting a random value created at the start of the flow - * @param { Object } value: The data needed to complete a flow, once the oauth code is available. - * in practice, `value` is: - * - `verifier`: A base=64 URL-safe string representing the PKCE code verifier - * - `key`: The private key need to decrypt the JWE we recieve from the auth server - * - `requestedScopes`: The scopes the caller requested, meant to be compared against the scopes the server authorized - */ - addFlow(state, value) { - this.#flow[state] = value; - } - - /** - * Clears all started flows - */ - clearAllFlows() { - this.#flow = {}; - } - - /* - * Gets a stored flow - * @param { string } state: The base-64 URL-safe state string that was created at the start of the flow - * @returns { Object }: The values initially stored when startign th eoauth flow - * in practice, the return value is: - * - `verifier`: A base=64 URL-safe string representing the PKCE code verifier - * - `key`: The private key need to decrypt the JWE we recieve from the auth server - * - ``requestedScopes`: The scopes the caller requested, meant to be compared against the scopes the server authorized - */ - getFlow(state) { - return this.#flow[state]; - } - - /** - * Begins an OAuth flow, to be completed with a an OAuth code and state. - * - * This function stores needed information to complete the flow. You must call `completeOAuthFlow` - * on the same instance of `FxAccountsOAuth`, otherwise the completing of the oauth flow will fail. - * - * @param { string[] } scopes: The OAuth scopes the client should request from FxA - * - * @returns { Object }: Returns an object representing the query parameters that should be - * added to the FxA authorization URL to initialize an oAuth flow. - * In practice, the query parameters are: - * - `client_id`: The OAuth client ID for Firefox Desktop - * - `scope`: The scopes given by the caller, space seperated - * - `action`: This will always be `email` - * - `response_type`: This will always be `code` - * - `access_type`: This will always be `offline` - * - `state`: A URL-safe base-64 string randomly generated - * - `code_challenge`: A URL-safe base-64 string representing the PKCE challenge - * - `code_challenge_method`: This will always be `S256` - * For more informatio about PKCE, read https://datatracker.ietf.org/doc/html/rfc7636 - * - `keys_jwk`: A URL-safe base-64 representing a JWK to be used as a public key by the server - * to generate a JWE - */ - async beginOAuthFlow(scopes) { - if ( - !Array.isArray(scopes) && - scopes.some(scope => !VALID_SCOPES.contains(scope)) - ) { - throw new Error("Invalid scopes"); - } - const queryParams = { - client_id: FX_OAUTH_CLIENT_ID, - action: "email", - response_type: "code", - access_type: "offline", - scope: scopes.join(" "), - }; - - // Generate a random, 16 byte value to represent a state that we verify - // once we complete the oauth flow, to ensure that we only conclude - // an oauth flow that we started - const state = new Uint8Array(16); - crypto.getRandomValues(state); - const stateB64 = ChromeUtils.base64URLEncode(state, { pad: false }); - queryParams.state = stateB64; - - // Generate a 43 byte code verifier for PKCE, in accordance with - // https://datatracker.ietf.org/doc/html/rfc7636#section-7.1 which recommends a - // 43-octet URL safe string - const codeVerifier = new Uint8Array(43); - crypto.getRandomValues(codeVerifier); - const codeVerifierB64 = ChromeUtils.base64URLEncode(codeVerifier, { - pad: false, - }); - const challenge = await crypto.subtle.digest( - "SHA-256", - new TextEncoder().encode(codeVerifierB64) - ); - const challengeB64 = ChromeUtils.base64URLEncode(challenge, { pad: false }); - queryParams.code_challenge = challengeB64; - queryParams.code_challenge_method = "S256"; - - // Generate a public, private key pair to be used during the oauth flow - // to encrypt scoped-keys as they roundtrip through the auth server - const ECDH_KEY = { name: "ECDH", namedCurve: "P-256" }; - const key = await crypto.subtle.generateKey(ECDH_KEY, true, ["deriveKey"]); - const publicKey = await crypto.subtle.exportKey("jwk", key.publicKey); - const privateKey = key.privateKey; - - // We encode the public key as URL-safe base64 to be included in the query parameters - const encodedPublicKey = ChromeUtils.base64URLEncode( - new TextEncoder().encode(JSON.stringify(publicKey)), - { pad: false } - ); - queryParams.keys_jwk = encodedPublicKey; - - // We store the state in-memory, to verify once the oauth flow is completed - this.addFlow(stateB64, { - key: privateKey, - verifier: codeVerifierB64, - requestedScopes: scopes, - }); - return queryParams; - } -} diff --git a/services/fxaccounts/moz.build b/services/fxaccounts/moz.build index 0244821a23f2..fe79196daf7a 100644 --- a/services/fxaccounts/moz.build +++ b/services/fxaccounts/moz.build @@ -22,7 +22,6 @@ EXTRA_JS_MODULES += [ "FxAccountsConfig.sys.mjs", "FxAccountsDevice.sys.mjs", "FxAccountsKeys.sys.mjs", - "FxAccountsOAuth.sys.mjs", "FxAccountsPairing.sys.mjs", "FxAccountsPairingChannel.sys.mjs", "FxAccountsProfile.sys.mjs", diff --git a/services/fxaccounts/tests/xpcshell/test_oauth_flow.js b/services/fxaccounts/tests/xpcshell/test_oauth_flow.js deleted file mode 100644 index 4ed84fcf5ed1..000000000000 --- a/services/fxaccounts/tests/xpcshell/test_oauth_flow.js +++ /dev/null @@ -1,88 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -/* global crypto */ - -"use strict"; - -const { FxAccountsOAuth } = ChromeUtils.importESModule( - "resource://gre/modules/FxAccountsOAuth.sys.mjs" -); - -const { SCOPE_PROFILE, FX_OAUTH_CLIENT_ID } = ChromeUtils.importESModule( - "resource://gre/modules/FxAccountsCommon.sys.mjs" -); - -ChromeUtils.defineESModuleGetters(this, { - jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs", -}); - -initTestLogging("Trace"); - -add_task(function test_begin_oauth_flow() { - const oauth = new FxAccountsOAuth(); - add_task(async function test_begin_oauth_flow_invalid_scopes() { - try { - await oauth.beginOAuthFlow("foo,fi,fum", "foo"); - Assert.fail("Should have thrown error, scopes must be an array"); - } catch { - // OK - } - try { - await oauth.beginOAuthFlow(["not-a-real-scope", SCOPE_PROFILE]); - Assert.fail("Should have thrown an error, must use a valid scope"); - } catch { - // OK - } - }); - add_task(async function test_begin_oauth_flow_ok() { - const scopes = [SCOPE_PROFILE, SCOPE_OLD_SYNC]; - const queryParams = await oauth.beginOAuthFlow(scopes); - - // First verify default query parameters - Assert.equal(queryParams.client_id, FX_OAUTH_CLIENT_ID); - Assert.equal(queryParams.action, "email"); - Assert.equal(queryParams.response_type, "code"); - Assert.equal(queryParams.access_type, "offline"); - Assert.equal(queryParams.scope, [SCOPE_PROFILE, SCOPE_OLD_SYNC].join(" ")); - - // Then, we verify that the state is a valid Base64 value - const state = queryParams.state; - ChromeUtils.base64URLDecode(state, { padding: "reject" }); - - // Then, we verify that the codeVerifier, can be used to verify the code_challenge - const code_challenge = queryParams.code_challenge; - Assert.equal(queryParams.code_challenge_method, "S256"); - const oauthFlow = oauth.getFlow(state); - const codeVerifierB64 = oauthFlow.verifier; - const expectedChallenge = await crypto.subtle.digest( - "SHA-256", - new TextEncoder().encode(codeVerifierB64) - ); - const expectedChallengeB64 = ChromeUtils.base64URLEncode( - expectedChallenge, - { pad: false } - ); - Assert.equal(expectedChallengeB64, code_challenge); - - // Then, we verify that something encrypted with the `keys_jwk`, can be decrypted using the private key - const keysJwk = queryParams.keys_jwk; - const decodedKeysJwk = JSON.parse( - new TextDecoder().decode( - ChromeUtils.base64URLDecode(keysJwk, { padding: "reject" }) - ) - ); - const plaintext = "text to be encrypted and decrypted!"; - delete decodedKeysJwk.key_ops; - const jwe = await jwcrypto.generateJWE( - decodedKeysJwk, - new TextEncoder().encode(plaintext) - ); - const privateKey = oauthFlow.key; - const decrypted = await jwcrypto.decryptJWE(jwe, privateKey); - Assert.equal(new TextDecoder().decode(decrypted), plaintext); - - // Finally, we verify that we stored the requested scopes - Assert.deepEqual(oauthFlow.requestedScopes, scopes); - }); -}); diff --git a/services/fxaccounts/tests/xpcshell/xpcshell.ini b/services/fxaccounts/tests/xpcshell/xpcshell.ini index 9a5050b4bf5d..2086db834955 100644 --- a/services/fxaccounts/tests/xpcshell/xpcshell.ini +++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini @@ -15,7 +15,6 @@ support-files = [test_device.js] [test_keys.js] [test_loginmgr_storage.js] -[test_oauth_flow.js] [test_oauth_tokens.js] [test_oauth_token_storage.js] [test_pairing.js]