From c5045b59ed45a69037ae96b55d4c29663a704cd1 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 13:35:41 -0700 Subject: [PATCH 1/7] user record changes for getAccountInfo() --- src/auth/user-record.ts | 72 +++++++++++++++++++++++++ test/unit/auth/user-record.spec.ts | 86 +++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 5b00151401..8b6aed3061 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -56,6 +56,13 @@ export interface TotpInfoResponse { [key: string]: unknown; } +export interface PasskeyInfoResponse { + name?: string; + credentialId?: string; + displayName?: string; + [key: string]: unknown; +} + export interface ProviderUserInfoResponse { rawId: string; displayName?: string; @@ -81,6 +88,7 @@ export interface GetAccountInfoUserResponse { tenantId?: string; providerUserInfo?: ProviderUserInfoResponse[]; mfaInfo?: MultiFactorInfoResponse[]; + passkeyInfo?: PasskeyInfoResponse[]; createdAt?: string; lastLoginAt?: string; lastRefreshAt?: string; @@ -357,6 +365,50 @@ export class MultiFactorSettings { } } +/** + * Information about passkeys + */ +export class PasskeyInfo { + /** + * The name of the user. + */ + public readonly name?: string; + /** + * Identifier for the registered credential. + */ + public readonly credentialId: string; + /** + * The human-readable name of the user, intended for display. + */ + public readonly displayName?: string; + + constructor(response: PasskeyInfoResponse) { + if (!isNonNullObject(response)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid passkey info response'); + } + utils.addReadonlyGetter(this, 'name', response.name); + utils.addReadonlyGetter(this, 'credentialId', response.credentialId); + utils.addReadonlyGetter(this, 'displayName', response.displayName); + } + + /** + * Returns a JSON-serializable representation of this passkey info object. + * + * @returns A JSON-serializable representation of this passkey info object. + */ + public toJSON(): object { + const json: any = { + name: this.name, + credentialId: this.credentialId, + displayName: this.displayName, + } + return json; + } + +} + /** * Represents a user's metadata. */ @@ -582,6 +634,11 @@ export class UserRecord { */ public readonly multiFactor?: MultiFactorSettings; + /** + * Information about Passkeys + */ + public readonly passkeyInfo?: PasskeyInfo[]; + /** * @param response - The server side response returned from the getAccountInfo * endpoint. @@ -637,6 +694,15 @@ export class UserRecord { if (multiFactor.enrolledFactors.length > 0) { utils.addReadonlyGetter(this, 'multiFactor', multiFactor); } + if(response.passkeyInfo) { + let passkeys: PasskeyInfo[] = []; + response.passkeyInfo.forEach((passkey) => { + passkeys.push(new PasskeyInfo(passkey)); + }); + if(passkeys.length > 0) { + utils.addReadonlyGetter(this, 'passkeyInfo', passkeys); + } + } } /** @@ -664,6 +730,12 @@ export class UserRecord { if (this.multiFactor) { json.multiFactor = this.multiFactor.toJSON(); } + if(this.passkeyInfo) { + json.passkeyInfo = []; + this.passkeyInfo.forEach((passkey) => { + json.passkeyInfo.push(passkey.toJSON()); + }) + } json.providerData = []; for (const entry of this.providerData) { // Convert each provider data to json. diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index dc332c13b9..d635617294 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -21,7 +21,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import { deepCopy } from '../../../src/utils/deep-copy'; import { - GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactorInfoResponse, TotpMultiFactorInfo, + GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactorInfoResponse, TotpMultiFactorInfo, PasskeyInfo, } from '../../../src/auth/user-record'; import { UserInfo, UserMetadata, UserRecord, MultiFactorSettings, MultiFactorInfo, PhoneMultiFactorInfo, @@ -100,6 +100,18 @@ function getValidUserResponse(tenantId?: string): GetAccountInfoUserResponse { phoneInfo: '+16505556789', }, ], + passkeyInfo: [ + { + name: "name1@google.com", + credentialId: "credentialId1", + displayName: "passkey1", + }, + { + name: "name2@google.com", + credentialId: "credentialId2", + displayName: "passkey2", + } + ] }; if (typeof tenantId !== 'undefined') { response.tenantId = tenantId; @@ -185,6 +197,18 @@ function getUserJSON(tenantId?: string): object { }, ], }, + passkeyInfo: [ + { + name: "name1@google.com", + credentialId: "credentialId1", + displayName: "passkey1", + }, + { + name: "name2@google.com", + credentialId: "credentialId2", + displayName: "passkey2", + } + ] }; } @@ -663,6 +687,66 @@ describe('MultiFactorSettings', () => { }); }); +describe('PasskeyInfo', () => { + const passkeyInfoData = { + name: 'John Doe', + credentialId: 'credential123', + displayName: 'john.doe@example.com', + }; + const passkeyInfo = new PasskeyInfo(passkeyInfoData); + + describe('constructor', () => { + it('should create a PasskeyInfo object with valid data', () => { + expect(passkeyInfo).to.be.an.instanceOf(PasskeyInfo); + }); + + it('should throw when missing required fields', () => { + expect(() => { + return new PasskeyInfo({}); + }).to.throw('INTERNAL ASSERT FAILED: Invalid passkey info response'); + }); + }); + + describe('getters', () => { + it('should return the expected name', () => { + expect(passkeyInfo.name).to.equal(passkeyInfoData.name); + }); + + it('should throw when modifying readonly name property', () => { + expect(() => { + (passkeyInfo as any).name = 'Modified Name'; + }).to.throw(Error); + }); + + it('should return the expected credentialId', () => { + expect(passkeyInfo.credentialId).to.equal(passkeyInfoData.credentialId); + }); + + it('should throw when modifying readonly credentialId property', () => { + expect(() => { + (passkeyInfo as any).credentialId = 'modifiedCredential'; + }).to.throw(Error); + }); + + it('should return the expected displayName', () => { + expect(passkeyInfo.displayName).to.equal(passkeyInfoData.displayName); + }); + + it('should throw when modifying readonly displayName property', () => { + expect(() => { + (passkeyInfo as any).displayName = 'modifiedDisplayName'; + }).to.throw(Error); + }); + }); + + describe('toJSON', () => { + it('should return the expected JSON object', () => { + expect(passkeyInfo.toJSON()).to.deep.equal(passkeyInfoData); + }); + }); +}); + + describe('UserInfo', () => { describe('constructor', () => { it('should throw when an empty object is provided', () => { From da7aa6e965b70476f6084e343c041fbd692e9d9d Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 15:49:51 -0700 Subject: [PATCH 2/7] lint and api-extractor fixes --- etc/firebase-admin.auth.api.md | 9 +++++++++ src/auth/index.ts | 1 + src/auth/user-record.ts | 25 +++++++++++++++---------- test/unit/auth/user-record.spec.ts | 28 ++++++++++++++-------------- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 42e217ba60..7969c494e0 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -365,6 +365,14 @@ export interface PasskeyConfigRequest { expectedOrigins?: string[]; } +// @public +export class PasskeyInfo { + readonly credentialId: string; + readonly displayName?: string; + readonly name: string; + toJSON(): object; +} + // @public export interface PasswordPolicyConfig { constraints?: CustomStrengthOptionsConfig; @@ -664,6 +672,7 @@ export class UserRecord { readonly emailVerified: boolean; readonly metadata: UserMetadata; readonly multiFactor?: MultiFactorSettings; + readonly passkeyInfo?: PasskeyInfo[]; readonly passwordHash?: string; readonly passwordSalt?: string; readonly phoneNumber?: string; diff --git a/src/auth/index.ts b/src/auth/index.ts index b3f5f954c7..e2819ca89f 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -172,4 +172,5 @@ export { UserInfo, UserMetadata, UserRecord, + PasskeyInfo, } from './user-record'; diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 8b6aed3061..9d2a6501c4 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -372,7 +372,7 @@ export class PasskeyInfo { /** * The name of the user. */ - public readonly name?: string; + public readonly name: string; /** * Identifier for the registered credential. */ @@ -382,6 +382,13 @@ export class PasskeyInfo { */ public readonly displayName?: string; + /** + * Initializes the PasskeyInfo object using the server side response. + * + * @param response - The server side response. + * @constructor + * @internal + */ constructor(response: PasskeyInfoResponse) { if (!isNonNullObject(response)) { throw new FirebaseAuthError( @@ -399,14 +406,12 @@ export class PasskeyInfo { * @returns A JSON-serializable representation of this passkey info object. */ public toJSON(): object { - const json: any = { + return { name: this.name, credentialId: this.credentialId, displayName: this.displayName, - } - return json; - } - + }; + } } /** @@ -694,12 +699,12 @@ export class UserRecord { if (multiFactor.enrolledFactors.length > 0) { utils.addReadonlyGetter(this, 'multiFactor', multiFactor); } - if(response.passkeyInfo) { - let passkeys: PasskeyInfo[] = []; + if (response.passkeyInfo) { + const passkeys: PasskeyInfo[] = []; response.passkeyInfo.forEach((passkey) => { passkeys.push(new PasskeyInfo(passkey)); }); - if(passkeys.length > 0) { + if (passkeys.length > 0) { utils.addReadonlyGetter(this, 'passkeyInfo', passkeys); } } @@ -730,7 +735,7 @@ export class UserRecord { if (this.multiFactor) { json.multiFactor = this.multiFactor.toJSON(); } - if(this.passkeyInfo) { + if (this.passkeyInfo) { json.passkeyInfo = []; this.passkeyInfo.forEach((passkey) => { json.passkeyInfo.push(passkey.toJSON()); diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index d635617294..346dcd0729 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -102,14 +102,14 @@ function getValidUserResponse(tenantId?: string): GetAccountInfoUserResponse { ], passkeyInfo: [ { - name: "name1@google.com", - credentialId: "credentialId1", - displayName: "passkey1", + name: 'name1@google.com', + credentialId: 'credentialId1', + displayName: 'passkey1', }, { - name: "name2@google.com", - credentialId: "credentialId2", - displayName: "passkey2", + name: 'name2@google.com', + credentialId: 'credentialId2', + displayName: 'passkey2', } ] }; @@ -199,14 +199,14 @@ function getUserJSON(tenantId?: string): object { }, passkeyInfo: [ { - name: "name1@google.com", - credentialId: "credentialId1", - displayName: "passkey1", + name: 'name1@google.com', + credentialId: 'credentialId1', + displayName: 'passkey1', }, { - name: "name2@google.com", - credentialId: "credentialId2", - displayName: "passkey2", + name: 'name2@google.com', + credentialId: 'credentialId2', + displayName: 'passkey2', } ] }; @@ -699,10 +699,10 @@ describe('PasskeyInfo', () => { it('should create a PasskeyInfo object with valid data', () => { expect(passkeyInfo).to.be.an.instanceOf(PasskeyInfo); }); - + it('should throw when missing required fields', () => { expect(() => { - return new PasskeyInfo({}); + return new PasskeyInfo(null as any); }).to.throw('INTERNAL ASSERT FAILED: Invalid passkey info response'); }); }); From ffb982e933f4c930e4f736f82021eedaea236523 Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:47:38 -0700 Subject: [PATCH 3/7] Apply suggestions from code review Co-authored-by: Kevin Cheung --- src/auth/user-record.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 9d2a6501c4..ff856421d0 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -366,7 +366,7 @@ export class MultiFactorSettings { } /** - * Information about passkeys + * Interface representing a user-enrolled passkey. */ export class PasskeyInfo { /** @@ -640,7 +640,7 @@ export class UserRecord { public readonly multiFactor?: MultiFactorSettings; /** - * Information about Passkeys + * Passkey-related properties for the current user, if available. */ public readonly passkeyInfo?: PasskeyInfo[]; From a269d5aefd79d54ec5b96c5f61d47da93d243283 Mon Sep 17 00:00:00 2001 From: Pragati Date: Mon, 23 Oct 2023 10:29:02 -0700 Subject: [PATCH 4/7] remove `[key: string]: unknown;` field from `PasskeyInfoResponse` --- src/auth/user-record.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 9d2a6501c4..0f83c54d93 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -60,7 +60,6 @@ export interface PasskeyInfoResponse { name?: string; credentialId?: string; displayName?: string; - [key: string]: unknown; } export interface ProviderUserInfoResponse { From 100af72d04c5f710ba3777b78be9376b28165c43 Mon Sep 17 00:00:00 2001 From: Pragati Date: Tue, 2 Apr 2024 14:28:26 -0700 Subject: [PATCH 5/7] add undefined displayName case --- test/unit/auth/user-record.spec.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index 346dcd0729..40e7b8da99 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -110,6 +110,10 @@ function getValidUserResponse(tenantId?: string): GetAccountInfoUserResponse { name: 'name2@google.com', credentialId: 'credentialId2', displayName: 'passkey2', + }, + { + name: 'name3@google.com', + credentialId: 'credentialId3', } ] }; @@ -207,6 +211,11 @@ function getUserJSON(tenantId?: string): object { name: 'name2@google.com', credentialId: 'credentialId2', displayName: 'passkey2', + }, + { + name: 'name3@google.com', + credentialId: 'credentialId3', + displayName: undefined, } ] }; @@ -699,7 +708,7 @@ describe('PasskeyInfo', () => { it('should create a PasskeyInfo object with valid data', () => { expect(passkeyInfo).to.be.an.instanceOf(PasskeyInfo); }); - + it('should throw when missing required fields', () => { expect(() => { return new PasskeyInfo(null as any); From a6de4bb3378d9316fb4528be368c1aaf32ef5706 Mon Sep 17 00:00:00 2001 From: Pragati Date: Tue, 2 Apr 2024 14:32:45 -0700 Subject: [PATCH 6/7] name and credentialId are not optional --- src/auth/user-record.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 4d6d70532e..e94f020cf1 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -57,8 +57,8 @@ export interface TotpInfoResponse { } export interface PasskeyInfoResponse { - name?: string; - credentialId?: string; + name: string; + credentialId: string; displayName?: string; } @@ -383,7 +383,7 @@ export class PasskeyInfo { /** * Initializes the PasskeyInfo object using the server side response. - * + * * @param response - The server side response. * @constructor * @internal @@ -410,7 +410,7 @@ export class PasskeyInfo { credentialId: this.credentialId, displayName: this.displayName, }; - } + } } /** From d3d17515a23302b58986d2ec49949c043b862141 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 3 Apr 2024 07:32:50 -0700 Subject: [PATCH 7/7] add `rpId` to update --- src/auth/auth-api-request.ts | 14 +++---- src/auth/passkey-config-manager.ts | 24 ++++++------ src/auth/passkey-config.ts | 38 ++++++++++--------- test/unit/auth/passkey-config-manager.spec.ts | 21 +++++----- test/unit/auth/passkey-config.spec.ts | 37 ++++++++++-------- 5 files changed, 71 insertions(+), 63 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 7f36461fc4..6afba07be6 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -1612,9 +1612,9 @@ export abstract class AbstractAuthRequestHandler { public getEmailActionLink( requestType: string, email: string, actionCodeSettings?: ActionCodeSettings, newEmail?: string): Promise { - let request = { - requestType, - email, + let request = { + requestType, + email, returnOobLink: true, ...(typeof newEmail !== 'undefined') && { newEmail }, }; @@ -2297,7 +2297,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { } public getPasskeyConfig(tenantId?: string): Promise { - return this.invokeRequestHandler(this.authResourceUrlBuilder, + return this.invokeRequestHandler(this.authResourceUrlBuilder, tenantId? GET_TENANT_PASSKEY_CONFIG: GET_PASSKEY_CONFIG, {}, {}) .then((response: any) => { return response as PasskeyConfigServerResponse; @@ -2305,12 +2305,12 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { } public updatePasskeyConfig(isCreateRequest: boolean, tenantId?: string, - options?: PasskeyConfigRequest, rpId?: string): Promise { + options?: PasskeyConfigRequest): Promise { try { - const request = PasskeyConfig.buildServerRequest(isCreateRequest, options, rpId); + const request = PasskeyConfig.buildServerRequest(isCreateRequest, options); const updateMask = utils.generateUpdateMask(request); return this.invokeRequestHandler( - this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG, + this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG, request, { updateMask: updateMask.join(',') }) .then((response: any) => { return response as PasskeyConfigServerResponse; diff --git a/src/auth/passkey-config-manager.ts b/src/auth/passkey-config-manager.ts index 100ba2df89..ba89fbeb0a 100644 --- a/src/auth/passkey-config-manager.ts +++ b/src/auth/passkey-config-manager.ts @@ -17,11 +17,11 @@ import { App } from '../app'; import { AuthRequestHandler, } from './auth-api-request'; -import { - PasskeyConfig, - PasskeyConfigClientRequest, - PasskeyConfigRequest, - PasskeyConfigServerResponse +import { + PasskeyConfig, + PasskeyConfigClientRequest, + PasskeyConfigRequest, + PasskeyConfigServerResponse } from './passkey-config'; /** @@ -29,10 +29,10 @@ import { */ export class PasskeyConfigManager { private readonly authRequestHandler: AuthRequestHandler; - + /** * Initializes a PasskeyConfigManager instance for a specified FirebaseApp. - * + * * @param app - The Firebase app associated with this PasskeyConfigManager instance. * * @constructor @@ -44,7 +44,7 @@ export class PasskeyConfigManager { /** * Retrieves the Passkey Configuration. - * + * * @param tenantId - (optional) The tenant ID if querying passkeys on a specific tenant. * @returns A promise fulfilled with the passkey configuration. */ @@ -57,15 +57,15 @@ export class PasskeyConfigManager { /** * Creates a new passkey configuration. - * + * * @param rpId - The relying party ID. * @param passkeyConfigRequest - Configuration details for the passkey. * @param tenantId - (optional) The tenant ID for which the passkey config is created. * @returns A promise fulfilled with the newly created passkey configuration. */ - public createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, + public createPasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { - return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId) + return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest) .then((response: PasskeyConfigClientRequest) => { return new PasskeyConfig(response); }); @@ -73,7 +73,7 @@ export class PasskeyConfigManager { /** * Updates an existing passkey configuration. - * + * * @param passkeyConfigRequest - Updated configuration details for the passkey. * @param tenantId - (optional) The tenant ID for which the passkey config is updated. * @returns A promise fulfilled with the updated passkey configuration. diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts index 68e1b66237..20d2f0c9fb 100644 --- a/src/auth/passkey-config.ts +++ b/src/auth/passkey-config.ts @@ -21,6 +21,7 @@ import { deepCopy } from '../utils/deep-copy'; * Interface representing the properties to update in the provided passkey config. */ export interface PasskeyConfigRequest { + rpId?: string; /** * An array of website or app origins associated with the customer's sites or apps. * Only challenges signed from these origins will be allowed for signing in with passkeys. @@ -70,24 +71,24 @@ export class PasskeyConfig { * @param passkeyConfigRequest - Passkey config to be set. * @param rpId - (optional) Relying party ID if it's a create request. * @throws FirebaseAuthError - If validation fails. - * + * * @internal */ - private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): void { + private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest): void { // Validation for creating a new PasskeyConfig. - if (isCreateRequest && !validator.isNonEmptyString(rpId)) { + if (isCreateRequest && !validator.isNonEmptyString(passkeyConfigRequest?.rpId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, "'rpId' must be a valid non-empty string.", ); } - // Validation for updating an existing PasskeyConfig. - if (!isCreateRequest && typeof rpId !== 'undefined') { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - "'rpId' cannot be changed once created.", - ); - } + // // Validation for updating an existing PasskeyConfig. + // if (!isCreateRequest && typeof rpId !== 'undefined') { + // throw new FirebaseAuthError( + // AuthClientErrorCode.INVALID_ARGUMENT, + // "'rpId' cannot be changed once created.", + // ); + // } if (!validator.isNonNullObject(passkeyConfigRequest)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -95,6 +96,7 @@ export class PasskeyConfig { ); } const validKeys = { + rpId: true, expectedOrigins: true, }; // Check for unsupported top-level attributes. @@ -119,7 +121,7 @@ export class PasskeyConfig { "'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.", ); } - } + } } /** @@ -129,15 +131,15 @@ export class PasskeyConfig { * @param rpId - (optional) Relying party ID for the request if it's a create request. * @returns The equivalent server request. * @throws FirebaseAuthError - If validation fails. - * + * * @internal */ - public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, - rpId?: string): PasskeyConfigClientRequest { - PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest, rpId); + public static buildServerRequest(isCreateRequest: boolean, + passkeyConfigRequest?: PasskeyConfigRequest): PasskeyConfigClientRequest { + PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest); const request: PasskeyConfigClientRequest = {}; - if (isCreateRequest && typeof rpId !== 'undefined') { - request.rpId = rpId; + if (typeof passkeyConfigRequest?.rpId !== 'undefined') { + request.rpId = passkeyConfigRequest.rpId; } if (typeof passkeyConfigRequest?.expectedOrigins !== 'undefined') { request.expectedOrigins = passkeyConfigRequest.expectedOrigins; @@ -149,7 +151,7 @@ export class PasskeyConfig { * The Passkey Config object constructor. * @param response - The server-side response used to initialize the Passkey Config object. * @constructor - * + * * @internal */ constructor(response: PasskeyConfigServerResponse) { diff --git a/test/unit/auth/passkey-config-manager.spec.ts b/test/unit/auth/passkey-config-manager.spec.ts index 5ce7c576ca..be7235cd58 100644 --- a/test/unit/auth/passkey-config-manager.spec.ts +++ b/test/unit/auth/passkey-config-manager.spec.ts @@ -124,6 +124,7 @@ describe('PasskeyConfigManager', () => { const rpId = 'project-id.firebaseapp.com'; const expectedOrigins: string[] = ['app1', 'example.com'] const passkeyConfigRequest: PasskeyConfigRequest = { + rpId: rpId, expectedOrigins: expectedOrigins , }; const expectedPasskeyConfig = new PasskeyConfig(GET_CONFIG_RESPONSE); @@ -140,19 +141,19 @@ describe('PasskeyConfigManager', () => { return (passkeyConfigManager as any).createPasskeyConfig(null as unknown as PasskeyConfigRequest) .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); - + it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest) + return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(passkeyConfigRequest) .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); - + it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest) + return malformedAccessTokenPasskeyConfigManager.createPasskeyConfig(passkeyConfigRequest) .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); - + it('should be rejected given an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest) + return rejectedPromiseAccessTokenPasskeyConfigManager.createPasskeyConfig(passkeyConfigRequest) .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); @@ -161,10 +162,10 @@ describe('PasskeyConfigManager', () => { const stub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig') .returns(Promise.resolve(GET_CONFIG_RESPONSE)); stubs.push(stub); - return passkeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest) + return passkeyConfigManager.createPasskeyConfig(passkeyConfigRequest) .then((actualPasskeyConfig) => { // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest, rpId); + expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest); // Confirm expected Passkey Config object returned. expect(actualPasskeyConfig).to.deep.equal(expectedPasskeyConfig); }); @@ -175,12 +176,12 @@ describe('PasskeyConfigManager', () => { const stub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig') .returns(Promise.reject(expectedError)); stubs.push(stub); - return passkeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest) + return passkeyConfigManager.createPasskeyConfig(passkeyConfigRequest) .then(() => { throw new Error('Unexpected success'); }, (error) => { // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest, rpId); + expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest); // Confirm expected error returned. expect(error).to.equal(expectedError); }); diff --git a/test/unit/auth/passkey-config.spec.ts b/test/unit/auth/passkey-config.spec.ts index 1d61bf6ec0..bd19310773 100644 --- a/test/unit/auth/passkey-config.spec.ts +++ b/test/unit/auth/passkey-config.spec.ts @@ -36,50 +36,55 @@ describe('PasskeyConfig', () => { expectedOrigins: ['app1', 'example.com'], }; const passkeyConfigRequest: PasskeyConfigRequest = { + rpId: 'project-id.firebaseapp.com', expectedOrigins: ['app1', 'example.com'], }; describe('buildServerRequest', () => { describe('for a create request', () => { - const validRpId = 'project-id.firebaseapp.com'; it('should create a client request with valid params', () => { const expectedRequest: PasskeyConfigClientRequest = { - rpId: validRpId, + rpId: passkeyConfigRequest.rpId, expectedOrigins: passkeyConfigRequest.expectedOrigins, }; - expect(PasskeyConfig.buildServerRequest(true, passkeyConfigRequest, validRpId)).to.deep.equal(expectedRequest); + expect(PasskeyConfig.buildServerRequest(true, passkeyConfigRequest)).to.deep.equal(expectedRequest); }); const invalidRpId = [null, NaN, 0, 1, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidRpId.forEach((rpId) => { it(`should throw on invalid rpId ${rpId}`, () => { - expect(() => PasskeyConfig.buildServerRequest(true, passkeyConfigRequest, rpId as any)).to.throw( + const passkeyConfigRequestWithInvalidRpId: PasskeyConfigRequest = { + rpId: rpId as any, + expectedOrigins: passkeyConfigRequest.expectedOrigins, + }; + expect(() => PasskeyConfig.buildServerRequest(true, passkeyConfigRequestWithInvalidRpId)).to.throw( '\'rpId\' must be a valid non-empty string'); }); }); - }); + }); describe('for update request', () => { - it('should throw error if rpId is defined', () => { - expect(() => { - PasskeyConfig.buildServerRequest(false, passkeyConfigRequest, 'rpId'); - }).to.throw('\'rpId\' cannot be changed once created.'); - }); + // it('should throw error if rpId is defined', () => { + // expect(() => { + // PasskeyConfig.buildServerRequest(false, passkeyConfigRequest); + // }).to.throw('\'rpId\' cannot be changed once created.'); + // }); it('should create a client request with valid params', () => { - const expectedRequest: PasskeyConfigClientRequest = { + const expectedRequest: PasskeyConfigClientRequest = { + rpId: passkeyConfigRequest.rpId, expectedOrigins: passkeyConfigRequest.expectedOrigins, }; expect(PasskeyConfig.buildServerRequest(false, passkeyConfigRequest)).to.deep.equal(expectedRequest); }); }); - + describe('for passkey config request', () => { const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { - it('should throw on invalid PasskeyConfigRequest:' + JSON.stringify(request), () => { + it('should throw on invalid PasskeyConfigRequest: ' + JSON.stringify(request), () => { expect(() => { PasskeyConfig.buildServerRequest(false, request as any); - }).to.throw('\'passkeyConfigRequest\' must be a valid non-empty object.\''); + }).to.throw('\'passkeyConfigRequest\' must be a valid non-empty object.'); }); }); @@ -88,7 +93,7 @@ describe('PasskeyConfig', () => { invalidAttributeObject.invalidAttribute = 'invalid'; expect(() => { PasskeyConfig.buildServerRequest(false, invalidAttributeObject); - }).to.throw('\'invalidAttribute\' is not a valid PasskeyConfigRequest parameter.'); + }).to.throw('\'invalidAttribute\' is not a valid PasskeyConfigRequest parameter.'); }); const invalidExpectedOriginsObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; @@ -98,7 +103,7 @@ describe('PasskeyConfig', () => { request.expectedOrigins = expectedOriginsObject; expect(() => { PasskeyConfig.buildServerRequest(false, request as any); - }).to.throw('\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\''); + }).to.throw('\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.'); }); }); });