From 4320e2b57d52cd903e9d7e9721f5bbfe46eddb4f Mon Sep 17 00:00:00 2001 From: Alvin Dai Date: Mon, 23 Sep 2024 17:48:04 -0400 Subject: [PATCH 1/5] feat(sdk-api): authenticateWithPasskey method add a new method to support authenticating with passkey Ticket: WP-2592 --- modules/sdk-api/src/bitgoAPI.ts | 117 +++++++++++++++++++++++++++++--- modules/sdk-api/src/types.ts | 26 +++++++ 2 files changed, 134 insertions(+), 9 deletions(-) diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index fa204bdb9a..72e18a3cee 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -47,6 +47,7 @@ import { AddAccessTokenOptions, AddAccessTokenResponse, AuthenticateOptions, + AuthenticateWithPasskeyOptions, AuthenticateWithAuthCodeOptions, BitGoAPIOptions, BitGoJson, @@ -64,6 +65,7 @@ import { LoginResponse, PingOptions, ProcessedAuthenticationOptions, + ProcessedAuthenticationPasskeyOptions, ReconstitutedSecret, ReconstituteSecretOptions, RegisterPushTokenOptions, @@ -427,16 +429,16 @@ export class BitGoAPI implements BitGoBase { */ const newOnFulfilled = onfulfilled ? (response: superagent.Response) => { - // HMAC verification is only allowed to be skipped in certain environments. - // This is checked in the constructor, but checking it again at request time - // will help prevent against tampering of this property after the object is created - if (!this._hmacVerification && !common.Environments[this.getEnv()].hmacVerificationEnforced) { - return onfulfilled(response); - } - - const verifiedResponse = verifyResponse(this, this._token, method, req, response); - return onfulfilled(verifiedResponse); + // HMAC verification is only allowed to be skipped in certain environments. + // This is checked in the constructor, but checking it again at request time + // will help prevent against tampering of this property after the object is created + if (!this._hmacVerification && !common.Environments[this.getEnv()].hmacVerificationEnforced) { + return onfulfilled(response); } + + const verifiedResponse = verifyResponse(this, this._token, method, req, response); + return onfulfilled(verifiedResponse); + } : null; return originalThen(newOnFulfilled).catch(onrejected); }; @@ -772,6 +774,58 @@ export class BitGoAPI implements BitGoBase { return authParams; } + /** + * Process auth passkey options into an object for bitgo authentication. + */ + preprocessAuthenticationPasskeyParams(params: AuthenticateWithPasskeyOptions): ProcessedAuthenticationPasskeyOptions { + if (!_.isString(params.username)) { + throw new Error('expected string username'); + } + + if (!_.isString(params.credId)) { + throw new Error('expected string credId'); + } + + if (!_.isObject(params.response)) { + throw new Error('required object params.response'); + } + + if (!_.isString(params.response.authenticatorData)) { + throw new Error('required object params.response.authenticatorData'); + } + + if (!_.isString(params.response.signature)) { + throw new Error('required object params.response.signature'); + } + + if (!_.isString(params.response.clientDataJSON)) { + throw new Error('required object params.response.clientDataJSON'); + } + + const processedParams: ProcessedAuthenticationPasskeyOptions = { + username: params.username, + credId: params.credId, + response: params.response, + } + + if (params.otp) { + processedParams.otp = params.otp; + } + + if (params.extensible) { + this._extensionKey = makeRandomKey(); + processedParams.extensible = true; + processedParams.extensionAddress = getAddressP2PKH(this._extensionKey); + } + + if (params.forReset2FA) { + processedParams.forReset2FA = true; + } + + return params; + } + + /** * Synchronous method for activating an access token. */ @@ -915,6 +969,51 @@ export class BitGoAPI implements BitGoBase { } } + /** + * Login to the bitgo platform with passkey. + */ + async authenticateWithPasskey(params: AuthenticateWithPasskeyOptions): Promise { + try { + if (!_.isObject(params)) { + throw new Error('required object params'); + } + + if (this._token) { + return new Error('already logged in'); + } + + const authUrl = this.microservicesUrl('/api/auth/v1/session'); + const authParams = this.preprocessAuthenticationPasskeyParams(params); + const request = this.post(authUrl); + + const response: superagent.Response = await request.send(authParams); + // extract body and user information + const body = response.body; + this._user = body.user; + + if (body.access_token) { + this._token = body.access_token; + } else { + //TODO: Issue token + + // const responseDetails = this.handleTokenIssuance(response.body, password); + // this._token = responseDetails.token; + // this._ecdhXprv = responseDetails.ecdhXprv; + + // // verify the response's authenticity + // verifyResponse(this, responseDetails.token, 'post', request, response); + + // // add the remaining component for easier access + // response.body.access_token = this._token; + } + + return handleResponseResult()(response); + } catch (e) { + handleResponseError(e); + } + } + + /** * * @param responseBody Response body object diff --git a/modules/sdk-api/src/types.ts b/modules/sdk-api/src/types.ts index dbd77b8b86..a1bba58aa4 100644 --- a/modules/sdk-api/src/types.ts +++ b/modules/sdk-api/src/types.ts @@ -104,6 +104,22 @@ export interface AuthenticateOptions { forReset2FA?: boolean; } +export interface AuthenticateWithPasskeyData { + authenticatorData: string, + signature: string, + clientDataJSON: string, + userHandle?: string +} + +export interface AuthenticateWithPasskeyOptions { + username: string, + credId: string, + response: AuthenticateWithPasskeyData, + otp?: string, + extensible?: boolean; + forReset2FA?: boolean; +} + export interface ProcessedAuthenticationOptions { email: string; password: string; @@ -116,6 +132,16 @@ export interface ProcessedAuthenticationOptions { forReset2FA?: boolean; } +export interface ProcessedAuthenticationPasskeyOptions { + username: string, + credId: string, + response: AuthenticateWithPasskeyData, + otp?: string, + extensible?: boolean; + extensionAddress?: boolean; + forReset2FA?: boolean; +} + export interface User { username: string; } From 6b8dec1975ce99d6b144829f5a3d77a293e51d57 Mon Sep 17 00:00:00 2001 From: Alvin Dai Date: Tue, 24 Sep 2024 11:51:14 -0400 Subject: [PATCH 2/5] feat(sdk-api): assume unencrypted access token Ticket: WP-2592 --- modules/sdk-api/src/bitgoAPI.ts | 39 +++++++++++++-------------------- modules/sdk-api/src/types.ts | 2 +- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index 72e18a3cee..5945203ad3 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -429,16 +429,16 @@ export class BitGoAPI implements BitGoBase { */ const newOnFulfilled = onfulfilled ? (response: superagent.Response) => { - // HMAC verification is only allowed to be skipped in certain environments. - // This is checked in the constructor, but checking it again at request time - // will help prevent against tampering of this property after the object is created - if (!this._hmacVerification && !common.Environments[this.getEnv()].hmacVerificationEnforced) { - return onfulfilled(response); + // HMAC verification is only allowed to be skipped in certain environments. + // This is checked in the constructor, but checking it again at request time + // will help prevent against tampering of this property after the object is created + if (!this._hmacVerification && !common.Environments[this.getEnv()].hmacVerificationEnforced) { + return onfulfilled(response); + } + + const verifiedResponse = verifyResponse(this, this._token, method, req, response); + return onfulfilled(verifiedResponse); } - - const verifiedResponse = verifyResponse(this, this._token, method, req, response); - return onfulfilled(verifiedResponse); - } : null; return originalThen(newOnFulfilled).catch(onrejected); }; @@ -791,22 +791,22 @@ export class BitGoAPI implements BitGoBase { } if (!_.isString(params.response.authenticatorData)) { - throw new Error('required object params.response.authenticatorData'); + throw new Error('required string params.response.authenticatorData'); } if (!_.isString(params.response.signature)) { - throw new Error('required object params.response.signature'); + throw new Error('required string params.response.signature'); } if (!_.isString(params.response.clientDataJSON)) { - throw new Error('required object params.response.clientDataJSON'); + throw new Error('required string params.response.clientDataJSON'); } const processedParams: ProcessedAuthenticationPasskeyOptions = { username: params.username, credId: params.credId, response: params.response, - } + }; if (params.otp) { processedParams.otp = params.otp; @@ -991,20 +991,11 @@ export class BitGoAPI implements BitGoBase { const body = response.body; this._user = body.user; + //Expecting unencrypted access token in response for now if (body.access_token) { this._token = body.access_token; } else { - //TODO: Issue token - - // const responseDetails = this.handleTokenIssuance(response.body, password); - // this._token = responseDetails.token; - // this._ecdhXprv = responseDetails.ecdhXprv; - - // // verify the response's authenticity - // verifyResponse(this, responseDetails.token, 'post', request, response); - - // // add the remaining component for easier access - // response.body.access_token = this._token; + throw new Error("failed to create access token"); } return handleResponseResult()(response); diff --git a/modules/sdk-api/src/types.ts b/modules/sdk-api/src/types.ts index a1bba58aa4..f4c7a11690 100644 --- a/modules/sdk-api/src/types.ts +++ b/modules/sdk-api/src/types.ts @@ -138,7 +138,7 @@ export interface ProcessedAuthenticationPasskeyOptions { response: AuthenticateWithPasskeyData, otp?: string, extensible?: boolean; - extensionAddress?: boolean; + extensionAddress?: string; forReset2FA?: boolean; } From 0dce834c5415d469e2332723998ac02f9ce1f98c Mon Sep 17 00:00:00 2001 From: Alvin Dai Date: Wed, 25 Sep 2024 10:37:19 -0400 Subject: [PATCH 3/5] feat(sdk-api): authenticateWithPasskey flatten response and fix types Ticket: WP-25962 TICKET: WP-2592 --- modules/sdk-api/src/bitgoAPI.ts | 30 +++++++++++------------------- modules/sdk-api/src/types.ts | 25 ++++++++++--------------- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index 5945203ad3..694cb33cbf 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -786,32 +786,26 @@ export class BitGoAPI implements BitGoBase { throw new Error('expected string credId'); } - if (!_.isObject(params.response)) { - throw new Error('required object params.response'); + if (!_.isString(params.authenticatorData)) { + throw new Error('required string authenticatorData'); } - if (!_.isString(params.response.authenticatorData)) { - throw new Error('required string params.response.authenticatorData'); + if (!_.isString(params.signature)) { + throw new Error('required string signature'); } - if (!_.isString(params.response.signature)) { - throw new Error('required string params.response.signature'); - } - - if (!_.isString(params.response.clientDataJSON)) { - throw new Error('required string params.response.clientDataJSON'); + if (!_.isString(params.clientDataJSON)) { + throw new Error('required string clientDataJSON'); } const processedParams: ProcessedAuthenticationPasskeyOptions = { username: params.username, credId: params.credId, - response: params.response, + authenticatorData: params.authenticatorData, + signature: params.signature, + clientDataJSON: params.clientDataJSON, }; - if (params.otp) { - processedParams.otp = params.otp; - } - if (params.extensible) { this._extensionKey = makeRandomKey(); processedParams.extensible = true; @@ -825,7 +819,6 @@ export class BitGoAPI implements BitGoBase { return params; } - /** * Synchronous method for activating an access token. */ @@ -991,11 +984,11 @@ export class BitGoAPI implements BitGoBase { const body = response.body; this._user = body.user; - //Expecting unencrypted access token in response for now + // Expecting unencrypted access token in response for now if (body.access_token) { this._token = body.access_token; } else { - throw new Error("failed to create access token"); + throw new Error('failed to create access token'); } return handleResponseResult()(response); @@ -1004,7 +997,6 @@ export class BitGoAPI implements BitGoBase { } } - /** * * @param responseBody Response body object diff --git a/modules/sdk-api/src/types.ts b/modules/sdk-api/src/types.ts index f4c7a11690..bc0ac82548 100644 --- a/modules/sdk-api/src/types.ts +++ b/modules/sdk-api/src/types.ts @@ -104,18 +104,12 @@ export interface AuthenticateOptions { forReset2FA?: boolean; } -export interface AuthenticateWithPasskeyData { - authenticatorData: string, - signature: string, - clientDataJSON: string, - userHandle?: string -} - export interface AuthenticateWithPasskeyOptions { - username: string, - credId: string, - response: AuthenticateWithPasskeyData, - otp?: string, + username: string; + credId: string; + authenticatorData: string; + signature: string; + clientDataJSON: string; extensible?: boolean; forReset2FA?: boolean; } @@ -133,10 +127,11 @@ export interface ProcessedAuthenticationOptions { } export interface ProcessedAuthenticationPasskeyOptions { - username: string, - credId: string, - response: AuthenticateWithPasskeyData, - otp?: string, + username: string; + credId: string; + authenticatorData: string; + signature: string; + clientDataJSON: string; extensible?: boolean; extensionAddress?: string; forReset2FA?: boolean; From 6b4b22279e52a8cd355d30aba4fac3ac6015eaab Mon Sep 17 00:00:00 2001 From: Pranav Jain Date: Fri, 27 Sep 2024 13:44:22 -0400 Subject: [PATCH 4/5] chore(sdk-core): use the raw response returned from webauthn Ticket: WP-2592 TICKET: WP-2592 --- modules/sdk-api/src/bitgoAPI.ts | 54 +++++++++++---------------------- modules/sdk-api/src/types.ts | 18 +---------- 2 files changed, 19 insertions(+), 53 deletions(-) diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index 694cb33cbf..b1fe9f829c 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -65,7 +65,6 @@ import { LoginResponse, PingOptions, ProcessedAuthenticationOptions, - ProcessedAuthenticationPasskeyOptions, ReconstitutedSecret, ReconstituteSecretOptions, RegisterPushTokenOptions, @@ -775,48 +774,29 @@ export class BitGoAPI implements BitGoBase { } /** - * Process auth passkey options into an object for bitgo authentication. + * Validate the passkey response is in the expected format + * Should be as is returned from navigator.credentials.get() */ - preprocessAuthenticationPasskeyParams(params: AuthenticateWithPasskeyOptions): ProcessedAuthenticationPasskeyOptions { + validateWebauthnResponse(params: AuthenticateWithPasskeyOptions): void { if (!_.isString(params.username)) { throw new Error('expected string username'); } - - if (!_.isString(params.credId)) { - throw new Error('expected string credId'); + const webauthnResponse = JSON.parse(params.webauthnResponse); + if (!webauthnResponse && !webauthnResponse.response) { + throw new Error('unexpected webauthnResponse'); } - - if (!_.isString(params.authenticatorData)) { - throw new Error('required string authenticatorData'); + if (!_.isString(webauthnResponse.id)) { + throw new Error('id is missing'); } - - if (!_.isString(params.signature)) { - throw new Error('required string signature'); + if (!_.isString(webauthnResponse.response.authenticatorData)) { + throw new Error('authenticatorData is missing'); } - - if (!_.isString(params.clientDataJSON)) { - throw new Error('required string clientDataJSON'); + if (!_.isString(webauthnResponse.response.clientDataJSON)) { + throw new Error('clientDataJSON is missing'); } - - const processedParams: ProcessedAuthenticationPasskeyOptions = { - username: params.username, - credId: params.credId, - authenticatorData: params.authenticatorData, - signature: params.signature, - clientDataJSON: params.clientDataJSON, - }; - - if (params.extensible) { - this._extensionKey = makeRandomKey(); - processedParams.extensible = true; - processedParams.extensionAddress = getAddressP2PKH(this._extensionKey); - } - - if (params.forReset2FA) { - processedParams.forReset2FA = true; + if (!_.isString(webauthnResponse.response.signature)) { + throw new Error('signature is missing'); } - - return params; } /** @@ -976,15 +956,17 @@ export class BitGoAPI implements BitGoBase { } const authUrl = this.microservicesUrl('/api/auth/v1/session'); - const authParams = this.preprocessAuthenticationPasskeyParams(params); const request = this.post(authUrl); - const response: superagent.Response = await request.send(authParams); + this.validateWebauthnResponse(params); + + const response: superagent.Response = await request.send(params); // extract body and user information const body = response.body; this._user = body.user; // Expecting unencrypted access token in response for now + // TODO (WP-2733): Use GPG encryption to decrypt access token if (body.access_token) { this._token = body.access_token; } else { diff --git a/modules/sdk-api/src/types.ts b/modules/sdk-api/src/types.ts index bc0ac82548..cdc601e6c9 100644 --- a/modules/sdk-api/src/types.ts +++ b/modules/sdk-api/src/types.ts @@ -106,12 +106,7 @@ export interface AuthenticateOptions { export interface AuthenticateWithPasskeyOptions { username: string; - credId: string; - authenticatorData: string; - signature: string; - clientDataJSON: string; - extensible?: boolean; - forReset2FA?: boolean; + webauthnResponse: string; } export interface ProcessedAuthenticationOptions { @@ -126,17 +121,6 @@ export interface ProcessedAuthenticationOptions { forReset2FA?: boolean; } -export interface ProcessedAuthenticationPasskeyOptions { - username: string; - credId: string; - authenticatorData: string; - signature: string; - clientDataJSON: string; - extensible?: boolean; - extensionAddress?: string; - forReset2FA?: boolean; -} - export interface User { username: string; } From 77d569a071158c91ac90d2499fdb25f5a58fa799 Mon Sep 17 00:00:00 2001 From: Pranav Jain Date: Fri, 27 Sep 2024 15:00:19 -0400 Subject: [PATCH 5/5] chore(sdk-api): extract userId from userHandle for passkeys Ticket: WP-2592 TICKET: WP-2592 --- modules/sdk-api/src/bitgoAPI.ts | 35 ++++++++++++++++----------------- modules/sdk-api/src/types.ts | 5 ----- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index b1fe9f829c..072afd82cd 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -47,7 +47,6 @@ import { AddAccessTokenOptions, AddAccessTokenResponse, AuthenticateOptions, - AuthenticateWithPasskeyOptions, AuthenticateWithAuthCodeOptions, BitGoAPIOptions, BitGoJson, @@ -777,26 +776,26 @@ export class BitGoAPI implements BitGoBase { * Validate the passkey response is in the expected format * Should be as is returned from navigator.credentials.get() */ - validateWebauthnResponse(params: AuthenticateWithPasskeyOptions): void { - if (!_.isString(params.username)) { - throw new Error('expected string username'); - } - const webauthnResponse = JSON.parse(params.webauthnResponse); - if (!webauthnResponse && !webauthnResponse.response) { + validatePasskeyResponse(passkeyResponse: string): void { + const parsedPasskeyResponse = JSON.parse(passkeyResponse); + if (!parsedPasskeyResponse && !parsedPasskeyResponse.response) { throw new Error('unexpected webauthnResponse'); } - if (!_.isString(webauthnResponse.id)) { + if (!_.isString(parsedPasskeyResponse.id)) { throw new Error('id is missing'); } - if (!_.isString(webauthnResponse.response.authenticatorData)) { + if (!_.isString(parsedPasskeyResponse.response.authenticatorData)) { throw new Error('authenticatorData is missing'); } - if (!_.isString(webauthnResponse.response.clientDataJSON)) { + if (!_.isString(parsedPasskeyResponse.response.clientDataJSON)) { throw new Error('clientDataJSON is missing'); } - if (!_.isString(webauthnResponse.response.signature)) { + if (!_.isString(parsedPasskeyResponse.response.signature)) { throw new Error('signature is missing'); } + if (!_.isString(parsedPasskeyResponse.response.userHandle)) { + throw new Error('userHandle is missing'); + } } /** @@ -945,12 +944,8 @@ export class BitGoAPI implements BitGoBase { /** * Login to the bitgo platform with passkey. */ - async authenticateWithPasskey(params: AuthenticateWithPasskeyOptions): Promise { + async authenticateWithPasskey(passkey: string): Promise { try { - if (!_.isObject(params)) { - throw new Error('required object params'); - } - if (this._token) { return new Error('already logged in'); } @@ -958,9 +953,13 @@ export class BitGoAPI implements BitGoBase { const authUrl = this.microservicesUrl('/api/auth/v1/session'); const request = this.post(authUrl); - this.validateWebauthnResponse(params); + this.validatePasskeyResponse(passkey); + const userId = JSON.parse(passkey).response.userHandle; - const response: superagent.Response = await request.send(params); + const response: superagent.Response = await request.send({ + passkey: passkey, + userId: userId, + }); // extract body and user information const body = response.body; this._user = body.user; diff --git a/modules/sdk-api/src/types.ts b/modules/sdk-api/src/types.ts index cdc601e6c9..dbd77b8b86 100644 --- a/modules/sdk-api/src/types.ts +++ b/modules/sdk-api/src/types.ts @@ -104,11 +104,6 @@ export interface AuthenticateOptions { forReset2FA?: boolean; } -export interface AuthenticateWithPasskeyOptions { - username: string; - webauthnResponse: string; -} - export interface ProcessedAuthenticationOptions { email: string; password: string;