From 3f5a09c9600cb176868c43529f6ef0c36abedc1f Mon Sep 17 00:00:00 2001 From: arvind-bg Date: Fri, 13 Feb 2026 17:28:43 +0530 Subject: [PATCH] feat(sdk-api): implement V4 token issuance flow Ticket: CAAS-783 --- modules/sdk-api/src/bitgoAPI.ts | 18 + modules/sdk-api/src/types.ts | 5 +- modules/sdk-api/test/unit/bitgoAPI.ts | 582 ++++++++++++++++++++++++++ modules/sdk-hmac/src/hmac.ts | 2 +- modules/sdk-hmac/src/types.ts | 2 +- 5 files changed, 606 insertions(+), 3 deletions(-) diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index f1e126b71a..9f91c06f8b 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -123,6 +123,7 @@ export class BitGoAPI implements BitGoBase { protected _extensionKey?: ECPairInterface; protected _reqId?: IRequestTracer; protected _token?: string; + protected _tokenId?: string; // V4: separate token identifier protected _version = pjson.version; protected _userAgent?: string; protected _ecdhXprv?: string; @@ -735,6 +736,7 @@ export class BitGoAPI implements BitGoBase { return { user: this._user, token: this._token, + tokenId: this._tokenId, extensionKey: this._extensionKey ? this._extensionKey.toWIF() : undefined, ecdhXprv: this._ecdhXprv, }; @@ -758,6 +760,7 @@ export class BitGoAPI implements BitGoBase { fromJSON(json: BitGoJson): void { this._user = json.user; this._token = json.token; + this._tokenId = json.tokenId; this._ecdhXprv = json.ecdhXprv; if (json.extensionKey) { const network = common.Environments[this.getEnv()].network; @@ -980,6 +983,11 @@ export class BitGoAPI implements BitGoBase { this._token = responseDetails.token; this._ecdhXprv = responseDetails.ecdhXprv; + // V4: store separate token identifier + if (this._authVersion === 4 && body.id) { + this._tokenId = body.id; + } + // verify the response's authenticity verifyResponse(this, responseDetails.token, 'post', request, response, this._authVersion); @@ -1131,6 +1139,7 @@ export class BitGoAPI implements BitGoBase { // TODO: are there any other fields which should be cleared? this._user = undefined; this._token = undefined; + this._tokenId = undefined; this._refreshToken = undefined; this._ecdhXprv = undefined; } @@ -1271,9 +1280,18 @@ export class BitGoAPI implements BitGoBase { // verify the authenticity of the server's response before proceeding any further verifyResponse(this, this._token, 'post', request, response, this._authVersion); + // Decrypt token using ECDH (same for V2/V3/V4) const responseDetails = this.handleTokenIssuance(response.body); response.body.token = responseDetails.token; + // V4: Store separate tokenId alongside signing key + if (this._authVersion === 4) { + if (!response.body.id) { + throw new Error('Invalid V4 token issuance response: missing id field'); + } + response.body.tokenId = response.body.id; + } + return handleResponseResult()(response); } catch (e) { handleResponseError(e); diff --git a/modules/sdk-api/src/types.ts b/modules/sdk-api/src/types.ts index b3d878e7be..92ee2b45a5 100644 --- a/modules/sdk-api/src/types.ts +++ b/modules/sdk-api/src/types.ts @@ -24,7 +24,7 @@ export { } from '@bitgo/sdk-hmac'; export interface BitGoAPIOptions { accessToken?: string; - authVersion?: 2 | 3; + authVersion?: 2 | 3 | 4; clientConstants?: | Record | { @@ -137,6 +137,7 @@ export interface User { export interface BitGoJson { user?: User; token?: string; + tokenId?: string; // V4: separate token identifier extensionKey?: string; ecdhXprv?: string; } @@ -149,6 +150,7 @@ export interface TokenIssuanceResponse { derivationPath: string; encryptedToken: string; encryptedECDHXprv?: string; + id?: string; // V4: token identifier } export interface TokenIssuance { @@ -189,6 +191,7 @@ export interface AddAccessTokenResponse { encryptedToken: string; derivationPath: string; token: string; + tokenId?: string; // V4: separate token identifier enterprise?: string; extensionAddress?: string; } diff --git a/modules/sdk-api/test/unit/bitgoAPI.ts b/modules/sdk-api/test/unit/bitgoAPI.ts index 626f79fdde..5a91c8f267 100644 --- a/modules/sdk-api/test/unit/bitgoAPI.ts +++ b/modules/sdk-api/test/unit/bitgoAPI.ts @@ -579,3 +579,585 @@ describe('Constructor', function () { }); }); }); + +describe('V4 Token Issuance', function () { + it('should allow V4 authentication to be configured', function () { + (() => { + new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + }).should.not.throw(); + }); + + it('should validate V4 response structure in addAccessToken', async function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + // Set ecdhXprv so we get past that check + (bitgo as any)._ecdhXprv = + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k'; + + // Mock a V4 response with encryptedToken and derivationPath but missing V4-specific 'id' field + const mockResponse = { + body: { + encryptedToken: 'encrypted', + derivationPath: 'm/999999/0/0', + // Missing V4-specific 'id' field + label: 'test-token', + }, + }; + + // Stub handleTokenIssuance to return a token (simulating successful ECDH decryption) + const handleTokenIssuanceStub = sinon.stub(bitgo, 'handleTokenIssuance').returns({ token: 'decrypted_token' }); + + // Stub the request + const postStub = sinon.stub(bitgo, 'post').returns({ + forceV1Auth: false, + send: sinon.stub().resolves(mockResponse), + } as any); + + // Stub verifyResponse to pass + const verifyResponseStub = sinon.stub().returns(undefined); + const verifyStub = sinon.stub(require('../../src/api'), 'verifyResponse').callsFake(verifyResponseStub); + + try { + await bitgo.addAccessToken({ + label: 'test', + scope: ['wallet:read'], + }); + throw new Error('Should have thrown validation error'); + } catch (e) { + e.message.should.match(/Invalid V4 token issuance response/); + } + + handleTokenIssuanceStub.restore(); + postStub.restore(); + verifyStub.restore(); + }); +}); + +describe('V4 authenticate() flow', function () { + afterEach(function () { + sinon.restore(); + }); + + it('should store tokenId when V4 login succeeds', async function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + const mockResponseBody = { + user: { username: 'testuser@example.com', id: 'user123' }, + id: 'v4_token_id_12345', + encryptedToken: + '{"iv":"test","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"test","ct":"test"}', + derivationPath: 'm/999999/54719676/90455048', + encryptedECDHXprv: + '{"iv":"test","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"test","ct":"test"}', + }; + + const mockResponse = { + status: 200, + body: mockResponseBody, + header: {}, + }; + + // Stub the post method + const postStub = sinon.stub(bitgo, 'post').returns({ + send: sinon.stub().resolves(mockResponse), + } as any); + + // Stub handleTokenIssuance to return a token + const handleTokenIssuanceStub = sinon.stub(bitgo, 'handleTokenIssuance').returns({ + token: 'decrypted_signing_key_12345', + ecdhXprv: + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k', + }); + + // Stub verifyResponse + const verifyResponseStub = sinon.stub().returns(undefined); + sinon.stub(require('../../src/api'), 'verifyResponse').callsFake(verifyResponseStub); + + const result = await bitgo.authenticate({ + username: 'testuser@example.com', + password: 'testpassword', + }); + + // Verify tokenId is stored + (bitgo as any)._tokenId.should.equal('v4_token_id_12345'); + (bitgo as any)._token.should.equal('decrypted_signing_key_12345'); + + // Verify response includes the token + result.should.have.property('access_token', 'decrypted_signing_key_12345'); + + postStub.restore(); + handleTokenIssuanceStub.restore(); + }); + + it('should not store tokenId for V2 login', async function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 2, + }); + + const mockResponseBody = { + user: { username: 'testuser@example.com', id: 'user123' }, + // V2 response does not include 'id' field + encryptedToken: + '{"iv":"test","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"test","ct":"test"}', + derivationPath: 'm/999999/54719676/90455048', + encryptedECDHXprv: + '{"iv":"test","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"test","ct":"test"}', + }; + + const mockResponse = { + status: 200, + body: mockResponseBody, + header: {}, + }; + + const postStub = sinon.stub(bitgo, 'post').returns({ + send: sinon.stub().resolves(mockResponse), + } as any); + + const handleTokenIssuanceStub = sinon.stub(bitgo, 'handleTokenIssuance').returns({ + token: 'v2_token_hash_12345', + ecdhXprv: + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k', + }); + + const verifyResponseStub = sinon.stub().returns(undefined); + sinon.stub(require('../../src/api'), 'verifyResponse').callsFake(verifyResponseStub); + + await bitgo.authenticate({ + username: 'testuser@example.com', + password: 'testpassword', + }); + + // Verify tokenId is NOT set for V2 + const tokenId = (bitgo as any)._tokenId; + (tokenId === undefined).should.be.true(); + + // Verify token is still set + (bitgo as any)._token.should.equal('v2_token_hash_12345'); + + postStub.restore(); + handleTokenIssuanceStub.restore(); + }); + + it('should not store tokenId for V3 login', async function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 3, + }); + + const mockResponseBody = { + user: { username: 'testuser@example.com', id: 'user123' }, + // V3 response does not include 'id' field + encryptedToken: + '{"iv":"test","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"test","ct":"test"}', + derivationPath: 'm/999999/54719676/90455048', + encryptedECDHXprv: + '{"iv":"test","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"test","ct":"test"}', + }; + + const mockResponse = { + status: 200, + body: mockResponseBody, + header: {}, + }; + + const postStub = sinon.stub(bitgo, 'post').returns({ + send: sinon.stub().resolves(mockResponse), + } as any); + + const handleTokenIssuanceStub = sinon.stub(bitgo, 'handleTokenIssuance').returns({ + token: 'v3_token_hash_12345', + ecdhXprv: + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k', + }); + + const verifyResponseStub = sinon.stub().returns(undefined); + sinon.stub(require('../../src/api'), 'verifyResponse').callsFake(verifyResponseStub); + + await bitgo.authenticate({ + username: 'testuser@example.com', + password: 'testpassword', + }); + + // Verify tokenId is NOT set for V3 + const tokenId = (bitgo as any)._tokenId; + (tokenId === undefined).should.be.true(); + + // Verify token is still set + (bitgo as any)._token.should.equal('v3_token_hash_12345'); + + postStub.restore(); + handleTokenIssuanceStub.restore(); + }); +}); + +describe('V4 serialization', function () { + it('should serialize tokenId in toJSON()', function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + // Manually set V4 session state + (bitgo as any)._user = { username: 'testuser@example.com', id: 'user123' }; + (bitgo as any)._token = 'signing_key_12345'; + (bitgo as any)._tokenId = 'v4_token_id_12345'; + (bitgo as any)._ecdhXprv = + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k'; + + const json = bitgo.toJSON(); + + // Verify tokenId is included in serialization + json.should.have.property('tokenId', 'v4_token_id_12345'); + json.should.have.property('token', 'signing_key_12345'); + json.should.have.property('user'); + json.should.have.property('ecdhXprv'); + }); + + it('should deserialize tokenId in fromJSON()', function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + const sessionData = { + user: { username: 'testuser@example.com', id: 'user123' }, + token: 'signing_key_12345', + tokenId: 'v4_token_id_12345', + ecdhXprv: + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k', + }; + + bitgo.fromJSON(sessionData); + + // Verify tokenId is restored + (bitgo as any)._tokenId.should.equal('v4_token_id_12345'); + (bitgo as any)._token.should.equal('signing_key_12345'); + (bitgo as any)._user.should.deepEqual({ username: 'testuser@example.com', id: 'user123' }); + (bitgo as any)._ecdhXprv.should.equal( + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k' + ); + }); + + it('should not serialize tokenId for V2/V3', function () { + const bitgoV2 = new BitGoAPI({ + env: 'test', + authVersion: 2, + }); + + // Set V2 session state (no tokenId) + (bitgoV2 as any)._user = { username: 'testuser@example.com', id: 'user123' }; + (bitgoV2 as any)._token = 'v2_token_hash_12345'; + (bitgoV2 as any)._ecdhXprv = + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k'; + + const json = bitgoV2.toJSON(); + + // Verify tokenId is NOT included for V2 + json.should.have.property('token', 'v2_token_hash_12345'); + json.should.have.property('user'); + json.should.have.property('ecdhXprv'); + + // tokenId may be present but should be undefined + const hasTokenId = 'tokenId' in json; + if (hasTokenId) { + (json.tokenId === undefined).should.be.true(); + } + }); + + it('should handle round-trip serialization for V4', function () { + const bitgo1 = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + // Set up V4 session + (bitgo1 as any)._user = { username: 'testuser@example.com', id: 'user123' }; + (bitgo1 as any)._token = 'signing_key_12345'; + (bitgo1 as any)._tokenId = 'v4_token_id_12345'; + (bitgo1 as any)._ecdhXprv = + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k'; + + // Serialize + const json = bitgo1.toJSON(); + + // Create new instance and deserialize + const bitgo2 = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + bitgo2.fromJSON(json); + + // Verify all state is preserved + (bitgo2 as any)._tokenId.should.equal((bitgo1 as any)._tokenId); + (bitgo2 as any)._token.should.equal((bitgo1 as any)._token); + (bitgo2 as any)._user.should.deepEqual((bitgo1 as any)._user); + (bitgo2 as any)._ecdhXprv.should.equal((bitgo1 as any)._ecdhXprv); + }); +}); + +describe('V4 clear() cleanup', function () { + it('should clear tokenId when clear() is called', function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + // Set up V4 session + (bitgo as any)._user = { username: 'testuser@example.com', id: 'user123' }; + (bitgo as any)._token = 'signing_key_12345'; + (bitgo as any)._tokenId = 'v4_token_id_12345'; + (bitgo as any)._ecdhXprv = + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k'; + + // Verify state is set + (bitgo as any)._tokenId.should.equal('v4_token_id_12345'); + (bitgo as any)._token.should.equal('signing_key_12345'); + + // Call clear + bitgo.clear(); + + // Verify all session state is cleared + const tokenId = (bitgo as any)._tokenId; + const token = (bitgo as any)._token; + const user = (bitgo as any)._user; + const ecdhXprv = (bitgo as any)._ecdhXprv; + + (tokenId === undefined).should.be.true(); + (token === undefined).should.be.true(); + (user === undefined).should.be.true(); + (ecdhXprv === undefined).should.be.true(); + }); + + it('should clear tokenId in logout()', async function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + // Set up V4 session + (bitgo as any)._user = { username: 'testuser@example.com', id: 'user123' }; + (bitgo as any)._token = 'signing_key_12345'; + (bitgo as any)._tokenId = 'v4_token_id_12345'; + + // Stub the get method for logout + const getStub = sinon.stub(bitgo, 'get').returns({ + result: sinon.stub().resolves({ success: true }), + } as any); + + await bitgo.logout(); + + // Verify all session state is cleared + const tokenId = (bitgo as any)._tokenId; + const token = (bitgo as any)._token; + const user = (bitgo as any)._user; + + (tokenId === undefined).should.be.true(); + (token === undefined).should.be.true(); + (user === undefined).should.be.true(); + + getStub.restore(); + }); + + it('should not affect V2/V3 clear() behavior', function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 2, + }); + + // Set up V2 session (no tokenId) + (bitgo as any)._user = { username: 'testuser@example.com', id: 'user123' }; + (bitgo as any)._token = 'v2_token_hash_12345'; + (bitgo as any)._ecdhXprv = + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k'; + + // Call clear + bitgo.clear(); + + // Verify session state is cleared + const token = (bitgo as any)._token; + const user = (bitgo as any)._user; + const ecdhXprv = (bitgo as any)._ecdhXprv; + + (token === undefined).should.be.true(); + (user === undefined).should.be.true(); + (ecdhXprv === undefined).should.be.true(); + }); +}); + +describe('V4 HMAC calculation', function () { + it('should use signing key for V4 HMAC calculation', function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + const signingKey = 'signing_key_for_hmac_12345'; + const message = 'test_message_to_hmac'; + + const hmac = bitgo.calculateHMAC(signingKey, message); + + // Verify HMAC is calculated (should be a hex string) + hmac.should.be.a.String(); + hmac.length.should.be.greaterThan(0); + }); + + it('should produce consistent HMAC values for same inputs', function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + const signingKey = 'signing_key_consistent'; + const message = 'consistent_message'; + + const hmac1 = bitgo.calculateHMAC(signingKey, message); + const hmac2 = bitgo.calculateHMAC(signingKey, message); + + hmac1.should.equal(hmac2); + }); + + it('should produce different HMAC values for different signing keys', function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + const signingKey1 = 'signing_key_1'; + const signingKey2 = 'signing_key_2'; + const message = 'same_message'; + + const hmac1 = bitgo.calculateHMAC(signingKey1, message); + const hmac2 = bitgo.calculateHMAC(signingKey2, message); + + hmac1.should.not.equal(hmac2); + }); +}); + +describe('V4 edge cases', function () { + afterEach(function () { + sinon.restore(); + }); + + it('should handle missing tokenId in V4 response gracefully', async function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + const mockResponseBody = { + user: { username: 'testuser@example.com', id: 'user123' }, + // Missing 'id' field (tokenId) + encryptedToken: + '{"iv":"test","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"test","ct":"test"}', + derivationPath: 'm/999999/54719676/90455048', + encryptedECDHXprv: + '{"iv":"test","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"test","ct":"test"}', + }; + + const mockResponse = { + status: 200, + body: mockResponseBody, + header: {}, + }; + + const postStub = sinon.stub(bitgo, 'post').returns({ + send: sinon.stub().resolves(mockResponse), + } as any); + + const handleTokenIssuanceStub = sinon.stub(bitgo, 'handleTokenIssuance').returns({ + token: 'decrypted_signing_key', + ecdhXprv: + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k', + }); + + const verifyResponseStub = sinon.stub().returns(undefined); + sinon.stub(require('../../src/api'), 'verifyResponse').callsFake(verifyResponseStub); + + await bitgo.authenticate({ + username: 'testuser@example.com', + password: 'testpassword', + }); + + // Verify tokenId is not set when missing from response + const tokenId = (bitgo as any)._tokenId; + (tokenId === undefined).should.be.true(); + + // Token should still be set + (bitgo as any)._token.should.equal('decrypted_signing_key'); + + postStub.restore(); + handleTokenIssuanceStub.restore(); + }); + + it('should handle fromJSON with missing tokenId field', function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + const sessionData = { + user: { username: 'testuser@example.com', id: 'user123' }, + token: 'signing_key_12345', + // tokenId is missing + ecdhXprv: + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k', + }; + + bitgo.fromJSON(sessionData); + + // Should not throw, tokenId should be undefined + const tokenId = (bitgo as any)._tokenId; + (tokenId === undefined).should.be.true(); + + // Other fields should be restored + (bitgo as any)._token.should.equal('signing_key_12345'); + }); + + it('should handle switching from V2 to V4 session', function () { + const bitgo = new BitGoAPI({ + env: 'test', + authVersion: 4, + }); + + // Start with V2 session (no tokenId) + const v2SessionData = { + user: { username: 'testuser@example.com', id: 'user123' }, + token: 'v2_token_hash', + ecdhXprv: + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k', + }; + + bitgo.fromJSON(v2SessionData); + + // Verify V2 session loaded + (bitgo as any)._token.should.equal('v2_token_hash'); + const tokenIdAfterV2 = (bitgo as any)._tokenId; + (tokenIdAfterV2 === undefined).should.be.true(); + + // Now switch to V4 session + const v4SessionData = { + user: { username: 'testuser@example.com', id: 'user123' }, + token: 'v4_signing_key', + tokenId: 'v4_token_id', + ecdhXprv: + 'xprv9s21ZrQH143K2fJ91S4BRsupcYrE6mmY96fcX5HkhoTrrwmwjd16Cn87cWinJjByrfpojjx7ezsJLx7TAKLT8m8hM5Kax9YcoxnBeJZ3t2k', + }; + + bitgo.fromJSON(v4SessionData); + + // Verify V4 session loaded + (bitgo as any)._token.should.equal('v4_signing_key'); + (bitgo as any)._tokenId.should.equal('v4_token_id'); + }); +}); diff --git a/modules/sdk-hmac/src/hmac.ts b/modules/sdk-hmac/src/hmac.ts index 993ba94c73..079392af30 100644 --- a/modules/sdk-hmac/src/hmac.ts +++ b/modules/sdk-hmac/src/hmac.ts @@ -27,7 +27,7 @@ export function calculateHMAC(key: string | BinaryLike | KeyObject, message: str * @param timestamp request timestamp from `Date.now()` * @param statusCode Only set for HTTP responses, leave blank for requests * @param method request method - * @param authVersion authentication version (2 or 3) + * @param authVersion authentication version (2, 3, or 4) * @param useOriginalPath whether to use the original urlPath without parsing (default false) * @returns {string | Buffer} */ diff --git a/modules/sdk-hmac/src/types.ts b/modules/sdk-hmac/src/types.ts index 5d9db2066f..af4a04be65 100644 --- a/modules/sdk-hmac/src/types.ts +++ b/modules/sdk-hmac/src/types.ts @@ -1,6 +1,6 @@ export const supportedRequestMethods = ['get', 'post', 'put', 'del', 'patch', 'options', 'delete'] as const; -export type AuthVersion = 2 | 3; +export type AuthVersion = 2 | 3 | 4; export interface CalculateHmacSubjectOptions { urlPath: string;