From f0655e0b63726268811956b33dff6e27a81f56fa Mon Sep 17 00:00:00 2001 From: Sergey Petushkov Date: Fri, 8 Nov 2024 14:15:12 +0100 Subject: [PATCH] fix(atlas-service): pass csrf tokens with the requests COMPASS-8490 (#6465) fix(atlas-service): pass csrf tokens with the requests --- .../atlas-service/src/atlas-service.spec.ts | 26 ++++++++++++++++++ packages/atlas-service/src/atlas-service.ts | 18 +++++++++++++ packages/compass-web/sandbox/index.tsx | 22 +++++++++++++-- .../sandbox/sandbox-atlas-sign-in.tsx | 27 ++++++++++++++----- .../compass-web/scripts/electron-proxy.js | 3 --- 5 files changed, 84 insertions(+), 12 deletions(-) diff --git a/packages/atlas-service/src/atlas-service.spec.ts b/packages/atlas-service/src/atlas-service.spec.ts index 956ee370462..b63d7a9a1d6 100644 --- a/packages/atlas-service/src/atlas-service.spec.ts +++ b/packages/atlas-service/src/atlas-service.spec.ts @@ -132,4 +132,30 @@ describe('AtlasService', function () { ); expect(getAuthHeadersFn.calledOnce).to.be.true; }); + + it('should set CSRF headers when available', async function () { + const fetchStub = sandbox.stub().resolves({ status: 200, ok: true }); + global.fetch = fetchStub; + document.head.append( + (() => { + const el = document.createElement('meta'); + el.setAttribute('name', 'csrf-token'); + el.setAttribute('content', 'token'); + return el; + })() + ); + document.head.append( + (() => { + const el = document.createElement('meta'); + el.setAttribute('name', 'CSRF-TIME'); + el.setAttribute('content', 'time'); + return el; + })() + ); + await atlasService.fetch('/foo/bar', { method: 'POST' }); + expect(fetchStub.firstCall.lastArg.headers).to.deep.eq({ + 'X-CSRF-Time': 'time', + 'X-CSRF-Token': 'token', + }); + }); }); diff --git a/packages/atlas-service/src/atlas-service.ts b/packages/atlas-service/src/atlas-service.ts index 528e4ef6de5..bdd401f7fdb 100644 --- a/packages/atlas-service/src/atlas-service.ts +++ b/packages/atlas-service/src/atlas-service.ts @@ -19,6 +19,23 @@ function normalizePath(path?: string) { return encodeURI(path); } +function getCSRFHeaders() { + return { + 'X-CSRF-Token': + document + .querySelector('meta[name="csrf-token" i]') + ?.getAttribute('content') ?? '', + 'X-CSRF-Time': + document + .querySelector('meta[name="csrf-time" i]') + ?.getAttribute('content') ?? '', + }; +} + +function shouldAddCSRFHeaders(method = 'get') { + return !/^(get|head|options|trace)$/.test(method.toLowerCase()); +} + export class AtlasService { private config: AtlasServiceConfig; constructor( @@ -60,6 +77,7 @@ export class AtlasService { ...init, headers: { ...this.options?.defaultHeaders, + ...(shouldAddCSRFHeaders(init?.method) && getCSRFHeaders()), ...init?.headers, }, }); diff --git a/packages/compass-web/sandbox/index.tsx b/packages/compass-web/sandbox/index.tsx index c539e9ad9f8..a481158c4dc 100644 --- a/packages/compass-web/sandbox/index.tsx +++ b/packages/compass-web/sandbox/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useLayoutEffect } from 'react'; import ReactDOM from 'react-dom'; import { resetGlobalCSS, css, Body } from '@mongodb-js/compass-components'; import { CompassWeb } from '../src/index'; @@ -16,9 +16,22 @@ const sandboxContainerStyles = css({ resetGlobalCSS(); +function getMetaEl(name: string) { + return ( + document.querySelector(`meta[name="${name}" i]`) ?? + (() => { + const el = document.createElement('meta'); + el.setAttribute('name', name); + document.head.prepend(el); + return el; + })() + ); +} + const App = () => { const [currentTab, updateCurrentTab] = useWorkspaceTabRouter(); - const { status, projectId } = useAtlasProxySignIn(); + const { status, projectParams } = useAtlasProxySignIn(); + const { projectId, csrfToken, csrfTime } = projectParams ?? {}; const atlasServiceSandboxBackendVariant = process.env.COMPASS_WEB_HTTP_PROXY_CLOUD_CONFIG === 'local' @@ -28,6 +41,11 @@ const App = () => { ? 'web-sandbox-atlas-dev' : 'web-sandbox-atlas'; + useLayoutEffect(() => { + getMetaEl('csrf-token').setAttribute('content', csrfToken ?? ''); + getMetaEl('csrf-time').setAttribute('content', csrfTime ?? ''); + }, [csrfToken, csrfTime]); + if (status === 'checking') { return null; } diff --git a/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx b/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx index 00b04fb64fb..f75dce93c3d 100644 --- a/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx +++ b/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx @@ -12,12 +12,18 @@ console.info( type SignInStatus = 'checking' | 'signed-in' | 'signed-out'; +type ProjectParams = { + projectId: string; + csrfToken: string; + csrfTime: string; +}; + type AtlasLoginReturnValue = | { status: 'checking' | 'signed-out'; - projectId: null; + projectParams: null; } - | { status: 'signed-in'; projectId: string }; + | { status: 'signed-in'; projectParams: ProjectParams }; const bodyContainerStyles = css({ display: 'flex', @@ -64,7 +70,9 @@ const IS_CI = export function useAtlasProxySignIn(): AtlasLoginReturnValue { const [status, setStatus] = useState('checking'); - const [projectId, setProjectId] = useState(null); + const [projectParams, setProjectParams] = useState( + null + ); const signIn = ((window as any).__signIn = useCallback(async () => { try { @@ -104,7 +112,12 @@ export function useAtlasProxySignIn(): AtlasLoginReturnValue { if (!projectId) { throw new Error('failed to get projectId'); } - setProjectId(projectId); + const { csrfToken, csrfTime } = await fetch( + `/cloud-mongodb-com/v2/${projectId}/params` + ).then((res) => { + return res.json(); + }); + setProjectParams({ projectId, csrfToken, csrfTime }); setStatus('signed-in'); if (IS_CI) { return; @@ -151,12 +164,12 @@ export function useAtlasProxySignIn(): AtlasLoginReturnValue { if (status === 'checking' || status === 'signed-out') { return { status, - projectId: null, + projectParams: null, }; } - if (status === 'signed-in' && projectId) { - return { status, projectId }; + if (status === 'signed-in' && projectParams) { + return { status, projectParams }; } throw new Error('Weird state, ask for help in Compass dev channel'); diff --git a/packages/compass-web/scripts/electron-proxy.js b/packages/compass-web/scripts/electron-proxy.js index df51a2d87bb..4b670414527 100644 --- a/packages/compass-web/scripts/electron-proxy.js +++ b/packages/compass-web/scripts/electron-proxy.js @@ -168,14 +168,11 @@ class AtlasCloudAuthenticator { } async getCloudHeaders() { - // Order is important, fetching data can update the cookies - const csrfHeaders = await this.#getCSRFHeaders(); const cookie = (await this.#getCloudSessionCookies()).join('; '); return { cookie, host: CLOUD_HOST, origin: CLOUD_ORIGIN, - ...csrfHeaders, }; }