From 7b1ab9853a01df08c4974ce02593b1b3a185d84c Mon Sep 17 00:00:00 2001 From: Sandra Wang Date: Mon, 13 Apr 2026 15:58:46 +0000 Subject: [PATCH] feat(express): forward X-BitGo-OTP header to downstream BitGo API The redirectRequest function was not forwarding any client headers to the BitGo API, which meant the X-BitGo-OTP header (used for 2FA) was being silently dropped. Add a FORWARDED_HEADERS whitelist and forward matching headers from incoming requests. Ticket: CS-7494 --- modules/express/src/clientRoutes.ts | 13 ++++++++ .../express/test/unit/clientRoutes/index.ts | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index ff0c454456..56e7e415f8 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -69,6 +69,12 @@ const debug = debugLib('bitgo:express'); const BITGOEXPRESS_USER_AGENT = `BitGoExpress/${pjson.version} BitGoJS/${version}`; +/** + * Headers from the incoming request that should be forwarded to the BitGo API. + * Header names must be lowercase (Express normalizes incoming headers to lowercase). + */ +const FORWARDED_HEADERS = ['x-bitgo-otp']; + function handlePing( req: ExpressApiRouteRequest<'express.ping', 'get'>, res: express.Response, @@ -1340,6 +1346,13 @@ export function redirectRequest( request.set('enterprise-id', req.params.enterpriseId); } + for (const header of FORWARDED_HEADERS) { + const value = req.headers[header]; + if (value) { + request.set(header, Array.isArray(value) ? value[0] : value); + } + } + return request.result().then((result) => { const status = request.res?.statusCode || 200; return { status, body: result }; diff --git a/modules/express/test/unit/clientRoutes/index.ts b/modules/express/test/unit/clientRoutes/index.ts index 5c01d135c1..c035cc3590 100644 --- a/modules/express/test/unit/clientRoutes/index.ts +++ b/modules/express/test/unit/clientRoutes/index.ts @@ -16,6 +16,7 @@ describe('common methods', () => { req = { body: {}, params: {}, + headers: {}, bitgo, } as express.Request; next = () => undefined; @@ -52,6 +53,36 @@ describe('common methods', () => { result.body.should.deepEqual({ success: true }); }); + it('should forward X-BitGo-OTP header when present', async () => { + const url = 'https://example.com/api'; + const setStub = sandbox.stub(); + const response = { res: { statusCode: 200 }, result: async () => ({ success: true }), set: setStub }; + sandbox + .stub(bitgo, 'get') + .withArgs(url) + .returns(response as any); + + req.headers = { 'x-bitgo-otp': '123456' } as any; + const result = await redirectRequest(bitgo, 'GET', url, req, next); + result.status.should.equal(200); + setStub.calledWith('x-bitgo-otp', '123456').should.be.true(); + }); + + it('should not forward X-BitGo-OTP header when not present', async () => { + const url = 'https://example.com/api'; + const setStub = sandbox.stub(); + const response = { res: { statusCode: 200 }, result: async () => ({ success: true }), set: setStub }; + sandbox + .stub(bitgo, 'get') + .withArgs(url) + .returns(response as any); + + req.headers = {} as any; + const result = await redirectRequest(bitgo, 'GET', url, req, next); + result.status.should.equal(200); + setStub.calledWith('x-bitgo-otp').should.be.false(); + }); + it('should handle error response and return status and body', async () => { const url = 'https://example.com/api'; const response = {