diff --git a/modules/bitgo/test/unit/bitgo.ts b/modules/bitgo/test/unit/bitgo.ts index c668521987..89f6dad384 100644 --- a/modules/bitgo/test/unit/bitgo.ts +++ b/modules/bitgo/test/unit/bitgo.ts @@ -114,7 +114,7 @@ describe('BitGo Prototype Methods', function () { }); }); - describe('Authenticate in Microservices', () => { + describe('Authenticate via v2 login', () => { let bitgo; const authenticateRequest = { username: 'test@bitgo.com', @@ -125,10 +125,10 @@ describe('BitGo Prototype Methods', function () { forceSMS: false, }; - it('goes to microservices', async function () { + it('goes to the v2 user login endpoint', async function () { bitgo = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri: 'https://microservices.uri' } as any); const scope = nock(BitGoJS.Environments[bitgo.getEnv()].uri) - .post('/api/auth/v1/session') + .post('/api/v2/user/login') .reply(200, { user: { username: 'test@bitgo.com', @@ -140,10 +140,10 @@ describe('BitGo Prototype Methods', function () { scope.isDone().should.be.true(); }); - it('goes to microservices even when microservicesUri is not specified', async function () { + it('goes to the v2 user login endpoint when microservicesUri is not specified', async function () { bitgo = TestBitGo.decorate(BitGo, { env: 'mock' }); const scope = nock(BitGoJS.Environments[bitgo.getEnv()].uri) - .post('/api/auth/v1/session') + .post('/api/v2/user/login') .reply(200, { user: { username: 'test@bitgo.com', @@ -379,7 +379,7 @@ describe('BitGo Prototype Methods', function () { before(async function () { nock('https://bitgo.fakeurl') - .post('/api/auth/v1/session') + .post('/api/v2/user/login') .reply(200, { access_token: 'access_token', user: { username: 'update_pw_tester@bitgo.com' }, @@ -685,7 +685,7 @@ describe('BitGo Prototype Methods', function () { it('should get the ecdhKeychain if ensureEcdhKeychain is set and user already has ecdhKeychain', async function () { nock('https://bitgo.fakeurl') - .post('/api/auth/v1/session') + .post('/api/v2/user/login') .reply(200, { access_token: 'access_token', user: { username: 'auth-test@bitgo.com' }, @@ -709,9 +709,10 @@ describe('BitGo Prototype Methods', function () { should.exist(response.user.ecdhKeychain); response.user.ecdhKeychain.should.equal('some-existing-xpub'); }); + it('should create the ecdhKeychain if ensureEcdhKeychain is set and the user does not already have ecdhKeychain', async function () { nock('https://bitgo.fakeurl') - .post('/api/auth/v1/session') + .post('/api/v2/user/login') .reply(200, { access_token: 'access_token', user: { username: 'auth-test@bitgo.com' }, diff --git a/modules/express/src/fetchEncryptedPrivKeys.ts b/modules/express/src/fetchEncryptedPrivKeys.ts index f3760a9f8f..e838760526 100644 --- a/modules/express/src/fetchEncryptedPrivKeys.ts +++ b/modules/express/src/fetchEncryptedPrivKeys.ts @@ -64,7 +64,7 @@ const walletWithPrv: WalletWithPrv = esWalletWithPrv ? JSON.parse(esWalletWithPr // ]; export async function fetchKeys(ids: WalletIds, token: string, accessToken?: string): Promise> { - bg.authenticateWithAccessToken({ accessToken: token }); + await bg.authenticateWithAccessToken({ accessToken: token }); // get the encrypted user privKey for each walletId and store in the JSON output const output: Output = {}; diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index 1c065ecc2d..6a8d0c31b8 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -1053,11 +1053,14 @@ export class BitGoAPI implements BitGoBase { } /** - * Synchronous method for activating an access token. + * Activate an access token and sync it to the HMAC auth strategy. + * Resolves once the token has been registered with the strategy, so callers + * can safely issue signed requests immediately after. */ - authenticateWithAccessToken({ accessToken }: AccessTokenOptions): void { + async authenticateWithAccessToken({ accessToken }: AccessTokenOptions): Promise { debug('now authenticating with access token %s', accessToken.substring(0, 8)); this._token = accessToken; + await this._hmacAuthStrategy.setToken?.(this._token); } /** @@ -1152,14 +1155,14 @@ export class BitGoAPI implements BitGoBase { return new Error('already logged in'); } - const authUrl = this.microservicesUrl('/api/auth/v1/session'); + const authUrl = this.url('/user/login', 2); const request = this.post(authUrl); if (forceV1Auth) { request.forceV1Auth = true; // tell the server that the client was forced to downgrade the authentication protocol authParams.forceV1Auth = true; - debug('forcing v1 auth for call to authenticate'); + debug('forcing v1 auth on request'); } const response: superagent.Response = await request.send(authParams); // extract body and user information diff --git a/modules/sdk-api/test/unit/bitgoAPI.ts b/modules/sdk-api/test/unit/bitgoAPI.ts index fe4bf9a044..597960f916 100644 --- a/modules/sdk-api/test/unit/bitgoAPI.ts +++ b/modules/sdk-api/test/unit/bitgoAPI.ts @@ -485,7 +485,7 @@ describe('Constructor', function () { const bitgo = new BitGoAPI({ env: 'custom', customRootURI: ROOT, hmacAuthStrategy: strategy }); nock(ROOT) - .post('/api/auth/v1/session') + .post('/api/v2/user/login') .reply(200, { user: { username: 'test@example.com' }, access_token: 'v2xmyaccesstoken', @@ -518,7 +518,7 @@ describe('Constructor', function () { const bitgo = new BitGoAPI({ env: 'custom', customRootURI: ROOT, hmacAuthStrategy: strategy }); nock(ROOT) - .post('/api/auth/v1/session') + .post('/api/v2/user/login') .reply(200, { user: { username: 'test@example.com' }, access_token: 'v2xmytoken', @@ -539,6 +539,36 @@ describe('Constructor', function () { keyReady.should.be.true(); }); + + it('handles an ECDH-encrypted token response (no access_token) through the v2 login flow', async function () { + const { strategy, setTokenStub } = makeStrategy(); + const bitgo = new BitGoAPI({ env: 'custom', customRootURI: ROOT, hmacAuthStrategy: strategy }); + + // Stub token issuance so the test does not depend on real crypto. The point is + // to assert the v2 login response with encryptedToken reaches handleTokenIssuance + // and that the decrypted token is then synced to the strategy. + const handleTokenIssuanceStub = sinon + .stub(bitgo, 'handleTokenIssuance') + .resolves({ token: 'v2xdecryptedtoken' }); + + nock(ROOT) + .post('/api/v2/user/login') + .reply(200, { + user: { username: 'test@example.com' }, + encryptedToken: 'encrypted-token-value', + encryptedECDHXprv: 'encrypted-ecdh-xprv', + derivationPath: 'm/999999/0/1', + }); + + await bitgo.authenticate({ username: 'test@example.com', password: 'hunter2' }); + + // The SDK must have taken the ECDH path (not the plain access_token path). + handleTokenIssuanceStub.calledOnce.should.be.true(); + handleTokenIssuanceStub.firstCall.args[0].encryptedToken.should.equal('encrypted-token-value'); + // The decrypted token must be synced to the strategy. + setTokenStub.calledOnce.should.be.true(); + setTokenStub.firstCall.args[0].should.equal('v2xdecryptedtoken'); + }); }); describe('authenticateWithPasskey()', function () { @@ -577,7 +607,7 @@ describe('Constructor', function () { const { strategy, clearTokenStub } = makeStrategy(); const bitgo = new BitGoAPI({ env: 'custom', customRootURI: ROOT, hmacAuthStrategy: strategy }); - bitgo.authenticateWithAccessToken({ accessToken: 'v2xsometoken' }); + await bitgo.authenticateWithAccessToken({ accessToken: 'v2xsometoken' }); (bitgo as any)._token.should.equal('v2xsometoken'); await bitgo.clearAsync(); @@ -641,13 +671,14 @@ describe('Constructor', function () { }); describe('sync token-setting methods', function () { - it('authenticateWithAccessToken does not call setToken (synchronous — caller must invoke setToken on the strategy manually)', function () { + it('authenticateWithAccessToken calls setToken (HMAC strategy must be kept in sync for v2/v3 signing)', async function () { const { strategy, setTokenStub } = makeStrategy(); const bitgo = new BitGoAPI({ env: 'custom', customRootURI: ROOT, hmacAuthStrategy: strategy }); - bitgo.authenticateWithAccessToken({ accessToken: 'v2xsynctoken' }); + await bitgo.authenticateWithAccessToken({ accessToken: 'v2xsynctoken' }); - setTokenStub.called.should.be.false(); + setTokenStub.calledOnce.should.be.true(); + setTokenStub.firstCall.args[0].should.equal('v2xsynctoken'); }); it('fromJSON does not call setToken (synchronous — caller must invoke setToken on the strategy manually)', function () { @@ -677,7 +708,7 @@ describe('Constructor', function () { }); const bitgo = new BitGoAPI({ env: 'custom', customRootURI: ROOT, hmacAuthStrategy: strategy }); // Do NOT set _ecdhXprv — simulates SSO/enterprise session (Okta, Entra, etc.) - bitgo.authenticateWithAccessToken({ accessToken: 'v2xstrategytoken' }); + await bitgo.authenticateWithAccessToken({ accessToken: 'v2xstrategytoken' }); const scope = nock(ROOT).post('/api/auth/v1/accesstoken').reply(200, { token: 'v2xnewplaintoken', @@ -725,7 +756,7 @@ describe('Constructor', function () { isAuthenticated: sinon.stub().returns(true), }); const bitgo = new BitGoAPI({ env: 'custom', customRootURI: ROOT, hmacAuthStrategy: strategy }); - bitgo.authenticateWithAccessToken({ accessToken: 'v2xstrategytoken' }); + await bitgo.authenticateWithAccessToken({ accessToken: 'v2xstrategytoken' }); nock(ROOT).post('/api/auth/v1/accesstoken').reply(200, { token: 'v2xplaintoken', @@ -746,7 +777,7 @@ describe('Constructor', function () { isAuthenticated: sinon.stub().returns(false), }); const bitgo = new BitGoAPI({ env: 'custom', customRootURI: ROOT, hmacAuthStrategy: strategy }); - bitgo.authenticateWithAccessToken({ accessToken: 'v2xlegacytoken' }); + await bitgo.authenticateWithAccessToken({ accessToken: 'v2xlegacytoken' }); nock(ROOT).post('/api/auth/v1/accesstoken').reply(200, { token: 'v2xlegacyresult', @@ -761,7 +792,7 @@ describe('Constructor', function () { it('should force V1 auth when isAuthenticated is not defined on strategy', async function () { const { strategy } = makeStrategy(); const bitgo = new BitGoAPI({ env: 'custom', customRootURI: ROOT, hmacAuthStrategy: strategy }); - bitgo.authenticateWithAccessToken({ accessToken: 'v2xnoauthmethod' }); + await bitgo.authenticateWithAccessToken({ accessToken: 'v2xnoauthmethod' }); nock(ROOT).post('/api/auth/v1/accesstoken').reply(200, { token: 'v2xresult', diff --git a/modules/sdk-api/test/unit/hmacStrategy.ts b/modules/sdk-api/test/unit/hmacStrategy.ts index 7284c9fa46..fdbfaeee56 100644 --- a/modules/sdk-api/test/unit/hmacStrategy.ts +++ b/modules/sdk-api/test/unit/hmacStrategy.ts @@ -146,7 +146,7 @@ describe('BitGoAPI HMAC Strategy Injection', function () { }); nock(TEST_URI) - .post('/api/auth/v1/session') + .post('/api/v2/user/login') .reply( 200, { access_token: TEST_TOKEN, user: { username: 'test@test.com' } }, diff --git a/scripts/upgrade-wallet-encryption.ts b/scripts/upgrade-wallet-encryption.ts index 31c6f38608..b7a6ac3ea9 100644 --- a/scripts/upgrade-wallet-encryption.ts +++ b/scripts/upgrade-wallet-encryption.ts @@ -76,7 +76,7 @@ async function main() { parseArgs(); const bitgo = new BitGo({ env }); - bitgo.authenticateWithAccessToken({ accessToken }); + await bitgo.authenticateWithAccessToken({ accessToken }); const wallet = await bitgo.coin(coin).wallets().get({ id: walletId }); const result = await wallet.upgradeEncryption({