From 5fe6d135727e5e3bb8e61d92471f7b0c59bbd9bc Mon Sep 17 00:00:00 2001 From: martgil Date: Thu, 7 May 2026 10:40:37 +0800 Subject: [PATCH 01/14] refactor: use extension-specific oauth redirect url --- extension/js/common/api/authentication/generic/oauth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/api/authentication/generic/oauth.ts b/extension/js/common/api/authentication/generic/oauth.ts index e6d92bf0e60..39dc57b4d9b 100644 --- a/extension/js/common/api/authentication/generic/oauth.ts +++ b/extension/js/common/api/authentication/generic/oauth.ts @@ -40,7 +40,7 @@ export class OAuth { public static GOOGLE_OAUTH_CONFIG = { client_id: '717284730244-5oejn54f10gnrektjdc4fv4rbic1bj1p.apps.googleusercontent.com', client_secret: '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', - redirect_uri: 'https://www.google.com/robots.txt', + redirect_uri: chrome.identity.getRedirectURL('oauth'), url_code: `${GOOGLE_OAUTH_SCREEN_HOST}/o/oauth2/auth`, url_tokens: `${OAUTH_GOOGLE_API_HOST}/token`, state_header: 'CRYPTUP_STATE_', From 9b15b3aeee58ae1c5a6acf3d004fd1e1d08a96d3 Mon Sep 17 00:00:00 2001 From: martgil Date: Thu, 7 May 2026 17:54:33 +0800 Subject: [PATCH 02/14] feat: update manifest.json with key field --- extension/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/extension/manifest.json b/extension/manifest.json index 1dacb7dea2a..c8c513ff9ee 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -3,6 +3,7 @@ "name": "FlowCrypt: Encrypt Gmail with PGP", "description": "Simple end-to-end encryption to secure email and attachments on Google.", "version": "[will be replaced during build]", + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArV4mhxGkdt2FcJoWJhZrzNUftI0S7i55jMooL+FLjRSyK1hh6G4so7KLYhY/Tc327luMwWFkCAcdsamjbhOfJneBMZ0IT7swAS3zsC87vLE5YeWO2CX02FvHjgXm60T1Fk4gJh/zqCp4OLjyawoJRyuovvVN0LwH4j6DjHn3nodl2YeY+4K7jzFGHj6+68tlok9BtI6k8tntIbnFToRr9gVR85UT+W8rKXqx20Kne14k2my5fjrGZjEpK74YU6QNlKRzprVqpNEE989sxNk1tL6xKYgoDO9m8JZtuFKFsoQY/fV8xhNkXB19KLVJEW8aqPG/0ZTTMg9llZI8c6Yx+QIDAQAB", "action": { "default_icon": { "16": "/img/logo/flowcrypt-logo-16-16.png", From bd9302008d926dbb6ef25f6d133d9b480791f10b Mon Sep 17 00:00:00 2001 From: martgil Date: Fri, 8 May 2026 16:53:20 +0800 Subject: [PATCH 03/14] wip: add manifest.json key for chrome-consumer-local --- extension/manifest.json | 1 - tooling/build-types-and-manifests.ts | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/extension/manifest.json b/extension/manifest.json index c8c513ff9ee..1dacb7dea2a 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -3,7 +3,6 @@ "name": "FlowCrypt: Encrypt Gmail with PGP", "description": "Simple end-to-end encryption to secure email and attachments on Google.", "version": "[will be replaced during build]", - "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArV4mhxGkdt2FcJoWJhZrzNUftI0S7i55jMooL+FLjRSyK1hh6G4so7KLYhY/Tc327luMwWFkCAcdsamjbhOfJneBMZ0IT7swAS3zsC87vLE5YeWO2CX02FvHjgXm60T1Fk4gJh/zqCp4OLjyawoJRyuovvVN0LwH4j6DjHn3nodl2YeY+4K7jzFGHj6+68tlok9BtI6k8tntIbnFToRr9gVR85UT+W8rKXqx20Kne14k2my5fjrGZjEpK74YU6QNlKRzprVqpNEE989sxNk1tL6xKYgoDO9m8JZtuFKFsoQY/fV8xhNkXB19KLVJEW8aqPG/0ZTTMg9llZI8c6Yx+QIDAQAB", "action": { "default_icon": { "16": "/img/logo/flowcrypt-logo-16-16.png", diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index b68d68aed61..c07ed2d83e1 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -1,4 +1,6 @@ /* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ +/// + import { copySync } from 'fs-extra'; import { readFileSync, writeFileSync } from 'fs'; const MOCK_PORT = '[TEST_REPLACEABLE_MOCK_PORT]'; @@ -198,9 +200,22 @@ const makeContentScriptTestsBuild = (sourceBuildType: string) => { ); }; +const makeConsumerLocalBuild = () => { + const localBuildType = 'chrome-consumer-local'; + const publicKey = + 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArV4mhxGkdt2FcJoWJhZrzNUftI0S7i55jMooL+FLjRSyK1hh6G4so7KLYhY/Tc327luMwWFkCAcdsamjbhOfJneBMZ0IT7swAS3zsC87vLE5YeWO2CX02FvHjgXm60T1Fk4gJh/zqCp4OLjyawoJRyuovvVN0LwH4j6DjHn3nodl2YeY+4K7jzFGHj6+68tlok9BtI6k8tntIbnFToRr9gVR85UT+W8rKXqx20Kne14k2my5fjrGZjEpK74YU6QNlKRzprVqpNEE989sxNk1tL6xKYgoDO9m8JZtuFKFsoQY/fV8xhNkXB19KLVJEW8aqPG/0ZTTMg9llZI8c6Yx+QIDAQAB'; + copySync(buildDir(CHROME_CONSUMER), buildDir(localBuildType)); + edit(`${buildDir(localBuildType)}/manifest.json`, code => { + const manifest = JSON.parse(code) as chrome.runtime.ManifestV3; + manifest.key = publicKey; + return JSON.stringify(manifest, undefined, 2); + }); +}; + updateEnterpriseBuild(); makeMockBuild(CHROME_CONSUMER); makeMockBuild(CHROME_ENTERPRISE); makeLocalFesBuild(CHROME_ENTERPRISE); +makeConsumerLocalBuild(); makeContentScriptTestsBuild('chrome-consumer-mock'); // makeContentScriptTestsBuild('firefox-consumer'); // for manual testing of content script in Firefox From f5d6873c5aa40dad988dffea28a51d8e00757318 Mon Sep 17 00:00:00 2001 From: martgil Date: Mon, 11 May 2026 13:40:01 +0800 Subject: [PATCH 04/14] wip: update base manifest.json --- extension/manifest.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/extension/manifest.json b/extension/manifest.json index 1dacb7dea2a..34b28103a29 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -56,11 +56,6 @@ "/lib/emailjs/emailjs-mime-parser.js", "/js/content_scripts/webmail_bundle.js" ] - }, - { - "matches": ["https://www.google.com/robots.txt*"], - "js": ["/js/common/oauth2/oauth2_inject.js"], - "run_at": "document_start" } ], "background": { From 40be1011e89a39c0278fabfc2b97585aee2035dc Mon Sep 17 00:00:00 2001 From: martgil Date: Wed, 13 May 2026 13:14:37 +0800 Subject: [PATCH 05/14] wip: change mock oauth2 callback path --- test/source/mock/lib/oauth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/source/mock/lib/oauth.ts b/test/source/mock/lib/oauth.ts index 491c5aef05d..796e5e646cd 100644 --- a/test/source/mock/lib/oauth.ts +++ b/test/source/mock/lib/oauth.ts @@ -46,7 +46,7 @@ export class OauthMock { this.accessTokenByRefreshToken[refreshToken] = accessToken; this.acctByAccessToken[accessToken] = acct; this.scopesByAccessToken[accessToken] = `${this.scopesByAccessToken[accessToken] ?? ''} ${scope}`; - const url = new URL(redirect_uri ?? `https://google.localhost:${port}/robots.txt`); + const url = new URL(redirect_uri ?? `https://google.localhost:${port}/oauth2/callback`); url.searchParams.set('code', authCode); url.searchParams.set('scope', scope); // return invalid state for test.invalid.csrf@gmail.com to check invalid csrf login From 06ec303f12bb8af3f6a5bab07f3cbed25f53a544 Mon Sep 17 00:00:00 2001 From: martgil Date: Wed, 13 May 2026 13:15:14 +0800 Subject: [PATCH 06/14] fix: refine content script matches for chrome-enterprise manifest --- tooling/build-types-and-manifests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index c07ed2d83e1..c3c49b9d674 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -102,7 +102,7 @@ addManifest('chrome-enterprise', manifest => { 'https://flowcrypt.com/*', ]; for (const csDef of manifest.content_scripts ?? []) { - csDef.matches = csDef.matches?.filter(host => host === 'https://mail.google.com/*' || host === 'https://www.google.com/robots.txt*'); + csDef.matches = csDef.matches?.filter(host => host === 'https://mail.google.com/*'); } manifest.content_scripts = (manifest.content_scripts ?? []).filter(csDef => csDef.matches?.length); // remove empty defs if (!manifest.content_scripts.length) { From b44f6186255cbed5925fe0036fda0f9fd6305a64 Mon Sep 17 00:00:00 2001 From: martgil Date: Wed, 13 May 2026 13:55:43 +0800 Subject: [PATCH 07/14] refactor: use extension-specific redirect URI for OAuth flows --- .../authentication/configured-idp-oauth.ts | 6 +-- .../api/authentication/generic/oauth.ts | 49 +++++++++++-------- .../api/authentication/google/google-oauth.ts | 4 +- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/extension/js/common/api/authentication/configured-idp-oauth.ts b/extension/js/common/api/authentication/configured-idp-oauth.ts index 0283970ddfd..0913d2e5417 100644 --- a/extension/js/common/api/authentication/configured-idp-oauth.ts +++ b/extension/js/common/api/authentication/configured-idp-oauth.ts @@ -101,7 +101,7 @@ export class ConfiguredIdpOAuth extends OAuth { refreshToken, client_id: authConf.oauth.clientId, - redirect_uri: chrome.identity.getRedirectURL('oauth'), + redirect_uri: this.getRedirectUri(), }, dataType: 'JSON', /* eslint-enable @typescript-eslint/naming-convention */ @@ -121,7 +121,7 @@ export class ConfiguredIdpOAuth extends OAuth { prompt: 'login', state, - redirect_uri: chrome.identity.getRedirectURL('oauth'), + redirect_uri: this.getRedirectUri(), scope: this.OAUTH_REQUEST_SCOPES.join(' '), login_hint: acctEmail, }); @@ -207,7 +207,7 @@ export class ConfiguredIdpOAuth extends OAuth { code, client_id: authConf.oauth.clientId, - redirect_uri: chrome.identity.getRedirectURL('oauth'), + redirect_uri: this.getRedirectUri(), }, dataType: 'JSON', /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/extension/js/common/api/authentication/generic/oauth.ts b/extension/js/common/api/authentication/generic/oauth.ts index 39dc57b4d9b..4ddfc6f80bf 100644 --- a/extension/js/common/api/authentication/generic/oauth.ts +++ b/extension/js/common/api/authentication/generic/oauth.ts @@ -37,27 +37,36 @@ export type AuthorizationHeader = { export class OAuth { /* eslint-disable @typescript-eslint/naming-convention */ - public static GOOGLE_OAUTH_CONFIG = { - client_id: '717284730244-5oejn54f10gnrektjdc4fv4rbic1bj1p.apps.googleusercontent.com', - client_secret: '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', - redirect_uri: chrome.identity.getRedirectURL('oauth'), - url_code: `${GOOGLE_OAUTH_SCREEN_HOST}/o/oauth2/auth`, - url_tokens: `${OAUTH_GOOGLE_API_HOST}/token`, - state_header: 'CRYPTUP_STATE_', - scopes: { - email: 'email', - openid: 'openid', - profile: 'https://www.googleapis.com/auth/userinfo.profile', // needed so that `name` is present in `id_token`, which is required for key-server auth when in use - compose: 'https://www.googleapis.com/auth/gmail.compose', - modify: 'https://www.googleapis.com/auth/gmail.modify', - readContacts: 'https://www.googleapis.com/auth/contacts.readonly', - readOtherContacts: 'https://www.googleapis.com/auth/contacts.other.readonly', - }, - legacy_scopes: { - gmail: 'https://mail.google.com/', // causes a freakish oauth warn: "can permannently delete all your email" ... - }, - }; public static OAUTH_REQUEST_SCOPES = ['offline_access', 'openid', 'profile', 'email']; + + public static get GOOGLE_OAUTH_CONFIG() { + return { + client_id: '717284730244-5oejn54f10gnrektjdc4fv4rbic1bj1p.apps.googleusercontent.com', + client_secret: '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', + url_code: `${GOOGLE_OAUTH_SCREEN_HOST}/o/oauth2/auth`, + url_tokens: `${OAUTH_GOOGLE_API_HOST}/token`, + state_header: 'CRYPTUP_STATE_', + scopes: { + email: 'email', + openid: 'openid', + profile: 'https://www.googleapis.com/auth/userinfo.profile', // needed so that `name` is present in `id_token`, which is required for key-server auth when in use + compose: 'https://www.googleapis.com/auth/gmail.compose', + modify: 'https://www.googleapis.com/auth/gmail.modify', + readContacts: 'https://www.googleapis.com/auth/contacts.readonly', + readOtherContacts: 'https://www.googleapis.com/auth/contacts.other.readonly', + }, + legacy_scopes: { + gmail: 'https://mail.google.com/', // causes a freakish oauth warn: "can permannently delete all your email" ... + }, + }; + } + + public static getRedirectUri(): string { + if (!chrome?.identity?.getRedirectURL) { + throw new Error('chrome.identity.getRedirectURL is not available in this context'); + } + return chrome.identity.getRedirectURL('oauth'); + } /* eslint-enable @typescript-eslint/naming-convention */ /** * Happens on enterprise builds diff --git a/extension/js/common/api/authentication/google/google-oauth.ts b/extension/js/common/api/authentication/google/google-oauth.ts index 2c33fbb168e..4085700bf84 100644 --- a/extension/js/common/api/authentication/google/google-oauth.ts +++ b/extension/js/common/api/authentication/google/google-oauth.ts @@ -277,7 +277,7 @@ export class GoogleOAuth extends OAuth { access_type: 'offline', prompt: 'consent', state: authReq.expectedState, - redirect_uri: this.GOOGLE_OAUTH_CONFIG.redirect_uri, + redirect_uri: this.getRedirectUri(), scope: (authReq.scopes || []).join(' '), login_hint: authReq.acctEmail, }); @@ -309,7 +309,7 @@ export class GoogleOAuth extends OAuth { code, client_id: this.GOOGLE_OAUTH_CONFIG.client_id, client_secret: this.GOOGLE_OAUTH_CONFIG.client_secret, - redirect_uri: this.GOOGLE_OAUTH_CONFIG.redirect_uri, + redirect_uri: this.getRedirectUri(), }), /* eslint-enable @typescript-eslint/naming-convention */ method: 'POST', From 53149e4f6445166477f5d1d96e1400915764d3dd Mon Sep 17 00:00:00 2001 From: martgil Date: Wed, 13 May 2026 16:34:21 +0800 Subject: [PATCH 08/14] feat: inject OAuth2 callback script and update mock API regex for OAuth2 redirection --- extension/manifest.json | 5 +++++ test/source/mock/lib/api.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/extension/manifest.json b/extension/manifest.json index 34b28103a29..713a5dade75 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -56,6 +56,11 @@ "/lib/emailjs/emailjs-mime-parser.js", "/js/content_scripts/webmail_bundle.js" ] + }, + { + "matches": ["https://www.google.com/oauth2/callback*"], + "js": ["/js/common/oauth2/oauth2_inject.js"], + "run_at": "document_start" } ], "background": { diff --git a/test/source/mock/lib/api.ts b/test/source/mock/lib/api.ts index ee864d3a1eb..e5402444391 100644 --- a/test/source/mock/lib/api.ts +++ b/test/source/mock/lib/api.ts @@ -308,7 +308,7 @@ export class Api { private throttledResponse = async (response: http2.Http2ServerResponse, data: Buffer) => { // If google oauth2 or custom oauth login, then redirect to url - if (/^https:\/\/(google\.localhost:[0-9]+\/robots\.txt|[a-zA-Z0-9]+\.chromiumapp\.org)/.test(data.toString())) { + if (/^https:\/\/(google\.localhost:[0-9]+\/oauth2\/callback|[a-zA-Z0-9]+\.chromiumapp\.org)/.test(data.toString())) { response.writeHead(302, { Location: data.toString() }); // eslint-disable-line @typescript-eslint/naming-convention } else { const chunkSize = 100 * 1024; From 3a8c3e333206b02ebb4175286d7827e93313b283 Mon Sep 17 00:00:00 2001 From: martgil Date: Wed, 13 May 2026 17:23:33 +0800 Subject: [PATCH 09/14] fix: include oauth callback URL in content script match patterns --- tooling/build-types-and-manifests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index c3c49b9d674..c38791d0aaa 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -102,7 +102,7 @@ addManifest('chrome-enterprise', manifest => { 'https://flowcrypt.com/*', ]; for (const csDef of manifest.content_scripts ?? []) { - csDef.matches = csDef.matches?.filter(host => host === 'https://mail.google.com/*'); + csDef.matches = csDef.matches?.filter(host => host === 'https://mail.google.com/*' || host === 'https://www.google.com/oauth2/callback*'); } manifest.content_scripts = (manifest.content_scripts ?? []).filter(csDef => csDef.matches?.length); // remove empty defs if (!manifest.content_scripts.length) { From 59430047d414398b8c1141c728ff61336452c927 Mon Sep 17 00:00:00 2001 From: martgil Date: Thu, 14 May 2026 13:25:09 +0800 Subject: [PATCH 10/14] refactor: replace custom window-based OAuth flow with chrome.identity.launchWebAuthFlow --- .../compose-recipients-module.ts | 1 - extension/chrome/elements/oauth2.htm | 8 --- .../api/authentication/google/google-oauth.ts | 40 ++++---------- extension/js/common/browser/browser-msg.ts | 2 +- extension/js/common/notifications.ts | 1 - extension/js/common/oauth2/oauth2.ts | 53 ------------------- extension/js/common/oauth2/oauth2_finish.ts | 4 -- extension/js/common/oauth2/oauth2_inject.ts | 11 ---- extension/manifest.json | 6 --- test/source/mock/google/google-endpoints.ts | 2 +- test/source/mock/lib/api.ts | 2 +- tooling/build-types-and-manifests.ts | 7 +-- 12 files changed, 15 insertions(+), 122 deletions(-) delete mode 100644 extension/chrome/elements/oauth2.htm delete mode 100644 extension/js/common/oauth2/oauth2.ts delete mode 100644 extension/js/common/oauth2/oauth2_finish.ts delete mode 100644 extension/js/common/oauth2/oauth2_inject.ts diff --git a/extension/chrome/elements/compose-modules/compose-recipients-module.ts b/extension/chrome/elements/compose-modules/compose-recipients-module.ts index 5158b78c1f8..55bd1a601cf 100644 --- a/extension/chrome/elements/compose-modules/compose-recipients-module.ts +++ b/extension/chrome/elements/compose-modules/compose-recipients-module.ts @@ -878,7 +878,6 @@ export class ComposeRecipientsModule extends ViewModule { const authResult = await BrowserMsg.send.bg.await.reconnectAcctAuthPopup({ acctEmail: this.view.acctEmail, scopes: GoogleOAuth.defaultScopes('contacts'), - screenDimensions: Ui.getScreenDimensions(), }); if (authResult.result === 'Success') { this.googleContactsSearchEnabled = true; diff --git a/extension/chrome/elements/oauth2.htm b/extension/chrome/elements/oauth2.htm deleted file mode 100644 index 4f504a22f9e..00000000000 --- a/extension/chrome/elements/oauth2.htm +++ /dev/null @@ -1,8 +0,0 @@ - - - - OAuth 2.0 Finish Page - - - - diff --git a/extension/js/common/api/authentication/google/google-oauth.ts b/extension/js/common/api/authentication/google/google-oauth.ts index 4085700bf84..7b9a3b2e01a 100644 --- a/extension/js/common/api/authentication/google/google-oauth.ts +++ b/extension/js/common/api/authentication/google/google-oauth.ts @@ -6,10 +6,7 @@ import { Url } from '../../../core/common.js'; import { FLAVOR, OAUTH_GOOGLE_API_HOST } from '../../../core/const.js'; import { ApiErr } from '../../shared/api-error.js'; import { Ajax, Api } from '../../shared/api.js'; - -import { Bm, ScreenDimensions } from '../../../browser/browser-msg.js'; import { InMemoryStoreKeys } from '../../../core/const.js'; -import { OAuth2 } from '../../../oauth2/oauth2.js'; import { CatchHelper } from '../../../platform/catch-helper.js'; import { AcctStore, AcctStoreDict } from '../../../platform/store/acct-store.js'; import { InMemoryStore } from '../../../platform/store/in-memory-store.js'; @@ -18,7 +15,6 @@ import { AuthorizationHeader, AuthReq, AuthRes, OAuth, OAuthTokensResponse } fro import { ExternalService } from '../../account-servers/external-service.js'; import { GoogleAuthErr } from '../../shared/api-error.js'; import { Assert, AssertError } from '../../../assert.js'; -import { Ui } from '../../../browser/ui.js'; import { ConfiguredIdpOAuth } from '../configured-idp-oauth.js'; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -110,17 +106,7 @@ export class GoogleOAuth extends OAuth { } } - public static async newAuthPopup({ - acctEmail, - scopes, - save, - screenDimensions, - }: { - acctEmail?: string; - scopes?: string[]; - save?: boolean; - screenDimensions?: ScreenDimensions; - }): Promise { + public static async newAuthPopup({ acctEmail, scopes, save }: { acctEmail?: string; scopes?: string[]; save?: boolean }): Promise { if (acctEmail) { acctEmail = acctEmail.toLowerCase(); } @@ -133,18 +119,12 @@ export class GoogleOAuth extends OAuth { } const authRequest = GoogleOAuth.newAuthRequest(acctEmail, scopes); const authUrl = GoogleOAuth.apiGoogleAuthCodeUrl(authRequest); - // Added below logic because in service worker, it's not possible to access window object. - // Therefore need to retrieve screenDimensions when calling service worker and pass it to OAuth2 - if (!screenDimensions) { - screenDimensions = Ui.getScreenDimensions(); - } - const authWindowResult = await OAuth2.webAuthFlow(authUrl, screenDimensions); const authRes = await GoogleOAuth.getAuthRes({ acctEmail, save, requestedScopes: scopes, expectedState: authRequest.expectedState, - authWindowResult, + authUrl, }); if (authRes.result === 'Success') { if (!authRes.id_token) { @@ -211,24 +191,24 @@ export class GoogleOAuth extends OAuth { save, requestedScopes, expectedState, - authWindowResult, + authUrl, }: { acctEmail?: string; save: boolean; requestedScopes: string[]; expectedState: string; - authWindowResult: Bm.AuthWindowResult; + authUrl: string; }): Promise { /* eslint-disable @typescript-eslint/naming-convention */ try { - if (!authWindowResult.url) { - return { acctEmail, result: 'Denied', error: 'Invalid response url', id_token: undefined }; + const redirectUri = await chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true }); + if (chrome.runtime.lastError || !redirectUri || redirectUri?.includes('access_denied')) { + return { acctEmail, result: 'Denied', error: `Failed to launch web auth flow`, id_token: undefined }; } - if (authWindowResult.error) { - return { acctEmail, result: 'Denied', error: authWindowResult.error, id_token: undefined }; + if (!redirectUri) { + return { acctEmail, result: 'Denied', error: 'Invalid response url', id_token: undefined }; } - - const uncheckedUrlParams = Url.parse(['scope', 'code', 'state'], authWindowResult.url); + const uncheckedUrlParams = Url.parse(['scope', 'code', 'state'], redirectUri); const allowedScopes = Assert.urlParamRequire.string(uncheckedUrlParams, 'scope'); const code = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'code'); const receivedState = Assert.urlParamRequire.string(uncheckedUrlParams, 'state'); diff --git a/extension/js/common/browser/browser-msg.ts b/extension/js/common/browser/browser-msg.ts index 3321888a884..e133cf65e02 100644 --- a/extension/js/common/browser/browser-msg.ts +++ b/extension/js/common/browser/browser-msg.ts @@ -85,7 +85,7 @@ export namespace Bm { }; export type InMemoryStoreGet = { acctEmail: string; key: string }; export type GetApiAuthorization = { idToken: string }; - export type ReconnectAcctAuthPopup = { acctEmail: string; scopes?: string[]; screenDimensions: ScreenDimensions }; + export type ReconnectAcctAuthPopup = { acctEmail: string; scopes?: string[] }; export type ReconnectCustomIDPAcctAuthPopup = { acctEmail: string }; export type Ajax = { req: ApiAjax; resFmt: ResFmt }; export type AjaxProgress = { operationId: string; percent?: number; loaded: number; total: number; expectedTransferSize: number }; diff --git a/extension/js/common/notifications.ts b/extension/js/common/notifications.ts index a02c5092321..43b88c12dbe 100644 --- a/extension/js/common/notifications.ts +++ b/extension/js/common/notifications.ts @@ -81,7 +81,6 @@ export class Notifications { private reconnectAcctAuthPopup = async (acctEmail: string) => { const authRes = await BrowserMsg.send.bg.await.reconnectAcctAuthPopup({ acctEmail, - screenDimensions: Ui.getScreenDimensions(), }); if (authRes.result === 'Success') { this.show(`Connected successfully. You may need to reload the tab. Close`); diff --git a/extension/js/common/oauth2/oauth2.ts b/extension/js/common/oauth2/oauth2.ts deleted file mode 100644 index e34fe4d9934..00000000000 --- a/extension/js/common/oauth2/oauth2.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ - -import { Bm, BrowserMsg, ScreenDimensions } from '../browser/browser-msg.js'; -import { windowsCreate } from '../browser/chrome.js'; - -export class OAuth2 { - public static webAuthFlow = async (url: string, screenDimensions: ScreenDimensions): Promise => { - let adaptiveWidth = Math.floor(screenDimensions.width * 0.4); - if (adaptiveWidth < 550) { - adaptiveWidth = Math.min(550, Math.floor(screenDimensions.width * 0.9)); - } - const adaptiveHeight = Math.floor(screenDimensions.height * 0.9); - const leftOffset = Math.floor(screenDimensions.width / 2 - adaptiveWidth / 2 + screenDimensions.availLeft); - const topOffset = Math.floor(screenDimensions.height / 2 - adaptiveHeight / 2 + screenDimensions.availTop); - - const oauthWin = await windowsCreate({ - url, - left: leftOffset, - top: topOffset, - height: adaptiveHeight, - width: adaptiveWidth, - type: 'popup', - }); - - if (!oauthWin?.tabs?.length || !oauthWin.id) { - return { error: 'No oauth window returned after initiating it' }; - } - const tabId = oauthWin?.tabs?.[0].id; - return await new Promise(resolve => { - // need to use chrome.runtime.onMessage because BrowserMsg.addListener doesn't work - // In gmail page reconnect auth popup, it sends event to background page (BrowserMsg.send.bg.await.reconnectAcctAuthPopup) - // thefore BrowserMsg.addListener doesn't work - - chrome.runtime.onMessage.addListener((message: Bm.Raw) => { - if (message.name === 'auth_window_result') { - void chrome.tabs.remove(tabId!); // eslint-disable-line @typescript-eslint/no-non-null-assertion - resolve(message.data.bm as Bm.AuthWindowResult); - } - }); - - chrome.tabs.onRemoved.addListener(removedTabId => { - // Only reject error when auth result not successful - if (removedTabId === tabId) { - resolve({ error: 'Canceled by user' }); - } - }); - }); - }; - - public static finishAuth = (url: string) => { - BrowserMsg.send.authWindowResult('broadcast', { url }); - }; -} diff --git a/extension/js/common/oauth2/oauth2_finish.ts b/extension/js/common/oauth2/oauth2_finish.ts deleted file mode 100644 index d4b28087b40..00000000000 --- a/extension/js/common/oauth2/oauth2_finish.ts +++ /dev/null @@ -1,4 +0,0 @@ -/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ -import { OAuth2 } from './oauth2.js'; - -OAuth2.finishAuth(window.location.href); diff --git a/extension/js/common/oauth2/oauth2_inject.ts b/extension/js/common/oauth2/oauth2_inject.ts deleted file mode 100644 index b757ceb72a2..00000000000 --- a/extension/js/common/oauth2/oauth2_inject.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* ©️ 2016 - present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com */ - -// Declared win variable to avoid `Type 'string' is not assignable to type 'Location | (string & Location)'.ts(2322)` error -const win: Window = window; - -// Redirect back to the extension itself so that we have priveledged access again -// Need to send BrowserMsg event back to GoogleAuth - -const redirect = chrome.runtime.getURL('/chrome/elements/oauth2.htm'); - -win.location = redirect + win.location.search; diff --git a/extension/manifest.json b/extension/manifest.json index 713a5dade75..3bde29d83cd 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -56,11 +56,6 @@ "/lib/emailjs/emailjs-mime-parser.js", "/js/content_scripts/webmail_bundle.js" ] - }, - { - "matches": ["https://www.google.com/oauth2/callback*"], - "js": ["/js/common/oauth2/oauth2_inject.js"], - "run_at": "document_start" } ], "background": { @@ -90,7 +85,6 @@ "/chrome/elements/add_pubkey.htm", "/chrome/elements/pgp_pubkey.htm", "/chrome/elements/backup.htm", - "/chrome/elements/oauth2.htm", "/js/common/core/feature-config-injector.js" ], "matches": ["https://mail.google.com/*", "https://accounts.google.com/*", "https://www.google.com/*"] diff --git a/test/source/mock/google/google-endpoints.ts b/test/source/mock/google/google-endpoints.ts index 0d9d69e5780..46b8b8e47a6 100644 --- a/test/source/mock/google/google-endpoints.ts +++ b/test/source/mock/google/google-endpoints.ts @@ -133,7 +133,7 @@ export const getMockGoogleEndpoints = (oauth: OauthMock, config: GoogleConfig | } else if (!proceed) { return oauth.renderText('redirect with proceed=true to continue'); } else { - return oauth.successResult(parsePort(req), login_hint, state, scope); + return oauth.successResult(parsePort(req), login_hint, state, scope, redirect_uri); } } else if (client_id === OauthMock.customIDPClientId) { if (!proceed) { diff --git a/test/source/mock/lib/api.ts b/test/source/mock/lib/api.ts index e5402444391..fd2fe526192 100644 --- a/test/source/mock/lib/api.ts +++ b/test/source/mock/lib/api.ts @@ -308,7 +308,7 @@ export class Api { private throttledResponse = async (response: http2.Http2ServerResponse, data: Buffer) => { // If google oauth2 or custom oauth login, then redirect to url - if (/^https:\/\/(google\.localhost:[0-9]+\/oauth2\/callback|[a-zA-Z0-9]+\.chromiumapp\.org)/.test(data.toString())) { + if (/^https:\/\/[a-zA-Z0-9]+\.chromiumapp\.org/.test(data.toString())) { response.writeHead(302, { Location: data.toString() }); // eslint-disable-line @typescript-eslint/naming-convention } else { const chunkSize = 100 * 1024; diff --git a/tooling/build-types-and-manifests.ts b/tooling/build-types-and-manifests.ts index c38791d0aaa..97054225ffa 100644 --- a/tooling/build-types-and-manifests.ts +++ b/tooling/build-types-and-manifests.ts @@ -102,7 +102,7 @@ addManifest('chrome-enterprise', manifest => { 'https://flowcrypt.com/*', ]; for (const csDef of manifest.content_scripts ?? []) { - csDef.matches = csDef.matches?.filter(host => host === 'https://mail.google.com/*' || host === 'https://www.google.com/oauth2/callback*'); + csDef.matches = csDef.matches?.filter(host => host === 'https://mail.google.com/*'); } manifest.content_scripts = (manifest.content_scripts ?? []).filter(csDef => csDef.matches?.length); // remove empty defs if (!manifest.content_scripts.length) { @@ -174,10 +174,7 @@ const makeMockBuild = (sourceBuildType: string) => { edit(`${buildDir(mockBuildType)}/js/common/platform/catch.js`, editor); edit(`${buildDir(mockBuildType)}/js/content_scripts/webmail_bundle.js`, editor); edit(`${buildDir(mockBuildType)}/manifest.json`, code => - code - .replace(/https:\/\/mail\.google\.com/g, mockGmailPage) - .replace(/https:\/\/www\.google\.com/g, `https://google.localhost:${MOCK_PORT}`) - .replace(/https:\/\/\*\.google.com\/\*/, 'https://google.localhost/*') + code.replace(/https:\/\/mail\.google\.com/g, mockGmailPage).replace(/https:\/\/\*\.google.com\/\*/, 'https://google.localhost/*') ); }; From 1e49abe136a77f6d5a2255acb50b359b38104954 Mon Sep 17 00:00:00 2001 From: martgil Date: Thu, 14 May 2026 14:47:09 +0800 Subject: [PATCH 11/14] fix: differentiate user-cancelled oauth flows and update test setup for auth closing behavior --- .../common/api/authentication/google/google-oauth.ts | 12 +++++++++++- test/source/tests/setup.ts | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/extension/js/common/api/authentication/google/google-oauth.ts b/extension/js/common/api/authentication/google/google-oauth.ts index 7b9a3b2e01a..af2529f1c4e 100644 --- a/extension/js/common/api/authentication/google/google-oauth.ts +++ b/extension/js/common/api/authentication/google/google-oauth.ts @@ -203,7 +203,17 @@ export class GoogleOAuth extends OAuth { try { const redirectUri = await chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true }); if (chrome.runtime.lastError || !redirectUri || redirectUri?.includes('access_denied')) { - return { acctEmail, result: 'Denied', error: `Failed to launch web auth flow`, id_token: undefined }; + const errorMsg = chrome.runtime.lastError?.message || 'access_denied'; + if ( + errorMsg.toLowerCase().includes('user') || + errorMsg.toLowerCase().includes('cancel') || + errorMsg.toLowerCase().includes('deny') || + errorMsg.toLowerCase().includes('denied') || + errorMsg.toLowerCase().includes('close') + ) { + return { acctEmail, result: 'Closed', error: errorMsg, id_token: undefined }; + } + return { acctEmail, result: 'Denied', error: `Failed to launch web auth flow: ${errorMsg}`, id_token: undefined }; } if (!redirectUri) { return { acctEmail, result: 'Denied', error: 'Invalid response url', id_token: undefined }; diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index f571256f370..5979d3b5143 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -100,6 +100,9 @@ export const defineSetupTests = (testVariant: TestVariant, testWithBrowser: Test test( 'settings > login > close oauth window > close popup', testWithBrowser(async (t, browser) => { + t.context.mockApi!.configProvider = new ConfigurationProvider({ + attester: { pubkeyLookup: {} }, + }); const settingsPage = await BrowserRecipe.openSettingsLoginButCloseOauthWindowBeforeGrantingPermission( t, browser, From 7842dd78e84837071bf5b8a311e004b5ac929953 Mon Sep 17 00:00:00 2001 From: martgil Date: Thu, 14 May 2026 15:42:32 +0800 Subject: [PATCH 12/14] refactor: remove redundant port parameter from oauth success result logic --- test/source/mock/google/google-endpoints.ts | 4 ++-- test/source/mock/lib/oauth.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/source/mock/google/google-endpoints.ts b/test/source/mock/google/google-endpoints.ts index 46b8b8e47a6..9493ba3b3ae 100644 --- a/test/source/mock/google/google-endpoints.ts +++ b/test/source/mock/google/google-endpoints.ts @@ -133,13 +133,13 @@ export const getMockGoogleEndpoints = (oauth: OauthMock, config: GoogleConfig | } else if (!proceed) { return oauth.renderText('redirect with proceed=true to continue'); } else { - return oauth.successResult(parsePort(req), login_hint, state, scope, redirect_uri); + return oauth.successResult(login_hint, state, scope, redirect_uri); } } else if (client_id === OauthMock.customIDPClientId) { if (!proceed) { return oauth.renderText('redirect with proceed=true to continue'); } - return oauth.successResult(parsePort(req), login_hint, state, scope, redirect_uri); + return oauth.successResult(login_hint, state, scope, redirect_uri); } } throw new HttpClientErr(`Method not implemented for ${req.url}: ${req.method}`); diff --git a/test/source/mock/lib/oauth.ts b/test/source/mock/lib/oauth.ts index 796e5e646cd..9430c89a41b 100644 --- a/test/source/mock/lib/oauth.ts +++ b/test/source/mock/lib/oauth.ts @@ -37,7 +37,7 @@ export class OauthMock { }; // eslint-disable-next-line @typescript-eslint/naming-convention - public successResult = (port: string, acct: string, state: string, scope: string, redirect_uri?: string) => { + public successResult = (acct: string, state: string, scope: string, redirect_uri: string) => { const authCode = `mock-auth-code-${Str.sloppyRandom(4)}-${acct.replace(/[^a-z0-9]+/g, '')}`; const refreshToken = `mock-refresh-token-${Str.sloppyRandom(4)}-${acct.replace(/[^a-z0-9]+/g, '')}`; const accessToken = `mock-access-token-${Str.sloppyRandom(4)}-${acct.replace(/[^a-z0-9]+/g, '')}`; @@ -46,7 +46,7 @@ export class OauthMock { this.accessTokenByRefreshToken[refreshToken] = accessToken; this.acctByAccessToken[accessToken] = acct; this.scopesByAccessToken[accessToken] = `${this.scopesByAccessToken[accessToken] ?? ''} ${scope}`; - const url = new URL(redirect_uri ?? `https://google.localhost:${port}/oauth2/callback`); + const url = new URL(redirect_uri); url.searchParams.set('code', authCode); url.searchParams.set('scope', scope); // return invalid state for test.invalid.csrf@gmail.com to check invalid csrf login From 9d07b1c3d57daaf32be987f5e7ae60b695a0a5e3 Mon Sep 17 00:00:00 2001 From: martgil Date: Fri, 15 May 2026 14:25:31 +0800 Subject: [PATCH 13/14] refactor: simplify user cancellation logic in Google OAuth and remove redundant redirect URI check --- .../api/authentication/google/google-oauth.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/extension/js/common/api/authentication/google/google-oauth.ts b/extension/js/common/api/authentication/google/google-oauth.ts index af2529f1c4e..784786bdf5c 100644 --- a/extension/js/common/api/authentication/google/google-oauth.ts +++ b/extension/js/common/api/authentication/google/google-oauth.ts @@ -204,20 +204,13 @@ export class GoogleOAuth extends OAuth { const redirectUri = await chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true }); if (chrome.runtime.lastError || !redirectUri || redirectUri?.includes('access_denied')) { const errorMsg = chrome.runtime.lastError?.message || 'access_denied'; - if ( - errorMsg.toLowerCase().includes('user') || - errorMsg.toLowerCase().includes('cancel') || - errorMsg.toLowerCase().includes('deny') || - errorMsg.toLowerCase().includes('denied') || - errorMsg.toLowerCase().includes('close') - ) { + const normalizedErrorMsg = errorMsg.toLowerCase(); + const userCancelled = ['user', 'cancel', 'deny', 'denied', 'close'].some(keyword => normalizedErrorMsg.includes(keyword)); + if (userCancelled) { return { acctEmail, result: 'Closed', error: errorMsg, id_token: undefined }; } return { acctEmail, result: 'Denied', error: `Failed to launch web auth flow: ${errorMsg}`, id_token: undefined }; } - if (!redirectUri) { - return { acctEmail, result: 'Denied', error: 'Invalid response url', id_token: undefined }; - } const uncheckedUrlParams = Url.parse(['scope', 'code', 'state'], redirectUri); const allowedScopes = Assert.urlParamRequire.string(uncheckedUrlParams, 'scope'); const code = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'code'); From 5032b159eb28d9f443135220658dc73b741092f3 Mon Sep 17 00:00:00 2001 From: martgil Date: Fri, 15 May 2026 14:50:39 +0800 Subject: [PATCH 14/14] feat: add Firefox-specific OAuth redirect URI and update mock API validation accordingly --- extension/js/common/api/authentication/generic/oauth.ts | 8 +++++++- test/source/mock/lib/api.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/extension/js/common/api/authentication/generic/oauth.ts b/extension/js/common/api/authentication/generic/oauth.ts index 4ddfc6f80bf..97af5d91f96 100644 --- a/extension/js/common/api/authentication/generic/oauth.ts +++ b/extension/js/common/api/authentication/generic/oauth.ts @@ -65,7 +65,13 @@ export class OAuth { if (!chrome?.identity?.getRedirectURL) { throw new Error('chrome.identity.getRedirectURL is not available in this context'); } - return chrome.identity.getRedirectURL('oauth'); + const redirectUri = chrome.identity.getRedirectURL('oauth'); + if (navigator.userAgent.includes('Firefox')) { + const url = new URL(redirectUri); + const subdomain = url.hostname.split('.')[0]; + return `http://127.0.0.1/mozoauth2/${subdomain}`; + } + return redirectUri; } /* eslint-enable @typescript-eslint/naming-convention */ /** diff --git a/test/source/mock/lib/api.ts b/test/source/mock/lib/api.ts index fd2fe526192..c44d31d8f6d 100644 --- a/test/source/mock/lib/api.ts +++ b/test/source/mock/lib/api.ts @@ -308,7 +308,7 @@ export class Api { private throttledResponse = async (response: http2.Http2ServerResponse, data: Buffer) => { // If google oauth2 or custom oauth login, then redirect to url - if (/^https:\/\/[a-zA-Z0-9]+\.chromiumapp\.org/.test(data.toString())) { + if (/^(https:\/\/[a-zA-Z0-9-]+\.(chromiumapp\.org|extensions\.mozilla\.org)|http:\/\/127\.0\.0\.1\/mozoauth2\/)/.test(data.toString())) { response.writeHead(302, { Location: data.toString() }); // eslint-disable-line @typescript-eslint/naming-convention } else { const chunkSize = 100 * 1024;