From 629f8ac95c30e9e5437651fb439d24cf7e35d379 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 4 Oct 2023 13:03:13 -0400 Subject: [PATCH 01/24] passkey config admin changes --- etc/firebase-admin.auth.api.md | 36 +++++++ package.json | 2 +- src/auth/auth-api-request.ts | 70 +++++++++++++ src/auth/auth.ts | 12 +++ src/auth/index.ts | 9 ++ src/auth/passkey-config-manager.ts | 50 +++++++++ src/auth/passkey-config.ts | 131 ++++++++++++++++++++++++ test/integration/auth.spec.ts | 51 +++++++++- test/unit/auth/passkey-config.spec.ts | 140 ++++++++++++++++++++++++++ 9 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 src/auth/passkey-config-manager.ts create mode 100644 src/auth/passkey-config.ts create mode 100644 test/unit/auth/passkey-config.spec.ts diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 3723abd051..fe03cb5ed8 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -51,6 +51,7 @@ export interface AllowlistOnlyWrap { export class Auth extends BaseAuth { // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts get app(): App; + passkeyConfigManager(): PasskeyConfigManager; projectConfigManager(): ProjectConfigManager; tenantManager(): TenantManager; } @@ -344,6 +345,41 @@ export interface OIDCUpdateAuthProviderRequest { responseType?: OAuthResponseType; } +// @public (undocumented) +export class PasskeyConfig { + // Warning: (ae-forgotten-export) The symbol "PasskeyConfigServerResponse" needs to be exported by the entry point index.d.ts + constructor(response: PasskeyConfigServerResponse); + // Warning: (ae-forgotten-export) The symbol "PasskeyConfigClientRequest" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest; + // (undocumented) + readonly expectedOrigins?: string[]; + // (undocumented) + readonly name?: string; + // (undocumented) + readonly rpId?: string; + // (undocumented) + toJSON(): object; +} + +// @public (undocumented) +export class PasskeyConfigManager { + constructor(app: App); + // (undocumented) + createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; + // (undocumented) + getPasskeyConfig(tenantId?: string): Promise; + // (undocumented) + updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; +} + +// @public (undocumented) +export interface PasskeyConfigRequest { + // (undocumented) + expectedOrigins?: string[]; +} + // @public export interface PasswordPolicyConfig { constraints?: CustomStrengthOptionsConfig; diff --git a/package.json b/package.json index 446ad2cf42..68f51b1157 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "11.10.1", + "version": "11.11.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index c4ba2ac811..e7eac78547 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -43,6 +43,7 @@ import { SAMLUpdateAuthProviderRequest } from './auth-config'; import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; +import {PasskeyConfig, PasskeyConfigServerResponse, PasskeyConfigRequest} from './passkey-config'; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { @@ -2070,6 +2071,54 @@ const CREATE_TENANT = new ApiSettings('/tenants', 'POST') } }); +/** Instantiates the getPasskeyConfig endpoint settings. */ +const GET_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig', 'GET') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + +/** Instantiates the getPasskeyConfig endpoint settings. */ +const GET_TENANT_PASSKEY_CONFIG = new ApiSettings('/tenants/{tenantId}/passkeyConfig', 'GET') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + +/** Instantiates the getPasskeyConfig endpoint settings. */ +const UPDATE_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig?updateMask={updateMask}', 'PATCH') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + +/** Instantiates the getPasskeyConfig endpoint settings. */ +const UPDATE_TENANT_PASSKEY_CONFIG = new ApiSettings('/tenant/{tenantId}/passkeyConfig?updateMask={updateMask}', 'PATCH') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + /** * Utility for sending requests to Auth server that are Auth instance related. This includes user, tenant, @@ -2245,6 +2294,27 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { return Promise.reject(e); } } + + public getPasskeyConfig(tenantId?: string): Promise { + return this.invokeRequestHandler(this.authResourceUrlBuilder, tenantId? GET_TENANT_PASSKEY_CONFIG: GET_PASSKEY_CONFIG, {}, {}) + .then((response: any) => { + return response as PasskeyConfigServerResponse; + }); + } + + public updatePasskeyConfig(isCreateRequest: boolean, tenantId?: string, options?: PasskeyConfigRequest, rpId?: string): Promise { + try { + const request = PasskeyConfig.buildServerRequest(isCreateRequest, options, rpId); + const updateMask = utils.generateUpdateMask(request); + return this.invokeRequestHandler( + this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG, request, { updateMask: updateMask.join(',') }) + .then((response: any) => { + return response as PasskeyConfigServerResponse; + }); + } catch (e) { + return Promise.reject(e); + } + } } /** diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 4808fbbdc0..f31ed2dab0 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -20,6 +20,7 @@ import { AuthRequestHandler } from './auth-api-request'; import { TenantManager } from './tenant-manager'; import { BaseAuth } from './base-auth'; import { ProjectConfigManager } from './project-config-manager'; +import { PasskeyConfigManager } from './passkey-config-manager'; /** * Auth service bound to the provided app. @@ -29,6 +30,7 @@ export class Auth extends BaseAuth { private readonly tenantManager_: TenantManager; private readonly projectConfigManager_: ProjectConfigManager; + private readonly passkeyConfigManager_: PasskeyConfigManager; private readonly app_: App; /** @@ -41,6 +43,7 @@ export class Auth extends BaseAuth { this.app_ = app; this.tenantManager_ = new TenantManager(app); this.projectConfigManager_ = new ProjectConfigManager(app); + this.passkeyConfigManager_ = new PasskeyConfigManager(app); } /** @@ -69,4 +72,13 @@ export class Auth extends BaseAuth { public projectConfigManager(): ProjectConfigManager { return this.projectConfigManager_; } + + /** + * Returns the passkey config manager instance. + * + * @returns The passkey config manager instance . + */ + public passkeyConfigManager(): PasskeyConfigManager { + return this.passkeyConfigManager_; + } } diff --git a/src/auth/index.ts b/src/auth/index.ts index a559a706f8..b3f5f954c7 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -142,6 +142,15 @@ export { ProjectConfigManager, } from './project-config-manager'; +export { + PasskeyConfigRequest, + PasskeyConfig, +} from './passkey-config'; + +export { + PasskeyConfigManager, +} from './passkey-config-manager'; + export { DecodedIdToken, DecodedAuthBlockingToken diff --git a/src/auth/passkey-config-manager.ts b/src/auth/passkey-config-manager.ts new file mode 100644 index 0000000000..184f96a8bd --- /dev/null +++ b/src/auth/passkey-config-manager.ts @@ -0,0 +1,50 @@ +/*! + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { App } from '../app'; +import { + AuthRequestHandler, +} from './auth-api-request'; +import {PasskeyConfig, PasskeyConfigClientRequest, PasskeyConfigRequest, PasskeyConfigServerResponse} from './passkey-config'; + + +export class PasskeyConfigManager { + private readonly authRequestHandler: AuthRequestHandler; + + constructor(app: App) { + this.authRequestHandler = new AuthRequestHandler(app); + } + + public getPasskeyConfig(tenantId?: string): Promise { + return this.authRequestHandler.getPasskeyConfig() + .then((response: PasskeyConfigServerResponse) => { + return new PasskeyConfig(response); + }); + } + + public createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { + return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId) + .then((response: PasskeyConfigClientRequest) => { + return new PasskeyConfig(response); + }) + } + + public updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { + return this.authRequestHandler.updatePasskeyConfig(false, tenantId, passkeyConfigRequest) + .then((response: PasskeyConfigClientRequest) => { + return new PasskeyConfig(response); + }) + } +} diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts new file mode 100644 index 0000000000..667e46333d --- /dev/null +++ b/src/auth/passkey-config.ts @@ -0,0 +1,131 @@ +/*! + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as validator from '../utils/validator'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import {deepCopy} from '../utils/deep-copy'; + +export interface PasskeyConfigRequest { + expectedOrigins?: string[]; +} + +export interface PasskeyConfigServerResponse { + name?: string; + rpId?: string; + expectedOrigins?: string[]; +} + +export interface PasskeyConfigClientRequest { + rpId?: string; + expectedOrigins?: string[]; +} + + +export class PasskeyConfig { + public readonly name?: string; + public readonly rpId?: string; + public readonly expectedOrigins?: string[]; + + private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string) { + if(isCreateRequest && !validator.isNonEmptyString(rpId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `'rpId' must be a valid non-empty string'`, + ); + } + 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, + `'passkeyConfigRequest' must be a valid non-empty object.'`, + ); + } + const validKeys = { + expectedOrigins: true, + }; + // Check for unsupported top level attributes. + for (const key in passkeyConfigRequest) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `'${key}' is not a valid PasskeyConfigRequest parameter.`, + ); + } + } + if(!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins) || !validator.isNonNullObject(passkeyConfigRequest.expectedOrigins)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, + ); + } + for(const origin in passkeyConfigRequest.expectedOrigins) { + if(!validator.isString(origin)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, + ); + } + } + }; + + public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest { + PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest, rpId); + let request: PasskeyConfigClientRequest = {}; + if(isCreateRequest && typeof rpId !== 'undefined') { + request.rpId = rpId; + } + if(typeof request.expectedOrigins !== 'undefined') { + request.expectedOrigins = passkeyConfigRequest?.expectedOrigins; + } + return request; + }; + + constructor(response: PasskeyConfigServerResponse) { + if(typeof response.name !== 'undefined') { + this.name = response.name; + } + if(typeof response.rpId !== 'undefined') { + this.rpId = response.rpId; + }; + if(typeof response.expectedOrigins !== 'undefined') { + this.expectedOrigins = response.expectedOrigins; + } + }; + + public toJSON(): object { + const json = { + name: deepCopy(this.name), + rpId: deepCopy(this.rpId), + expectedOrigins: deepCopy(this.expectedOrigins), + }; + if(typeof json.name === 'undefined') { + delete json.name; + } + if(typeof json.rpId === 'undefined') { + delete json.rpId; + } + if(typeof json.expectedOrigins === 'undefined') { + delete json.expectedOrigins; + } + return json; + } + +}; + diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 7b113b3156..9473ba64f1 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -32,10 +32,11 @@ import { AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, UserMetadata, MultiFactorConfig, - PasswordPolicyConfig, SmsRegionConfig, + PasswordPolicyConfig, SmsRegionConfig } from '../../lib/auth/index'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; +import {PasskeyConfigRequest} from '../../src/auth'; const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires @@ -2197,6 +2198,54 @@ describe('admin.auth', () => { }); }); + describe('Passkey config management operations', () => { + // Define expected passkey configuration + const expectedPasskeyConfig = { + name: `projects/{$projectId}/passkeyConfig`, + rpId: `{$projectId}.firebaseapp.com`, + expectedOrigins: ['app1', 'example.com'], + }; + + // Helper function to reset passkey config to the initial state + async function resetPasskeyConfig() { + const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; + await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); + } + + // Before each test, reset the passkey config to the initial state + beforeEach(async () => { + await resetPasskeyConfig(); + }); + + it('createPasskeyConfig() should create passkey config with expected passkeyConfig', async () => { + const rpId = `{$projectId}.firebaseapp.com`; + const createRequest = { expectedOrigins: ['app1', 'example.com'] }; + + const createdPasskeyConfig = await getAuth().passkeyConfigManager().createPasskeyConfig(rpId, createRequest); + const passkeyConfigObj = createdPasskeyConfig.toJSON(); + + expect(passkeyConfigObj).to.deep.equal(expectedPasskeyConfig); + }); + + it('getPasskeyConfig() should resolve with expected passkeyConfig', async () => { + const actualPasskeyConfig = await getAuth().passkeyConfigManager().getPasskeyConfig(); + const actualPasskeyConfigObj = actualPasskeyConfig.toJSON(); + + expect(actualPasskeyConfigObj).to.deep.equal(expectedPasskeyConfig); + }); + + it('updatePasskeyConfig() should resolve with updated expectedOrigins', async () => { + const updateRequest = { expectedOrigins: ['app1', 'example.com', 'app2'] }; + const expectedUpdatedPasskeyConfig = { ...expectedPasskeyConfig, expectedOrigins: updateRequest.expectedOrigins }; + + const updatedPasskeyConfig = await getAuth().passkeyConfigManager().updatePasskeyConfig(updateRequest); + const passkeyConfigObj = updatedPasskeyConfig.toJSON(); + + expect(passkeyConfigObj).to.deep.equal(expectedUpdatedPasskeyConfig); + }); + }); + + describe('SAML configuration operations', () => { const authProviderConfig1 = { providerId: randomSamlProviderId(), diff --git a/test/unit/auth/passkey-config.spec.ts b/test/unit/auth/passkey-config.spec.ts new file mode 100644 index 0000000000..f8c3887930 --- /dev/null +++ b/test/unit/auth/passkey-config.spec.ts @@ -0,0 +1,140 @@ +/*! + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { + PasskeyConfig, PasskeyConfigRequest, PasskeyConfigServerResponse, PasskeyConfigClientRequest + } from '../../../src/auth/passkey-config'; +import {deepCopy} from '../../../src/utils/deep-copy'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('PasskeyConfig', () => { + const serverResponse: PasskeyConfigServerResponse = { + name: `projects/project-id/passkeyConfig`, + rpId: `project-id.firebaseapp.com`, + expectedOrigins: ['app1', 'example.com'], + }; + const passkeyConfigRequest: PasskeyConfigRequest = { + expectedOrigins: ['app1', 'website.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, + expectedOrigins: passkeyConfigRequest.expectedOrigins, + }; + expect(PasskeyConfig.buildServerRequest(true, passkeyConfigRequest, validRpId)).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(`'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, 'project-id.firebaseapp.com')).to.throw(`'rpId' must be a valid non-empty string'`); + }); + + it('should create a client request with valid params', () => { + const expectedRequest: PasskeyConfigClientRequest = { + 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), () => { + expect(() => { + PasskeyConfig.buildServerRequest(true, request as any); + }).to.throw(`'passkeyConfigRequest' must be a valid non-empty object.'`); + }); + }); + + it('should throw for invalid passkey config request attribute', () => { + const invalidAttributeObject = deepCopy(passkeyConfigRequest) as any; + invalidAttributeObject.invalidAttribute = 'invalid'; + expect(() => { + PasskeyConfig.buildServerRequest(invalidAttributeObject); + }).to.throw(`'invalidAttribute' is not a valid PasskeyConfigRequest parameter.`); + }); + + const invalidExpectedOriginsObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidExpectedOriginsObjects.forEach((expectedOriginsObject) => { + it('should throw for invalid expected origins values', () => { + let request = deepCopy(passkeyConfigRequest) as any; + request.expectedOrigins = expectedOriginsObject; + expect(() => { + PasskeyConfig.buildServerRequest(true, request as any); + }).to.throw(`'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`); + }); + }); + }); + }); + + describe('constructor', () => { + const passkeyConfig = new PasskeyConfig(serverResponse); + it('should not throw on valid initialization', () => { + expect(() => new PasskeyConfig(serverResponse)).not.to.throw(); + }); + + it('should set readonly properties', () => { + const expectedServerResponse = { + name: `projects/project-id/passkeyConfig`, + rpId: `project-id.firebaseapp.com`, + expectedOrigins: ['app1', 'example.com'], + }; + expect(passkeyConfig.name).to.equal(expectedServerResponse.name); + expect(passkeyConfig.rpId).to.equal(expectedServerResponse.rpId); + expect(passkeyConfig.expectedOrigins).to.equal(expectedServerResponse.expectedOrigins); + }); + }); + + describe('toJSON', () => { + it('should return the expected object representation of passkey config', () => { + expect(new PasskeyConfig(serverResponse).toJSON()).to.deep.equal({ + name: deepCopy(serverResponse.name), + rpId: deepCopy(serverResponse).rpId, + expectedOrigins: deepCopy(serverResponse.expectedOrigins), + }); + }); + + it('should not populate optional fields if not available', () => { + const serverResponseOptionalCopy: PasskeyConfigServerResponse = deepCopy(serverResponse); + delete serverResponseOptionalCopy.rpId; + delete serverResponseOptionalCopy.expectedOrigins; + expect(new PasskeyConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ + name: deepCopy(serverResponse.name), + }); + }); + }); +}); \ No newline at end of file From a2c24312303f19e28fecc683a242509e3204e280 Mon Sep 17 00:00:00 2001 From: Rishabh Ajay <63163183+rishabhAjay@users.noreply.github.com> Date: Wed, 4 Oct 2023 00:00:09 +0530 Subject: [PATCH 02/24] Bug Fix for issue #2320 (#2321) --- src/auth/auth-api-request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index e7eac78547..945d1f93d9 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -2133,7 +2133,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { * The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp. * * @param app - The app used to fetch access tokens to sign API requests. - * @constructor. + * @constructor */ constructor(app: App) { super(app); From 62b00037b1a519456da8c9f78ff0781c2b04c2e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:20:40 +0000 Subject: [PATCH 03/24] fixed unit test errors --- etc/firebase-admin.auth.api.md | 4 +- package-lock.json | 8 +- src/auth/passkey-config-manager.ts | 8 +- src/auth/passkey-config.ts | 22 +- test/integration/auth.spec.ts | 12 +- test/unit/auth/passkey-config-manager.spec.ts | 256 ++++++++++++++++++ test/unit/auth/passkey-config.spec.ts | 18 +- 7 files changed, 290 insertions(+), 38 deletions(-) create mode 100644 test/unit/auth/passkey-config-manager.spec.ts diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index fe03cb5ed8..be128aafab 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -367,11 +367,11 @@ export class PasskeyConfig { export class PasskeyConfigManager { constructor(app: App); // (undocumented) - createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; + createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; // (undocumented) getPasskeyConfig(tenantId?: string): Promise; // (undocumented) - updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; + updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; } // @public (undocumented) diff --git a/package-lock.json b/package-lock.json index 40a768beba..576846d5ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "11.10.1", + "version": "11.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1405,9 +1405,9 @@ } }, "@types/firebase-token-generator": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.30.tgz", - "integrity": "sha512-GcNz25MRki9ZpVfvNNrthx4t3XXjgIZ2wv729ea9F4n/1PZf4QIZlzTGoDTDeV417vmd6cPTYKUzPf4rR+qGhw==", + "version": "2.0.31", + "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.31.tgz", + "integrity": "sha512-mR6GuDPxFiD7nZ3x2NddRvo42ZBS22PHHNdAWuQvqp/1LfiYshZ6PzJD2+JOyLw5ZErgmw7F8R1nfpXnvBOxGg==", "dev": true }, "@types/glob": { diff --git a/src/auth/passkey-config-manager.ts b/src/auth/passkey-config-manager.ts index 184f96a8bd..045f753684 100644 --- a/src/auth/passkey-config-manager.ts +++ b/src/auth/passkey-config-manager.ts @@ -17,7 +17,7 @@ import { App } from '../app'; import { AuthRequestHandler, } from './auth-api-request'; -import {PasskeyConfig, PasskeyConfigClientRequest, PasskeyConfigRequest, PasskeyConfigServerResponse} from './passkey-config'; +import { PasskeyConfig, PasskeyConfigClientRequest, PasskeyConfigRequest, PasskeyConfigServerResponse } from './passkey-config'; export class PasskeyConfigManager { @@ -28,7 +28,7 @@ export class PasskeyConfigManager { } public getPasskeyConfig(tenantId?: string): Promise { - return this.authRequestHandler.getPasskeyConfig() + return this.authRequestHandler.getPasskeyConfig(tenantId) .then((response: PasskeyConfigServerResponse) => { return new PasskeyConfig(response); }); @@ -38,13 +38,13 @@ export class PasskeyConfigManager { return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId) .then((response: PasskeyConfigClientRequest) => { return new PasskeyConfig(response); - }) + }); } public updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { return this.authRequestHandler.updatePasskeyConfig(false, tenantId, passkeyConfigRequest) .then((response: PasskeyConfigClientRequest) => { return new PasskeyConfig(response); - }) + }); } } diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts index 667e46333d..efa3b90805 100644 --- a/src/auth/passkey-config.ts +++ b/src/auth/passkey-config.ts @@ -69,20 +69,20 @@ export class PasskeyConfig { ); } } - if(!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins) || !validator.isNonNullObject(passkeyConfigRequest.expectedOrigins)) { + if(!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, ); } - for(const origin in passkeyConfigRequest.expectedOrigins) { - if(!validator.isString(origin)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, - ); - } - } + for (const origin of passkeyConfigRequest.expectedOrigins) { + if (!validator.isNonEmptyString(origin)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, + ); + } + } }; public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest { @@ -91,8 +91,8 @@ export class PasskeyConfig { if(isCreateRequest && typeof rpId !== 'undefined') { request.rpId = rpId; } - if(typeof request.expectedOrigins !== 'undefined') { - request.expectedOrigins = passkeyConfigRequest?.expectedOrigins; + if(typeof passkeyConfigRequest?.expectedOrigins !== 'undefined') { + request.expectedOrigins = passkeyConfigRequest.expectedOrigins; } return request; }; diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 9473ba64f1..1d38e836d6 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -36,7 +36,6 @@ import { } from '../../lib/auth/index'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; -import {PasskeyConfigRequest} from '../../src/auth'; const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires @@ -2206,15 +2205,10 @@ describe('admin.auth', () => { expectedOrigins: ['app1', 'example.com'], }; - // Helper function to reset passkey config to the initial state - async function resetPasskeyConfig() { - const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; - await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); - } - // Before each test, reset the passkey config to the initial state beforeEach(async () => { - await resetPasskeyConfig(); + const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; + await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); }); it('createPasskeyConfig() should create passkey config with expected passkeyConfig', async () => { @@ -2223,7 +2217,7 @@ describe('admin.auth', () => { const createdPasskeyConfig = await getAuth().passkeyConfigManager().createPasskeyConfig(rpId, createRequest); const passkeyConfigObj = createdPasskeyConfig.toJSON(); - + expect(passkeyConfigObj).to.deep.equal(expectedPasskeyConfig); }); diff --git a/test/unit/auth/passkey-config-manager.spec.ts b/test/unit/auth/passkey-config-manager.spec.ts new file mode 100644 index 0000000000..b896fd35ec --- /dev/null +++ b/test/unit/auth/passkey-config-manager.spec.ts @@ -0,0 +1,256 @@ +/*! + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { PasskeyConfigManager } from '../../../src/auth/passkey-config-manager'; +import { + PasskeyConfig, PasskeyConfigServerResponse, PasskeyConfigRequest, +} from '../../../src/auth/passkey-config'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('PasskeyConfigManager', () => { + let mockApp: FirebaseApp; + let passkeyConfigManager: PasskeyConfigManager; + let nullAccessTokenPasskeyConfigManager: PasskeyConfigManager; + let malformedAccessTokenPasskeyConfigManager: PasskeyConfigManager; + let rejectedPromiseAccessTokenPasskeyConfigManager: PasskeyConfigManager; + const GET_CONFIG_RESPONSE: PasskeyConfigServerResponse = { + name: `projects/project-id/passkeyConfig`, + rpId: `project-id.firebaseapp.com`, + expectedOrigins: ['app1', 'example.com'], +}; + + before(() => { + mockApp = mocks.app(); + passkeyConfigManager = new PasskeyConfigManager(mockApp); + nullAccessTokenPasskeyConfigManager = new PasskeyConfigManager( + mocks.appReturningNullAccessToken()); + malformedAccessTokenPasskeyConfigManager = new PasskeyConfigManager( + mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenPasskeyConfigManager = new PasskeyConfigManager( + mocks.appRejectedWhileFetchingAccessToken()); + }); + + after(() => { + return mockApp.delete(); + }); + + describe('getPasskeyConfig()', () => { + const expectedPasskeyConfig = new PasskeyConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenPasskeyConfigManager.getPasskeyConfig() + .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.getPasskeyConfig() + .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.getPasskeyConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a Passkey Config on success', () => { + // Stub getPasskeyConfig to return expected result. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getPasskeyConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(stub); + return passkeyConfigManager.getPasskeyConfig() + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce; + // Confirm expected project config returned. + expect(result).to.deep.equal(expectedPasskeyConfig); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getConfig to throw a backend error. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getPasskeyConfig') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return passkeyConfigManager.getPasskeyConfig() + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce; + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('createPasskeyConfig()', () => { + const rpId: string = 'project-id.firebaseapp.com'; + const expectedOrigins: string[] = ['app1', 'example.com'] + const passkeyConfigRequest: PasskeyConfigRequest = { + expectedOrigins: expectedOrigins , + }; + const expectedPasskeyConfig = new PasskeyConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to create the config provided.'); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + sinon.restore(); + }); + + it('should be rejected given no passkeyConfigOptions', () => { + 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', () => { + console.log("TEST===" + JSON.stringify(passkeyConfigRequest)); + return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, 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) + .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) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a PasskeyConfig on createPasskeyConfig request success', () => { + // Stub createPasskeyConfig to return expected result. + const stub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(stub); + return passkeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest) + .then((actualPasskeyConfig) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest, rpId); + // Confirm expected Passkey Config object returned. + expect(actualPasskeyConfig).to.deep.equal(expectedPasskeyConfig); + }); + }); + + it('should throw an error when createPasskeyConfig returns an error', () => { + // Stub createPasskeyConfig to throw a backend error. + const stub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return passkeyConfigManager.createPasskeyConfig(rpId, 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); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('updatePasskeyConfig()', () => { + const passkeyConfigOptions: PasskeyConfigRequest = { + expectedOrigins: ['app1', 'example.com', 'app2'], + }; + const expectedPasskeyConfig = new PasskeyConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to update the config provided.'); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no passkeyConfigOptions', () => { + return (passkeyConfigManager as any).updatePasskeyConfig(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.updatePasskeyConfig(passkeyConfigOptions) + .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.updatePasskeyConfig(passkeyConfigOptions) + .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.updatePasskeyConfig(passkeyConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a PasskeyConfig on updatePasskeyConfig request success', () => { + // Stub updatePasskeyConfig to return expected result. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(updateConfigStub); + return passkeyConfigManager.updatePasskeyConfig(passkeyConfigOptions) + .then((actualPasskeyConfig) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(false, undefined, passkeyConfigOptions); + // Confirm expected Project Config object returned. + expect(actualPasskeyConfig).to.deep.equal(expectedPasskeyConfig); + }); + }); + + it('should throw an error when updatePasskeyConfig returns an error', () => { + // Stub updatePasskeyConfig to throw a backend error. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig') + .returns(Promise.reject(expectedError)); + stubs.push(updateConfigStub); + return passkeyConfigManager.updatePasskeyConfig(passkeyConfigOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(false, undefined, passkeyConfigOptions); + // 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 f8c3887930..2dc9ee2301 100644 --- a/test/unit/auth/passkey-config.spec.ts +++ b/test/unit/auth/passkey-config.spec.ts @@ -36,7 +36,7 @@ describe('PasskeyConfig', () => { expectedOrigins: ['app1', 'example.com'], }; const passkeyConfigRequest: PasskeyConfigRequest = { - expectedOrigins: ['app1', 'website.com'], + expectedOrigins: ['app1', 'example.com'], }; describe('buildServerRequest', () => { describe('for a create request', () => { @@ -51,15 +51,17 @@ describe('PasskeyConfig', () => { 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(`'rpId' must be a valid non-empty string'`); + it(`should throw on invalid rpId ${rpId}`, () => { + expect(() => PasskeyConfig.buildServerRequest(true, passkeyConfigRequest, rpId as any)).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, 'project-id.firebaseapp.com')).to.throw(`'rpId' must be a valid non-empty string'`); + expect(() => { + PasskeyConfig.buildServerRequest(false, passkeyConfigRequest, 'rpId'); + }).to.throw(`'rpId' cannot be changed once created.`); }); it('should create a client request with valid params', () => { @@ -75,7 +77,7 @@ describe('PasskeyConfig', () => { nonObjects.forEach((request) => { it('should throw on invalid PasskeyConfigRequest:' + JSON.stringify(request), () => { expect(() => { - PasskeyConfig.buildServerRequest(true, request as any); + PasskeyConfig.buildServerRequest(false, request as any); }).to.throw(`'passkeyConfigRequest' must be a valid non-empty object.'`); }); }); @@ -84,7 +86,7 @@ describe('PasskeyConfig', () => { const invalidAttributeObject = deepCopy(passkeyConfigRequest) as any; invalidAttributeObject.invalidAttribute = 'invalid'; expect(() => { - PasskeyConfig.buildServerRequest(invalidAttributeObject); + PasskeyConfig.buildServerRequest(false, invalidAttributeObject); }).to.throw(`'invalidAttribute' is not a valid PasskeyConfigRequest parameter.`); }); @@ -94,7 +96,7 @@ describe('PasskeyConfig', () => { let request = deepCopy(passkeyConfigRequest) as any; request.expectedOrigins = expectedOriginsObject; expect(() => { - PasskeyConfig.buildServerRequest(true, request as any); + PasskeyConfig.buildServerRequest(false, request as any); }).to.throw(`'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`); }); }); @@ -115,7 +117,7 @@ describe('PasskeyConfig', () => { }; expect(passkeyConfig.name).to.equal(expectedServerResponse.name); expect(passkeyConfig.rpId).to.equal(expectedServerResponse.rpId); - expect(passkeyConfig.expectedOrigins).to.equal(expectedServerResponse.expectedOrigins); + expect(passkeyConfig.expectedOrigins).to.deep.equal(expectedServerResponse.expectedOrigins); }); }); From f0b0ea6ed2c0405d1cef109dd0e1c115d5778302 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 15:59:30 +0000 Subject: [PATCH 04/24] lint fixes + integration --- package-lock.json | 40 ++-- src/auth/auth-api-request.ts | 14 +- src/auth/passkey-config-manager.ts | 10 +- src/auth/passkey-config.ts | 101 ++++----- test/integration/auth.spec.ts | 45 ++-- test/unit/auth/passkey-config-manager.spec.ts | 41 ++-- test/unit/auth/passkey-config.spec.ts | 199 +++++++++--------- 7 files changed, 230 insertions(+), 220 deletions(-) diff --git a/package-lock.json b/package-lock.json index 576846d5ab..f12dfd4f5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -431,9 +431,9 @@ } }, "@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "dev": true }, "@fastify/busboy": { @@ -3549,15 +3549,15 @@ } }, "eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", + "@eslint/js": "8.51.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -3594,9 +3594,9 @@ }, "dependencies": { "@eslint-community/regexpp": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.2.tgz", - "integrity": "sha512-0MGxAVt1m/ZK+LTJp/j0qF7Hz97D9O/FH9Ms3ltnyIdDD57cbb1ACIQTkbHvNXtWDv5TPq7w5Kq56+cNukbo7g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", "dev": true }, "acorn": { @@ -4256,12 +4256,12 @@ "dev": true }, "flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "requires": { - "flatted": "^3.2.7", + "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } @@ -4757,9 +4757,9 @@ } }, "globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -6126,9 +6126,9 @@ } }, "keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "requires": { "json-buffer": "3.0.1" diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 945d1f93d9..f469a8cd46 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -43,7 +43,7 @@ import { SAMLUpdateAuthProviderRequest } from './auth-config'; import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; -import {PasskeyConfig, PasskeyConfigServerResponse, PasskeyConfigRequest} from './passkey-config'; +import { PasskeyConfig, PasskeyConfigServerResponse, PasskeyConfigRequest } from './passkey-config'; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { @@ -2108,7 +2108,8 @@ const UPDATE_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig?updateMask={update }); /** Instantiates the getPasskeyConfig endpoint settings. */ -const UPDATE_TENANT_PASSKEY_CONFIG = new ApiSettings('/tenant/{tenantId}/passkeyConfig?updateMask={updateMask}', 'PATCH') +const UPDATE_TENANT_PASSKEY_CONFIG = new ApiSettings( + '/tenant/{tenantId}/passkeyConfig?updateMask={updateMask}', 'PATCH') .setResponseValidator((response: any) => { // Response should always contain at least the config name. if (!validator.isNonEmptyString(response.name)) { @@ -2296,18 +2297,21 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { } public getPasskeyConfig(tenantId?: string): Promise { - return this.invokeRequestHandler(this.authResourceUrlBuilder, tenantId? GET_TENANT_PASSKEY_CONFIG: GET_PASSKEY_CONFIG, {}, {}) + return this.invokeRequestHandler(this.authResourceUrlBuilder, + tenantId? GET_TENANT_PASSKEY_CONFIG: GET_PASSKEY_CONFIG, {}, {}) .then((response: any) => { return response as PasskeyConfigServerResponse; }); } - public updatePasskeyConfig(isCreateRequest: boolean, tenantId?: string, options?: PasskeyConfigRequest, rpId?: string): Promise { + public updatePasskeyConfig(isCreateRequest: boolean, tenantId?: string, + options?: PasskeyConfigRequest, rpId?: string): Promise { try { const request = PasskeyConfig.buildServerRequest(isCreateRequest, options, rpId); const updateMask = utils.generateUpdateMask(request); return this.invokeRequestHandler( - this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG, request, { updateMask: updateMask.join(',') }) + 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 045f753684..a0793d1d23 100644 --- a/src/auth/passkey-config-manager.ts +++ b/src/auth/passkey-config-manager.ts @@ -17,7 +17,12 @@ import { App } from '../app'; import { AuthRequestHandler, } from './auth-api-request'; -import { PasskeyConfig, PasskeyConfigClientRequest, PasskeyConfigRequest, PasskeyConfigServerResponse } from './passkey-config'; +import { + PasskeyConfig, + PasskeyConfigClientRequest, + PasskeyConfigRequest, + PasskeyConfigServerResponse +} from './passkey-config'; export class PasskeyConfigManager { @@ -34,7 +39,8 @@ export class PasskeyConfigManager { }); } - public createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { + public createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, + tenantId?: string): Promise { return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId) .then((response: PasskeyConfigClientRequest) => { return new PasskeyConfig(response); diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts index efa3b90805..043189eb99 100644 --- a/src/auth/passkey-config.ts +++ b/src/auth/passkey-config.ts @@ -15,7 +15,7 @@ */ import * as validator from '../utils/validator'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; -import {deepCopy} from '../utils/deep-copy'; +import { deepCopy } from '../utils/deep-copy'; export interface PasskeyConfigRequest { expectedOrigins?: string[]; @@ -38,76 +38,77 @@ export class PasskeyConfig { public readonly rpId?: string; public readonly expectedOrigins?: string[]; - private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string) { - if(isCreateRequest && !validator.isNonEmptyString(rpId)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `'rpId' must be a valid non-empty string'`, - ); + private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): void { + if (isCreateRequest && !validator.isNonEmptyString(rpId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '\'rpId\' must be a valid non-empty string\'', + ); } - if(!isCreateRequest && typeof rpId !== 'undefined') { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `'rpId' cannot be changed once created.'`, - ); + 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, - `'passkeyConfigRequest' must be a valid non-empty object.'`, - ); + if (!validator.isNonNullObject(passkeyConfigRequest)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '\'passkeyConfigRequest\' must be a valid non-empty object.\'', + ); } const validKeys = { - expectedOrigins: true, + expectedOrigins: true, }; // Check for unsupported top level attributes. for (const key in passkeyConfigRequest) { - if (!(key in validKeys)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `'${key}' is not a valid PasskeyConfigRequest parameter.`, - ); - } - } - if(!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins)) { + if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, - ); + AuthClientErrorCode.INVALID_ARGUMENT, + `'${key}' is not a valid PasskeyConfigRequest parameter.`, + ); + } + } + if (!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\'', + ); } for (const origin of passkeyConfigRequest.expectedOrigins) { if (!validator.isNonEmptyString(origin)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, + '\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\'', ); } } - }; + } - public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest { + public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, + rpId?: string): PasskeyConfigClientRequest { PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest, rpId); - let request: PasskeyConfigClientRequest = {}; - if(isCreateRequest && typeof rpId !== 'undefined') { - request.rpId = rpId; + const request: PasskeyConfigClientRequest = {}; + if (isCreateRequest && typeof rpId !== 'undefined') { + request.rpId = rpId; } - if(typeof passkeyConfigRequest?.expectedOrigins !== 'undefined') { - request.expectedOrigins = passkeyConfigRequest.expectedOrigins; + if (typeof passkeyConfigRequest?.expectedOrigins !== 'undefined') { + request.expectedOrigins = passkeyConfigRequest.expectedOrigins; } return request; - }; + } constructor(response: PasskeyConfigServerResponse) { - if(typeof response.name !== 'undefined') { - this.name = response.name; + if (typeof response.name !== 'undefined') { + this.name = response.name; } - if(typeof response.rpId !== 'undefined') { - this.rpId = response.rpId; - }; - if(typeof response.expectedOrigins !== 'undefined') { - this.expectedOrigins = response.expectedOrigins; + if (typeof response.rpId !== 'undefined') { + this.rpId = response.rpId; } - }; + if (typeof response.expectedOrigins !== 'undefined') { + this.expectedOrigins = response.expectedOrigins; + } + } public toJSON(): object { const json = { @@ -115,17 +116,17 @@ export class PasskeyConfig { rpId: deepCopy(this.rpId), expectedOrigins: deepCopy(this.expectedOrigins), }; - if(typeof json.name === 'undefined') { + if (typeof json.name === 'undefined') { delete json.name; } - if(typeof json.rpId === 'undefined') { + if (typeof json.rpId === 'undefined') { delete json.rpId; } - if(typeof json.expectedOrigins === 'undefined') { + if (typeof json.expectedOrigins === 'undefined') { delete json.expectedOrigins; } return json; } -}; +} diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 1d38e836d6..7aea619078 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -2198,44 +2198,43 @@ describe('admin.auth', () => { }); describe('Passkey config management operations', () => { - // Define expected passkey configuration - const expectedPasskeyConfig = { - name: `projects/{$projectId}/passkeyConfig`, - rpId: `{$projectId}.firebaseapp.com`, - expectedOrigins: ['app1', 'example.com'], - }; // Before each test, reset the passkey config to the initial state beforeEach(async () => { - const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; - await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); + // const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; + // await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); }); it('createPasskeyConfig() should create passkey config with expected passkeyConfig', async () => { - const rpId = `{$projectId}.firebaseapp.com`; + const rpId = projectId + '.firebaseapp.com'; const createRequest = { expectedOrigins: ['app1', 'example.com'] }; - const createdPasskeyConfig = await getAuth().passkeyConfigManager().createPasskeyConfig(rpId, createRequest); - const passkeyConfigObj = createdPasskeyConfig.toJSON(); - expect(passkeyConfigObj).to.deep.equal(expectedPasskeyConfig); + expect(createdPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); + expect(createdPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); + expect(createdPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com']); }); + + // TODO: uncomment when the GET endpoint is released in prod + // it('getPasskeyConfig() should resolve with expected passkeyConfig', async () => { + // const actualPasskeyConfig = await getAuth().passkeyConfigManager().getPasskeyConfig(); - it('getPasskeyConfig() should resolve with expected passkeyConfig', async () => { - const actualPasskeyConfig = await getAuth().passkeyConfigManager().getPasskeyConfig(); - const actualPasskeyConfigObj = actualPasskeyConfig.toJSON(); - - expect(actualPasskeyConfigObj).to.deep.equal(expectedPasskeyConfig); - }); + // expect(actualPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); + // expect(actualPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); + // expect(actualPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com']); + // }); it('updatePasskeyConfig() should resolve with updated expectedOrigins', async () => { - const updateRequest = { expectedOrigins: ['app1', 'example.com', 'app2'] }; - const expectedUpdatedPasskeyConfig = { ...expectedPasskeyConfig, expectedOrigins: updateRequest.expectedOrigins }; + const updateRequest = { + expectedOrigins: ['app1', 'example.com', 'app2'] + }; const updatedPasskeyConfig = await getAuth().passkeyConfigManager().updatePasskeyConfig(updateRequest); - const passkeyConfigObj = updatedPasskeyConfig.toJSON(); - - expect(passkeyConfigObj).to.deep.equal(expectedUpdatedPasskeyConfig); + + expect(updatedPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); + // TODO: backend validation needs to fixed in order for this statement to succeed. + // expect(updatedPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); + expect(updatedPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com', 'app2']); }); }); diff --git a/test/unit/auth/passkey-config-manager.spec.ts b/test/unit/auth/passkey-config-manager.spec.ts index b896fd35ec..5ce7c576ca 100644 --- a/test/unit/auth/passkey-config-manager.spec.ts +++ b/test/unit/auth/passkey-config-manager.spec.ts @@ -44,10 +44,10 @@ describe('PasskeyConfigManager', () => { let malformedAccessTokenPasskeyConfigManager: PasskeyConfigManager; let rejectedPromiseAccessTokenPasskeyConfigManager: PasskeyConfigManager; const GET_CONFIG_RESPONSE: PasskeyConfigServerResponse = { - name: `projects/project-id/passkeyConfig`, - rpId: `project-id.firebaseapp.com`, + name: 'projects/project-id/passkeyConfig', + rpId: 'project-id.firebaseapp.com', expectedOrigins: ['app1', 'example.com'], -}; + }; before(() => { mockApp = mocks.app(); @@ -121,7 +121,7 @@ describe('PasskeyConfigManager', () => { }); describe('createPasskeyConfig()', () => { - const rpId: string = 'project-id.firebaseapp.com'; + const rpId = 'project-id.firebaseapp.com'; const expectedOrigins: string[] = ['app1', 'example.com'] const passkeyConfigRequest: PasskeyConfigRequest = { expectedOrigins: expectedOrigins , @@ -131,31 +131,30 @@ describe('PasskeyConfigManager', () => { AuthClientErrorCode.INTERNAL_ERROR, 'Unable to create the config provided.'); // Stubs used to simulate underlying API calls. - let stubs: sinon.SinonStub[] = []; + const stubs: sinon.SinonStub[] = []; afterEach(() => { sinon.restore(); }); it('should be rejected given no passkeyConfigOptions', () => { - return (passkeyConfigManager as any).createPasskeyConfig(null as unknown as PasskeyConfigRequest) - .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); - }); + 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', () => { - console.log("TEST===" + JSON.stringify(passkeyConfigRequest)); - return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, 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) - .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) + .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) - .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) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); it('should resolve with a PasskeyConfig on createPasskeyConfig request success', () => { // Stub createPasskeyConfig to return expected result. diff --git a/test/unit/auth/passkey-config.spec.ts b/test/unit/auth/passkey-config.spec.ts index 2dc9ee2301..298615af79 100644 --- a/test/unit/auth/passkey-config.spec.ts +++ b/test/unit/auth/passkey-config.spec.ts @@ -19,9 +19,9 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import { - PasskeyConfig, PasskeyConfigRequest, PasskeyConfigServerResponse, PasskeyConfigClientRequest - } from '../../../src/auth/passkey-config'; -import {deepCopy} from '../../../src/utils/deep-copy'; + PasskeyConfig, PasskeyConfigRequest, PasskeyConfigServerResponse, PasskeyConfigClientRequest +} from '../../../src/auth/passkey-config'; +import { deepCopy } from '../../../src/utils/deep-copy'; chai.should(); chai.use(sinonChai); @@ -30,113 +30,114 @@ chai.use(chaiAsPromised); const expect = chai.expect; describe('PasskeyConfig', () => { - const serverResponse: PasskeyConfigServerResponse = { - name: `projects/project-id/passkeyConfig`, - rpId: `project-id.firebaseapp.com`, - expectedOrigins: ['app1', 'example.com'], - }; - const passkeyConfigRequest: PasskeyConfigRequest = { - 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, - expectedOrigins: passkeyConfigRequest.expectedOrigins, - }; - expect(PasskeyConfig.buildServerRequest(true, passkeyConfigRequest, validRpId)).to.deep.equal(expectedRequest); - }); + const serverResponse: PasskeyConfigServerResponse = { + name: 'projects/project-id/passkeyConfig', + rpId: 'project-id.firebaseapp.com', + expectedOrigins: ['app1', 'example.com'], + }; + const passkeyConfigRequest: PasskeyConfigRequest = { + 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, + expectedOrigins: passkeyConfigRequest.expectedOrigins, + }; + expect(PasskeyConfig.buildServerRequest(true, passkeyConfigRequest, validRpId)).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(`'rpId' must be a valid non-empty string`); - }); - }); - }); + 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( + '\'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.`); - }); + 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 create a client request with valid params', () => { - const expectedRequest: PasskeyConfigClientRequest = { - expectedOrigins: passkeyConfigRequest.expectedOrigins, - }; - expect(PasskeyConfig.buildServerRequest(false, passkeyConfigRequest)).to.deep.equal(expectedRequest); - }); - }); + it('should create a client request with valid params', () => { + const expectedRequest: PasskeyConfigClientRequest = { + 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), () => { - expect(() => { - PasskeyConfig.buildServerRequest(false, request as any); - }).to.throw(`'passkeyConfigRequest' must be a valid non-empty object.'`); - }); - }); + 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), () => { + expect(() => { + PasskeyConfig.buildServerRequest(false, request as any); + }).to.throw('\'passkeyConfigRequest\' must be a valid non-empty object.\''); + }); + }); - it('should throw for invalid passkey config request attribute', () => { - const invalidAttributeObject = deepCopy(passkeyConfigRequest) as any; - invalidAttributeObject.invalidAttribute = 'invalid'; - expect(() => { - PasskeyConfig.buildServerRequest(false, invalidAttributeObject); - }).to.throw(`'invalidAttribute' is not a valid PasskeyConfigRequest parameter.`); - }); + it('should throw for invalid passkey config request attribute', () => { + const invalidAttributeObject = deepCopy(passkeyConfigRequest) as any; + invalidAttributeObject.invalidAttribute = 'invalid'; + expect(() => { + PasskeyConfig.buildServerRequest(false, invalidAttributeObject); + }).to.throw('\'invalidAttribute\' is not a valid PasskeyConfigRequest parameter.'); + }); - const invalidExpectedOriginsObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; - invalidExpectedOriginsObjects.forEach((expectedOriginsObject) => { - it('should throw for invalid expected origins values', () => { - let request = deepCopy(passkeyConfigRequest) as any; - request.expectedOrigins = expectedOriginsObject; - expect(() => { - PasskeyConfig.buildServerRequest(false, request as any); - }).to.throw(`'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`); - }); - }); - }); + const invalidExpectedOriginsObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidExpectedOriginsObjects.forEach((expectedOriginsObject) => { + it('should throw for invalid expected origins values', () => { + const request = deepCopy(passkeyConfigRequest) as any; + request.expectedOrigins = expectedOriginsObject; + expect(() => { + PasskeyConfig.buildServerRequest(false, request as any); + }).to.throw('\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\''); }); + }); + }); + }); - describe('constructor', () => { - const passkeyConfig = new PasskeyConfig(serverResponse); - it('should not throw on valid initialization', () => { - expect(() => new PasskeyConfig(serverResponse)).not.to.throw(); - }); + describe('constructor', () => { + const passkeyConfig = new PasskeyConfig(serverResponse); + it('should not throw on valid initialization', () => { + expect(() => new PasskeyConfig(serverResponse)).not.to.throw(); + }); - it('should set readonly properties', () => { - const expectedServerResponse = { - name: `projects/project-id/passkeyConfig`, - rpId: `project-id.firebaseapp.com`, - expectedOrigins: ['app1', 'example.com'], - }; - expect(passkeyConfig.name).to.equal(expectedServerResponse.name); - expect(passkeyConfig.rpId).to.equal(expectedServerResponse.rpId); - expect(passkeyConfig.expectedOrigins).to.deep.equal(expectedServerResponse.expectedOrigins); - }); + it('should set readonly properties', () => { + const expectedServerResponse = { + name: 'projects/project-id/passkeyConfig', + rpId: 'project-id.firebaseapp.com', + expectedOrigins: ['app1', 'example.com'], + }; + expect(passkeyConfig.name).to.equal(expectedServerResponse.name); + expect(passkeyConfig.rpId).to.equal(expectedServerResponse.rpId); + expect(passkeyConfig.expectedOrigins).to.deep.equal(expectedServerResponse.expectedOrigins); }); + }); - describe('toJSON', () => { - it('should return the expected object representation of passkey config', () => { - expect(new PasskeyConfig(serverResponse).toJSON()).to.deep.equal({ - name: deepCopy(serverResponse.name), - rpId: deepCopy(serverResponse).rpId, - expectedOrigins: deepCopy(serverResponse.expectedOrigins), - }); - }); + describe('toJSON', () => { + it('should return the expected object representation of passkey config', () => { + expect(new PasskeyConfig(serverResponse).toJSON()).to.deep.equal({ + name: deepCopy(serverResponse.name), + rpId: deepCopy(serverResponse).rpId, + expectedOrigins: deepCopy(serverResponse.expectedOrigins), + }); + }); - it('should not populate optional fields if not available', () => { - const serverResponseOptionalCopy: PasskeyConfigServerResponse = deepCopy(serverResponse); - delete serverResponseOptionalCopy.rpId; - delete serverResponseOptionalCopy.expectedOrigins; - expect(new PasskeyConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ - name: deepCopy(serverResponse.name), - }); - }); + it('should not populate optional fields if not available', () => { + const serverResponseOptionalCopy: PasskeyConfigServerResponse = deepCopy(serverResponse); + delete serverResponseOptionalCopy.rpId; + delete serverResponseOptionalCopy.expectedOrigins; + expect(new PasskeyConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ + name: deepCopy(serverResponse.name), + }); }); + }); }); \ No newline at end of file From ac9a82f44c48d4ef699be3c50c7aec632606b34f Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 10:41:33 -0700 Subject: [PATCH 05/24] remove integration tests --- test/integration/auth.spec.ts | 44 +---------------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 7aea619078..7b113b3156 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -32,7 +32,7 @@ import { AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, UserMetadata, MultiFactorConfig, - PasswordPolicyConfig, SmsRegionConfig + PasswordPolicyConfig, SmsRegionConfig, } from '../../lib/auth/index'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; @@ -2197,48 +2197,6 @@ describe('admin.auth', () => { }); }); - describe('Passkey config management operations', () => { - - // Before each test, reset the passkey config to the initial state - beforeEach(async () => { - // const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; - // await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); - }); - - it('createPasskeyConfig() should create passkey config with expected passkeyConfig', async () => { - const rpId = projectId + '.firebaseapp.com'; - const createRequest = { expectedOrigins: ['app1', 'example.com'] }; - const createdPasskeyConfig = await getAuth().passkeyConfigManager().createPasskeyConfig(rpId, createRequest); - - expect(createdPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); - expect(createdPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); - expect(createdPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com']); - }); - - // TODO: uncomment when the GET endpoint is released in prod - // it('getPasskeyConfig() should resolve with expected passkeyConfig', async () => { - // const actualPasskeyConfig = await getAuth().passkeyConfigManager().getPasskeyConfig(); - - // expect(actualPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); - // expect(actualPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); - // expect(actualPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com']); - // }); - - it('updatePasskeyConfig() should resolve with updated expectedOrigins', async () => { - const updateRequest = { - expectedOrigins: ['app1', 'example.com', 'app2'] - }; - - const updatedPasskeyConfig = await getAuth().passkeyConfigManager().updatePasskeyConfig(updateRequest); - - expect(updatedPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); - // TODO: backend validation needs to fixed in order for this statement to succeed. - // expect(updatedPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); - expect(updatedPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com', 'app2']); - }); - }); - - describe('SAML configuration operations', () => { const authProviderConfig1 = { providerId: randomSamlProviderId(), From b40b1ab0997a3a480c654ef4baf94592d50bde81 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 11:20:20 -0700 Subject: [PATCH 06/24] adding comments --- etc/firebase-admin.auth.api.md | 21 ++------- src/auth/auth-api-request.ts | 24 +++++----- src/auth/passkey-config-manager.ts | 33 ++++++++++++- src/auth/passkey-config.ts | 74 ++++++++++++++++++++++++++---- 4 files changed, 112 insertions(+), 40 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index be128aafab..42e217ba60 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -345,38 +345,23 @@ export interface OIDCUpdateAuthProviderRequest { responseType?: OAuthResponseType; } -// @public (undocumented) +// @public export class PasskeyConfig { - // Warning: (ae-forgotten-export) The symbol "PasskeyConfigServerResponse" needs to be exported by the entry point index.d.ts - constructor(response: PasskeyConfigServerResponse); - // Warning: (ae-forgotten-export) The symbol "PasskeyConfigClientRequest" needs to be exported by the entry point index.d.ts - // - // (undocumented) - static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest; - // (undocumented) readonly expectedOrigins?: string[]; - // (undocumented) readonly name?: string; - // (undocumented) readonly rpId?: string; - // (undocumented) toJSON(): object; } -// @public (undocumented) +// @public export class PasskeyConfigManager { - constructor(app: App); - // (undocumented) createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; - // (undocumented) getPasskeyConfig(tenantId?: string): Promise; - // (undocumented) updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; } -// @public (undocumented) +// @public export interface PasskeyConfigRequest { - // (undocumented) expectedOrigins?: string[]; } diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index f469a8cd46..7f36461fc4 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -2071,51 +2071,51 @@ const CREATE_TENANT = new ApiSettings('/tenants', 'POST') } }); -/** Instantiates the getPasskeyConfig endpoint settings. */ +/** Instantiates the GET_PASSKEY_CONFIG endpoint settings. */ const GET_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig', 'GET') .setResponseValidator((response: any) => { - // Response should always contain at least the config name. + // Validate the response for GET_PASSKEY_CONFIG. if (!validator.isNonEmptyString(response.name)) { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get project config', + 'INTERNAL ASSERT FAILED: Unable to get passkey config', ); } }); -/** Instantiates the getPasskeyConfig endpoint settings. */ +/** Instantiates the GET_TENANT_PASSKEY_CONFIG endpoint settings. */ const GET_TENANT_PASSKEY_CONFIG = new ApiSettings('/tenants/{tenantId}/passkeyConfig', 'GET') .setResponseValidator((response: any) => { - // Response should always contain at least the config name. + // Validate the response for GET_TENANT_PASSKEY_CONFIG. if (!validator.isNonEmptyString(response.name)) { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get project config', + 'INTERNAL ASSERT FAILED: Unable to get tenant passkey config', ); } }); -/** Instantiates the getPasskeyConfig endpoint settings. */ +/** Instantiates the UPDATE_PASSKEY_CONFIG endpoint settings. */ const UPDATE_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig?updateMask={updateMask}', 'PATCH') .setResponseValidator((response: any) => { - // Response should always contain at least the config name. + // Validate the response for UPDATE_PASSKEY_CONFIG. if (!validator.isNonEmptyString(response.name)) { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get project config', + 'INTERNAL ASSERT FAILED: Unable to update passkey config', ); } }); -/** Instantiates the getPasskeyConfig endpoint settings. */ +/** Instantiates the UPDATE_TENANT_PASSKEY_CONFIG endpoint settings. */ const UPDATE_TENANT_PASSKEY_CONFIG = new ApiSettings( '/tenant/{tenantId}/passkeyConfig?updateMask={updateMask}', 'PATCH') .setResponseValidator((response: any) => { - // Response should always contain at least the config name. + // Validate the response for UPDATE_TENANT_PASSKEY_CONFIG. if (!validator.isNonEmptyString(response.name)) { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get project config', + 'INTERNAL ASSERT FAILED: Unable to update tenant passkey config', ); } }); diff --git a/src/auth/passkey-config-manager.ts b/src/auth/passkey-config-manager.ts index a0793d1d23..100ba2df89 100644 --- a/src/auth/passkey-config-manager.ts +++ b/src/auth/passkey-config-manager.ts @@ -24,14 +24,30 @@ import { PasskeyConfigServerResponse } from './passkey-config'; - +/** + * Manages Passkey Configuration for a Firebase app. + */ 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 + * @internal + */ constructor(app: App) { this.authRequestHandler = new AuthRequestHandler(app); } + /** + * 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. + */ public getPasskeyConfig(tenantId?: string): Promise { return this.authRequestHandler.getPasskeyConfig(tenantId) .then((response: PasskeyConfigServerResponse) => { @@ -39,6 +55,14 @@ 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, tenantId?: string): Promise { return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId) @@ -47,6 +71,13 @@ 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. + */ public updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { return this.authRequestHandler.updatePasskeyConfig(false, tenantId, passkeyConfigRequest) .then((response: PasskeyConfigClientRequest) => { diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts index 043189eb99..68e1b66237 100644 --- a/src/auth/passkey-config.ts +++ b/src/auth/passkey-config.ts @@ -17,50 +17,87 @@ import * as validator from '../utils/validator'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { deepCopy } from '../utils/deep-copy'; +/** + * Interface representing the properties to update in the provided passkey config. + */ export interface PasskeyConfigRequest { + /** + * 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. + */ expectedOrigins?: string[]; } +/** + * Response received from the server when retrieving, creating, or updating the passkey config. + */ export interface PasskeyConfigServerResponse { name?: string; rpId?: string; expectedOrigins?: string[]; } +/** + * Request for creating or updating the passkey config on the server. + */ export interface PasskeyConfigClientRequest { rpId?: string; expectedOrigins?: string[]; } - +/** + * Configuration for signing in users using passkeys. + */ export class PasskeyConfig { + /** + * The name of the PasskeyConfig resource. + */ public readonly name?: string; + /** + * The relying party ID for passkey verifications. + * This cannot be changed once created. + */ public readonly rpId?: string; + /** + * The 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. + */ public readonly expectedOrigins?: string[]; + /** + * Validates a passkey config request object and throws an error on failure. + * @param isCreateRequest - A boolean indicating if it's a create request or not. + * @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 { + // Validation for creating a new PasskeyConfig. if (isCreateRequest && !validator.isNonEmptyString(rpId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - '\'rpId\' must be a valid non-empty string\'', + "'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.\'', + "'rpId' cannot be changed once created.", ); } if (!validator.isNonNullObject(passkeyConfigRequest)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - '\'passkeyConfigRequest\' must be a valid non-empty object.\'', + "'passkeyConfigRequest' must be a valid non-empty object.", ); } const validKeys = { expectedOrigins: true, }; - // Check for unsupported top level attributes. + // Check for unsupported top-level attributes. for (const key in passkeyConfigRequest) { if (!(key in validKeys)) { throw new FirebaseAuthError( @@ -72,19 +109,29 @@ export class PasskeyConfig { if (!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - '\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\'', + "'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.", ); } for (const origin of passkeyConfigRequest.expectedOrigins) { if (!validator.isNonEmptyString(origin)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - '\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\'', + "'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.", ); } } } + /** + * Build the corresponding server request for a Passkey Config object. + * @param isCreateRequest - A boolean stating if it's a create request. + * @param passkeyConfigRequest - Passkey config to be updated. + * @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); @@ -98,6 +145,13 @@ export class PasskeyConfig { return request; } + /** + * The Passkey Config object constructor. + * @param response - The server-side response used to initialize the Passkey Config object. + * @constructor + * + * @internal + */ constructor(response: PasskeyConfigServerResponse) { if (typeof response.name !== 'undefined') { this.name = response.name; @@ -110,6 +164,10 @@ export class PasskeyConfig { } } + /** + * Returns a JSON-serializable representation of this object. + * @returns A JSON-serializable representation of this object. + */ public toJSON(): object { const json = { name: deepCopy(this.name), @@ -127,6 +185,4 @@ export class PasskeyConfig { } return json; } - } - From b35cb33564d4b4eb69d91e0f88f1e3ad9f6bacfd Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 11:36:46 -0700 Subject: [PATCH 07/24] undo package json changes --- package-lock.json | 48 +++++++++++++-------------- package.json | 2 +- test/unit/auth/passkey-config.spec.ts | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index f12dfd4f5c..40a768beba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "11.11.0", + "version": "11.10.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -431,9 +431,9 @@ } }, "@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "dev": true }, "@fastify/busboy": { @@ -1405,9 +1405,9 @@ } }, "@types/firebase-token-generator": { - "version": "2.0.31", - "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.31.tgz", - "integrity": "sha512-mR6GuDPxFiD7nZ3x2NddRvo42ZBS22PHHNdAWuQvqp/1LfiYshZ6PzJD2+JOyLw5ZErgmw7F8R1nfpXnvBOxGg==", + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.30.tgz", + "integrity": "sha512-GcNz25MRki9ZpVfvNNrthx4t3XXjgIZ2wv729ea9F4n/1PZf4QIZlzTGoDTDeV417vmd6cPTYKUzPf4rR+qGhw==", "dev": true }, "@types/glob": { @@ -3549,15 +3549,15 @@ } }, "eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", + "@eslint/js": "8.50.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -3594,9 +3594,9 @@ }, "dependencies": { "@eslint-community/regexpp": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", - "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.2.tgz", + "integrity": "sha512-0MGxAVt1m/ZK+LTJp/j0qF7Hz97D9O/FH9Ms3ltnyIdDD57cbb1ACIQTkbHvNXtWDv5TPq7w5Kq56+cNukbo7g==", "dev": true }, "acorn": { @@ -4256,12 +4256,12 @@ "dev": true }, "flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", "dev": true, "requires": { - "flatted": "^3.2.9", + "flatted": "^3.2.7", "keyv": "^4.5.3", "rimraf": "^3.0.2" } @@ -4757,9 +4757,9 @@ } }, "globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -6126,9 +6126,9 @@ } }, "keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", "dev": true, "requires": { "json-buffer": "3.0.1" diff --git a/package.json b/package.json index 68f51b1157..446ad2cf42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "11.11.0", + "version": "11.10.1", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", diff --git a/test/unit/auth/passkey-config.spec.ts b/test/unit/auth/passkey-config.spec.ts index 298615af79..1d61bf6ec0 100644 --- a/test/unit/auth/passkey-config.spec.ts +++ b/test/unit/auth/passkey-config.spec.ts @@ -140,4 +140,4 @@ describe('PasskeyConfig', () => { }); }); }); -}); \ No newline at end of file +}); From 5c16959fd0130e46506879de2b96d10e9b49f307 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 4 Oct 2023 13:03:13 -0400 Subject: [PATCH 08/24] passkey config admin changes --- etc/firebase-admin.auth.api.md | 36 +++++++ src/auth/auth-api-request.ts | 70 +++++++++++++ src/auth/auth.ts | 12 +++ src/auth/index.ts | 9 ++ src/auth/passkey-config-manager.ts | 50 +++++++++ src/auth/passkey-config.ts | 131 ++++++++++++++++++++++++ test/integration/auth.spec.ts | 51 +++++++++- test/unit/auth/passkey-config.spec.ts | 140 ++++++++++++++++++++++++++ 8 files changed, 498 insertions(+), 1 deletion(-) create mode 100644 src/auth/passkey-config-manager.ts create mode 100644 src/auth/passkey-config.ts create mode 100644 test/unit/auth/passkey-config.spec.ts diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 3723abd051..fe03cb5ed8 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -51,6 +51,7 @@ export interface AllowlistOnlyWrap { export class Auth extends BaseAuth { // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts get app(): App; + passkeyConfigManager(): PasskeyConfigManager; projectConfigManager(): ProjectConfigManager; tenantManager(): TenantManager; } @@ -344,6 +345,41 @@ export interface OIDCUpdateAuthProviderRequest { responseType?: OAuthResponseType; } +// @public (undocumented) +export class PasskeyConfig { + // Warning: (ae-forgotten-export) The symbol "PasskeyConfigServerResponse" needs to be exported by the entry point index.d.ts + constructor(response: PasskeyConfigServerResponse); + // Warning: (ae-forgotten-export) The symbol "PasskeyConfigClientRequest" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest; + // (undocumented) + readonly expectedOrigins?: string[]; + // (undocumented) + readonly name?: string; + // (undocumented) + readonly rpId?: string; + // (undocumented) + toJSON(): object; +} + +// @public (undocumented) +export class PasskeyConfigManager { + constructor(app: App); + // (undocumented) + createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; + // (undocumented) + getPasskeyConfig(tenantId?: string): Promise; + // (undocumented) + updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; +} + +// @public (undocumented) +export interface PasskeyConfigRequest { + // (undocumented) + expectedOrigins?: string[]; +} + // @public export interface PasswordPolicyConfig { constraints?: CustomStrengthOptionsConfig; diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 9fd535777c..945d1f93d9 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -43,6 +43,7 @@ import { SAMLUpdateAuthProviderRequest } from './auth-config'; import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; +import {PasskeyConfig, PasskeyConfigServerResponse, PasskeyConfigRequest} from './passkey-config'; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { @@ -2070,6 +2071,54 @@ const CREATE_TENANT = new ApiSettings('/tenants', 'POST') } }); +/** Instantiates the getPasskeyConfig endpoint settings. */ +const GET_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig', 'GET') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + +/** Instantiates the getPasskeyConfig endpoint settings. */ +const GET_TENANT_PASSKEY_CONFIG = new ApiSettings('/tenants/{tenantId}/passkeyConfig', 'GET') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + +/** Instantiates the getPasskeyConfig endpoint settings. */ +const UPDATE_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig?updateMask={updateMask}', 'PATCH') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + +/** Instantiates the getPasskeyConfig endpoint settings. */ +const UPDATE_TENANT_PASSKEY_CONFIG = new ApiSettings('/tenant/{tenantId}/passkeyConfig?updateMask={updateMask}', 'PATCH') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + /** * Utility for sending requests to Auth server that are Auth instance related. This includes user, tenant, @@ -2245,6 +2294,27 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { return Promise.reject(e); } } + + public getPasskeyConfig(tenantId?: string): Promise { + return this.invokeRequestHandler(this.authResourceUrlBuilder, tenantId? GET_TENANT_PASSKEY_CONFIG: GET_PASSKEY_CONFIG, {}, {}) + .then((response: any) => { + return response as PasskeyConfigServerResponse; + }); + } + + public updatePasskeyConfig(isCreateRequest: boolean, tenantId?: string, options?: PasskeyConfigRequest, rpId?: string): Promise { + try { + const request = PasskeyConfig.buildServerRequest(isCreateRequest, options, rpId); + const updateMask = utils.generateUpdateMask(request); + return this.invokeRequestHandler( + this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG, request, { updateMask: updateMask.join(',') }) + .then((response: any) => { + return response as PasskeyConfigServerResponse; + }); + } catch (e) { + return Promise.reject(e); + } + } } /** diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 4808fbbdc0..f31ed2dab0 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -20,6 +20,7 @@ import { AuthRequestHandler } from './auth-api-request'; import { TenantManager } from './tenant-manager'; import { BaseAuth } from './base-auth'; import { ProjectConfigManager } from './project-config-manager'; +import { PasskeyConfigManager } from './passkey-config-manager'; /** * Auth service bound to the provided app. @@ -29,6 +30,7 @@ export class Auth extends BaseAuth { private readonly tenantManager_: TenantManager; private readonly projectConfigManager_: ProjectConfigManager; + private readonly passkeyConfigManager_: PasskeyConfigManager; private readonly app_: App; /** @@ -41,6 +43,7 @@ export class Auth extends BaseAuth { this.app_ = app; this.tenantManager_ = new TenantManager(app); this.projectConfigManager_ = new ProjectConfigManager(app); + this.passkeyConfigManager_ = new PasskeyConfigManager(app); } /** @@ -69,4 +72,13 @@ export class Auth extends BaseAuth { public projectConfigManager(): ProjectConfigManager { return this.projectConfigManager_; } + + /** + * Returns the passkey config manager instance. + * + * @returns The passkey config manager instance . + */ + public passkeyConfigManager(): PasskeyConfigManager { + return this.passkeyConfigManager_; + } } diff --git a/src/auth/index.ts b/src/auth/index.ts index a559a706f8..b3f5f954c7 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -142,6 +142,15 @@ export { ProjectConfigManager, } from './project-config-manager'; +export { + PasskeyConfigRequest, + PasskeyConfig, +} from './passkey-config'; + +export { + PasskeyConfigManager, +} from './passkey-config-manager'; + export { DecodedIdToken, DecodedAuthBlockingToken diff --git a/src/auth/passkey-config-manager.ts b/src/auth/passkey-config-manager.ts new file mode 100644 index 0000000000..184f96a8bd --- /dev/null +++ b/src/auth/passkey-config-manager.ts @@ -0,0 +1,50 @@ +/*! + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { App } from '../app'; +import { + AuthRequestHandler, +} from './auth-api-request'; +import {PasskeyConfig, PasskeyConfigClientRequest, PasskeyConfigRequest, PasskeyConfigServerResponse} from './passkey-config'; + + +export class PasskeyConfigManager { + private readonly authRequestHandler: AuthRequestHandler; + + constructor(app: App) { + this.authRequestHandler = new AuthRequestHandler(app); + } + + public getPasskeyConfig(tenantId?: string): Promise { + return this.authRequestHandler.getPasskeyConfig() + .then((response: PasskeyConfigServerResponse) => { + return new PasskeyConfig(response); + }); + } + + public createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { + return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId) + .then((response: PasskeyConfigClientRequest) => { + return new PasskeyConfig(response); + }) + } + + public updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { + return this.authRequestHandler.updatePasskeyConfig(false, tenantId, passkeyConfigRequest) + .then((response: PasskeyConfigClientRequest) => { + return new PasskeyConfig(response); + }) + } +} diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts new file mode 100644 index 0000000000..667e46333d --- /dev/null +++ b/src/auth/passkey-config.ts @@ -0,0 +1,131 @@ +/*! + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as validator from '../utils/validator'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import {deepCopy} from '../utils/deep-copy'; + +export interface PasskeyConfigRequest { + expectedOrigins?: string[]; +} + +export interface PasskeyConfigServerResponse { + name?: string; + rpId?: string; + expectedOrigins?: string[]; +} + +export interface PasskeyConfigClientRequest { + rpId?: string; + expectedOrigins?: string[]; +} + + +export class PasskeyConfig { + public readonly name?: string; + public readonly rpId?: string; + public readonly expectedOrigins?: string[]; + + private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string) { + if(isCreateRequest && !validator.isNonEmptyString(rpId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `'rpId' must be a valid non-empty string'`, + ); + } + 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, + `'passkeyConfigRequest' must be a valid non-empty object.'`, + ); + } + const validKeys = { + expectedOrigins: true, + }; + // Check for unsupported top level attributes. + for (const key in passkeyConfigRequest) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `'${key}' is not a valid PasskeyConfigRequest parameter.`, + ); + } + } + if(!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins) || !validator.isNonNullObject(passkeyConfigRequest.expectedOrigins)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, + ); + } + for(const origin in passkeyConfigRequest.expectedOrigins) { + if(!validator.isString(origin)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, + ); + } + } + }; + + public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest { + PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest, rpId); + let request: PasskeyConfigClientRequest = {}; + if(isCreateRequest && typeof rpId !== 'undefined') { + request.rpId = rpId; + } + if(typeof request.expectedOrigins !== 'undefined') { + request.expectedOrigins = passkeyConfigRequest?.expectedOrigins; + } + return request; + }; + + constructor(response: PasskeyConfigServerResponse) { + if(typeof response.name !== 'undefined') { + this.name = response.name; + } + if(typeof response.rpId !== 'undefined') { + this.rpId = response.rpId; + }; + if(typeof response.expectedOrigins !== 'undefined') { + this.expectedOrigins = response.expectedOrigins; + } + }; + + public toJSON(): object { + const json = { + name: deepCopy(this.name), + rpId: deepCopy(this.rpId), + expectedOrigins: deepCopy(this.expectedOrigins), + }; + if(typeof json.name === 'undefined') { + delete json.name; + } + if(typeof json.rpId === 'undefined') { + delete json.rpId; + } + if(typeof json.expectedOrigins === 'undefined') { + delete json.expectedOrigins; + } + return json; + } + +}; + diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 7b113b3156..9473ba64f1 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -32,10 +32,11 @@ import { AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, UserMetadata, MultiFactorConfig, - PasswordPolicyConfig, SmsRegionConfig, + PasswordPolicyConfig, SmsRegionConfig } from '../../lib/auth/index'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; +import {PasskeyConfigRequest} from '../../src/auth'; const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires @@ -2197,6 +2198,54 @@ describe('admin.auth', () => { }); }); + describe('Passkey config management operations', () => { + // Define expected passkey configuration + const expectedPasskeyConfig = { + name: `projects/{$projectId}/passkeyConfig`, + rpId: `{$projectId}.firebaseapp.com`, + expectedOrigins: ['app1', 'example.com'], + }; + + // Helper function to reset passkey config to the initial state + async function resetPasskeyConfig() { + const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; + await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); + } + + // Before each test, reset the passkey config to the initial state + beforeEach(async () => { + await resetPasskeyConfig(); + }); + + it('createPasskeyConfig() should create passkey config with expected passkeyConfig', async () => { + const rpId = `{$projectId}.firebaseapp.com`; + const createRequest = { expectedOrigins: ['app1', 'example.com'] }; + + const createdPasskeyConfig = await getAuth().passkeyConfigManager().createPasskeyConfig(rpId, createRequest); + const passkeyConfigObj = createdPasskeyConfig.toJSON(); + + expect(passkeyConfigObj).to.deep.equal(expectedPasskeyConfig); + }); + + it('getPasskeyConfig() should resolve with expected passkeyConfig', async () => { + const actualPasskeyConfig = await getAuth().passkeyConfigManager().getPasskeyConfig(); + const actualPasskeyConfigObj = actualPasskeyConfig.toJSON(); + + expect(actualPasskeyConfigObj).to.deep.equal(expectedPasskeyConfig); + }); + + it('updatePasskeyConfig() should resolve with updated expectedOrigins', async () => { + const updateRequest = { expectedOrigins: ['app1', 'example.com', 'app2'] }; + const expectedUpdatedPasskeyConfig = { ...expectedPasskeyConfig, expectedOrigins: updateRequest.expectedOrigins }; + + const updatedPasskeyConfig = await getAuth().passkeyConfigManager().updatePasskeyConfig(updateRequest); + const passkeyConfigObj = updatedPasskeyConfig.toJSON(); + + expect(passkeyConfigObj).to.deep.equal(expectedUpdatedPasskeyConfig); + }); + }); + + describe('SAML configuration operations', () => { const authProviderConfig1 = { providerId: randomSamlProviderId(), diff --git a/test/unit/auth/passkey-config.spec.ts b/test/unit/auth/passkey-config.spec.ts new file mode 100644 index 0000000000..f8c3887930 --- /dev/null +++ b/test/unit/auth/passkey-config.spec.ts @@ -0,0 +1,140 @@ +/*! + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { + PasskeyConfig, PasskeyConfigRequest, PasskeyConfigServerResponse, PasskeyConfigClientRequest + } from '../../../src/auth/passkey-config'; +import {deepCopy} from '../../../src/utils/deep-copy'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('PasskeyConfig', () => { + const serverResponse: PasskeyConfigServerResponse = { + name: `projects/project-id/passkeyConfig`, + rpId: `project-id.firebaseapp.com`, + expectedOrigins: ['app1', 'example.com'], + }; + const passkeyConfigRequest: PasskeyConfigRequest = { + expectedOrigins: ['app1', 'website.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, + expectedOrigins: passkeyConfigRequest.expectedOrigins, + }; + expect(PasskeyConfig.buildServerRequest(true, passkeyConfigRequest, validRpId)).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(`'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, 'project-id.firebaseapp.com')).to.throw(`'rpId' must be a valid non-empty string'`); + }); + + it('should create a client request with valid params', () => { + const expectedRequest: PasskeyConfigClientRequest = { + 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), () => { + expect(() => { + PasskeyConfig.buildServerRequest(true, request as any); + }).to.throw(`'passkeyConfigRequest' must be a valid non-empty object.'`); + }); + }); + + it('should throw for invalid passkey config request attribute', () => { + const invalidAttributeObject = deepCopy(passkeyConfigRequest) as any; + invalidAttributeObject.invalidAttribute = 'invalid'; + expect(() => { + PasskeyConfig.buildServerRequest(invalidAttributeObject); + }).to.throw(`'invalidAttribute' is not a valid PasskeyConfigRequest parameter.`); + }); + + const invalidExpectedOriginsObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidExpectedOriginsObjects.forEach((expectedOriginsObject) => { + it('should throw for invalid expected origins values', () => { + let request = deepCopy(passkeyConfigRequest) as any; + request.expectedOrigins = expectedOriginsObject; + expect(() => { + PasskeyConfig.buildServerRequest(true, request as any); + }).to.throw(`'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`); + }); + }); + }); + }); + + describe('constructor', () => { + const passkeyConfig = new PasskeyConfig(serverResponse); + it('should not throw on valid initialization', () => { + expect(() => new PasskeyConfig(serverResponse)).not.to.throw(); + }); + + it('should set readonly properties', () => { + const expectedServerResponse = { + name: `projects/project-id/passkeyConfig`, + rpId: `project-id.firebaseapp.com`, + expectedOrigins: ['app1', 'example.com'], + }; + expect(passkeyConfig.name).to.equal(expectedServerResponse.name); + expect(passkeyConfig.rpId).to.equal(expectedServerResponse.rpId); + expect(passkeyConfig.expectedOrigins).to.equal(expectedServerResponse.expectedOrigins); + }); + }); + + describe('toJSON', () => { + it('should return the expected object representation of passkey config', () => { + expect(new PasskeyConfig(serverResponse).toJSON()).to.deep.equal({ + name: deepCopy(serverResponse.name), + rpId: deepCopy(serverResponse).rpId, + expectedOrigins: deepCopy(serverResponse.expectedOrigins), + }); + }); + + it('should not populate optional fields if not available', () => { + const serverResponseOptionalCopy: PasskeyConfigServerResponse = deepCopy(serverResponse); + delete serverResponseOptionalCopy.rpId; + delete serverResponseOptionalCopy.expectedOrigins; + expect(new PasskeyConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ + name: deepCopy(serverResponse.name), + }); + }); + }); +}); \ No newline at end of file From 218e7d363d138730b4f889ecf80334e1fd14865f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:20:40 +0000 Subject: [PATCH 09/24] fixed unit test errors --- etc/firebase-admin.auth.api.md | 4 +- src/auth/passkey-config-manager.ts | 8 +- src/auth/passkey-config.ts | 22 +- test/integration/auth.spec.ts | 12 +- test/unit/auth/passkey-config-manager.spec.ts | 256 ++++++++++++++++++ test/unit/auth/passkey-config.spec.ts | 18 +- 6 files changed, 286 insertions(+), 34 deletions(-) create mode 100644 test/unit/auth/passkey-config-manager.spec.ts diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index fe03cb5ed8..be128aafab 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -367,11 +367,11 @@ export class PasskeyConfig { export class PasskeyConfigManager { constructor(app: App); // (undocumented) - createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; + createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; // (undocumented) getPasskeyConfig(tenantId?: string): Promise; // (undocumented) - updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; + updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; } // @public (undocumented) diff --git a/src/auth/passkey-config-manager.ts b/src/auth/passkey-config-manager.ts index 184f96a8bd..045f753684 100644 --- a/src/auth/passkey-config-manager.ts +++ b/src/auth/passkey-config-manager.ts @@ -17,7 +17,7 @@ import { App } from '../app'; import { AuthRequestHandler, } from './auth-api-request'; -import {PasskeyConfig, PasskeyConfigClientRequest, PasskeyConfigRequest, PasskeyConfigServerResponse} from './passkey-config'; +import { PasskeyConfig, PasskeyConfigClientRequest, PasskeyConfigRequest, PasskeyConfigServerResponse } from './passkey-config'; export class PasskeyConfigManager { @@ -28,7 +28,7 @@ export class PasskeyConfigManager { } public getPasskeyConfig(tenantId?: string): Promise { - return this.authRequestHandler.getPasskeyConfig() + return this.authRequestHandler.getPasskeyConfig(tenantId) .then((response: PasskeyConfigServerResponse) => { return new PasskeyConfig(response); }); @@ -38,13 +38,13 @@ export class PasskeyConfigManager { return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId) .then((response: PasskeyConfigClientRequest) => { return new PasskeyConfig(response); - }) + }); } public updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { return this.authRequestHandler.updatePasskeyConfig(false, tenantId, passkeyConfigRequest) .then((response: PasskeyConfigClientRequest) => { return new PasskeyConfig(response); - }) + }); } } diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts index 667e46333d..efa3b90805 100644 --- a/src/auth/passkey-config.ts +++ b/src/auth/passkey-config.ts @@ -69,20 +69,20 @@ export class PasskeyConfig { ); } } - if(!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins) || !validator.isNonNullObject(passkeyConfigRequest.expectedOrigins)) { + if(!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, ); } - for(const origin in passkeyConfigRequest.expectedOrigins) { - if(!validator.isString(origin)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, - ); - } - } + for (const origin of passkeyConfigRequest.expectedOrigins) { + if (!validator.isNonEmptyString(origin)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, + ); + } + } }; public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest { @@ -91,8 +91,8 @@ export class PasskeyConfig { if(isCreateRequest && typeof rpId !== 'undefined') { request.rpId = rpId; } - if(typeof request.expectedOrigins !== 'undefined') { - request.expectedOrigins = passkeyConfigRequest?.expectedOrigins; + if(typeof passkeyConfigRequest?.expectedOrigins !== 'undefined') { + request.expectedOrigins = passkeyConfigRequest.expectedOrigins; } return request; }; diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 9473ba64f1..1d38e836d6 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -36,7 +36,6 @@ import { } from '../../lib/auth/index'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; -import {PasskeyConfigRequest} from '../../src/auth'; const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires @@ -2206,15 +2205,10 @@ describe('admin.auth', () => { expectedOrigins: ['app1', 'example.com'], }; - // Helper function to reset passkey config to the initial state - async function resetPasskeyConfig() { - const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; - await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); - } - // Before each test, reset the passkey config to the initial state beforeEach(async () => { - await resetPasskeyConfig(); + const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; + await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); }); it('createPasskeyConfig() should create passkey config with expected passkeyConfig', async () => { @@ -2223,7 +2217,7 @@ describe('admin.auth', () => { const createdPasskeyConfig = await getAuth().passkeyConfigManager().createPasskeyConfig(rpId, createRequest); const passkeyConfigObj = createdPasskeyConfig.toJSON(); - + expect(passkeyConfigObj).to.deep.equal(expectedPasskeyConfig); }); diff --git a/test/unit/auth/passkey-config-manager.spec.ts b/test/unit/auth/passkey-config-manager.spec.ts new file mode 100644 index 0000000000..b896fd35ec --- /dev/null +++ b/test/unit/auth/passkey-config-manager.spec.ts @@ -0,0 +1,256 @@ +/*! + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { PasskeyConfigManager } from '../../../src/auth/passkey-config-manager'; +import { + PasskeyConfig, PasskeyConfigServerResponse, PasskeyConfigRequest, +} from '../../../src/auth/passkey-config'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('PasskeyConfigManager', () => { + let mockApp: FirebaseApp; + let passkeyConfigManager: PasskeyConfigManager; + let nullAccessTokenPasskeyConfigManager: PasskeyConfigManager; + let malformedAccessTokenPasskeyConfigManager: PasskeyConfigManager; + let rejectedPromiseAccessTokenPasskeyConfigManager: PasskeyConfigManager; + const GET_CONFIG_RESPONSE: PasskeyConfigServerResponse = { + name: `projects/project-id/passkeyConfig`, + rpId: `project-id.firebaseapp.com`, + expectedOrigins: ['app1', 'example.com'], +}; + + before(() => { + mockApp = mocks.app(); + passkeyConfigManager = new PasskeyConfigManager(mockApp); + nullAccessTokenPasskeyConfigManager = new PasskeyConfigManager( + mocks.appReturningNullAccessToken()); + malformedAccessTokenPasskeyConfigManager = new PasskeyConfigManager( + mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenPasskeyConfigManager = new PasskeyConfigManager( + mocks.appRejectedWhileFetchingAccessToken()); + }); + + after(() => { + return mockApp.delete(); + }); + + describe('getPasskeyConfig()', () => { + const expectedPasskeyConfig = new PasskeyConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenPasskeyConfigManager.getPasskeyConfig() + .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.getPasskeyConfig() + .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.getPasskeyConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a Passkey Config on success', () => { + // Stub getPasskeyConfig to return expected result. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getPasskeyConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(stub); + return passkeyConfigManager.getPasskeyConfig() + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce; + // Confirm expected project config returned. + expect(result).to.deep.equal(expectedPasskeyConfig); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getConfig to throw a backend error. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getPasskeyConfig') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return passkeyConfigManager.getPasskeyConfig() + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce; + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('createPasskeyConfig()', () => { + const rpId: string = 'project-id.firebaseapp.com'; + const expectedOrigins: string[] = ['app1', 'example.com'] + const passkeyConfigRequest: PasskeyConfigRequest = { + expectedOrigins: expectedOrigins , + }; + const expectedPasskeyConfig = new PasskeyConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to create the config provided.'); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + sinon.restore(); + }); + + it('should be rejected given no passkeyConfigOptions', () => { + 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', () => { + console.log("TEST===" + JSON.stringify(passkeyConfigRequest)); + return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, 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) + .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) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a PasskeyConfig on createPasskeyConfig request success', () => { + // Stub createPasskeyConfig to return expected result. + const stub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(stub); + return passkeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest) + .then((actualPasskeyConfig) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(true, undefined, passkeyConfigRequest, rpId); + // Confirm expected Passkey Config object returned. + expect(actualPasskeyConfig).to.deep.equal(expectedPasskeyConfig); + }); + }); + + it('should throw an error when createPasskeyConfig returns an error', () => { + // Stub createPasskeyConfig to throw a backend error. + const stub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return passkeyConfigManager.createPasskeyConfig(rpId, 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); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('updatePasskeyConfig()', () => { + const passkeyConfigOptions: PasskeyConfigRequest = { + expectedOrigins: ['app1', 'example.com', 'app2'], + }; + const expectedPasskeyConfig = new PasskeyConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to update the config provided.'); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no passkeyConfigOptions', () => { + return (passkeyConfigManager as any).updatePasskeyConfig(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.updatePasskeyConfig(passkeyConfigOptions) + .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.updatePasskeyConfig(passkeyConfigOptions) + .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.updatePasskeyConfig(passkeyConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a PasskeyConfig on updatePasskeyConfig request success', () => { + // Stub updatePasskeyConfig to return expected result. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(updateConfigStub); + return passkeyConfigManager.updatePasskeyConfig(passkeyConfigOptions) + .then((actualPasskeyConfig) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(false, undefined, passkeyConfigOptions); + // Confirm expected Project Config object returned. + expect(actualPasskeyConfig).to.deep.equal(expectedPasskeyConfig); + }); + }); + + it('should throw an error when updatePasskeyConfig returns an error', () => { + // Stub updatePasskeyConfig to throw a backend error. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updatePasskeyConfig') + .returns(Promise.reject(expectedError)); + stubs.push(updateConfigStub); + return passkeyConfigManager.updatePasskeyConfig(passkeyConfigOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(false, undefined, passkeyConfigOptions); + // 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 f8c3887930..2dc9ee2301 100644 --- a/test/unit/auth/passkey-config.spec.ts +++ b/test/unit/auth/passkey-config.spec.ts @@ -36,7 +36,7 @@ describe('PasskeyConfig', () => { expectedOrigins: ['app1', 'example.com'], }; const passkeyConfigRequest: PasskeyConfigRequest = { - expectedOrigins: ['app1', 'website.com'], + expectedOrigins: ['app1', 'example.com'], }; describe('buildServerRequest', () => { describe('for a create request', () => { @@ -51,15 +51,17 @@ describe('PasskeyConfig', () => { 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(`'rpId' must be a valid non-empty string'`); + it(`should throw on invalid rpId ${rpId}`, () => { + expect(() => PasskeyConfig.buildServerRequest(true, passkeyConfigRequest, rpId as any)).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, 'project-id.firebaseapp.com')).to.throw(`'rpId' must be a valid non-empty string'`); + expect(() => { + PasskeyConfig.buildServerRequest(false, passkeyConfigRequest, 'rpId'); + }).to.throw(`'rpId' cannot be changed once created.`); }); it('should create a client request with valid params', () => { @@ -75,7 +77,7 @@ describe('PasskeyConfig', () => { nonObjects.forEach((request) => { it('should throw on invalid PasskeyConfigRequest:' + JSON.stringify(request), () => { expect(() => { - PasskeyConfig.buildServerRequest(true, request as any); + PasskeyConfig.buildServerRequest(false, request as any); }).to.throw(`'passkeyConfigRequest' must be a valid non-empty object.'`); }); }); @@ -84,7 +86,7 @@ describe('PasskeyConfig', () => { const invalidAttributeObject = deepCopy(passkeyConfigRequest) as any; invalidAttributeObject.invalidAttribute = 'invalid'; expect(() => { - PasskeyConfig.buildServerRequest(invalidAttributeObject); + PasskeyConfig.buildServerRequest(false, invalidAttributeObject); }).to.throw(`'invalidAttribute' is not a valid PasskeyConfigRequest parameter.`); }); @@ -94,7 +96,7 @@ describe('PasskeyConfig', () => { let request = deepCopy(passkeyConfigRequest) as any; request.expectedOrigins = expectedOriginsObject; expect(() => { - PasskeyConfig.buildServerRequest(true, request as any); + PasskeyConfig.buildServerRequest(false, request as any); }).to.throw(`'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`); }); }); @@ -115,7 +117,7 @@ describe('PasskeyConfig', () => { }; expect(passkeyConfig.name).to.equal(expectedServerResponse.name); expect(passkeyConfig.rpId).to.equal(expectedServerResponse.rpId); - expect(passkeyConfig.expectedOrigins).to.equal(expectedServerResponse.expectedOrigins); + expect(passkeyConfig.expectedOrigins).to.deep.equal(expectedServerResponse.expectedOrigins); }); }); From 0977e7a822264bfbcdd18da0d06b732d242234f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 15:59:30 +0000 Subject: [PATCH 10/24] lint fixes + integration --- src/auth/auth-api-request.ts | 14 +- src/auth/passkey-config-manager.ts | 10 +- src/auth/passkey-config.ts | 101 ++++----- test/integration/auth.spec.ts | 45 ++-- test/unit/auth/passkey-config-manager.spec.ts | 41 ++-- test/unit/auth/passkey-config.spec.ts | 199 +++++++++--------- 6 files changed, 210 insertions(+), 200 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 945d1f93d9..f469a8cd46 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -43,7 +43,7 @@ import { SAMLUpdateAuthProviderRequest } from './auth-config'; import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; -import {PasskeyConfig, PasskeyConfigServerResponse, PasskeyConfigRequest} from './passkey-config'; +import { PasskeyConfig, PasskeyConfigServerResponse, PasskeyConfigRequest } from './passkey-config'; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { @@ -2108,7 +2108,8 @@ const UPDATE_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig?updateMask={update }); /** Instantiates the getPasskeyConfig endpoint settings. */ -const UPDATE_TENANT_PASSKEY_CONFIG = new ApiSettings('/tenant/{tenantId}/passkeyConfig?updateMask={updateMask}', 'PATCH') +const UPDATE_TENANT_PASSKEY_CONFIG = new ApiSettings( + '/tenant/{tenantId}/passkeyConfig?updateMask={updateMask}', 'PATCH') .setResponseValidator((response: any) => { // Response should always contain at least the config name. if (!validator.isNonEmptyString(response.name)) { @@ -2296,18 +2297,21 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { } public getPasskeyConfig(tenantId?: string): Promise { - return this.invokeRequestHandler(this.authResourceUrlBuilder, tenantId? GET_TENANT_PASSKEY_CONFIG: GET_PASSKEY_CONFIG, {}, {}) + return this.invokeRequestHandler(this.authResourceUrlBuilder, + tenantId? GET_TENANT_PASSKEY_CONFIG: GET_PASSKEY_CONFIG, {}, {}) .then((response: any) => { return response as PasskeyConfigServerResponse; }); } - public updatePasskeyConfig(isCreateRequest: boolean, tenantId?: string, options?: PasskeyConfigRequest, rpId?: string): Promise { + public updatePasskeyConfig(isCreateRequest: boolean, tenantId?: string, + options?: PasskeyConfigRequest, rpId?: string): Promise { try { const request = PasskeyConfig.buildServerRequest(isCreateRequest, options, rpId); const updateMask = utils.generateUpdateMask(request); return this.invokeRequestHandler( - this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG, request, { updateMask: updateMask.join(',') }) + 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 045f753684..a0793d1d23 100644 --- a/src/auth/passkey-config-manager.ts +++ b/src/auth/passkey-config-manager.ts @@ -17,7 +17,12 @@ import { App } from '../app'; import { AuthRequestHandler, } from './auth-api-request'; -import { PasskeyConfig, PasskeyConfigClientRequest, PasskeyConfigRequest, PasskeyConfigServerResponse } from './passkey-config'; +import { + PasskeyConfig, + PasskeyConfigClientRequest, + PasskeyConfigRequest, + PasskeyConfigServerResponse +} from './passkey-config'; export class PasskeyConfigManager { @@ -34,7 +39,8 @@ export class PasskeyConfigManager { }); } - public createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { + public createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, + tenantId?: string): Promise { return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId) .then((response: PasskeyConfigClientRequest) => { return new PasskeyConfig(response); diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts index efa3b90805..043189eb99 100644 --- a/src/auth/passkey-config.ts +++ b/src/auth/passkey-config.ts @@ -15,7 +15,7 @@ */ import * as validator from '../utils/validator'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; -import {deepCopy} from '../utils/deep-copy'; +import { deepCopy } from '../utils/deep-copy'; export interface PasskeyConfigRequest { expectedOrigins?: string[]; @@ -38,76 +38,77 @@ export class PasskeyConfig { public readonly rpId?: string; public readonly expectedOrigins?: string[]; - private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string) { - if(isCreateRequest && !validator.isNonEmptyString(rpId)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `'rpId' must be a valid non-empty string'`, - ); + private static validate(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): void { + if (isCreateRequest && !validator.isNonEmptyString(rpId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '\'rpId\' must be a valid non-empty string\'', + ); } - if(!isCreateRequest && typeof rpId !== 'undefined') { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `'rpId' cannot be changed once created.'`, - ); + 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, - `'passkeyConfigRequest' must be a valid non-empty object.'`, - ); + if (!validator.isNonNullObject(passkeyConfigRequest)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '\'passkeyConfigRequest\' must be a valid non-empty object.\'', + ); } const validKeys = { - expectedOrigins: true, + expectedOrigins: true, }; // Check for unsupported top level attributes. for (const key in passkeyConfigRequest) { - if (!(key in validKeys)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `'${key}' is not a valid PasskeyConfigRequest parameter.`, - ); - } - } - if(!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins)) { + if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, - ); + AuthClientErrorCode.INVALID_ARGUMENT, + `'${key}' is not a valid PasskeyConfigRequest parameter.`, + ); + } + } + if (!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\'', + ); } for (const origin of passkeyConfigRequest.expectedOrigins) { if (!validator.isNonEmptyString(origin)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`, + '\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\'', ); } } - }; + } - public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest { + public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, + rpId?: string): PasskeyConfigClientRequest { PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest, rpId); - let request: PasskeyConfigClientRequest = {}; - if(isCreateRequest && typeof rpId !== 'undefined') { - request.rpId = rpId; + const request: PasskeyConfigClientRequest = {}; + if (isCreateRequest && typeof rpId !== 'undefined') { + request.rpId = rpId; } - if(typeof passkeyConfigRequest?.expectedOrigins !== 'undefined') { - request.expectedOrigins = passkeyConfigRequest.expectedOrigins; + if (typeof passkeyConfigRequest?.expectedOrigins !== 'undefined') { + request.expectedOrigins = passkeyConfigRequest.expectedOrigins; } return request; - }; + } constructor(response: PasskeyConfigServerResponse) { - if(typeof response.name !== 'undefined') { - this.name = response.name; + if (typeof response.name !== 'undefined') { + this.name = response.name; } - if(typeof response.rpId !== 'undefined') { - this.rpId = response.rpId; - }; - if(typeof response.expectedOrigins !== 'undefined') { - this.expectedOrigins = response.expectedOrigins; + if (typeof response.rpId !== 'undefined') { + this.rpId = response.rpId; } - }; + if (typeof response.expectedOrigins !== 'undefined') { + this.expectedOrigins = response.expectedOrigins; + } + } public toJSON(): object { const json = { @@ -115,17 +116,17 @@ export class PasskeyConfig { rpId: deepCopy(this.rpId), expectedOrigins: deepCopy(this.expectedOrigins), }; - if(typeof json.name === 'undefined') { + if (typeof json.name === 'undefined') { delete json.name; } - if(typeof json.rpId === 'undefined') { + if (typeof json.rpId === 'undefined') { delete json.rpId; } - if(typeof json.expectedOrigins === 'undefined') { + if (typeof json.expectedOrigins === 'undefined') { delete json.expectedOrigins; } return json; } -}; +} diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 1d38e836d6..7aea619078 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -2198,44 +2198,43 @@ describe('admin.auth', () => { }); describe('Passkey config management operations', () => { - // Define expected passkey configuration - const expectedPasskeyConfig = { - name: `projects/{$projectId}/passkeyConfig`, - rpId: `{$projectId}.firebaseapp.com`, - expectedOrigins: ['app1', 'example.com'], - }; // Before each test, reset the passkey config to the initial state beforeEach(async () => { - const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; - await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); + // const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; + // await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); }); it('createPasskeyConfig() should create passkey config with expected passkeyConfig', async () => { - const rpId = `{$projectId}.firebaseapp.com`; + const rpId = projectId + '.firebaseapp.com'; const createRequest = { expectedOrigins: ['app1', 'example.com'] }; - const createdPasskeyConfig = await getAuth().passkeyConfigManager().createPasskeyConfig(rpId, createRequest); - const passkeyConfigObj = createdPasskeyConfig.toJSON(); - expect(passkeyConfigObj).to.deep.equal(expectedPasskeyConfig); + expect(createdPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); + expect(createdPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); + expect(createdPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com']); }); + + // TODO: uncomment when the GET endpoint is released in prod + // it('getPasskeyConfig() should resolve with expected passkeyConfig', async () => { + // const actualPasskeyConfig = await getAuth().passkeyConfigManager().getPasskeyConfig(); - it('getPasskeyConfig() should resolve with expected passkeyConfig', async () => { - const actualPasskeyConfig = await getAuth().passkeyConfigManager().getPasskeyConfig(); - const actualPasskeyConfigObj = actualPasskeyConfig.toJSON(); - - expect(actualPasskeyConfigObj).to.deep.equal(expectedPasskeyConfig); - }); + // expect(actualPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); + // expect(actualPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); + // expect(actualPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com']); + // }); it('updatePasskeyConfig() should resolve with updated expectedOrigins', async () => { - const updateRequest = { expectedOrigins: ['app1', 'example.com', 'app2'] }; - const expectedUpdatedPasskeyConfig = { ...expectedPasskeyConfig, expectedOrigins: updateRequest.expectedOrigins }; + const updateRequest = { + expectedOrigins: ['app1', 'example.com', 'app2'] + }; const updatedPasskeyConfig = await getAuth().passkeyConfigManager().updatePasskeyConfig(updateRequest); - const passkeyConfigObj = updatedPasskeyConfig.toJSON(); - - expect(passkeyConfigObj).to.deep.equal(expectedUpdatedPasskeyConfig); + + expect(updatedPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); + // TODO: backend validation needs to fixed in order for this statement to succeed. + // expect(updatedPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); + expect(updatedPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com', 'app2']); }); }); diff --git a/test/unit/auth/passkey-config-manager.spec.ts b/test/unit/auth/passkey-config-manager.spec.ts index b896fd35ec..5ce7c576ca 100644 --- a/test/unit/auth/passkey-config-manager.spec.ts +++ b/test/unit/auth/passkey-config-manager.spec.ts @@ -44,10 +44,10 @@ describe('PasskeyConfigManager', () => { let malformedAccessTokenPasskeyConfigManager: PasskeyConfigManager; let rejectedPromiseAccessTokenPasskeyConfigManager: PasskeyConfigManager; const GET_CONFIG_RESPONSE: PasskeyConfigServerResponse = { - name: `projects/project-id/passkeyConfig`, - rpId: `project-id.firebaseapp.com`, + name: 'projects/project-id/passkeyConfig', + rpId: 'project-id.firebaseapp.com', expectedOrigins: ['app1', 'example.com'], -}; + }; before(() => { mockApp = mocks.app(); @@ -121,7 +121,7 @@ describe('PasskeyConfigManager', () => { }); describe('createPasskeyConfig()', () => { - const rpId: string = 'project-id.firebaseapp.com'; + const rpId = 'project-id.firebaseapp.com'; const expectedOrigins: string[] = ['app1', 'example.com'] const passkeyConfigRequest: PasskeyConfigRequest = { expectedOrigins: expectedOrigins , @@ -131,31 +131,30 @@ describe('PasskeyConfigManager', () => { AuthClientErrorCode.INTERNAL_ERROR, 'Unable to create the config provided.'); // Stubs used to simulate underlying API calls. - let stubs: sinon.SinonStub[] = []; + const stubs: sinon.SinonStub[] = []; afterEach(() => { sinon.restore(); }); it('should be rejected given no passkeyConfigOptions', () => { - return (passkeyConfigManager as any).createPasskeyConfig(null as unknown as PasskeyConfigRequest) - .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); - }); + 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', () => { - console.log("TEST===" + JSON.stringify(passkeyConfigRequest)); - return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, passkeyConfigRequest) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenPasskeyConfigManager.createPasskeyConfig(rpId, 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) - .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) + .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) - .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) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); it('should resolve with a PasskeyConfig on createPasskeyConfig request success', () => { // Stub createPasskeyConfig to return expected result. diff --git a/test/unit/auth/passkey-config.spec.ts b/test/unit/auth/passkey-config.spec.ts index 2dc9ee2301..298615af79 100644 --- a/test/unit/auth/passkey-config.spec.ts +++ b/test/unit/auth/passkey-config.spec.ts @@ -19,9 +19,9 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import { - PasskeyConfig, PasskeyConfigRequest, PasskeyConfigServerResponse, PasskeyConfigClientRequest - } from '../../../src/auth/passkey-config'; -import {deepCopy} from '../../../src/utils/deep-copy'; + PasskeyConfig, PasskeyConfigRequest, PasskeyConfigServerResponse, PasskeyConfigClientRequest +} from '../../../src/auth/passkey-config'; +import { deepCopy } from '../../../src/utils/deep-copy'; chai.should(); chai.use(sinonChai); @@ -30,113 +30,114 @@ chai.use(chaiAsPromised); const expect = chai.expect; describe('PasskeyConfig', () => { - const serverResponse: PasskeyConfigServerResponse = { - name: `projects/project-id/passkeyConfig`, - rpId: `project-id.firebaseapp.com`, - expectedOrigins: ['app1', 'example.com'], - }; - const passkeyConfigRequest: PasskeyConfigRequest = { - 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, - expectedOrigins: passkeyConfigRequest.expectedOrigins, - }; - expect(PasskeyConfig.buildServerRequest(true, passkeyConfigRequest, validRpId)).to.deep.equal(expectedRequest); - }); + const serverResponse: PasskeyConfigServerResponse = { + name: 'projects/project-id/passkeyConfig', + rpId: 'project-id.firebaseapp.com', + expectedOrigins: ['app1', 'example.com'], + }; + const passkeyConfigRequest: PasskeyConfigRequest = { + 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, + expectedOrigins: passkeyConfigRequest.expectedOrigins, + }; + expect(PasskeyConfig.buildServerRequest(true, passkeyConfigRequest, validRpId)).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(`'rpId' must be a valid non-empty string`); - }); - }); - }); + 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( + '\'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.`); - }); + 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 create a client request with valid params', () => { - const expectedRequest: PasskeyConfigClientRequest = { - expectedOrigins: passkeyConfigRequest.expectedOrigins, - }; - expect(PasskeyConfig.buildServerRequest(false, passkeyConfigRequest)).to.deep.equal(expectedRequest); - }); - }); + it('should create a client request with valid params', () => { + const expectedRequest: PasskeyConfigClientRequest = { + 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), () => { - expect(() => { - PasskeyConfig.buildServerRequest(false, request as any); - }).to.throw(`'passkeyConfigRequest' must be a valid non-empty object.'`); - }); - }); + 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), () => { + expect(() => { + PasskeyConfig.buildServerRequest(false, request as any); + }).to.throw('\'passkeyConfigRequest\' must be a valid non-empty object.\''); + }); + }); - it('should throw for invalid passkey config request attribute', () => { - const invalidAttributeObject = deepCopy(passkeyConfigRequest) as any; - invalidAttributeObject.invalidAttribute = 'invalid'; - expect(() => { - PasskeyConfig.buildServerRequest(false, invalidAttributeObject); - }).to.throw(`'invalidAttribute' is not a valid PasskeyConfigRequest parameter.`); - }); + it('should throw for invalid passkey config request attribute', () => { + const invalidAttributeObject = deepCopy(passkeyConfigRequest) as any; + invalidAttributeObject.invalidAttribute = 'invalid'; + expect(() => { + PasskeyConfig.buildServerRequest(false, invalidAttributeObject); + }).to.throw('\'invalidAttribute\' is not a valid PasskeyConfigRequest parameter.'); + }); - const invalidExpectedOriginsObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; - invalidExpectedOriginsObjects.forEach((expectedOriginsObject) => { - it('should throw for invalid expected origins values', () => { - let request = deepCopy(passkeyConfigRequest) as any; - request.expectedOrigins = expectedOriginsObject; - expect(() => { - PasskeyConfig.buildServerRequest(false, request as any); - }).to.throw(`'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.'`); - }); - }); - }); + const invalidExpectedOriginsObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidExpectedOriginsObjects.forEach((expectedOriginsObject) => { + it('should throw for invalid expected origins values', () => { + const request = deepCopy(passkeyConfigRequest) as any; + request.expectedOrigins = expectedOriginsObject; + expect(() => { + PasskeyConfig.buildServerRequest(false, request as any); + }).to.throw('\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\''); }); + }); + }); + }); - describe('constructor', () => { - const passkeyConfig = new PasskeyConfig(serverResponse); - it('should not throw on valid initialization', () => { - expect(() => new PasskeyConfig(serverResponse)).not.to.throw(); - }); + describe('constructor', () => { + const passkeyConfig = new PasskeyConfig(serverResponse); + it('should not throw on valid initialization', () => { + expect(() => new PasskeyConfig(serverResponse)).not.to.throw(); + }); - it('should set readonly properties', () => { - const expectedServerResponse = { - name: `projects/project-id/passkeyConfig`, - rpId: `project-id.firebaseapp.com`, - expectedOrigins: ['app1', 'example.com'], - }; - expect(passkeyConfig.name).to.equal(expectedServerResponse.name); - expect(passkeyConfig.rpId).to.equal(expectedServerResponse.rpId); - expect(passkeyConfig.expectedOrigins).to.deep.equal(expectedServerResponse.expectedOrigins); - }); + it('should set readonly properties', () => { + const expectedServerResponse = { + name: 'projects/project-id/passkeyConfig', + rpId: 'project-id.firebaseapp.com', + expectedOrigins: ['app1', 'example.com'], + }; + expect(passkeyConfig.name).to.equal(expectedServerResponse.name); + expect(passkeyConfig.rpId).to.equal(expectedServerResponse.rpId); + expect(passkeyConfig.expectedOrigins).to.deep.equal(expectedServerResponse.expectedOrigins); }); + }); - describe('toJSON', () => { - it('should return the expected object representation of passkey config', () => { - expect(new PasskeyConfig(serverResponse).toJSON()).to.deep.equal({ - name: deepCopy(serverResponse.name), - rpId: deepCopy(serverResponse).rpId, - expectedOrigins: deepCopy(serverResponse.expectedOrigins), - }); - }); + describe('toJSON', () => { + it('should return the expected object representation of passkey config', () => { + expect(new PasskeyConfig(serverResponse).toJSON()).to.deep.equal({ + name: deepCopy(serverResponse.name), + rpId: deepCopy(serverResponse).rpId, + expectedOrigins: deepCopy(serverResponse.expectedOrigins), + }); + }); - it('should not populate optional fields if not available', () => { - const serverResponseOptionalCopy: PasskeyConfigServerResponse = deepCopy(serverResponse); - delete serverResponseOptionalCopy.rpId; - delete serverResponseOptionalCopy.expectedOrigins; - expect(new PasskeyConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ - name: deepCopy(serverResponse.name), - }); - }); + it('should not populate optional fields if not available', () => { + const serverResponseOptionalCopy: PasskeyConfigServerResponse = deepCopy(serverResponse); + delete serverResponseOptionalCopy.rpId; + delete serverResponseOptionalCopy.expectedOrigins; + expect(new PasskeyConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ + name: deepCopy(serverResponse.name), + }); }); + }); }); \ No newline at end of file From dfe7f4a0d44651a87691e97e53d611082355d522 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 10:41:33 -0700 Subject: [PATCH 11/24] remove integration tests --- test/integration/auth.spec.ts | 44 +---------------------------------- 1 file changed, 1 insertion(+), 43 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 7aea619078..7b113b3156 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -32,7 +32,7 @@ import { AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, UserMetadata, MultiFactorConfig, - PasswordPolicyConfig, SmsRegionConfig + PasswordPolicyConfig, SmsRegionConfig, } from '../../lib/auth/index'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; @@ -2197,48 +2197,6 @@ describe('admin.auth', () => { }); }); - describe('Passkey config management operations', () => { - - // Before each test, reset the passkey config to the initial state - beforeEach(async () => { - // const resetRequest = { expectedOrigins: expectedPasskeyConfig.expectedOrigins }; - // await getAuth().passkeyConfigManager().updatePasskeyConfig(resetRequest); - }); - - it('createPasskeyConfig() should create passkey config with expected passkeyConfig', async () => { - const rpId = projectId + '.firebaseapp.com'; - const createRequest = { expectedOrigins: ['app1', 'example.com'] }; - const createdPasskeyConfig = await getAuth().passkeyConfigManager().createPasskeyConfig(rpId, createRequest); - - expect(createdPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); - expect(createdPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); - expect(createdPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com']); - }); - - // TODO: uncomment when the GET endpoint is released in prod - // it('getPasskeyConfig() should resolve with expected passkeyConfig', async () => { - // const actualPasskeyConfig = await getAuth().passkeyConfigManager().getPasskeyConfig(); - - // expect(actualPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); - // expect(actualPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); - // expect(actualPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com']); - // }); - - it('updatePasskeyConfig() should resolve with updated expectedOrigins', async () => { - const updateRequest = { - expectedOrigins: ['app1', 'example.com', 'app2'] - }; - - const updatedPasskeyConfig = await getAuth().passkeyConfigManager().updatePasskeyConfig(updateRequest); - - expect(updatedPasskeyConfig.name).to.deep.equal('projects/' + projectId + '/passkeyConfig'); - // TODO: backend validation needs to fixed in order for this statement to succeed. - // expect(updatedPasskeyConfig.rpId).to.deep.equal(projectId + '.firebaseapp.com'); - expect(updatedPasskeyConfig.expectedOrigins).to.deep.equal(['app1', 'example.com', 'app2']); - }); - }); - - describe('SAML configuration operations', () => { const authProviderConfig1 = { providerId: randomSamlProviderId(), From 1bb152676c334961e251800cc79a12834d87f8d5 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 11:36:46 -0700 Subject: [PATCH 12/24] undo package json changes --- package-lock.json | 48 +++++++++++++-------------- package.json | 2 +- test/unit/auth/passkey-config.spec.ts | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index f12dfd4f5c..40a768beba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "11.11.0", + "version": "11.10.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -431,9 +431,9 @@ } }, "@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", + "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", "dev": true }, "@fastify/busboy": { @@ -1405,9 +1405,9 @@ } }, "@types/firebase-token-generator": { - "version": "2.0.31", - "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.31.tgz", - "integrity": "sha512-mR6GuDPxFiD7nZ3x2NddRvo42ZBS22PHHNdAWuQvqp/1LfiYshZ6PzJD2+JOyLw5ZErgmw7F8R1nfpXnvBOxGg==", + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.30.tgz", + "integrity": "sha512-GcNz25MRki9ZpVfvNNrthx4t3XXjgIZ2wv729ea9F4n/1PZf4QIZlzTGoDTDeV417vmd6cPTYKUzPf4rR+qGhw==", "dev": true }, "@types/glob": { @@ -3549,15 +3549,15 @@ } }, "eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", + "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", + "@eslint/js": "8.50.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -3594,9 +3594,9 @@ }, "dependencies": { "@eslint-community/regexpp": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", - "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.2.tgz", + "integrity": "sha512-0MGxAVt1m/ZK+LTJp/j0qF7Hz97D9O/FH9Ms3ltnyIdDD57cbb1ACIQTkbHvNXtWDv5TPq7w5Kq56+cNukbo7g==", "dev": true }, "acorn": { @@ -4256,12 +4256,12 @@ "dev": true }, "flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", + "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", "dev": true, "requires": { - "flatted": "^3.2.9", + "flatted": "^3.2.7", "keyv": "^4.5.3", "rimraf": "^3.0.2" } @@ -4757,9 +4757,9 @@ } }, "globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.22.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", + "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -6126,9 +6126,9 @@ } }, "keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", + "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", "dev": true, "requires": { "json-buffer": "3.0.1" diff --git a/package.json b/package.json index 68f51b1157..446ad2cf42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "11.11.0", + "version": "11.10.1", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", diff --git a/test/unit/auth/passkey-config.spec.ts b/test/unit/auth/passkey-config.spec.ts index 298615af79..1d61bf6ec0 100644 --- a/test/unit/auth/passkey-config.spec.ts +++ b/test/unit/auth/passkey-config.spec.ts @@ -140,4 +140,4 @@ describe('PasskeyConfig', () => { }); }); }); -}); \ No newline at end of file +}); From abfb295f08d51ad7d527dacfa1fd79d765651959 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 11:20:20 -0700 Subject: [PATCH 13/24] adding comments --- etc/firebase-admin.auth.api.md | 21 ++------- src/auth/auth-api-request.ts | 24 +++++----- src/auth/passkey-config-manager.ts | 33 ++++++++++++- src/auth/passkey-config.ts | 74 ++++++++++++++++++++++++++---- 4 files changed, 112 insertions(+), 40 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index be128aafab..42e217ba60 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -345,38 +345,23 @@ export interface OIDCUpdateAuthProviderRequest { responseType?: OAuthResponseType; } -// @public (undocumented) +// @public export class PasskeyConfig { - // Warning: (ae-forgotten-export) The symbol "PasskeyConfigServerResponse" needs to be exported by the entry point index.d.ts - constructor(response: PasskeyConfigServerResponse); - // Warning: (ae-forgotten-export) The symbol "PasskeyConfigClientRequest" needs to be exported by the entry point index.d.ts - // - // (undocumented) - static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest; - // (undocumented) readonly expectedOrigins?: string[]; - // (undocumented) readonly name?: string; - // (undocumented) readonly rpId?: string; - // (undocumented) toJSON(): object; } -// @public (undocumented) +// @public export class PasskeyConfigManager { - constructor(app: App); - // (undocumented) createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; - // (undocumented) getPasskeyConfig(tenantId?: string): Promise; - // (undocumented) updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; } -// @public (undocumented) +// @public export interface PasskeyConfigRequest { - // (undocumented) expectedOrigins?: string[]; } diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index f469a8cd46..7f36461fc4 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -2071,51 +2071,51 @@ const CREATE_TENANT = new ApiSettings('/tenants', 'POST') } }); -/** Instantiates the getPasskeyConfig endpoint settings. */ +/** Instantiates the GET_PASSKEY_CONFIG endpoint settings. */ const GET_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig', 'GET') .setResponseValidator((response: any) => { - // Response should always contain at least the config name. + // Validate the response for GET_PASSKEY_CONFIG. if (!validator.isNonEmptyString(response.name)) { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get project config', + 'INTERNAL ASSERT FAILED: Unable to get passkey config', ); } }); -/** Instantiates the getPasskeyConfig endpoint settings. */ +/** Instantiates the GET_TENANT_PASSKEY_CONFIG endpoint settings. */ const GET_TENANT_PASSKEY_CONFIG = new ApiSettings('/tenants/{tenantId}/passkeyConfig', 'GET') .setResponseValidator((response: any) => { - // Response should always contain at least the config name. + // Validate the response for GET_TENANT_PASSKEY_CONFIG. if (!validator.isNonEmptyString(response.name)) { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get project config', + 'INTERNAL ASSERT FAILED: Unable to get tenant passkey config', ); } }); -/** Instantiates the getPasskeyConfig endpoint settings. */ +/** Instantiates the UPDATE_PASSKEY_CONFIG endpoint settings. */ const UPDATE_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig?updateMask={updateMask}', 'PATCH') .setResponseValidator((response: any) => { - // Response should always contain at least the config name. + // Validate the response for UPDATE_PASSKEY_CONFIG. if (!validator.isNonEmptyString(response.name)) { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get project config', + 'INTERNAL ASSERT FAILED: Unable to update passkey config', ); } }); -/** Instantiates the getPasskeyConfig endpoint settings. */ +/** Instantiates the UPDATE_TENANT_PASSKEY_CONFIG endpoint settings. */ const UPDATE_TENANT_PASSKEY_CONFIG = new ApiSettings( '/tenant/{tenantId}/passkeyConfig?updateMask={updateMask}', 'PATCH') .setResponseValidator((response: any) => { - // Response should always contain at least the config name. + // Validate the response for UPDATE_TENANT_PASSKEY_CONFIG. if (!validator.isNonEmptyString(response.name)) { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get project config', + 'INTERNAL ASSERT FAILED: Unable to update tenant passkey config', ); } }); diff --git a/src/auth/passkey-config-manager.ts b/src/auth/passkey-config-manager.ts index a0793d1d23..100ba2df89 100644 --- a/src/auth/passkey-config-manager.ts +++ b/src/auth/passkey-config-manager.ts @@ -24,14 +24,30 @@ import { PasskeyConfigServerResponse } from './passkey-config'; - +/** + * Manages Passkey Configuration for a Firebase app. + */ 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 + * @internal + */ constructor(app: App) { this.authRequestHandler = new AuthRequestHandler(app); } + /** + * 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. + */ public getPasskeyConfig(tenantId?: string): Promise { return this.authRequestHandler.getPasskeyConfig(tenantId) .then((response: PasskeyConfigServerResponse) => { @@ -39,6 +55,14 @@ 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, tenantId?: string): Promise { return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId) @@ -47,6 +71,13 @@ 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. + */ public updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { return this.authRequestHandler.updatePasskeyConfig(false, tenantId, passkeyConfigRequest) .then((response: PasskeyConfigClientRequest) => { diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts index 043189eb99..68e1b66237 100644 --- a/src/auth/passkey-config.ts +++ b/src/auth/passkey-config.ts @@ -17,50 +17,87 @@ import * as validator from '../utils/validator'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { deepCopy } from '../utils/deep-copy'; +/** + * Interface representing the properties to update in the provided passkey config. + */ export interface PasskeyConfigRequest { + /** + * 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. + */ expectedOrigins?: string[]; } +/** + * Response received from the server when retrieving, creating, or updating the passkey config. + */ export interface PasskeyConfigServerResponse { name?: string; rpId?: string; expectedOrigins?: string[]; } +/** + * Request for creating or updating the passkey config on the server. + */ export interface PasskeyConfigClientRequest { rpId?: string; expectedOrigins?: string[]; } - +/** + * Configuration for signing in users using passkeys. + */ export class PasskeyConfig { + /** + * The name of the PasskeyConfig resource. + */ public readonly name?: string; + /** + * The relying party ID for passkey verifications. + * This cannot be changed once created. + */ public readonly rpId?: string; + /** + * The 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. + */ public readonly expectedOrigins?: string[]; + /** + * Validates a passkey config request object and throws an error on failure. + * @param isCreateRequest - A boolean indicating if it's a create request or not. + * @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 { + // Validation for creating a new PasskeyConfig. if (isCreateRequest && !validator.isNonEmptyString(rpId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - '\'rpId\' must be a valid non-empty string\'', + "'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.\'', + "'rpId' cannot be changed once created.", ); } if (!validator.isNonNullObject(passkeyConfigRequest)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - '\'passkeyConfigRequest\' must be a valid non-empty object.\'', + "'passkeyConfigRequest' must be a valid non-empty object.", ); } const validKeys = { expectedOrigins: true, }; - // Check for unsupported top level attributes. + // Check for unsupported top-level attributes. for (const key in passkeyConfigRequest) { if (!(key in validKeys)) { throw new FirebaseAuthError( @@ -72,19 +109,29 @@ export class PasskeyConfig { if (!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - '\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\'', + "'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.", ); } for (const origin of passkeyConfigRequest.expectedOrigins) { if (!validator.isNonEmptyString(origin)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - '\'passkeyConfigRequest.expectedOrigins\' must be a valid non-empty array of strings.\'', + "'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.", ); } } } + /** + * Build the corresponding server request for a Passkey Config object. + * @param isCreateRequest - A boolean stating if it's a create request. + * @param passkeyConfigRequest - Passkey config to be updated. + * @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); @@ -98,6 +145,13 @@ export class PasskeyConfig { return request; } + /** + * The Passkey Config object constructor. + * @param response - The server-side response used to initialize the Passkey Config object. + * @constructor + * + * @internal + */ constructor(response: PasskeyConfigServerResponse) { if (typeof response.name !== 'undefined') { this.name = response.name; @@ -110,6 +164,10 @@ export class PasskeyConfig { } } + /** + * Returns a JSON-serializable representation of this object. + * @returns A JSON-serializable representation of this object. + */ public toJSON(): object { const json = { name: deepCopy(this.name), @@ -127,6 +185,4 @@ export class PasskeyConfig { } return json; } - } - From fff81ce53b27a352676f3fda491a7d9a9b05f6cc Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 12:33:46 -0700 Subject: [PATCH 14/24] undo package json changes --- package-lock.json | 48 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 40a768beba..f12dfd4f5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "11.10.1", + "version": "11.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -431,9 +431,9 @@ } }, "@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", + "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", "dev": true }, "@fastify/busboy": { @@ -1405,9 +1405,9 @@ } }, "@types/firebase-token-generator": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.30.tgz", - "integrity": "sha512-GcNz25MRki9ZpVfvNNrthx4t3XXjgIZ2wv729ea9F4n/1PZf4QIZlzTGoDTDeV417vmd6cPTYKUzPf4rR+qGhw==", + "version": "2.0.31", + "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.31.tgz", + "integrity": "sha512-mR6GuDPxFiD7nZ3x2NddRvo42ZBS22PHHNdAWuQvqp/1LfiYshZ6PzJD2+JOyLw5ZErgmw7F8R1nfpXnvBOxGg==", "dev": true }, "@types/glob": { @@ -3549,15 +3549,15 @@ } }, "eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", + "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", + "@eslint/js": "8.51.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -3594,9 +3594,9 @@ }, "dependencies": { "@eslint-community/regexpp": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.2.tgz", - "integrity": "sha512-0MGxAVt1m/ZK+LTJp/j0qF7Hz97D9O/FH9Ms3ltnyIdDD57cbb1ACIQTkbHvNXtWDv5TPq7w5Kq56+cNukbo7g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", "dev": true }, "acorn": { @@ -4256,12 +4256,12 @@ "dev": true }, "flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "requires": { - "flatted": "^3.2.7", + "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } @@ -4757,9 +4757,9 @@ } }, "globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -6126,9 +6126,9 @@ } }, "keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "requires": { "json-buffer": "3.0.1" diff --git a/package.json b/package.json index 446ad2cf42..68f51b1157 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "11.10.1", + "version": "11.11.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 9d69dd8aff0b8bb835a4e16e2646220c3c5de458 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 11:36:46 -0700 Subject: [PATCH 15/24] undo package json changes passkey config admin changes Bug Fix for issue #2320 (#2321) fixed unit test errors lint fixes + integration remove integration tests adding comments undo package json changes undo package json changes --- etc/firebase-admin.auth.api.md | 20 ++++++++++++++++++++ test/unit/auth/passkey-config.spec.ts | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 42e217ba60..042b27b577 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -365,6 +365,26 @@ export interface PasskeyConfigRequest { expectedOrigins?: string[]; } +// @public +export class PasskeyConfig { + readonly expectedOrigins?: string[]; + readonly name?: string; + readonly rpId?: string; + toJSON(): object; +} + +// @public +export class PasskeyConfigManager { + createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; + getPasskeyConfig(tenantId?: string): Promise; + updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; +} + +// @public +export interface PasskeyConfigRequest { + expectedOrigins?: string[]; +} + // @public export interface PasswordPolicyConfig { constraints?: CustomStrengthOptionsConfig; diff --git a/test/unit/auth/passkey-config.spec.ts b/test/unit/auth/passkey-config.spec.ts index 298615af79..1d61bf6ec0 100644 --- a/test/unit/auth/passkey-config.spec.ts +++ b/test/unit/auth/passkey-config.spec.ts @@ -140,4 +140,4 @@ describe('PasskeyConfig', () => { }); }); }); -}); \ No newline at end of file +}); From 1ab9273635fc401abb9f46dffd0ad7b4ee467aaf Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 18 Oct 2023 12:56:19 -0700 Subject: [PATCH 16/24] undo duplicate npm changes --- etc/firebase-admin.auth.api.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 042b27b577..42e217ba60 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -365,26 +365,6 @@ export interface PasskeyConfigRequest { expectedOrigins?: string[]; } -// @public -export class PasskeyConfig { - readonly expectedOrigins?: string[]; - readonly name?: string; - readonly rpId?: string; - toJSON(): object; -} - -// @public -export class PasskeyConfigManager { - createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; - getPasskeyConfig(tenantId?: string): Promise; - updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; -} - -// @public -export interface PasskeyConfigRequest { - expectedOrigins?: string[]; -} - // @public export interface PasswordPolicyConfig { constraints?: CustomStrengthOptionsConfig; From c294ff4a5f7ce5124f737c80aa7d74366465d538 Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:32:06 -0700 Subject: [PATCH 17/24] Apply suggestions from code review Co-authored-by: Kevin Cheung --- src/auth/passkey-config-manager.ts | 4 ++-- src/auth/passkey-config.ts | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/auth/passkey-config-manager.ts b/src/auth/passkey-config-manager.ts index 100ba2df89..d8607d6236 100644 --- a/src/auth/passkey-config-manager.ts +++ b/src/auth/passkey-config-manager.ts @@ -25,7 +25,7 @@ import { } from './passkey-config'; /** - * Manages Passkey Configuration for a Firebase app. + * Manages Passkey configuration for a Firebase app. */ export class PasskeyConfigManager { private readonly authRequestHandler: AuthRequestHandler; @@ -43,7 +43,7 @@ export class PasskeyConfigManager { } /** - * Retrieves the Passkey Configuration. + * 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. diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts index 68e1b66237..22e3513e3a 100644 --- a/src/auth/passkey-config.ts +++ b/src/auth/passkey-config.ts @@ -18,12 +18,12 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { deepCopy } from '../utils/deep-copy'; /** - * Interface representing the properties to update in the provided passkey config. + * Interface representing the properties to update in a passkey config. */ export interface PasskeyConfigRequest { /** - * 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. + * An array of website or app origins. Only challenges signed + * from these origins will be allowed for signing in with passkeys. */ expectedOrigins?: string[]; } @@ -59,7 +59,7 @@ export class PasskeyConfig { */ public readonly rpId?: string; /** - * The website or app origins associated with the customer's sites or apps. + * The allowed website or app origins. * Only challenges signed from these origins will be allowed for signing in with passkeys. */ public readonly expectedOrigins?: string[]; @@ -78,7 +78,7 @@ export class PasskeyConfig { if (isCreateRequest && !validator.isNonEmptyString(rpId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - "'rpId' must be a valid non-empty string.", + "'rpId' must be a non-empty string.", ); } // Validation for updating an existing PasskeyConfig. @@ -91,7 +91,7 @@ export class PasskeyConfig { if (!validator.isNonNullObject(passkeyConfigRequest)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - "'passkeyConfigRequest' must be a valid non-empty object.", + "'passkeyConfigRequest' must not be null.", ); } const validKeys = { @@ -109,21 +109,21 @@ export class PasskeyConfig { if (!validator.isNonEmptyArray(passkeyConfigRequest.expectedOrigins)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - "'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.", + "'passkeyConfigRequest.expectedOrigins' must contain at least one item.", ); } for (const origin of passkeyConfigRequest.expectedOrigins) { if (!validator.isNonEmptyString(origin)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - "'passkeyConfigRequest.expectedOrigins' must be a valid non-empty array of strings.", + "'passkeyConfigRequest.expectedOrigins' cannot contain empty strings.", ); } } } /** - * Build the corresponding server request for a Passkey Config object. + * Build a server request for a Passkey Config object. * @param isCreateRequest - A boolean stating if it's a create request. * @param passkeyConfigRequest - Passkey config to be updated. * @param rpId - (optional) Relying party ID for the request if it's a create request. From 649243844cfd4d21682442c554d9633fea0fe5f7 Mon Sep 17 00:00:00 2001 From: Pragati Date: Tue, 2 Apr 2024 14:13:44 -0700 Subject: [PATCH 18/24] update toJSON for string values --- src/auth/passkey-config.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/auth/passkey-config.ts b/src/auth/passkey-config.ts index 22e3513e3a..cfd16caf94 100644 --- a/src/auth/passkey-config.ts +++ b/src/auth/passkey-config.ts @@ -70,7 +70,7 @@ 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 { @@ -119,7 +119,7 @@ export class PasskeyConfig { "'passkeyConfigRequest.expectedOrigins' cannot contain empty strings.", ); } - } + } } /** @@ -129,10 +129,10 @@ 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, + public static buildServerRequest(isCreateRequest: boolean, passkeyConfigRequest?: PasskeyConfigRequest, rpId?: string): PasskeyConfigClientRequest { PasskeyConfig.validate(isCreateRequest, passkeyConfigRequest, rpId); const request: PasskeyConfigClientRequest = {}; @@ -149,7 +149,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) { @@ -170,8 +170,8 @@ export class PasskeyConfig { */ public toJSON(): object { const json = { - name: deepCopy(this.name), - rpId: deepCopy(this.rpId), + name: this.name, + rpId: this.rpId, expectedOrigins: deepCopy(this.expectedOrigins), }; if (typeof json.name === 'undefined') { From 041bf17982e13d1d5b03b6787b7e728b6c8a3c24 Mon Sep 17 00:00:00 2001 From: pragatimodi <110490169+pragatimodi@users.noreply.github.com> Date: Wed, 3 Apr 2024 07:44:31 -0700 Subject: [PATCH 19/24] user record changes for getAccountInfo() (#2341) * user record changes for getAccountInfo() * lint and api-extractor fixes * Apply suggestions from code review Co-authored-by: Kevin Cheung * remove `[key: string]: unknown;` field from `PasskeyInfoResponse` * add undefined displayName case * name and credentialId are not optional * add `rpId` to update --------- Co-authored-by: Kevin Cheung --- etc/firebase-admin.auth.api.md | 9 ++ src/auth/auth-api-request.ts | 14 +-- src/auth/index.ts | 1 + src/auth/passkey-config-manager.ts | 28 +++--- src/auth/passkey-config.ts | 31 +++--- src/auth/user-record.ts | 76 +++++++++++++++ test/unit/auth/passkey-config-manager.spec.ts | 21 ++-- test/unit/auth/passkey-config.spec.ts | 37 ++++---- test/unit/auth/user-record.spec.ts | 95 ++++++++++++++++++- 9 files changed, 248 insertions(+), 64 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/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/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/passkey-config-manager.ts b/src/auth/passkey-config-manager.ts index d8607d6236..decad12709 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 @@ -43,8 +43,8 @@ export class PasskeyConfigManager { } /** - * Retrieves the Passkey configuration. - * + * 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,13 @@ 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, - tenantId?: string): Promise { - return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest, rpId) + public createPasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise { + return this.authRequestHandler.updatePasskeyConfig(true, tenantId, passkeyConfigRequest) .then((response: PasskeyConfigClientRequest) => { return new PasskeyConfig(response); }); @@ -73,7 +71,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 cfd16caf94..0dad65a7a8 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 a passkey config. */ export interface PasskeyConfigRequest { + rpId?: string; /** * An array of website or app origins. Only challenges signed * from these origins will be allowed for signing in with passkeys. @@ -73,21 +74,21 @@ export class PasskeyConfig { * * @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 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. @@ -126,18 +128,17 @@ export class PasskeyConfig { * Build a server request for a Passkey Config object. * @param isCreateRequest - A boolean stating if it's a create request. * @param passkeyConfigRequest - Passkey config to be updated. - * @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; diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 5b00151401..e94f020cf1 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -56,6 +56,12 @@ export interface TotpInfoResponse { [key: string]: unknown; } +export interface PasskeyInfoResponse { + name: string; + credentialId: string; + displayName?: string; +} + export interface ProviderUserInfoResponse { rawId: string; displayName?: string; @@ -81,6 +87,7 @@ export interface GetAccountInfoUserResponse { tenantId?: string; providerUserInfo?: ProviderUserInfoResponse[]; mfaInfo?: MultiFactorInfoResponse[]; + passkeyInfo?: PasskeyInfoResponse[]; createdAt?: string; lastLoginAt?: string; lastRefreshAt?: string; @@ -357,6 +364,55 @@ export class MultiFactorSettings { } } +/** + * Interface representing a user-enrolled passkey. + */ +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; + + /** + * 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( + 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 { + return { + name: this.name, + credentialId: this.credentialId, + displayName: this.displayName, + }; + } +} + /** * Represents a user's metadata. */ @@ -582,6 +638,11 @@ export class UserRecord { */ public readonly multiFactor?: MultiFactorSettings; + /** + * Passkey-related properties for the current user, if available. + */ + public readonly passkeyInfo?: PasskeyInfo[]; + /** * @param response - The server side response returned from the getAccountInfo * endpoint. @@ -637,6 +698,15 @@ export class UserRecord { if (multiFactor.enrolledFactors.length > 0) { utils.addReadonlyGetter(this, 'multiFactor', multiFactor); } + if (response.passkeyInfo) { + const passkeys: PasskeyInfo[] = []; + response.passkeyInfo.forEach((passkey) => { + passkeys.push(new PasskeyInfo(passkey)); + }); + if (passkeys.length > 0) { + utils.addReadonlyGetter(this, 'passkeyInfo', passkeys); + } + } } /** @@ -664,6 +734,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/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.'); }); }); }); diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index dc332c13b9..40e7b8da99 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,22 @@ function getValidUserResponse(tenantId?: string): GetAccountInfoUserResponse { phoneInfo: '+16505556789', }, ], + passkeyInfo: [ + { + name: 'name1@google.com', + credentialId: 'credentialId1', + displayName: 'passkey1', + }, + { + name: 'name2@google.com', + credentialId: 'credentialId2', + displayName: 'passkey2', + }, + { + name: 'name3@google.com', + credentialId: 'credentialId3', + } + ] }; if (typeof tenantId !== 'undefined') { response.tenantId = tenantId; @@ -185,6 +201,23 @@ function getUserJSON(tenantId?: string): object { }, ], }, + passkeyInfo: [ + { + name: 'name1@google.com', + credentialId: 'credentialId1', + displayName: 'passkey1', + }, + { + name: 'name2@google.com', + credentialId: 'credentialId2', + displayName: 'passkey2', + }, + { + name: 'name3@google.com', + credentialId: 'credentialId3', + displayName: undefined, + } + ] }; } @@ -663,6 +696,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(null as any); + }).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 0389dc1c7ada7ecab5bcd9fa03f273425f754a46 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 3 Apr 2024 08:00:59 -0700 Subject: [PATCH 20/24] correct endpoint --- src/auth/auth-api-request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 6afba07be6..87ddbef22c 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -2109,7 +2109,7 @@ const UPDATE_PASSKEY_CONFIG = new ApiSettings('/passkeyConfig?updateMask={update /** Instantiates the UPDATE_TENANT_PASSKEY_CONFIG endpoint settings. */ const UPDATE_TENANT_PASSKEY_CONFIG = new ApiSettings( - '/tenant/{tenantId}/passkeyConfig?updateMask={updateMask}', 'PATCH') + '/tenants/{tenantId}/passkeyConfig?updateMask={updateMask}', 'PATCH') .setResponseValidator((response: any) => { // Validate the response for UPDATE_TENANT_PASSKEY_CONFIG. if (!validator.isNonEmptyString(response.name)) { From 53649385fde6ab07840a533703669ae0aece5850 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 3 Apr 2024 08:12:05 -0700 Subject: [PATCH 21/24] fix tenant endpoint handling --- src/auth/auth-api-request.ts | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 87ddbef22c..06f7eab958 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -2297,11 +2297,17 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { } public getPasskeyConfig(tenantId?: string): Promise { - return this.invokeRequestHandler(this.authResourceUrlBuilder, - tenantId? GET_TENANT_PASSKEY_CONFIG: GET_PASSKEY_CONFIG, {}, {}) - .then((response: any) => { - return response as PasskeyConfigServerResponse; - }); + if(tenantId) { + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT_PASSKEY_CONFIG, {}, {tenantId}) + .then((response: any) => { + return response as PasskeyConfigServerResponse; + }); + } else { + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_PASSKEY_CONFIG, {}, {}) + .then((response: any) => { + return response as PasskeyConfigServerResponse; + }); + } } public updatePasskeyConfig(isCreateRequest: boolean, tenantId?: string, @@ -2309,12 +2315,20 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { try { const request = PasskeyConfig.buildServerRequest(isCreateRequest, options); const updateMask = utils.generateUpdateMask(request); - return this.invokeRequestHandler( - this.authResourceUrlBuilder, tenantId? UPDATE_TENANT_PASSKEY_CONFIG: UPDATE_PASSKEY_CONFIG, - request, { updateMask: updateMask.join(',') }) - .then((response: any) => { - return response as PasskeyConfigServerResponse; - }); + if(tenantId) { + return this.invokeRequestHandler( + this.authResourceUrlBuilder, UPDATE_TENANT_PASSKEY_CONFIG, request, + {tenantId, updateMask: updateMask.join(',') }) + .then((response: any) => { + return response as PasskeyConfigServerResponse; + }); + } else { + return this.invokeRequestHandler( + this.authResourceUrlBuilder, UPDATE_PASSKEY_CONFIG, request, { updateMask: updateMask.join(',') }) + .then((response: any) => { + return response as PasskeyConfigServerResponse; + }); + } } catch (e) { return Promise.reject(e); } From 8d3384b1248e81ca6e7430561777b7bca7633f5b Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 3 Apr 2024 08:14:06 -0700 Subject: [PATCH 22/24] fix lint --- src/auth/auth-api-request.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 06f7eab958..022642e7ec 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -2297,8 +2297,8 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { } public getPasskeyConfig(tenantId?: string): Promise { - if(tenantId) { - return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT_PASSKEY_CONFIG, {}, {tenantId}) + if (tenantId) { + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT_PASSKEY_CONFIG, {}, { tenantId }) .then((response: any) => { return response as PasskeyConfigServerResponse; }); @@ -2315,10 +2315,10 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { try { const request = PasskeyConfig.buildServerRequest(isCreateRequest, options); const updateMask = utils.generateUpdateMask(request); - if(tenantId) { + if (tenantId) { return this.invokeRequestHandler( this.authResourceUrlBuilder, UPDATE_TENANT_PASSKEY_CONFIG, request, - {tenantId, updateMask: updateMask.join(',') }) + { tenantId, updateMask: updateMask.join(',') }) .then((response: any) => { return response as PasskeyConfigServerResponse; }); From cec0cbe1ffe221a7981d7f1f75a2a7f4656d1183 Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 3 Apr 2024 08:25:19 -0700 Subject: [PATCH 23/24] add api:extractor changes --- etc/firebase-admin.auth.api.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 7969c494e0..514737e3bf 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -355,7 +355,7 @@ export class PasskeyConfig { // @public export class PasskeyConfigManager { - createPasskeyConfig(rpId: string, passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; + createPasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; getPasskeyConfig(tenantId?: string): Promise; updatePasskeyConfig(passkeyConfigRequest: PasskeyConfigRequest, tenantId?: string): Promise; } @@ -363,6 +363,8 @@ export class PasskeyConfigManager { // @public export interface PasskeyConfigRequest { expectedOrigins?: string[]; + // (undocumented) + rpId?: string; } // @public From 2c70e61baaf162240a61478d378d2a914a73cb5d Mon Sep 17 00:00:00 2001 From: Pragati Date: Wed, 26 Jun 2024 16:56:04 -0700 Subject: [PATCH 24/24] remove uncommented code --- test/unit/auth/passkey-config.spec.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/unit/auth/passkey-config.spec.ts b/test/unit/auth/passkey-config.spec.ts index bd19310773..f292919f6d 100644 --- a/test/unit/auth/passkey-config.spec.ts +++ b/test/unit/auth/passkey-config.spec.ts @@ -63,12 +63,6 @@ describe('PasskeyConfig', () => { }); describe('for update request', () => { - // 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 = { rpId: passkeyConfigRequest.rpId,