From c7b6f51150a57b0064b5bfd0c41ef7ff7e4cd3f8 Mon Sep 17 00:00:00 2001 From: Tom J Date: Wed, 15 Dec 2021 15:02:59 +0000 Subject: [PATCH 1/5] issue 4194 refresh id token --- extension/js/common/api/account-server.ts | 3 +- .../api/account-servers/enterprise-server.ts | 56 ++++--------------- .../api/email-provider/gmail/google-auth.ts | 9 +-- extension/js/common/org-rules.ts | 10 ---- .../js/common/platform/store/acct-store.ts | 6 +- .../common/platform/store/in-memory-store.ts | 2 + 6 files changed, 21 insertions(+), 65 deletions(-) diff --git a/extension/js/common/api/account-server.ts b/extension/js/common/api/account-server.ts index 90e48c2977b..68fe1a7d0ef 100644 --- a/extension/js/common/api/account-server.ts +++ b/extension/js/common/api/account-server.ts @@ -20,8 +20,7 @@ export class AccountServer extends Api { public loginWithOpenid = async (acctEmail: string, uuid: string, idToken: string): Promise => { if (await this.isFesUsed()) { - const fes = new EnterpriseServer(this.acctEmail); - await fes.authenticateAndUpdateLocalStore(idToken); + // FES doesn't issue any access tokens } else { await FlowCryptComApi.loginWithOpenid(acctEmail, uuid, idToken); } diff --git a/extension/js/common/api/account-servers/enterprise-server.ts b/extension/js/common/api/account-servers/enterprise-server.ts index 5df27ad0546..719435413bd 100644 --- a/extension/js/common/api/account-servers/enterprise-server.ts +++ b/extension/js/common/api/account-servers/enterprise-server.ts @@ -10,12 +10,12 @@ import { AcctStore } from '../../platform/store/acct-store.js'; import { BackendRes, ProfileUpdate } from './flowcrypt-com-api.js'; import { Dict } from '../../core/common.js'; import { ErrorReport, UnreportableError } from '../../platform/catch.js'; -import { ApiErr } from '../shared/api-error.js'; +import { ApiErr, BackendAuthErr } from '../shared/api-error.js'; import { FLAVOR } from '../../core/const.js'; import { Attachment } from '../../core/attachment.js'; import { Recipients } from '../email-provider/email-provider-api.js'; import { Buf } from '../../core/buf.js'; -import { DomainRulesJson, OrgRules } from '../../org-rules.js'; +import { DomainRulesJson } from '../../org-rules.js'; import { InMemoryStore } from '../../platform/store/in-memory-store.js'; // todo - decide which tags to use @@ -43,8 +43,6 @@ export class EnterpriseServer extends Api { private apiVersion = 'v1'; private domainsThatUseLaxFesCheckEvenOnEnterprise = ['dmFsZW8uY29t']; - private IN_MEMORY_ID_TOKEN_STORAGE_KEY = 'idTokenForOnPremAuth'; - constructor(private acctEmail: string) { super(); this.domain = acctEmail.toLowerCase().split('@').pop()!; @@ -87,16 +85,6 @@ export class EnterpriseServer extends Api { return await this.request('GET', `/api/`); }; - public authenticateAndUpdateLocalStore = async (idToken: string): Promise => { - if ((await OrgRules.newInstance(this.acctEmail)).disableFesAccessToken()) { - // the OIDC ID Token itself is used for auth, typically expires in 1 hour - await InMemoryStore.set(this.acctEmail, this.IN_MEMORY_ID_TOKEN_STORAGE_KEY, idToken); - } else { - const r = await this.request('GET', `/api/${this.apiVersion}/account/access-token`, { Authorization: `Bearer ${idToken}` }); - await AcctStore.set(this.acctEmail, { fesAccessToken: r.accessToken }); - } - }; - public fetchAndSaveOrgRules = async (): Promise => { const r = await this.request('GET', `/api/${this.apiVersion}/client-configuration?domain=${this.domain}`); await AcctStore.set(this.acctEmail, { rules: r.clientConfiguration }); @@ -104,26 +92,16 @@ export class EnterpriseServer extends Api { }; public reportException = async (errorReport: ErrorReport): Promise => { - if ((await OrgRules.newInstance(this.acctEmail)).disableFesAccessToken()) { - console.info('Reporting exceptions to FES is disabled when DISABLE_FES_ACCESS_TOKEN OrgRule is used'); - return; - } - await this.request('POST', `/api/${this.apiVersion}/log-collector/exception`, - await this.authHdr('accessToken'), errorReport); + await this.request('POST', `/api/${this.apiVersion}/log-collector/exception`, await this.authHdr(), errorReport); }; public reportEvent = async (tags: EventTag[], message: string, details?: string): Promise => { - if ((await OrgRules.newInstance(this.acctEmail)).disableFesAccessToken()) { - console.info('Reporting events to FES is disabled when DISABLE_FES_ACCESS_TOKEN OrgRule is used'); - return; - } await this.request('POST', `/api/${this.apiVersion}/log-collector/exception`, - await this.authHdr('accessToken'), { tags, message, details }); + await this.authHdr(), { tags, message, details }); }; public webPortalMessageNewReplyToken = async (): Promise => { - const disableAccessToken = (await OrgRules.newInstance(this.acctEmail)).disableFesAccessToken(); - const authHdr = await this.authHdr(disableAccessToken ? 'OIDC' : 'accessToken'); + const authHdr = await this.authHdr(); return await this.request('POST', `/api/${this.apiVersion}/message/new-reply-token`, authHdr, {}); }; @@ -151,8 +129,7 @@ export class EnterpriseServer extends Api { })) }); const multipartBody = { content, details }; - const disableAccessToken = (await OrgRules.newInstance(this.acctEmail)).disableFesAccessToken(); - const authHdr = await this.authHdr(disableAccessToken ? 'OIDC' : 'accessToken'); + const authHdr = await this.authHdr(); return await EnterpriseServer.apiCall( this.url, `/api/${this.apiVersion}/message`, multipartBody, 'FORM', { upload: progressCb }, authHdr, 'json', 'POST' @@ -164,22 +141,13 @@ export class EnterpriseServer extends Api { throw new UnreportableError('Account update not implemented when using FlowCrypt Enterprise Server'); }; - private authHdr = async (type: 'accessToken' | 'OIDC'): Promise> => { - if (type === 'accessToken') { - const { fesAccessToken } = await AcctStore.get(this.acctEmail, ['fesAccessToken']); - return { Authorization: `Bearer ${fesAccessToken}` }; - } else { - const idToken = await InMemoryStore.get(this.acctEmail, this.IN_MEMORY_ID_TOKEN_STORAGE_KEY); - if (idToken) { - return { Authorization: `Bearer ${idToken}` }; - } - // some customers may choose to disable authentication on FES - // in such cases, omitting the authentication header will not produce an error - // because client app doesn't know how is the server configured, it will try the request anyway - // worst case a 401 comes back which triggers a re-authentication prompt in this app - // the FES property responsible for this is api.portal.upload.authenticate=true|false - return {}; + private authHdr = async (): Promise> => { + const idToken = await InMemoryStore.get(this.acctEmail, InMemoryStore.ID_TOKEN_STORAGE_KEY); + if (idToken) { + return { Authorization: `Bearer ${idToken}` }; } + // todo: test this. user should not actually see the message, should be presented with login prompt + throw new BackendAuthErr('Missing id token, please re-authenticate'); }; private request = async (method: ReqMethod, path: string, headers: Dict = {}, vals?: Dict): Promise => { diff --git a/extension/js/common/api/email-provider/gmail/google-auth.ts b/extension/js/common/api/email-provider/gmail/google-auth.ts index b1915d77942..d547714dbd7 100644 --- a/extension/js/common/api/email-provider/gmail/google-auth.ts +++ b/extension/js/common/api/email-provider/gmail/google-auth.ts @@ -20,6 +20,7 @@ import { Ui } from '../../../browser/ui.js'; import { AcctStore, AcctStoreDict } from '../../../platform/store/acct-store.js'; import { AccountServer } from '../../account-server.js'; import { EnterpriseServer } from '../../account-servers/enterprise-server.js'; +import { InMemoryStore } from '../../../platform/store/in-memory-store.js'; type GoogleAuthTokenInfo = { issued_to: string, audience: string, scope: string, expires_in: number, access_type: 'offline' }; type GoogleAuthTokensResponse = { access_token: string, expires_in: number, refresh_token?: string, id_token: string, token_type: 'Bearer' }; @@ -279,20 +280,20 @@ export class GoogleAuth { }; private static googleAuthSaveTokens = async (acctEmail: string, tokensObj: GoogleAuthTokensResponse, scopes: string[]) => { - const openid = GoogleAuth.parseIdToken(tokensObj.id_token); + const parsedOpenId = GoogleAuth.parseIdToken(tokensObj.id_token); const { full_name, picture } = await AcctStore.get(acctEmail, ['full_name', 'picture']); const toSave: AcctStoreDict = { - openid, google_token_access: tokensObj.access_token, google_token_expires: new Date().getTime() + (tokensObj.expires_in as number) * 1000, google_token_scopes: scopes, - full_name: full_name || openid.name, - picture: picture || openid.picture, + full_name: full_name || parsedOpenId.name, + picture: picture || parsedOpenId.picture, }; if (typeof tokensObj.refresh_token !== 'undefined') { toSave.google_token_refresh = tokensObj.refresh_token; } await AcctStore.set(acctEmail, toSave); + await InMemoryStore.set(acctEmail, InMemoryStore.ID_TOKEN_STORAGE_KEY, tokensObj.id_token); }; private static googleAuthGetTokens = async (code: string) => { diff --git a/extension/js/common/org-rules.ts b/extension/js/common/org-rules.ts index 0aa8f77600d..41447e4bf85 100644 --- a/extension/js/common/org-rules.ts +++ b/extension/js/common/org-rules.ts @@ -197,14 +197,4 @@ export class OrgRules { return (this.domainRules.flags || []).includes('HIDE_ARMOR_META'); }; - /** - * Ask the client app to not fetch access token from FES, and - * instead OIDC ID Token directly for each authenticated call. - * The ID Token is kept in-memory, and typically expires in 1 hour depending on IdP settings - * This is more secure, but user may need to re-authenticate frequently. - */ - public disableFesAccessToken = (): boolean => { - return (this.domainRules.flags || []).includes('DISABLE_FES_ACCESS_TOKEN'); - }; - } diff --git a/extension/js/common/platform/store/acct-store.ts b/extension/js/common/platform/store/acct-store.ts index 969847364cb..deb1f028f6b 100644 --- a/extension/js/common/platform/store/acct-store.ts +++ b/extension/js/common/platform/store/acct-store.ts @@ -4,7 +4,6 @@ import { Env } from '../../browser/env.js'; import { GoogleAuth } from '../../api/email-provider/gmail/google-auth.js'; import { KeyInfo } from '../../core/crypto/key.js'; import { Dict } from '../../core/common.js'; -import { GmailRes } from '../../api/email-provider/gmail/gmail-parser.js'; import { DomainRulesJson } from '../../org-rules.js'; import { BrowserMsg, BgNotReadyErr } from '../../browser/browser-msg.js'; import { Ui } from '../../browser/ui.js'; @@ -31,8 +30,7 @@ export type AccountIndex = 'keys' | 'notification_setup_needed_dismissed' | 'ema 'google_token_refresh' | 'hide_message_password' | 'sendAs' | 'pubkey_sent_to' | 'full_name' | 'cryptup_enabled' | 'setup_done' | 'successfully_received_at_leat_one_message' | 'notification_setup_done_seen' | 'picture' | - 'outgoing_language' | 'setup_date' | 'openid' | 'uuid' | 'use_rich_text' | 'rules' | - 'fesUrl' | 'fesAccessToken'; + 'outgoing_language' | 'setup_date' | 'uuid' | 'use_rich_text' | 'rules' | 'fesUrl'; export type SendAsAlias = { isPrimary: boolean; @@ -62,11 +60,9 @@ export type AcctStoreDict = { outgoing_language?: 'EN' | 'DE'; setup_date?: number; use_rich_text?: boolean; - openid?: GmailRes.OpenId; uuid?: string; rules?: DomainRulesJson; fesUrl?: string; // url where FlowCrypt Enterprise Server is deployed - fesAccessToken?: string; }; /** diff --git a/extension/js/common/platform/store/in-memory-store.ts b/extension/js/common/platform/store/in-memory-store.ts index 57f0b777b6d..85d2fa03210 100644 --- a/extension/js/common/platform/store/in-memory-store.ts +++ b/extension/js/common/platform/store/in-memory-store.ts @@ -9,6 +9,8 @@ import { BrowserMsg } from '../../browser/browser-msg.js'; */ export class InMemoryStore extends AbstractStore { + public static ID_TOKEN_STORAGE_KEY = 'idToken'; + public static set = async (acctEmail: string, key: string, value: string | undefined) => { return await BrowserMsg.send.bg.await.inMemoryStoreSet({ acctEmail, key, value }); }; From 30da9ffcd1735e3d3b9c67c115bc6bafb8d689a2 Mon Sep 17 00:00:00 2001 From: Tom J Date: Wed, 15 Dec 2021 15:05:22 +0000 Subject: [PATCH 2/5] fix --- extension/chrome/settings/modules/debug_api.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extension/chrome/settings/modules/debug_api.ts b/extension/chrome/settings/modules/debug_api.ts index f2fa633e766..902a5fd4d9e 100644 --- a/extension/chrome/settings/modules/debug_api.ts +++ b/extension/chrome/settings/modules/debug_api.ts @@ -31,7 +31,6 @@ View.run(class DebugApiView extends View { } catch (e) { this.renderCallRes('gmail.fetchAcctAliases', {}, undefined, e); } - this.renderCallRes('Store.getAcct.openid', { acctEmail: this.acctEmail }, await AcctStore.get(this.acctEmail, ['openid'])); } else if (this.which === 'flowcrypt_account') { Xss.sanitizeAppend('#content', `Unsupported which: ${Xss.escape(this.which)} (not implemented)`); } else if (this.which === 'flowcrypt_subscription') { @@ -40,7 +39,7 @@ View.run(class DebugApiView extends View { const storage = await AcctStore.get(this.acctEmail, [ 'notification_setup_needed_dismissed', 'email_provider', 'google_token_scopes', 'hide_message_password', 'sendAs', 'outgoing_language', 'full_name', 'cryptup_enabled', 'setup_done', - 'successfully_received_at_leat_one_message', 'notification_setup_done_seen', 'openid', + 'successfully_received_at_leat_one_message', 'notification_setup_done_seen', 'rules', 'use_rich_text', 'fesUrl' ]); this.renderCallRes('Local account storage', { acctEmail: this.acctEmail }, storage); From b6ded5cb9bc1ab929540d2d77329b99cc6fe19d3 Mon Sep 17 00:00:00 2001 From: Tom J Date: Wed, 15 Dec 2021 15:48:29 +0000 Subject: [PATCH 3/5] cleanup test --- .../api/email-provider/gmail/google-auth.ts | 2 +- extension/js/common/org-rules.ts | 3 +- test/source/mock/fes/fes-endpoints.ts | 6 ---- test/source/mock/google/google-endpoints.ts | 2 +- .../strategies/send-message-strategy.ts | 23 +-------------- test/source/mock/lib/oauth.ts | 2 +- test/source/tests/compose.ts | 28 ++----------------- 7 files changed, 7 insertions(+), 59 deletions(-) diff --git a/extension/js/common/api/email-provider/gmail/google-auth.ts b/extension/js/common/api/email-provider/gmail/google-auth.ts index d547714dbd7..aaf56c6bb70 100644 --- a/extension/js/common/api/email-provider/gmail/google-auth.ts +++ b/extension/js/common/api/email-provider/gmail/google-auth.ts @@ -148,7 +148,7 @@ export class GoogleAuth { const acctServer = new AccountServer(authRes.acctEmail); // fetch and store OrgRules (not authenticated) await acctServer.accountGetAndUpdateLocalStore({ account: authRes.acctEmail, uuid }); - // depending on DISABLE_FES_ACCESS_TOKEN, either fetch and store access token, or the ID token itself + // this is a no-op if FES is used. uuid is generated / stored if flowcrypt.com/api is used await acctServer.loginWithOpenid(authRes.acctEmail, uuid, authRes.id_token); } else { // eventually this branch will be dropped once a public FES instance is run for these customers diff --git a/extension/js/common/org-rules.ts b/extension/js/common/org-rules.ts index 41447e4bf85..0bafa36ec96 100644 --- a/extension/js/common/org-rules.ts +++ b/extension/js/common/org-rules.ts @@ -8,8 +8,7 @@ import { KeyAlgo } from './core/crypto/key.js'; type DomainRules$flag = 'NO_PRV_CREATE' | 'NO_PRV_BACKUP' | 'PRV_AUTOIMPORT_OR_AUTOGEN' | 'PASS_PHRASE_QUIET_AUTOGEN' | 'ENFORCE_ATTESTER_SUBMIT' | 'NO_ATTESTER_SUBMIT' | 'USE_LEGACY_ATTESTER_SUBMIT' | - 'DEFAULT_REMEMBER_PASS_PHRASE' | 'HIDE_ARMOR_META' | 'FORBID_STORING_PASS_PHRASE' | - 'DISABLE_FES_ACCESS_TOKEN'; + 'DEFAULT_REMEMBER_PASS_PHRASE' | 'HIDE_ARMOR_META' | 'FORBID_STORING_PASS_PHRASE'; export type DomainRulesJson = { flags?: DomainRules$flag[], diff --git a/test/source/mock/fes/fes-endpoints.ts b/test/source/mock/fes/fes-endpoints.ts index b11faacb71d..95dae2f8860 100644 --- a/test/source/mock/fes/fes-endpoints.ts +++ b/test/source/mock/fes/fes-endpoints.ts @@ -52,7 +52,6 @@ export const mockFesEndpoints: HandlersDefinition = { }, '/api/v1/client-configuration': async ({ }, req) => { // individual OrgRules are tested using FlowCrypt backend mock, see BackendData.getOrgRules - // (except for DISABLE_FES_ACCESS_TOKEN which is FES specific and returned below) if (req.method !== 'GET') { throw new HttpClientErr('Unsupported method'); } @@ -61,11 +60,6 @@ export const mockFesEndpoints: HandlersDefinition = { clientConfiguration: { disallow_attester_search_for_domains: ['got.this@fromstandardfes.com'] }, }; } - if (req.headers.host === disableAccessTokenFesUrl && req.url === `/api/v1/client-configuration?domain=disablefesaccesstoken.test:8001`) { - return { - clientConfiguration: { flags: ['DISABLE_FES_ACCESS_TOKEN'] }, - }; - } throw new HttpClientErr(`Unexpected FES domain "${req.headers.host}" and url "${req.url}"`); }, '/api/v1/message/new-reply-token': async ({ }, req) => { diff --git a/test/source/mock/google/google-endpoints.ts b/test/source/mock/google/google-endpoints.ts index 043d9063611..aa7ff6dc7e2 100644 --- a/test/source/mock/google/google-endpoints.ts +++ b/test/source/mock/google/google-endpoints.ts @@ -35,7 +35,7 @@ export const mockGoogleEndpoints: HandlersDefinition = { if (isPost(req) && grant_type === 'authorization_code' && code && client_id === oauth.clientId) { // auth code from auth screen gets exchanged for access and refresh tokens return oauth.getRefreshTokenResponse(code); } else if (isPost(req) && grant_type === 'refresh_token' && refreshToken && client_id === oauth.clientId) { // here also later refresh token gets exchanged for access token - return oauth.getAccessTokenResponse(refreshToken); + return oauth.getTokenResponse(refreshToken); } throw new Error(`Method not implemented for ${req.url}: ${req.method}`); }, diff --git a/test/source/mock/google/strategies/send-message-strategy.ts b/test/source/mock/google/strategies/send-message-strategy.ts index 708ac6dbe0e..c67f5e96e1c 100644 --- a/test/source/mock/google/strategies/send-message-strategy.ts +++ b/test/source/mock/google/strategies/send-message-strategy.ts @@ -38,7 +38,7 @@ class PwdEncryptedMessageWithFlowCryptComApiTestStrategy implements ITestMsgStra }; } -class PwdEncryptedMessageWithFesAccessTokenTestStrategy implements ITestMsgStrategy { +class PwdEncryptedMessageWithFesIdTokenTestStrategy implements ITestMsgStrategy { public test = async (mimeMsg: ParsedMail) => { const senderEmail = Str.parseEmail(mimeMsg.from!.text).email; const expectedSenderEmail = 'user@standardsubdomainfes.test:8001'; @@ -57,25 +57,6 @@ class PwdEncryptedMessageWithFesAccessTokenTestStrategy implements ITestMsgStrat }; } -class PwdEncryptedMessageWithFesIdTokenTestStrategy implements ITestMsgStrategy { - public test = async (mimeMsg: ParsedMail) => { - const senderEmail = Str.parseEmail(mimeMsg.from!.text).email; - const expectedSenderEmail = 'user@disablefesaccesstoken.test:8001'; - if (senderEmail !== expectedSenderEmail) { - throw new HttpClientErr(`Unexpected sender email ${senderEmail}, expecting ${expectedSenderEmail}`); - } - if (!mimeMsg.text?.includes(`${senderEmail} has sent you a password-encrypted email`)) { - throw new HttpClientErr(`Error checking sent text in:\n\n${mimeMsg.text}`); - } - if (!mimeMsg.text?.includes('http://fes.disablefesaccesstoken.test:8001/message/FES-MOCK-MESSAGE-ID')) { - throw new HttpClientErr(`Error: cannot find pwd encrypted FES link in:\n\n${mimeMsg.text}`); - } - if (!mimeMsg.text?.includes('Follow this link to open it')) { - throw new HttpClientErr(`Error: cannot find pwd encrypted open link prompt in ${mimeMsg.text}`); - } - }; -} - class MessageWithFooterTestStrategy implements ITestMsgStrategy { private readonly footer = 'flowcrypt.compatibility test footer with an img'; @@ -238,8 +219,6 @@ export class TestBySubjectStrategyContext { this.strategy = new MessageWithFooterTestStrategy(); } else if (subject.includes('PWD encrypted message with flowcrypt.com/api')) { this.strategy = new PwdEncryptedMessageWithFlowCryptComApiTestStrategy(); - } else if (subject.includes('PWD encrypted message with FES - access token')) { - this.strategy = new PwdEncryptedMessageWithFesAccessTokenTestStrategy(); } else if (subject.includes('PWD encrypted message with FES - ID TOKEN')) { this.strategy = new PwdEncryptedMessageWithFesIdTokenTestStrategy(); } else if (subject.includes('Message With Image')) { diff --git a/test/source/mock/lib/oauth.ts b/test/source/mock/lib/oauth.ts index f74bdeb7b16..5816553b47d 100644 --- a/test/source/mock/lib/oauth.ts +++ b/test/source/mock/lib/oauth.ts @@ -44,7 +44,7 @@ export class OauthMock { return { access_token, refresh_token, expires_in: this.expiresIn, id_token, token_type: 'refresh_token' }; // guessed the token_type }; - public getAccessTokenResponse = (refreshToken: string) => { + public getTokenResponse = (refreshToken: string) => { try { const access_token = this.getAccessToken(refreshToken); const acct = this.acctByAccessToken[access_token]; diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 95acc49bbcc..b42351d5d5e 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -1523,38 +1523,14 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te * 127.0.0.1 standardsubdomainfes.test * 127.0.0.1 fes.standardsubdomainfes.test */ - ava.default('compose - user@standardsubdomainfes.test:8001 - PWD encrypted message with FES web portal', testWithBrowser(undefined, async (t, browser) => { + ava.default.only('user@standardsubdomainfes.test:8001 - PWD encrypted message with FES web portal', testWithBrowser(undefined, async (t, browser) => { const acct = 'user@standardsubdomainfes.test:8001'; // added port to trick extension into calling the mock const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); await SetupPageRecipe.manualEnter(settingsPage, 'flowcrypt.test.key.used.pgp', { submitPubkey: false, usedPgpBefore: false }, { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); const msgPwd = 'super hard password for the message'; - const subject = 'PWD encrypted message with FES - access token'; - const composePage = await ComposePageRecipe.openStandalone(t, browser, 'user@standardsubdomainfes.test:8001'); - await ComposePageRecipe.fillMsg(composePage, { to: 'to@example.com', bcc: 'bcc@example.com' }, subject); - const fileInput = await composePage.target.$('input[type=file]'); - await fileInput!.uploadFile('test/samples/small.txt'); - await ComposePageRecipe.sendAndClose(composePage, { password: msgPwd }); - // this test is using PwdEncryptedMessageWithFesAccessTokenTestStrategy to check sent result based on subject "PWD encrypted message with flowcrypt.com/api" - // also see '/api/v1/message' in fes-endpoints.ts mock - })); - - /** - * You need the following line in /etc/hosts: - * 127.0.0.1 fes.disablefesaccesstoken.test - */ - ava.default('user@disablefesaccesstoken.test:8001 - DISABLE_FES_ACCESS_TOKEN - PWD encrypted message with FES web portal', testWithBrowser(undefined, async (t, browser) => { - const acct = 'user@disablefesaccesstoken.test:8001'; // added port to trick extension into calling the mock - const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); - await SetupPageRecipe.manualEnter(settingsPage, 'flowcrypt.test.key.used.pgp', { submitPubkey: false, usedPgpBefore: false }, - { isSavePassphraseChecked: false, isSavePassphraseHidden: false }); - const debugFrame = await SettingsPageRecipe.awaitNewPageFrame(settingsPage, '@action-show-local-store-contents', ['debug_api.htm']); - await debugFrame.waitForContent('@container-pre', 'fes.disablefesaccesstoken.test:8001'); // FES url on standard subdomain - await debugFrame.waitForContent('@container-pre', 'DISABLE_FES_ACCESS_TOKEN'); // org rules from FES - await SettingsPageRecipe.closeDialog(settingsPage); - const msgPwd = 'super hard password for the message'; const subject = 'PWD encrypted message with FES - ID TOKEN'; - const composePage = await ComposePageRecipe.openStandalone(t, browser, 'user@disablefesaccesstoken.test:8001'); + const composePage = await ComposePageRecipe.openStandalone(t, browser, 'user@standardsubdomainfes.test:8001'); await ComposePageRecipe.fillMsg(composePage, { to: 'to@example.com', bcc: 'bcc@example.com' }, subject); const fileInput = await composePage.target.$('input[type=file]'); await fileInput!.uploadFile('test/samples/small.txt'); From be8734e8670e294eed932efce52755b25d77ec9e Mon Sep 17 00:00:00 2001 From: Tom J Date: Wed, 15 Dec 2021 16:04:07 +0000 Subject: [PATCH 4/5] update fes mock --- .../api/account-servers/enterprise-server.ts | 1 - test/source/mock/fes/fes-endpoints.ts | 36 ++----------------- test/source/tests/compose.ts | 2 +- 3 files changed, 3 insertions(+), 36 deletions(-) diff --git a/extension/js/common/api/account-servers/enterprise-server.ts b/extension/js/common/api/account-servers/enterprise-server.ts index 719435413bd..eef32cbb694 100644 --- a/extension/js/common/api/account-servers/enterprise-server.ts +++ b/extension/js/common/api/account-servers/enterprise-server.ts @@ -22,7 +22,6 @@ import { InMemoryStore } from '../../platform/store/in-memory-store.js'; type EventTag = 'compose' | 'decrypt' | 'setup' | 'settings' | 'import-pub' | 'import-prv'; export namespace FesRes { - export type AccessToken = { accessToken: string }; export type ReplyToken = { replyToken: string }; export type MessageUpload = { url: string }; export type ServiceInfo = { vendor: string, service: string, orgId: string, version: string, apiVersion: string }; diff --git a/test/source/mock/fes/fes-endpoints.ts b/test/source/mock/fes/fes-endpoints.ts index 95dae2f8860..63711d10046 100644 --- a/test/source/mock/fes/fes-endpoints.ts +++ b/test/source/mock/fes/fes-endpoints.ts @@ -7,13 +7,12 @@ import { HttpClientErr } from '../lib/api'; import { MockJwt } from '../lib/oauth'; const standardFesUrl = 'fes.standardsubdomainfes.test:8001'; -const disableAccessTokenFesUrl = 'fes.disablefesaccesstoken.test:8001'; const issuedAccessTokens: string[] = []; export const mockFesEndpoints: HandlersDefinition = { // standard fes location at https://fes.domain.com '/api/': async ({ }, req) => { - if ([standardFesUrl, disableAccessTokenFesUrl].includes(req.headers.host || '') && req.method === 'GET') { + if ([standardFesUrl].includes(req.headers.host || '') && req.method === 'GET') { return { "vendor": "Mock", "service": "enterprise-server", @@ -32,24 +31,8 @@ export const mockFesEndpoints: HandlersDefinition = { // this makes enterprise version tolerate missing FES - explicit 404 throw new HttpClientErr(`Not found`, 404); } - console.log('host', req.headers.host); throw new HttpClientErr(`Not running any FES here: ${req.headers.host}`); }, - '/api/v1/account/access-token': async ({ }, req) => { - if (req.headers.host === standardFesUrl && req.method === 'GET') { - const email = authenticate(req, 'oidc'); // 3rd party token - const fesToken = MockJwt.new(email); // fes-issued token - if (email.includes(disableAccessTokenFesUrl)) { - throw new HttpClientErr('Users on domain disablefesaccesstoken.test must not fetch access token from FES'); - } - issuedAccessTokens.push(fesToken); - return { 'accessToken': fesToken }; - } - if (req.headers.host === disableAccessTokenFesUrl && req.method === 'GET') { - throw new HttpClientErr('Users on domain disablefesaccesstoken.test must not fetch access token from FES'); - } - throw new HttpClientErr('Not Found', 404); - }, '/api/v1/client-configuration': async ({ }, req) => { // individual OrgRules are tested using FlowCrypt backend mock, see BackendData.getOrgRules if (req.method !== 'GET') { @@ -64,10 +47,6 @@ export const mockFesEndpoints: HandlersDefinition = { }, '/api/v1/message/new-reply-token': async ({ }, req) => { if (req.headers.host === standardFesUrl && req.method === 'POST') { - authenticate(req, 'fes'); - return { 'replyToken': 'mock-fes-reply-token' }; - } - if (req.headers.host === disableAccessTokenFesUrl && req.method === 'POST') { authenticate(req, 'oidc'); return { 'replyToken': 'mock-fes-reply-token' }; } @@ -77,7 +56,7 @@ export const mockFesEndpoints: HandlersDefinition = { // body is a mime-multipart string, we're doing a few smoke checks here without parsing it if (req.headers.host === standardFesUrl && req.method === 'POST') { // test: `compose - user@standardsubdomainfes.test:8001 - PWD encrypted message with FES web portal` - authenticate(req, 'fes'); + authenticate(req, 'oidc'); expect(body).to.contain('-----BEGIN PGP MESSAGE-----'); expect(body).to.contain('"associateReplyToken":"mock-fes-reply-token"'); expect(body).to.contain('"to":["to@example.com"]'); @@ -86,17 +65,6 @@ export const mockFesEndpoints: HandlersDefinition = { expect(body).to.contain('"from":"user@standardsubdomainfes.test:8001"'); return { 'url': `http://${standardFesUrl}/message/FES-MOCK-MESSAGE-ID` }; } - if (req.headers.host === disableAccessTokenFesUrl && req.method === 'POST') { - // test: `user@disablefesaccesstoken.test:8001 - DISABLE_FES_ACCESS_TOKEN - PWD encrypted message with FES web portal` - expect(body).to.contain('-----BEGIN PGP MESSAGE-----'); - expect(body).to.contain('"associateReplyToken":"mock-fes-reply-token"'); - expect(body).to.contain('"to":["to@example.com"]'); - expect(body).to.contain('"cc":[]'); - expect(body).to.contain('"bcc":["bcc@example.com"]'); - authenticate(req, 'oidc'); // important - due to DISABLE_FES_ACCESS_TOKEN - expect(body).to.contain('"from":"user@disablefesaccesstoken.test:8001"'); - return { 'url': `http://${disableAccessTokenFesUrl}/message/FES-MOCK-MESSAGE-ID` }; - } throw new HttpClientErr('Not Found', 404); }, }; diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index b42351d5d5e..0be61d1016e 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -1523,7 +1523,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te * 127.0.0.1 standardsubdomainfes.test * 127.0.0.1 fes.standardsubdomainfes.test */ - ava.default.only('user@standardsubdomainfes.test:8001 - PWD encrypted message with FES web portal', testWithBrowser(undefined, async (t, browser) => { + ava.default('user@standardsubdomainfes.test:8001 - PWD encrypted message with FES web portal', testWithBrowser(undefined, async (t, browser) => { const acct = 'user@standardsubdomainfes.test:8001'; // added port to trick extension into calling the mock const settingsPage = await BrowserRecipe.openSettingsLoginApprove(t, browser, acct); await SetupPageRecipe.manualEnter(settingsPage, 'flowcrypt.test.key.used.pgp', { submitPubkey: false, usedPgpBefore: false }, From 94e6979b48491d6660df43abd4bf342e3c311186 Mon Sep 17 00:00:00 2001 From: Tom J Date: Wed, 15 Dec 2021 16:09:26 +0000 Subject: [PATCH 5/5] updaed comment --- extension/js/common/api/account-servers/enterprise-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/js/common/api/account-servers/enterprise-server.ts b/extension/js/common/api/account-servers/enterprise-server.ts index eef32cbb694..e902791a7bc 100644 --- a/extension/js/common/api/account-servers/enterprise-server.ts +++ b/extension/js/common/api/account-servers/enterprise-server.ts @@ -145,7 +145,7 @@ export class EnterpriseServer extends Api { if (idToken) { return { Authorization: `Bearer ${idToken}` }; } - // todo: test this. user should not actually see the message, should be presented with login prompt + // user will not actually see this message, they'll see a generic login prompt throw new BackendAuthErr('Missing id token, please re-authenticate'); };