Skip to content

Commit b050c5a

Browse files
committed
feat(sdk-api): implement V4 token issuance flow
Ticket: CAAS-783
1 parent 168e035 commit b050c5a

File tree

5 files changed

+812
-5
lines changed

5 files changed

+812
-5
lines changed

modules/sdk-api/src/bitgoAPI.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ import {
7575
SplitSecretOptions,
7676
TokenIssuance,
7777
TokenIssuanceResponse,
78+
ProcessV4TokenIssuanceParams,
79+
ProcessV4TokenIssuanceResult,
7880
UnlockOptions,
7981
User,
8082
VerifyPasswordOptions,
@@ -123,6 +125,7 @@ export class BitGoAPI implements BitGoBase {
123125
protected _extensionKey?: ECPairInterface;
124126
protected _reqId?: IRequestTracer;
125127
protected _token?: string;
128+
protected _tokenId?: string; // V4: separate token identifier
126129
protected _version = pjson.version;
127130
protected _userAgent?: string;
128131
protected _ecdhXprv?: string;
@@ -735,6 +738,7 @@ export class BitGoAPI implements BitGoBase {
735738
return {
736739
user: this._user,
737740
token: this._token,
741+
tokenId: this._tokenId,
738742
extensionKey: this._extensionKey ? this._extensionKey.toWIF() : undefined,
739743
ecdhXprv: this._ecdhXprv,
740744
};
@@ -758,6 +762,7 @@ export class BitGoAPI implements BitGoBase {
758762
fromJSON(json: BitGoJson): void {
759763
this._user = json.user;
760764
this._token = json.token;
765+
this._tokenId = json.tokenId;
761766
this._ecdhXprv = json.ecdhXprv;
762767
if (json.extensionKey) {
763768
const network = common.Environments[this.getEnv()].network;
@@ -980,6 +985,11 @@ export class BitGoAPI implements BitGoBase {
980985
this._token = responseDetails.token;
981986
this._ecdhXprv = responseDetails.ecdhXprv;
982987

988+
// V4: store separate token identifier
989+
if (this._authVersion === 4 && body.id) {
990+
this._tokenId = body.id;
991+
}
992+
983993
// verify the response's authenticity
984994
verifyResponse(this, responseDetails.token, 'post', request, response, this._authVersion);
985995

@@ -1109,6 +1119,39 @@ export class BitGoAPI implements BitGoBase {
11091119
return response;
11101120
}
11111121

1122+
/**
1123+
* Process V4 token issuance response by decrypting the signing key.
1124+
* This helper decrypts the ECDH-encrypted signing key from V4 issuance
1125+
* responses (reuses existing V2/V3 ECDH decryption logic).
1126+
*
1127+
* V4 issuance provides:
1128+
* - tokenId (plaintext): Used to identify the token in API requests
1129+
* - encryptedToken: ECDH-encrypted signing key for HMAC authentication
1130+
*
1131+
* @param params - Token issuance parameters
1132+
* @param params.tokenId - Token identifier from server response (response.id)
1133+
* @param params.encryptedToken - ECDH-encrypted signing key from server response
1134+
* @param params.derivationPath - BIP32 derivation path for ECDH key agreement
1135+
* @returns Object containing token ID and decrypted signing key
1136+
*/
1137+
processV4TokenIssuance(params: ProcessV4TokenIssuanceParams): ProcessV4TokenIssuanceResult {
1138+
common.validateParams(params, ['tokenId', 'encryptedToken', 'derivationPath'], []);
1139+
1140+
// Reuse existing ECDH decryption logic from V2/V3 to decrypt the signing key
1141+
const tokenIssuanceResponse: TokenIssuanceResponse = {
1142+
encryptedToken: params.encryptedToken,
1143+
derivationPath: params.derivationPath,
1144+
};
1145+
1146+
const result = this.handleTokenIssuance(tokenIssuanceResponse);
1147+
const signingKey = result.token;
1148+
1149+
return {
1150+
tokenId: params.tokenId,
1151+
signingKey,
1152+
};
1153+
}
1154+
11121155
/**
11131156
*/
11141157
verifyPassword(params: VerifyPasswordOptions = {}): Promise<any> {
@@ -1131,6 +1174,7 @@ export class BitGoAPI implements BitGoBase {
11311174
// TODO: are there any other fields which should be cleared?
11321175
this._user = undefined;
11331176
this._token = undefined;
1177+
this._tokenId = undefined;
11341178
this._refreshToken = undefined;
11351179
this._ecdhXprv = undefined;
11361180
}
@@ -1271,8 +1315,30 @@ export class BitGoAPI implements BitGoBase {
12711315
// verify the authenticity of the server's response before proceeding any further
12721316
verifyResponse(this, this._token, 'post', request, response, this._authVersion);
12731317

1274-
const responseDetails = this.handleTokenIssuance(response.body);
1275-
response.body.token = responseDetails.token;
1318+
// V4 token issuance includes separate tokenId and encrypted signing key
1319+
if (this._authVersion === 4) {
1320+
// Validate V4 response structure
1321+
if (!response.body.id || !response.body.encryptedToken || !response.body.derivationPath) {
1322+
throw new Error(
1323+
'Invalid V4 token issuance response: missing required fields (id, encryptedToken, or derivationPath)'
1324+
);
1325+
}
1326+
1327+
const { tokenId, signingKey } = this.processV4TokenIssuance({
1328+
tokenId: response.body.id,
1329+
encryptedToken: response.body.encryptedToken,
1330+
derivationPath: response.body.derivationPath,
1331+
});
1332+
1333+
// Return the decrypted signing key in the response (like V2/V3)
1334+
response.body.token = signingKey;
1335+
// Include tokenId in response for V4
1336+
response.body.tokenId = tokenId;
1337+
} else {
1338+
// V2/V3 token issuance
1339+
const responseDetails = this.handleTokenIssuance(response.body);
1340+
response.body.token = responseDetails.token;
1341+
}
12761342

12771343
return handleResponseResult<AddAccessTokenResponse>()(response);
12781344
} catch (e) {

modules/sdk-api/src/types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export {
2424
} from '@bitgo/sdk-hmac';
2525
export interface BitGoAPIOptions {
2626
accessToken?: string;
27-
authVersion?: 2 | 3;
27+
authVersion?: 2 | 3 | 4;
2828
clientConstants?:
2929
| Record<string, any>
3030
| {
@@ -137,6 +137,7 @@ export interface User {
137137
export interface BitGoJson {
138138
user?: User;
139139
token?: string;
140+
tokenId?: string; // V4: separate token identifier
140141
extensionKey?: string;
141142
ecdhXprv?: string;
142143
}
@@ -149,13 +150,25 @@ export interface TokenIssuanceResponse {
149150
derivationPath: string;
150151
encryptedToken: string;
151152
encryptedECDHXprv?: string;
153+
id?: string; // V4: token identifier
152154
}
153155

154156
export interface TokenIssuance {
155157
token: string;
156158
ecdhXprv?: string;
157159
}
158160

161+
export interface ProcessV4TokenIssuanceParams {
162+
tokenId: string; // V4: token identifier from response.id
163+
encryptedToken: string; // V4: encrypted signing key
164+
derivationPath: string;
165+
}
166+
167+
export interface ProcessV4TokenIssuanceResult {
168+
tokenId: string;
169+
signingKey: string;
170+
}
171+
159172
export interface AccessTokenSpendingLimit {
160173
coin: string;
161174
txCount?: number;
@@ -189,6 +202,7 @@ export interface AddAccessTokenResponse {
189202
encryptedToken: string;
190203
derivationPath: string;
191204
token: string;
205+
tokenId?: string; // V4: separate token identifier
192206
enterprise?: string;
193207
extensionAddress?: string;
194208
}

0 commit comments

Comments
 (0)