Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1323,7 +1323,7 @@ function parseBody(req: express.Request, res: express.Response, next: express.Ne
* @param config
*/
function prepareBitGo(config: Config) {
const { env, customRootUri, customBitcoinNetwork } = config;
const { env, customRootUri, customBitcoinNetwork, authVersion } = config;

return function prepBitGo(req: express.Request, res: express.Response, next: express.NextFunction) {
// Get access token
Expand All @@ -1334,6 +1334,31 @@ function prepareBitGo(config: Config) {
accessToken = authSplit[1];
}
}

// Get token ID from custom header (required for v4 auth)
// For v2/v3, this header is ignored if present.
// For v4, if both accessToken and authVersion=4 are present, tokenId must be provided
// or the request will be rejected with a clear error message.
//
// Header name: `Access-Key-Id`
// Expected value: the MongoDB ObjectId (`_id`) of the access token document.
// When making v4-authenticated requests through BitGoExpress, clients MUST set this
// header to the token's `_id` so that BitGo can correctly identify the access token.
const tokenIdHeader = req.headers['access-key-id'];
const tokenId = tokenIdHeader ? (Array.isArray(tokenIdHeader) ? tokenIdHeader[0] : tokenIdHeader) : undefined;

// Enforce v4 auth requirements at the BitGoExpress middleware level to provide
// an immediate, clear error when the Access-Key-Id header (tokenId) is missing.
if (authVersion === 4 && accessToken && !tokenId) {
res.status(400).send({
error:
'BitGoExpress is configured with authVersion=4 and a bearer access token, ' +
'but the Access-Key-Id header is missing. ' +
'For v4 auth, requests must include the Access-Key-Id header identifying the access token.',
});
return;
}

const userAgent = req.headers['user-agent']
? BITGOEXPRESS_USER_AGENT + ' ' + req.headers['user-agent']
: BITGOEXPRESS_USER_AGENT;
Expand All @@ -1343,7 +1368,9 @@ function prepareBitGo(config: Config) {
env,
customRootURI: customRootUri,
customBitcoinNetwork,
authVersion,
accessToken,
...(tokenId ? { tokenId } : {}),
userAgent,
...(useProxyUrl
? {
Expand Down
24 changes: 21 additions & 3 deletions modules/express/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ import 'dotenv/config';

import { args } from './args';

// Normalizes authVersion to valid values (2, 3, or 4).
// Returns undefined when no value is provided, so mergeConfigs can fall through to other sources.
// Invalid values (e.g., 1, 5) are clamped to 2 for safety.
// Note: Previously, invalid values were passed through and handled at runtime (treated as v2).
// This change normalizes them at config parsing time, which is safer and more explicit.
function parseAuthVersion(val: number | undefined): 2 | 3 | 4 | undefined {
if (val === undefined || isNaN(val)) {
return undefined;
}
if (val === 2 || val === 3 || val === 4) {
return val;
}
console.warn(
`warning: invalid authVersion '${val}' provided; defaulting to authVersion 2. ` + `Valid values are 2, 3, or 4.`
);
return 2;
}

function readEnvVar(name, ...deprecatedAliases): string | undefined {
if (process.env[name] !== undefined && process.env[name] !== '') {
return process.env[name];
Expand Down Expand Up @@ -36,7 +54,7 @@ export interface Config {
timeout: number;
customRootUri?: string;
customBitcoinNetwork?: V1Network;
authVersion: number;
authVersion: 2 | 3 | 4;
externalSignerUrl?: string;
signerMode?: boolean;
signerFileSystemPath?: string;
Expand All @@ -62,7 +80,7 @@ export const ArgConfig = (args): Partial<Config> => ({
timeout: args.timeout,
customRootUri: args.customrooturi,
customBitcoinNetwork: args.custombitcoinnetwork,
authVersion: args.authVersion,
authVersion: parseAuthVersion(args.authversion),
externalSignerUrl: args.externalSignerUrl,
signerMode: args.signerMode,
signerFileSystemPath: args.signerFileSystemPath,
Expand All @@ -88,7 +106,7 @@ export const EnvConfig = (): Partial<Config> => ({
timeout: Number(readEnvVar('BITGO_TIMEOUT')),
customRootUri: readEnvVar('BITGO_CUSTOM_ROOT_URI'),
customBitcoinNetwork: readEnvVar('BITGO_CUSTOM_BITCOIN_NETWORK') as V1Network,
authVersion: Number(readEnvVar('BITGO_AUTH_VERSION')),
authVersion: parseAuthVersion(Number(readEnvVar('BITGO_AUTH_VERSION'))),
externalSignerUrl: readEnvVar('BITGO_EXTERNAL_SIGNER_URL'),
signerMode: readEnvVar('BITGO_SIGNER_MODE') ? true : undefined,
signerFileSystemPath: readEnvVar('BITGO_SIGNER_FILE_SYSTEM_PATH'),
Expand Down
83 changes: 83 additions & 0 deletions modules/express/test/unit/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,87 @@ describe('Config:', () => {
should.not.exist(parsed.disableproxy);
argvStub.restore();
});

describe('authVersion parsing', () => {
it('should correctly handle authVersion 2', () => {
const envStub = sinon.stub(process, 'env').value({ BITGO_AUTH_VERSION: '2' });
const { config: proxyConfig } = proxyquire('../../src/config', {
'./args': {
args: () => {
return {};
},
},
});
proxyConfig().authVersion.should.equal(2);
envStub.restore();
});

it('should correctly handle authVersion 3', () => {
const envStub = sinon.stub(process, 'env').value({ BITGO_AUTH_VERSION: '3' });
const { config: proxyConfig } = proxyquire('../../src/config', {
'./args': {
args: () => {
return {};
},
},
});
proxyConfig().authVersion.should.equal(3);
envStub.restore();
});

it('should correctly handle authVersion 4', () => {
const envStub = sinon.stub(process, 'env').value({ BITGO_AUTH_VERSION: '4' });
const { config: proxyConfig } = proxyquire('../../src/config', {
'./args': {
args: () => {
return {};
},
},
});
proxyConfig().authVersion.should.equal(4);
envStub.restore();
});

it('should clamp invalid authVersion values to 2', () => {
const consoleStub = sinon.stub(console, 'warn').returns(undefined);
const envStub = sinon.stub(process, 'env').value({ BITGO_AUTH_VERSION: '5' });
const { config: proxyConfig } = proxyquire('../../src/config', {
'./args': {
args: () => {
return {};
},
},
});
proxyConfig().authVersion.should.equal(2);
consoleStub.calledOnce.should.be.true();
consoleStub.restore();
envStub.restore();
});

it('should handle authVersion precedence: args override env', () => {
const envStub = sinon.stub(process, 'env').value({ BITGO_AUTH_VERSION: '3' });
const { config: proxyConfig } = proxyquire('../../src/config', {
'./args': {
args: () => {
return { authversion: 4 };
},
},
});
proxyConfig().authVersion.should.equal(4);
envStub.restore();
});

it('should default to authVersion 2 when not provided', () => {
const envStub = sinon.stub(process, 'env').value({});
const { config: proxyConfig } = proxyquire('../../src/config', {
'./args': {
args: () => {
return {};
},
},
});
proxyConfig().authVersion.should.equal(2);
envStub.restore();
});
});
});
Loading