From 817b61e77640b0f04f504d86dd879d71de1d85cf Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Thu, 3 Mar 2022 13:08:20 -0800 Subject: [PATCH 1/8] Recaptcha config changes in project config. - Implemented getProjectConfig. - Implemented updateProjectConfig. - Updated error code. --- src/auth/auth-api-request.ts | 69 +++++- src/auth/auth.ts | 12 + src/auth/index.ts | 9 + src/auth/project-config-manager.ts | 71 ++++++ src/auth/project-config.ts | 137 +++++++++++ src/utils/error.ts | 12 + test/unit/auth/project-config-manager.spec.ts | 210 ++++++++++++++++ test/unit/auth/project-config.spec.ts | 227 ++++++++++++++++++ 8 files changed, 740 insertions(+), 7 deletions(-) create mode 100644 src/auth/project-config-manager.ts create mode 100644 src/auth/project-config.ts create mode 100644 test/unit/auth/project-config-manager.spec.ts create mode 100644 test/unit/auth/project-config.spec.ts diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 13018337da..3ce3196f3d 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -42,6 +42,7 @@ import { OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest, SAMLUpdateAuthProviderRequest } from './auth-config'; +import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { @@ -102,6 +103,8 @@ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace( const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace( 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); +//https://identitytoolkit.googleapis.com/v2/projects/cicpclientproj/config + /** Maximum allowed number of tenants to download at one time. */ const MAX_LIST_TENANT_PAGE_SIZE = 1000; @@ -1961,6 +1964,29 @@ export abstract class AbstractAuthRequestHandler { } } +/** Instantiates the getConfig endpoint settings. */ +const GET_CONFIG = new ApiSettings('/config', '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 updateConfig endpoint settings. */ +const UPDATE_CONFIG = new ApiSettings('/config?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 update project config', + ); + } + }); /** Instantiates the getTenant endpoint settings. */ const GET_TENANT = new ApiSettings('/tenants/{tenantId}', 'GET') @@ -2035,7 +2061,7 @@ const CREATE_TENANT = new ApiSettings('/tenants', 'POST') */ export class AuthRequestHandler extends AbstractAuthRequestHandler { - protected readonly tenantMgmtResourceBuilder: AuthResourceUrlBuilder; + protected readonly v2ResourceBuilder: AuthResourceUrlBuilder; /** * The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp. @@ -2045,7 +2071,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ constructor(app: App) { super(app); - this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(app, 'v2'); + this.v2ResourceBuilder = new AuthResourceUrlBuilder(app, 'v2'); } /** @@ -2062,6 +2088,35 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { return new AuthResourceUrlBuilder(this.app, 'v2'); } + /** + * Get the current project's config + * @returns A promise that resolves with the project config information. + */ + public getConfig(): Promise { + return this.invokeRequestHandler(this.v2ResourceBuilder, GET_CONFIG, {}, {}) + .then((response: any) => { + return response as ProjectConfigServerResponse; + }); + } + + /** + * Update the current project's config. + * @returns A promise that resolves with the project config information. + */ + public updateConfig(recaptchaOptions: UpdateProjectConfigRequest): Promise { + try { + const request = ProjectConfig.buildServerRequest(recaptchaOptions); + const updateMask = utils.generateUpdateMask(request); + return this.invokeRequestHandler( + this.v2ResourceBuilder, UPDATE_CONFIG, request, { updateMask: updateMask.join(',') }) + .then((response: any) => { + return response as ProjectConfigServerResponse; + }); + } catch (e) { + return Promise.reject(e); + } + } + /** * Looks up a tenant by tenant ID. * @@ -2072,7 +2127,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, GET_TENANT, {}, { tenantId }) + return this.invokeRequestHandler(this.v2ResourceBuilder, GET_TENANT, {}, { tenantId }) .then((response: any) => { return response as TenantServerResponse; }); @@ -2102,7 +2157,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (typeof request.pageToken === 'undefined') { delete request.pageToken; } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, LIST_TENANTS, request) + return this.invokeRequestHandler(this.v2ResourceBuilder, LIST_TENANTS, request) .then((response: any) => { if (!response.tenants) { response.tenants = []; @@ -2122,7 +2177,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, DELETE_TENANT, undefined, { tenantId }) + return this.invokeRequestHandler(this.v2ResourceBuilder, DELETE_TENANT, undefined, { tenantId }) .then(() => { // Return nothing. }); @@ -2138,7 +2193,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { try { // Construct backend request. const request = Tenant.buildServerRequest(tenantOptions, true); - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, CREATE_TENANT, request) + return this.invokeRequestHandler(this.v2ResourceBuilder, CREATE_TENANT, request) .then((response: any) => { return response as TenantServerResponse; }); @@ -2164,7 +2219,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { // Do not traverse deep into testPhoneNumbers. The entire content should be replaced // and not just specific phone numbers. const updateMask = utils.generateUpdateMask(request, ['testPhoneNumbers']); - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, UPDATE_TENANT, request, + return this.invokeRequestHandler(this.v2ResourceBuilder, UPDATE_TENANT, request, { tenantId, updateMask: updateMask.join(',') }) .then((response: any) => { return response as TenantServerResponse; diff --git a/src/auth/auth.ts b/src/auth/auth.ts index d9b5aa7978..4808fbbdc0 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -19,6 +19,7 @@ import { App } from '../app/index'; import { AuthRequestHandler } from './auth-api-request'; import { TenantManager } from './tenant-manager'; import { BaseAuth } from './base-auth'; +import { ProjectConfigManager } from './project-config-manager'; /** * Auth service bound to the provided app. @@ -27,6 +28,7 @@ import { BaseAuth } from './base-auth'; export class Auth extends BaseAuth { private readonly tenantManager_: TenantManager; + private readonly projectConfigManager_: ProjectConfigManager; private readonly app_: App; /** @@ -38,6 +40,7 @@ export class Auth extends BaseAuth { super(app, new AuthRequestHandler(app)); this.app_ = app; this.tenantManager_ = new TenantManager(app); + this.projectConfigManager_ = new ProjectConfigManager(app); } /** @@ -57,4 +60,13 @@ export class Auth extends BaseAuth { public tenantManager(): TenantManager { return this.tenantManager_; } + + /** + * Returns the project config manager instance associated with the current project. + * + * @returns The project config manager instance associated with the current project. + */ + public projectConfigManager(): ProjectConfigManager { + return this.projectConfigManager_; + } } diff --git a/src/auth/index.ts b/src/auth/index.ts index 978cdfe8a1..3faa47cac3 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -122,6 +122,15 @@ export { TenantManager, } from './tenant-manager'; +export { + UpdateProjectConfigRequest, + ProjectConfig, +} from './project-config'; + +export { + ProjectConfigManager, +} from './project-config-manager'; + export { DecodedIdToken } from './token-verifier'; export { diff --git a/src/auth/project-config-manager.ts b/src/auth/project-config-manager.ts new file mode 100644 index 0000000000..f5597d75fc --- /dev/null +++ b/src/auth/project-config-manager.ts @@ -0,0 +1,71 @@ +/*! + * Copyright 2022 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 { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; +import { + AuthRequestHandler, +} from './auth-api-request'; + +/** + * Defines the project config manager used to help manage project config related operations. + * This includes: + *
    + *
  • The ability to update and get project config.
  • + *
  • Getting a `TenantAwareAuth` instance for running Auth related operations + * (user management, provider configuration management, token verification, + * email link generation, etc) in the context of a specified tenant.
  • + *
+ */ +export class ProjectConfigManager { + private readonly authRequestHandler: AuthRequestHandler; + + /** + * Initializes a ProjectConfigManager instance for a specified FirebaseApp. + * + * @param app - The app for this ProjectConfigManager instance. + * + * @constructor + * @internal + */ + constructor(app: App) { + this.authRequestHandler = new AuthRequestHandler(app); + } + + /** + * Get the project configuration. + * + * @returns A promise fulfilled with the project configuration. + */ + public getProjectConfig(): Promise { + return this.authRequestHandler.getConfig() + .then((response: ProjectConfigServerResponse) => { + return new ProjectConfig(response); + }) + } + /** + * Updates an existing project configuration. + * + * @param projectConfigOptions - The properties to update on the project. + * + * @returns A promise fulfilled with the update project config. + */ + public updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise { + return this.authRequestHandler.updateConfig(projectConfigOptions) + .then((response: ProjectConfigServerResponse) => { + return new ProjectConfig(response); + }) + } +} diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts new file mode 100644 index 0000000000..a9ac8bbe1f --- /dev/null +++ b/src/auth/project-config.ts @@ -0,0 +1,137 @@ +/*! + * Copyright 2022 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 { + ProviderRecaptchaConfig, + RecaptchaConfig, + RecaptchaConfigAuth, + RecaptchaKeyConfig, + RecaptchaManagedRules +} from './auth-config'; + +/** + * Interface representing the properties to update on the provided tenant. + */ +export interface UpdateProjectConfigRequest { + /** + * The recaptcha configuration to update on the project. + */ + recaptchaConfig?: RecaptchaConfig; +} + +/** + * We are only exposing the recaptcha config for now. + */ +export interface ProjectConfigServerResponse { + emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; + recaptchaManagedRules?: RecaptchaManagedRules; + recaptchaKeyConfig?: RecaptchaKeyConfig[]; +} + +export interface ProjectConfigClientRequest { + emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; + recaptchaManagedRules?: RecaptchaManagedRules; + recaptchaKeyConfig?: RecaptchaKeyConfig[]; +} + +/** +* Represents a project configuration. +*/ +export class ProjectConfig { + private readonly recaptchaConfig_?: RecaptchaConfigAuth; + + private static validate(request: any): void { + const validKeys = { + recaptchaConfig: true, + }; + if (!validator.isNonNullObject(request)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"UpdateProjectConfigRequest" must be a valid non-null object.', + ); + } + // Check for unsupported top level attributes. + for (const key in request) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${key}" is not a valid UpdateProjectConfigRequest parameter.`, + ); + } + } + RecaptchaConfigAuth.validate(request.recaptchaConfig); + } + + /** + * Build the corresponding server request for a UpdateProjectConfigRequest object. + * @param configOptions - The properties to convert to a server request. + * @returns The equivalent server request. + * + * @internal + */ + public static buildServerRequest(configOptions: UpdateProjectConfigRequest): ProjectConfigClientRequest { + ProjectConfig.validate(configOptions); + const request: ProjectConfigClientRequest = {}; + // reCAPTCHA Key Config cannot be updated. + if (typeof configOptions.recaptchaConfig?.emailPasswordRecaptchaConfig !== 'undefined') { + request.emailPasswordRecaptchaConfig = configOptions.recaptchaConfig.emailPasswordRecaptchaConfig; + } + if (typeof configOptions.recaptchaConfig?.recaptchaManagedRules !== 'undefined') { + request.recaptchaManagedRules = configOptions.recaptchaConfig.recaptchaManagedRules; + } + return request; + } + + /** + * The recaptcha configuration. + */ + get recaptchaConfig(): RecaptchaConfig | undefined { + return this.recaptchaConfig_; + } + /** + * The Project Config object constructor. + * + * @param response - The server side response used to initialize the Project Config object. + * @constructor + * @internal + */ + constructor(response: ProjectConfigServerResponse) { + if (typeof response.emailPasswordRecaptchaConfig !== 'undefined' + || typeof response.recaptchaManagedRules !== 'undefined' + || typeof response.recaptchaKeyConfig !== 'undefined') { + this.recaptchaConfig_ = new RecaptchaConfigAuth( + response.emailPasswordRecaptchaConfig, response.recaptchaManagedRules, + response.recaptchaKeyConfig); + } + } + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ + public toJSON(): object { + // JSON serialization + const json = { + recaptchaConfig: this.recaptchaConfig_?.toJSON(), + }; + if (typeof json.recaptchaConfig === 'undefined') { + delete json.recaptchaConfig; + } + return json; + } +} + diff --git a/src/utils/error.ts b/src/utils/error.ts index 10483da861..e6f59aefe2 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -729,6 +729,14 @@ export class AuthClientErrorCode { code: 'user-not-disabled', message: 'The user must be disabled in order to bulk delete it (or you must pass force=true).', }; + public static INVALID_RECAPTCHA_ACTION = { + code: 'invalid-recaptcha-action', + message: 'ReCAPTCHA must be "BLOCK".' + } + public static INVALID_RECAPTCHA_ENFORCEMENT_STATE = { + code: 'invalid-recaptcha-enforcement-state', + message: 'ReCAPTCHA enforcement state must be either "OFF", "AUDIT" or "ENFORCE".' + } } /** @@ -986,6 +994,10 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { USER_DISABLED: 'USER_DISABLED', // Password provided is too weak. WEAK_PASSWORD: 'INVALID_PASSWORD', + // Unrecognized reCAPTCHA action. + INVALID_RECAPTCHA_ACTION: 'INVALID_RECAPTCHA_ACTION', + // Unrecognized reCAPTCHA enforcement state. + INVALID_RECAPTCHA_ENFORCEMENT_STATE: 'INVALID_RECAPTCHA_ENFORCEMENT_STATE', }; /** @const {ServerToClientCode} Messaging server to client enum error codes. */ diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts new file mode 100644 index 0000000000..44fb0821a4 --- /dev/null +++ b/test/unit/auth/project-config-manager.spec.ts @@ -0,0 +1,210 @@ +/*! + * Copyright 2022 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 { ProjectConfigManager } from '../../../src/auth/project-config-manager'; +import { + ProjectConfig, + ProjectConfigServerResponse, + UpdateProjectConfigRequest +} from '../../../src/auth/project-config'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('ProjectConfigManager', () => { + let mockApp: FirebaseApp; + let projectConfigManager: ProjectConfigManager; + let nullAccessTokenProjectConfigManager: ProjectConfigManager; + let malformedAccessTokenProjectConfigManager: ProjectConfigManager; + let rejectedPromiseAccessTokenProjectConfigManager: ProjectConfigManager; + const GET_CONFIG_RESPONSE: ProjectConfigServerResponse = { + emailPasswordRecaptchaConfig: { + enforcementState: 'AUDIT' + }, + recaptchaManagedRules: { + ruleConfigs: [{ + endScore: 0.2, + action: 'BLOCK' + }] }, + recaptchaKeyConfig: [ { + clientType: 'WEB', + recaptchaKey: 'test-key-1' } + ], + }; + + before(() => { + mockApp = mocks.app(); + projectConfigManager = new ProjectConfigManager(mockApp); + nullAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appReturningNullAccessToken()); + malformedAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appRejectedWhileFetchingAccessToken()); + + }); + + after(() => { + return mockApp.delete(); + }); + + describe('getProjectConfig()', () => { + const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); + //TODO + 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 nullAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenProjectConfigManager.getProjectConfig() + .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 rejectedPromiseAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a Project Config on success', () => { + // Stub getProjectConfig to return expected result. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(stub); + return projectConfigManager.getProjectConfig() + .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(expectedProjectConfig); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getConfig to throw a backend error. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getConfig') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return projectConfigManager.getProjectConfig() + .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('updateProjectConfig()', () => { + const projectConfigOptions: UpdateProjectConfigRequest = { + recaptchaConfig: { + recaptchaManagedRules: { + ruleConfigs: [{ + endScore: 0.2, + action: 'BLOCK' + }] }, + emailPasswordRecaptchaConfig: { + enforcementState: 'OFF' + }, + } + }; + const expectedProjectConfig = new ProjectConfig(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 projectConfigOptions', () => { + return (projectConfigManager as any).updateProjectConfig(null as unknown as UpdateProjectConfigRequest) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .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 rejectedPromiseAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a ProjectConfig on updateProjectConfig request success', () => { + // Stub updateProjectConfig to return expected result. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(updateConfigStub); + return projectConfigManager.updateProjectConfig(projectConfigOptions) + .then((actualProjectConfig) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(projectConfigOptions); + // Confirm expected Project Config object returned. + expect(actualProjectConfig).to.deep.equal(expectedProjectConfig); + }); + }); + + it('should throw an error when updateProjectConfig returns an error', () => { + // Stub updateProjectConfig to throw a backend error. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateConfig') + .returns(Promise.reject(expectedError)); + stubs.push(updateConfigStub); + return projectConfigManager.updateProjectConfig(projectConfigOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(projectConfigOptions); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); +}); diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts new file mode 100644 index 0000000000..5b09b9539d --- /dev/null +++ b/test/unit/auth/project-config.spec.ts @@ -0,0 +1,227 @@ +/*! + * Copyright 2022 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 { deepCopy } from '../../../src/utils/deep-copy'; +import { RecaptchaConfigAuth } from '../../../src/auth/auth-config'; +import { + ProjectConfig, + ProjectConfigServerResponse, + UpdateProjectConfigRequest, +} from '../../../src/auth/project-config'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('ProjectConfig', () => { + const serverResponse: ProjectConfigServerResponse = { + emailPasswordRecaptchaConfig: { + enforcementState: 'OFF' + }, + recaptchaManagedRules: { + ruleConfigs: [{ + endScore: 0.2, + action: 'BLOCK' + }] }, + recaptchaKeyConfig: [ { + clientType: 'WEB', + recaptchaKey: 'test-key-1' } + ], + }; + + const updateProjectConfigRequest: UpdateProjectConfigRequest = { + recaptchaConfig: { + recaptchaManagedRules: { + ruleConfigs: [{ + endScore: 0.2, + action: 'BLOCK' + }] }, + emailPasswordRecaptchaConfig: { + enforcementState: 'OFF' + }, + } + }; + + describe('buildServerRequest()', () => { + + describe('for an update request', () => { + it('should throw on null RecaptchaConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig" must be a non-null object.'); + }); + + it('should throw on invalid RecaptchaConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.'); + }); + + it('should throw on null ProviderRecaptchaConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.emailPasswordRecaptchaConfig = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"ProviderRecaptchaConfig" must be a non-null object.'); + }); + + it('should throw on invalid ProviderRecaptchaConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig + .emailPasswordRecaptchaConfig.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid ProviderRecaptchaConfig parameter.'); + }); + + it('should throw on invalid ProviderRecaptchaConfig.enforcementState attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig + .emailPasswordRecaptchaConfig.enforcementState = 'INVALID'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"ProviderRecaptchaAuthConfig.enforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); + }); + + it('should throw on null RuleConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.recaptchaManagedRules = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RuleConfig" must be a non-null object.'); + }); + + it('should throw on invalid RecaptchaManagedRules attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid RecaptchaManagedRules parameter.'); + }); + + it('should throw on non-array RuleConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = 'non-array'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaManagedRules.ruleConfigs" must be an array of valid "RuleConfig".'); + }); + + it('should throw on invalid RuleConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + [{ 'score': 0.1, 'action': 'BLOCK' }]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"score" is not a valid RuleConfig parameter.'); + }); + + it('should throw on invalid RuleConfig.action attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + [{ 'endScore': 0.1, 'action': 'ALLOW' }]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RuleConfig.action" must be "BLOCK".'); + }); + + it('should not throw on valid client request object', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest); + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).not.to.throw; + }); + + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + nonObjects.forEach((request) => { + it('should throw on invalid UpdateProjectConfigRequest:' + JSON.stringify(request), () => { + expect(() => { + ProjectConfig.buildServerRequest(request as any); + }).to.throw('"UpdateProjectConfigRequest" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute for update request', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.unsupported = 'value'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"unsupported" is not a valid UpdateProjectConfigRequest parameter.'); + }); + }); + }); + + describe('constructor', () => { + const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + const projectConfig = new ProjectConfig(serverResponseCopy); + + it('should not throw on valid initialization', () => { + expect(() => new ProjectConfig(serverResponse)).not.to.throw(); + }); + + it('should set readonly property recaptchaConfig', () => { + const expectedRecaptchaConfig = new RecaptchaConfigAuth( + { enforcementState: 'OFF' }, + { + ruleConfigs: [{ + endScore: 0.2, + action: 'BLOCK' + }] }, + [{ + clientType: 'WEB', + recaptchaKey: 'test-key-1' } + ], + ); + expect(projectConfig.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig); + }); + }); + + describe('toJSON()', () => { + const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + it('should return the expected object representation of project config', () => { + expect(new ProjectConfig(serverResponseCopy).toJSON()).to.deep.equal({ + recaptchaConfig: { + emailPasswordRecaptchaConfig: deepCopy(serverResponse.emailPasswordRecaptchaConfig), + recaptchaKeyConfig: deepCopy(serverResponse.recaptchaKeyConfig), + recaptchaManagedRules: deepCopy(serverResponse.recaptchaManagedRules), + } + }); + }); + + it('should not populate optional fields if not available', () => { + const serverResponseOptionalCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + delete serverResponseOptionalCopy.emailPasswordRecaptchaConfig; + delete serverResponseOptionalCopy.recaptchaManagedRules; + + expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ + recaptchaConfig: { + recaptchaKeyConfig: deepCopy(serverResponse.recaptchaKeyConfig), + } + }); + }); + }); +}); From 86a788e94b804c2b54fe17a1b4ac8fee12d96d23 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Thu, 3 Mar 2022 15:42:14 -0800 Subject: [PATCH 2/8] Refactor ProjectConfig to accommodate backend API scheme changes. --- etc/firebase-admin.auth.api.md | 18 +++ src/auth/project-config.ts | 43 +++---- test/unit/auth/project-config-manager.spec.ts | 32 +++-- test/unit/auth/project-config.spec.ts | 114 +++++++----------- 4 files changed, 88 insertions(+), 119 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 83866b5081..fe83c48b6e 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -27,6 +27,7 @@ export interface ActionCodeSettings { 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; + projectConfigManager(): ProjectConfigManager; tenantManager(): TenantManager; } @@ -256,6 +257,18 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { toJSON(): object; } +// @public +export class ProjectConfig { + get recaptchaConfig(): RecaptchaConfig | undefined; + toJSON(): object; +} + +// @public +export class ProjectConfigManager { + getProjectConfig(): Promise; + updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise; +} + // @public export interface ProviderIdentifier { // (undocumented) @@ -367,6 +380,11 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor phoneNumber: string; } +// @public +export interface UpdateProjectConfigRequest { + recaptchaConfig?: RecaptchaConfig; +} + // @public export interface UpdateRequest { disabled?: boolean; diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index a9ac8bbe1f..41cc6c9c3b 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -16,11 +16,8 @@ import * as validator from '../utils/validator'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { - ProviderRecaptchaConfig, RecaptchaConfig, RecaptchaConfigAuth, - RecaptchaKeyConfig, - RecaptchaManagedRules } from './auth-config'; /** @@ -34,18 +31,19 @@ export interface UpdateProjectConfigRequest { } /** + * Response received from get/update project config. * We are only exposing the recaptcha config for now. */ export interface ProjectConfigServerResponse { - emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; - recaptchaManagedRules?: RecaptchaManagedRules; - recaptchaKeyConfig?: RecaptchaKeyConfig[]; + recaptchaConfig?: RecaptchaConfig; } +/** + * Request sent to update project config. + * We are only updating the recaptcha config for now. + */ export interface ProjectConfigClientRequest { - emailPasswordRecaptchaConfig?: ProviderRecaptchaConfig; - recaptchaManagedRules?: RecaptchaManagedRules; - recaptchaKeyConfig?: RecaptchaKeyConfig[]; + recaptchaConfig?: RecaptchaConfig; } /** @@ -55,15 +53,15 @@ export class ProjectConfig { private readonly recaptchaConfig_?: RecaptchaConfigAuth; private static validate(request: any): void { - const validKeys = { - recaptchaConfig: true, - }; if (!validator.isNonNullObject(request)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, '"UpdateProjectConfigRequest" must be a valid non-null object.', ); } + const validKeys = { + recaptchaConfig: true, + } // Check for unsupported top level attributes. for (const key in request) { if (!(key in validKeys)) { @@ -73,6 +71,7 @@ export class ProjectConfig { ); } } + RecaptchaConfigAuth.validate(request.recaptchaConfig); } @@ -84,16 +83,8 @@ export class ProjectConfig { * @internal */ public static buildServerRequest(configOptions: UpdateProjectConfigRequest): ProjectConfigClientRequest { - ProjectConfig.validate(configOptions); - const request: ProjectConfigClientRequest = {}; - // reCAPTCHA Key Config cannot be updated. - if (typeof configOptions.recaptchaConfig?.emailPasswordRecaptchaConfig !== 'undefined') { - request.emailPasswordRecaptchaConfig = configOptions.recaptchaConfig.emailPasswordRecaptchaConfig; - } - if (typeof configOptions.recaptchaConfig?.recaptchaManagedRules !== 'undefined') { - request.recaptchaManagedRules = configOptions.recaptchaConfig.recaptchaManagedRules; - } - return request; + ProjectConfig.validate(configOptions); + return configOptions as ProjectConfigClientRequest; } /** @@ -110,12 +101,8 @@ export class ProjectConfig { * @internal */ constructor(response: ProjectConfigServerResponse) { - if (typeof response.emailPasswordRecaptchaConfig !== 'undefined' - || typeof response.recaptchaManagedRules !== 'undefined' - || typeof response.recaptchaKeyConfig !== 'undefined') { - this.recaptchaConfig_ = new RecaptchaConfigAuth( - response.emailPasswordRecaptchaConfig, response.recaptchaManagedRules, - response.recaptchaKeyConfig); + if (response.recaptchaConfig !== 'undefined') { + this.recaptchaConfig_ = new RecaptchaConfigAuth(response.recaptchaConfig); } } /** diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts index 44fb0821a4..cd10e25e84 100644 --- a/test/unit/auth/project-config-manager.spec.ts +++ b/test/unit/auth/project-config-manager.spec.ts @@ -46,18 +46,17 @@ describe('ProjectConfigManager', () => { let malformedAccessTokenProjectConfigManager: ProjectConfigManager; let rejectedPromiseAccessTokenProjectConfigManager: ProjectConfigManager; const GET_CONFIG_RESPONSE: ProjectConfigServerResponse = { - emailPasswordRecaptchaConfig: { - enforcementState: 'AUDIT' - }, - recaptchaManagedRules: { - ruleConfigs: [{ + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { endScore: 0.2, action: 'BLOCK' - }] }, - recaptchaKeyConfig: [ { - clientType: 'WEB', - recaptchaKey: 'test-key-1' } - ], + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + } }; before(() => { @@ -136,14 +135,11 @@ describe('ProjectConfigManager', () => { describe('updateProjectConfig()', () => { const projectConfigOptions: UpdateProjectConfigRequest = { recaptchaConfig: { - recaptchaManagedRules: { - ruleConfigs: [{ - endScore: 0.2, - action: 'BLOCK' - }] }, - emailPasswordRecaptchaConfig: { - enforcementState: 'OFF' - }, + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], } }; const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index 5b09b9539d..ac0a8b2ff1 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -35,30 +35,26 @@ const expect = chai.expect; describe('ProjectConfig', () => { const serverResponse: ProjectConfigServerResponse = { - emailPasswordRecaptchaConfig: { - enforcementState: 'OFF' - }, - recaptchaManagedRules: { - ruleConfigs: [{ + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { endScore: 0.2, action: 'BLOCK' - }] }, - recaptchaKeyConfig: [ { - clientType: 'WEB', - recaptchaKey: 'test-key-1' } - ], + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + } }; const updateProjectConfigRequest: UpdateProjectConfigRequest = { recaptchaConfig: { - recaptchaManagedRules: { - ruleConfigs: [{ - endScore: 0.2, - action: 'BLOCK' - }] }, - emailPasswordRecaptchaConfig: { - enforcementState: 'OFF' - }, + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ] } }; @@ -81,72 +77,47 @@ describe('ProjectConfig', () => { }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.'); }); - it('should throw on null ProviderRecaptchaConfig attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; - configOptionsClientRequest.recaptchaConfig.emailPasswordRecaptchaConfig = null; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"ProviderRecaptchaConfig" must be a non-null object.'); - }); - - it('should throw on invalid ProviderRecaptchaConfig attribute', () => { + it('should throw on null emailPasswordEnforcementState attribute', () => { const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; - configOptionsClientRequest.recaptchaConfig - .emailPasswordRecaptchaConfig.invalidParameter = 'invalid'; + configOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"invalidParameter" is not a valid ProviderRecaptchaConfig parameter.'); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.'); }); - it('should throw on invalid ProviderRecaptchaConfig.enforcementState attribute', () => { + it('should throw on invalid emailPasswordEnforcementState attribute', () => { const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; configOptionsClientRequest.recaptchaConfig - .emailPasswordRecaptchaConfig.enforcementState = 'INVALID'; + .emailPasswordEnforcementState = 'INVALID'; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"ProviderRecaptchaAuthConfig.enforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); }); - it('should throw on null RuleConfig attribute', () => { + it('should throw on non-array managedRules attribute', () => { const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; - configOptionsClientRequest.recaptchaConfig.recaptchaManagedRules = null; + configOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"RuleConfig" must be a non-null object.'); + }).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".'); }); - it('should throw on invalid RecaptchaManagedRules attribute', () => { + it('should throw on invalid managedRules attribute', () => { const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; - configOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.invalidParameter = 'invalid'; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"invalidParameter" is not a valid RecaptchaManagedRules parameter.'); - }); - - it('should throw on non-array RuleConfig attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; - configOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = 'non-array'; - expect(() => { - ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"RecaptchaManagedRules.ruleConfigs" must be an array of valid "RuleConfig".'); - }); - - it('should throw on invalid RuleConfig attribute', () => { - const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; - configOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + configOptionsClientRequest.recaptchaConfig.managedRules = [{ 'score': 0.1, 'action': 'BLOCK' }]; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"score" is not a valid RuleConfig parameter.'); + }).to.throw('"score" is not a valid RecaptchaManagedRule parameter.'); }); - it('should throw on invalid RuleConfig.action attribute', () => { + it('should throw on invalid RecaptchaManagedRule.action attribute', () => { const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; - configOptionsClientRequest.recaptchaConfig.recaptchaManagedRules.ruleConfigs = + configOptionsClientRequest.recaptchaConfig.managedRules = [{ 'endScore': 0.1, 'action': 'ALLOW' }]; expect(() => { ProjectConfig.buildServerRequest(configOptionsClientRequest); - }).to.throw('"RuleConfig.action" must be "BLOCK".'); + }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); }); it('should not throw on valid client request object', () => { @@ -185,16 +156,17 @@ describe('ProjectConfig', () => { it('should set readonly property recaptchaConfig', () => { const expectedRecaptchaConfig = new RecaptchaConfigAuth( - { enforcementState: 'OFF' }, { - ruleConfigs: [{ + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { endScore: 0.2, action: 'BLOCK' - }] }, - [{ - clientType: 'WEB', - recaptchaKey: 'test-key-1' } - ], + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + } ); expect(projectConfig.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig); }); @@ -204,22 +176,18 @@ describe('ProjectConfig', () => { const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); it('should return the expected object representation of project config', () => { expect(new ProjectConfig(serverResponseCopy).toJSON()).to.deep.equal({ - recaptchaConfig: { - emailPasswordRecaptchaConfig: deepCopy(serverResponse.emailPasswordRecaptchaConfig), - recaptchaKeyConfig: deepCopy(serverResponse.recaptchaKeyConfig), - recaptchaManagedRules: deepCopy(serverResponse.recaptchaManagedRules), - } + recaptchaConfig: deepCopy(serverResponse.recaptchaConfig) }); }); it('should not populate optional fields if not available', () => { const serverResponseOptionalCopy: ProjectConfigServerResponse = deepCopy(serverResponse); - delete serverResponseOptionalCopy.emailPasswordRecaptchaConfig; - delete serverResponseOptionalCopy.recaptchaManagedRules; + delete serverResponseOptionalCopy.recaptchaConfig?.emailPasswordEnforcementState; + delete serverResponseOptionalCopy.recaptchaConfig?.managedRules; expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ recaptchaConfig: { - recaptchaKeyConfig: deepCopy(serverResponse.recaptchaKeyConfig), + recaptchaKeys: deepCopy(serverResponse.recaptchaConfig?.recaptchaKeys), } }); }); From ab4aac6eff1fabee6a0477c51a2a08005cc7b497 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Thu, 3 Mar 2022 15:50:29 -0800 Subject: [PATCH 3/8] code cleanup --- src/auth/auth-api-request.ts | 3 --- test/unit/auth/project-config-manager.spec.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 3ce3196f3d..e5bcb6955e 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -103,9 +103,6 @@ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace( const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace( 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); -//https://identitytoolkit.googleapis.com/v2/projects/cicpclientproj/config - - /** Maximum allowed number of tenants to download at one time. */ const MAX_LIST_TENANT_PAGE_SIZE = 1000; diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts index cd10e25e84..0366bf17b0 100644 --- a/test/unit/auth/project-config-manager.spec.ts +++ b/test/unit/auth/project-config-manager.spec.ts @@ -77,7 +77,6 @@ describe('ProjectConfigManager', () => { describe('getProjectConfig()', () => { const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); - //TODO const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); // Stubs used to simulate underlying API calls. let stubs: sinon.SinonStub[] = []; From f4d0839afd07f6f961439cb93e9f5ef02c2c7ca9 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Fri, 4 Mar 2022 10:42:33 -0800 Subject: [PATCH 4/8] address PR feedbacks --- src/auth/auth-api-request.ts | 12 ++++++------ src/auth/project-config-manager.ts | 4 ++-- src/auth/project-config.ts | 10 +++++----- src/utils/error.ts | 4 ++-- test/unit/auth/project-config-manager.spec.ts | 8 ++++---- test/unit/auth/project-config.spec.ts | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index e5bcb6955e..2577dc9411 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -1962,7 +1962,7 @@ export abstract class AbstractAuthRequestHandler { } /** Instantiates the getConfig endpoint settings. */ -const GET_CONFIG = new ApiSettings('/config', 'GET') +const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET') .setResponseValidator((response: any) => { // Response should always contain at least the config name. if (!validator.isNonEmptyString(response.name)) { @@ -1974,7 +1974,7 @@ const GET_CONFIG = new ApiSettings('/config', 'GET') }); /** Instantiates the updateConfig endpoint settings. */ -const UPDATE_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH') +const UPDATE_PROJECT_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH') .setResponseValidator((response: any) => { // Response should always contain at least the config name. if (!validator.isNonEmptyString(response.name)) { @@ -2089,8 +2089,8 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { * Get the current project's config * @returns A promise that resolves with the project config information. */ - public getConfig(): Promise { - return this.invokeRequestHandler(this.v2ResourceBuilder, GET_CONFIG, {}, {}) + public getProjectConfig(): Promise { + return this.invokeRequestHandler(this.v2ResourceBuilder, GET_PROJECT_CONFIG, {}, {}) .then((response: any) => { return response as ProjectConfigServerResponse; }); @@ -2100,12 +2100,12 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { * Update the current project's config. * @returns A promise that resolves with the project config information. */ - public updateConfig(recaptchaOptions: UpdateProjectConfigRequest): Promise { + public updateProjectConfig(recaptchaOptions: UpdateProjectConfigRequest): Promise { try { const request = ProjectConfig.buildServerRequest(recaptchaOptions); const updateMask = utils.generateUpdateMask(request); return this.invokeRequestHandler( - this.v2ResourceBuilder, UPDATE_CONFIG, request, { updateMask: updateMask.join(',') }) + this.v2ResourceBuilder, UPDATE_PROJECT_CONFIG, request, { updateMask: updateMask.join(',') }) .then((response: any) => { return response as ProjectConfigServerResponse; }); diff --git a/src/auth/project-config-manager.ts b/src/auth/project-config-manager.ts index f5597d75fc..ee744076a6 100644 --- a/src/auth/project-config-manager.ts +++ b/src/auth/project-config-manager.ts @@ -50,7 +50,7 @@ export class ProjectConfigManager { * @returns A promise fulfilled with the project configuration. */ public getProjectConfig(): Promise { - return this.authRequestHandler.getConfig() + return this.authRequestHandler.getProjectConfig() .then((response: ProjectConfigServerResponse) => { return new ProjectConfig(response); }) @@ -63,7 +63,7 @@ export class ProjectConfigManager { * @returns A promise fulfilled with the update project config. */ public updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise { - return this.authRequestHandler.updateConfig(projectConfigOptions) + return this.authRequestHandler.updateProjectConfig(projectConfigOptions) .then((response: ProjectConfigServerResponse) => { return new ProjectConfig(response); }) diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index 41cc6c9c3b..09f5504bc7 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -17,11 +17,11 @@ import * as validator from '../utils/validator'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { RecaptchaConfig, - RecaptchaConfigAuth, + RecaptchaAuthConfig, } from './auth-config'; /** - * Interface representing the properties to update on the provided tenant. + * Interface representing the properties to update on the provided project config. */ export interface UpdateProjectConfigRequest { /** @@ -50,7 +50,7 @@ export interface ProjectConfigClientRequest { * Represents a project configuration. */ export class ProjectConfig { - private readonly recaptchaConfig_?: RecaptchaConfigAuth; + private readonly recaptchaConfig_?: RecaptchaAuthConfig; private static validate(request: any): void { if (!validator.isNonNullObject(request)) { @@ -72,7 +72,7 @@ export class ProjectConfig { } } - RecaptchaConfigAuth.validate(request.recaptchaConfig); + RecaptchaAuthConfig.validate(request.recaptchaConfig); } /** @@ -102,7 +102,7 @@ export class ProjectConfig { */ constructor(response: ProjectConfigServerResponse) { if (response.recaptchaConfig !== 'undefined') { - this.recaptchaConfig_ = new RecaptchaConfigAuth(response.recaptchaConfig); + this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig); } } /** diff --git a/src/utils/error.ts b/src/utils/error.ts index e6f59aefe2..1f68bbba55 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -731,11 +731,11 @@ export class AuthClientErrorCode { }; public static INVALID_RECAPTCHA_ACTION = { code: 'invalid-recaptcha-action', - message: 'ReCAPTCHA must be "BLOCK".' + message: 'reCAPTCHA action must be "BLOCK".' } public static INVALID_RECAPTCHA_ENFORCEMENT_STATE = { code: 'invalid-recaptcha-enforcement-state', - message: 'ReCAPTCHA enforcement state must be either "OFF", "AUDIT" or "ENFORCE".' + message: 'reCAPTCHA enforcement state must be either "OFF", "AUDIT" or "ENFORCE".' } } diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts index 0366bf17b0..e76acaf8c6 100644 --- a/test/unit/auth/project-config-manager.spec.ts +++ b/test/unit/auth/project-config-manager.spec.ts @@ -102,7 +102,7 @@ describe('ProjectConfigManager', () => { it('should resolve with a Project Config on success', () => { // Stub getProjectConfig to return expected result. - const stub = sinon.stub(AuthRequestHandler.prototype, 'getConfig') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getProjectConfig') .returns(Promise.resolve(GET_CONFIG_RESPONSE)); stubs.push(stub); return projectConfigManager.getProjectConfig() @@ -116,7 +116,7 @@ describe('ProjectConfigManager', () => { it('should throw an error when the backend returns an error', () => { // Stub getConfig to throw a backend error. - const stub = sinon.stub(AuthRequestHandler.prototype, 'getConfig') + const stub = sinon.stub(AuthRequestHandler.prototype, 'getProjectConfig') .returns(Promise.reject(expectedError)); stubs.push(stub); return projectConfigManager.getProjectConfig() @@ -174,7 +174,7 @@ describe('ProjectConfigManager', () => { it('should resolve with a ProjectConfig on updateProjectConfig request success', () => { // Stub updateProjectConfig to return expected result. - const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateConfig') + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateProjectConfig') .returns(Promise.resolve(GET_CONFIG_RESPONSE)); stubs.push(updateConfigStub); return projectConfigManager.updateProjectConfig(projectConfigOptions) @@ -188,7 +188,7 @@ describe('ProjectConfigManager', () => { it('should throw an error when updateProjectConfig returns an error', () => { // Stub updateProjectConfig to throw a backend error. - const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateConfig') + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateProjectConfig') .returns(Promise.reject(expectedError)); stubs.push(updateConfigStub); return projectConfigManager.updateProjectConfig(projectConfigOptions) diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index ac0a8b2ff1..d0a5ec9575 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -20,7 +20,7 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import { deepCopy } from '../../../src/utils/deep-copy'; -import { RecaptchaConfigAuth } from '../../../src/auth/auth-config'; +import { RecaptchaAuthConfig } from '../../../src/auth/auth-config'; import { ProjectConfig, ProjectConfigServerResponse, @@ -155,7 +155,7 @@ describe('ProjectConfig', () => { }); it('should set readonly property recaptchaConfig', () => { - const expectedRecaptchaConfig = new RecaptchaConfigAuth( + const expectedRecaptchaConfig = new RecaptchaAuthConfig( { emailPasswordEnforcementState: 'AUDIT', managedRules: [ { From 0b9033c70587905548807d9ad7ea8c903fc298e1 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Fri, 4 Mar 2022 13:41:56 -0800 Subject: [PATCH 5/8] rename tenantMgmtResourceBuilder to authResouceUrlBuilder --- src/auth/auth-api-request.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 2577dc9411..2e9c22fc83 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -2052,13 +2052,13 @@ const CREATE_TENANT = new ApiSettings('/tenants', 'POST') /** - * Utility for sending requests to Auth server that are Auth instance related. This includes user and - * tenant management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines + * Utility for sending requests to Auth server that are Auth instance related. This includes user, tenant, + * and project config management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines * additional tenant management related APIs. */ export class AuthRequestHandler extends AbstractAuthRequestHandler { - protected readonly v2ResourceBuilder: AuthResourceUrlBuilder; + protected readonly authResourceUrlBuilder: AuthResourceUrlBuilder; /** * The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp. @@ -2068,7 +2068,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ constructor(app: App) { super(app); - this.v2ResourceBuilder = new AuthResourceUrlBuilder(app, 'v2'); + this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2'); } /** @@ -2090,7 +2090,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { * @returns A promise that resolves with the project config information. */ public getProjectConfig(): Promise { - return this.invokeRequestHandler(this.v2ResourceBuilder, GET_PROJECT_CONFIG, {}, {}) + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_PROJECT_CONFIG, {}, {}) .then((response: any) => { return response as ProjectConfigServerResponse; }); @@ -2105,7 +2105,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { const request = ProjectConfig.buildServerRequest(recaptchaOptions); const updateMask = utils.generateUpdateMask(request); return this.invokeRequestHandler( - this.v2ResourceBuilder, UPDATE_PROJECT_CONFIG, request, { updateMask: updateMask.join(',') }) + this.authResourceUrlBuilder, UPDATE_PROJECT_CONFIG, request, { updateMask: updateMask.join(',') }) .then((response: any) => { return response as ProjectConfigServerResponse; }); @@ -2124,7 +2124,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.v2ResourceBuilder, GET_TENANT, {}, { tenantId }) + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT, {}, { tenantId }) .then((response: any) => { return response as TenantServerResponse; }); @@ -2154,7 +2154,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (typeof request.pageToken === 'undefined') { delete request.pageToken; } - return this.invokeRequestHandler(this.v2ResourceBuilder, LIST_TENANTS, request) + return this.invokeRequestHandler(this.authResourceUrlBuilder, LIST_TENANTS, request) .then((response: any) => { if (!response.tenants) { response.tenants = []; @@ -2174,7 +2174,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.v2ResourceBuilder, DELETE_TENANT, undefined, { tenantId }) + return this.invokeRequestHandler(this.authResourceUrlBuilder, DELETE_TENANT, undefined, { tenantId }) .then(() => { // Return nothing. }); @@ -2190,7 +2190,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { try { // Construct backend request. const request = Tenant.buildServerRequest(tenantOptions, true); - return this.invokeRequestHandler(this.v2ResourceBuilder, CREATE_TENANT, request) + return this.invokeRequestHandler(this.authResourceUrlBuilder, CREATE_TENANT, request) .then((response: any) => { return response as TenantServerResponse; }); @@ -2216,7 +2216,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { // Do not traverse deep into testPhoneNumbers. The entire content should be replaced // and not just specific phone numbers. const updateMask = utils.generateUpdateMask(request, ['testPhoneNumbers']); - return this.invokeRequestHandler(this.v2ResourceBuilder, UPDATE_TENANT, request, + return this.invokeRequestHandler(this.authResourceUrlBuilder, UPDATE_TENANT, request, { tenantId, updateMask: updateMask.join(',') }) .then((response: any) => { return response as TenantServerResponse; From 657ab8d4401b540219ed89cdf32d8372a104a912 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Mon, 7 Mar 2022 13:52:53 -0800 Subject: [PATCH 6/8] minor changes --- src/auth/project-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index 09f5504bc7..483ce98f3b 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -101,7 +101,7 @@ export class ProjectConfig { * @internal */ constructor(response: ProjectConfigServerResponse) { - if (response.recaptchaConfig !== 'undefined') { + if (typeof response.recaptchaConfig !== 'undefined') { this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig); } } From acda70f81bc63bb6671b14bc64f48f7cd6ae9e57 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Mon, 7 Mar 2022 14:30:39 -0800 Subject: [PATCH 7/8] add legal consent for enabling recaptcha config --- src/auth/auth-config.ts | 1 + src/auth/project-config-manager.ts | 4 ---- src/auth/project-config.ts | 11 +++++++++++ src/auth/tenant.ts | 2 ++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index aecd686201..c4d3091f31 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1502,6 +1502,7 @@ export interface RecaptchaKey { /** * The request interface for updating a reCAPTCHA Config. + * By enabling reCAPTCHA Enterprise Integration you are agreeing to reCAPTCHA Enterprise Term of Service. */ export interface RecaptchaConfig { /** diff --git a/src/auth/project-config-manager.ts b/src/auth/project-config-manager.ts index ee744076a6..3f95a8ddf3 100644 --- a/src/auth/project-config-manager.ts +++ b/src/auth/project-config-manager.ts @@ -24,10 +24,6 @@ import { * This includes: *
    *
  • The ability to update and get project config.
  • - *
  • Getting a `TenantAwareAuth` instance for running Auth related operations - * (user management, provider configuration management, token verification, - * email link generation, etc) in the context of a specified tenant.
  • - *
*/ export class ProjectConfigManager { private readonly authRequestHandler: AuthRequestHandler; diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index 483ce98f3b..39034a32b0 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -26,6 +26,7 @@ import { export interface UpdateProjectConfigRequest { /** * The recaptcha configuration to update on the project. + * By enabling reCAPTCHA Enterprise Integration you are agreeing to reCAPTCHA Enterprise Term of Service. */ recaptchaConfig?: RecaptchaConfig; } @@ -50,8 +51,17 @@ export interface ProjectConfigClientRequest { * Represents a project configuration. */ export class ProjectConfig { + /** + * The recaptcha configuration to update on the project config. + * By enabling reCAPTCHA Enterprise Integration you are agreeing to reCAPTCHA Enterprise Term of Service. + */ private readonly recaptchaConfig_?: RecaptchaAuthConfig; + /** + * Validates a project config options object. Throws an error on failure. + * + * @param request - The project config options object to validate. + */ private static validate(request: any): void { if (!validator.isNonNullObject(request)) { throw new FirebaseAuthError( @@ -72,6 +82,7 @@ export class ProjectConfig { } } + // Validate reCAPTCHA config attribute. RecaptchaAuthConfig.validate(request.recaptchaConfig); } diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index a0ff7b242d..5af7017072 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -57,6 +57,7 @@ export interface UpdateTenantRequest { /** * The recaptcha configuration to update on the tenant. + * By enabling reCAPTCHA Enterprise Integration you are agreeing to reCAPTCHA Enterprise Term of Service. */ recaptchaConfig?: RecaptchaConfig; } @@ -132,6 +133,7 @@ export class Tenant { /* * The map conatining the reCAPTCHA config. + * By enabling reCAPTCHA Enterprise Integration you are agreeing to reCAPTCHA Enterprise Term of Service. */ private readonly recaptchaConfig_?: RecaptchaAuthConfig; /** From 0d55d4abb8de935c4a43a7d1fecebdb6e35bd18f Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Tue, 8 Mar 2022 11:37:40 -0800 Subject: [PATCH 8/8] Add Tos link to documentation --- src/auth/auth-config.ts | 4 +++- src/auth/project-config.ts | 8 ++++++-- src/auth/tenant.ts | 14 +++++++++----- test/unit/auth/project-config-manager.spec.ts | 1 - 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index c4d3091f31..f4504392e7 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -1502,7 +1502,9 @@ export interface RecaptchaKey { /** * The request interface for updating a reCAPTCHA Config. - * By enabling reCAPTCHA Enterprise Integration you are agreeing to reCAPTCHA Enterprise Term of Service. + * By enabling reCAPTCHA Enterprise Integration you are + * agreeing to reCAPTCHA Enterprise + * {@link https://cloud.google.com/terms/service-terms | Term of Service}. */ export interface RecaptchaConfig { /** diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index 39034a32b0..749a64bbb6 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -26,7 +26,9 @@ import { export interface UpdateProjectConfigRequest { /** * The recaptcha configuration to update on the project. - * By enabling reCAPTCHA Enterprise Integration you are agreeing to reCAPTCHA Enterprise Term of Service. + * By enabling reCAPTCHA Enterprise Integration you are + * agreeing to reCAPTCHA Enterprise + * {@link https://cloud.google.com/terms/service-terms | Term of Service}. */ recaptchaConfig?: RecaptchaConfig; } @@ -53,7 +55,9 @@ export interface ProjectConfigClientRequest { export class ProjectConfig { /** * The recaptcha configuration to update on the project config. - * By enabling reCAPTCHA Enterprise Integration you are agreeing to reCAPTCHA Enterprise Term of Service. + * By enabling reCAPTCHA Enterprise Integration you are + * agreeing to reCAPTCHA Enterprise + * {@link https://cloud.google.com/terms/service-terms | Term of Service}. */ private readonly recaptchaConfig_?: RecaptchaAuthConfig; diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 5af7017072..734e64569c 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -57,7 +57,9 @@ export interface UpdateTenantRequest { /** * The recaptcha configuration to update on the tenant. - * By enabling reCAPTCHA Enterprise Integration you are agreeing to reCAPTCHA Enterprise Term of Service. + * By enabling reCAPTCHA Enterprise Integration you are + * agreeing to reCAPTCHA Enterprise + * {@link https://cloud.google.com/terms/service-terms | Term of Service}. */ recaptchaConfig?: RecaptchaConfig; } @@ -131,10 +133,12 @@ export class Tenant { private readonly emailSignInConfig_?: EmailSignInConfig; private readonly multiFactorConfig_?: MultiFactorAuthConfig; - /* - * The map conatining the reCAPTCHA config. - * By enabling reCAPTCHA Enterprise Integration you are agreeing to reCAPTCHA Enterprise Term of Service. - */ + /** + * The map conatining the reCAPTCHA config. + * By enabling reCAPTCHA Enterprise Integration you are + * agreeing to reCAPTCHA Enterprise + * {@link https://cloud.google.com/terms/service-terms | Term of Service}. + */ private readonly recaptchaConfig_?: RecaptchaAuthConfig; /** * Builds the corresponding server request for a TenantOptions object. diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts index e76acaf8c6..22c0fbd50f 100644 --- a/test/unit/auth/project-config-manager.spec.ts +++ b/test/unit/auth/project-config-manager.spec.ts @@ -68,7 +68,6 @@ describe('ProjectConfigManager', () => { mocks.appReturningMalformedAccessToken()); rejectedPromiseAccessTokenProjectConfigManager = new ProjectConfigManager( mocks.appRejectedWhileFetchingAccessToken()); - }); after(() => {