Skip to content

Commit 63dfc7b

Browse files
feat(sdk-api): add verification flow for v4
TICKET: CAAS-819
1 parent 81d8ac4 commit 63dfc7b

File tree

9 files changed

+1516
-62
lines changed

9 files changed

+1516
-62
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1323,7 +1323,7 @@ function parseBody(req: express.Request, res: express.Response, next: express.Ne
13231323
* @param config
13241324
*/
13251325
function prepareBitGo(config: Config) {
1326-
const { env, customRootUri, customBitcoinNetwork } = config;
1326+
const { env, customRootUri, customBitcoinNetwork, authVersion } = config;
13271327

13281328
return function prepBitGo(req: express.Request, res: express.Response, next: express.NextFunction) {
13291329
// Get access token
@@ -1334,6 +1334,31 @@ function prepareBitGo(config: Config) {
13341334
accessToken = authSplit[1];
13351335
}
13361336
}
1337+
1338+
// Get token ID from custom header (required for v4 auth)
1339+
// For v2/v3, this header is ignored if present.
1340+
// For v4, if both accessToken and authVersion=4 are present, tokenId must be provided
1341+
// or the request will be rejected with a clear error message.
1342+
//
1343+
// Header name: `Access-Key-Id`
1344+
// Expected value: the MongoDB ObjectId (`_id`) of the access token document.
1345+
// When making v4-authenticated requests through BitGoExpress, clients MUST set this
1346+
// header to the token's `_id` so that BitGo can correctly identify the access token.
1347+
const tokenIdHeader = req.headers['access-key-id'];
1348+
const tokenId = tokenIdHeader ? (Array.isArray(tokenIdHeader) ? tokenIdHeader[0] : tokenIdHeader) : undefined;
1349+
1350+
// Enforce v4 auth requirements at the BitGoExpress middleware level to provide
1351+
// an immediate, clear error when the Access-Key-Id header (tokenId) is missing.
1352+
if (authVersion === 4 && accessToken && !tokenId) {
1353+
res.status(400).send({
1354+
error:
1355+
'BitGoExpress is configured with authVersion=4 and a bearer access token, ' +
1356+
'but the Access-Key-Id header is missing. ' +
1357+
'For v4 auth, requests must include the Access-Key-Id header identifying the access token.',
1358+
});
1359+
return;
1360+
}
1361+
13371362
const userAgent = req.headers['user-agent']
13381363
? BITGOEXPRESS_USER_AGENT + ' ' + req.headers['user-agent']
13391364
: BITGOEXPRESS_USER_AGENT;
@@ -1343,7 +1368,9 @@ function prepareBitGo(config: Config) {
13431368
env,
13441369
customRootURI: customRootUri,
13451370
customBitcoinNetwork,
1371+
authVersion,
13461372
accessToken,
1373+
...(tokenId ? { tokenId } : {}),
13471374
userAgent,
13481375
...(useProxyUrl
13491376
? {

modules/express/src/config.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ import 'dotenv/config';
44

55
import { args } from './args';
66

7+
// Normalizes authVersion to valid values (2, 3, or 4).
8+
// Returns undefined when no value is provided, so mergeConfigs can fall through to other sources.
9+
// Invalid values (e.g., 1, 5) are clamped to 2 for safety.
10+
// Note: Previously, invalid values were passed through and handled at runtime (treated as v2).
11+
// This change normalizes them at config parsing time, which is safer and more explicit.
12+
function parseAuthVersion(val: number | undefined): 2 | 3 | 4 | undefined {
13+
if (val === undefined || isNaN(val)) {
14+
return undefined;
15+
}
16+
if (val === 2 || val === 3 || val === 4) {
17+
return val;
18+
}
19+
console.warn(
20+
`warning: invalid authVersion '${val}' provided; defaulting to authVersion 2. ` + `Valid values are 2, 3, or 4.`
21+
);
22+
return 2;
23+
}
24+
725
function readEnvVar(name, ...deprecatedAliases): string | undefined {
826
if (process.env[name] !== undefined && process.env[name] !== '') {
927
return process.env[name];
@@ -36,7 +54,7 @@ export interface Config {
3654
timeout: number;
3755
customRootUri?: string;
3856
customBitcoinNetwork?: V1Network;
39-
authVersion: number;
57+
authVersion: 2 | 3 | 4;
4058
externalSignerUrl?: string;
4159
signerMode?: boolean;
4260
signerFileSystemPath?: string;
@@ -62,7 +80,7 @@ export const ArgConfig = (args): Partial<Config> => ({
6280
timeout: args.timeout,
6381
customRootUri: args.customrooturi,
6482
customBitcoinNetwork: args.custombitcoinnetwork,
65-
authVersion: args.authVersion,
83+
authVersion: parseAuthVersion(args.authversion),
6684
externalSignerUrl: args.externalSignerUrl,
6785
signerMode: args.signerMode,
6886
signerFileSystemPath: args.signerFileSystemPath,
@@ -88,7 +106,7 @@ export const EnvConfig = (): Partial<Config> => ({
88106
timeout: Number(readEnvVar('BITGO_TIMEOUT')),
89107
customRootUri: readEnvVar('BITGO_CUSTOM_ROOT_URI'),
90108
customBitcoinNetwork: readEnvVar('BITGO_CUSTOM_BITCOIN_NETWORK') as V1Network,
91-
authVersion: Number(readEnvVar('BITGO_AUTH_VERSION')),
109+
authVersion: parseAuthVersion(Number(readEnvVar('BITGO_AUTH_VERSION'))),
92110
externalSignerUrl: readEnvVar('BITGO_EXTERNAL_SIGNER_URL'),
93111
signerMode: readEnvVar('BITGO_SIGNER_MODE') ? true : undefined,
94112
signerFileSystemPath: readEnvVar('BITGO_SIGNER_FILE_SYSTEM_PATH'),

modules/express/test/unit/config.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,87 @@ describe('Config:', () => {
280280
should.not.exist(parsed.disableproxy);
281281
argvStub.restore();
282282
});
283+
284+
describe('authVersion parsing', () => {
285+
it('should correctly handle authVersion 2', () => {
286+
const envStub = sinon.stub(process, 'env').value({ BITGO_AUTH_VERSION: '2' });
287+
const { config: proxyConfig } = proxyquire('../../src/config', {
288+
'./args': {
289+
args: () => {
290+
return {};
291+
},
292+
},
293+
});
294+
proxyConfig().authVersion.should.equal(2);
295+
envStub.restore();
296+
});
297+
298+
it('should correctly handle authVersion 3', () => {
299+
const envStub = sinon.stub(process, 'env').value({ BITGO_AUTH_VERSION: '3' });
300+
const { config: proxyConfig } = proxyquire('../../src/config', {
301+
'./args': {
302+
args: () => {
303+
return {};
304+
},
305+
},
306+
});
307+
proxyConfig().authVersion.should.equal(3);
308+
envStub.restore();
309+
});
310+
311+
it('should correctly handle authVersion 4', () => {
312+
const envStub = sinon.stub(process, 'env').value({ BITGO_AUTH_VERSION: '4' });
313+
const { config: proxyConfig } = proxyquire('../../src/config', {
314+
'./args': {
315+
args: () => {
316+
return {};
317+
},
318+
},
319+
});
320+
proxyConfig().authVersion.should.equal(4);
321+
envStub.restore();
322+
});
323+
324+
it('should clamp invalid authVersion values to 2', () => {
325+
const consoleStub = sinon.stub(console, 'warn').returns(undefined);
326+
const envStub = sinon.stub(process, 'env').value({ BITGO_AUTH_VERSION: '5' });
327+
const { config: proxyConfig } = proxyquire('../../src/config', {
328+
'./args': {
329+
args: () => {
330+
return {};
331+
},
332+
},
333+
});
334+
proxyConfig().authVersion.should.equal(2);
335+
consoleStub.calledOnce.should.be.true();
336+
consoleStub.restore();
337+
envStub.restore();
338+
});
339+
340+
it('should handle authVersion precedence: args override env', () => {
341+
const envStub = sinon.stub(process, 'env').value({ BITGO_AUTH_VERSION: '3' });
342+
const { config: proxyConfig } = proxyquire('../../src/config', {
343+
'./args': {
344+
args: () => {
345+
return { authversion: 4 };
346+
},
347+
},
348+
});
349+
proxyConfig().authVersion.should.equal(4);
350+
envStub.restore();
351+
});
352+
353+
it('should default to authVersion 2 when not provided', () => {
354+
const envStub = sinon.stub(process, 'env').value({});
355+
const { config: proxyConfig } = proxyquire('../../src/config', {
356+
'./args': {
357+
args: () => {
358+
return {};
359+
},
360+
},
361+
});
362+
proxyConfig().authVersion.should.equal(2);
363+
envStub.restore();
364+
});
365+
});
283366
});

0 commit comments

Comments
 (0)