Skip to content

Commit

Permalink
Project config - Recaptcha config (#1595)
Browse files Browse the repository at this point in the history
* Recaptcha config changes in project config.
- Implemented getProjectConfig.
- Implemented updateProjectConfig.
- Updated error code.
- Add Term of Service consents.
Xiaoshouzi-gh committed Mar 2, 2023
1 parent 7e90c9a commit c39c189
Showing 9 changed files with 194 additions and 13 deletions.
2 changes: 2 additions & 0 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
@@ -337,6 +337,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {
// @public
export class ProjectConfig {
readonly smsRegionConfig?: SmsRegionConfig;
get recaptchaConfig(): RecaptchaConfig | undefined;
toJSON(): object;
}

@@ -464,6 +465,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor
// @public
export interface UpdateProjectConfigRequest {
smsRegionConfig?: SmsRegionConfig;
recaptchaConfig?: RecaptchaConfig;
}

// @public
3 changes: 3 additions & 0 deletions src/auth/auth-config.ts
Original file line number Diff line number Diff line change
@@ -1645,6 +1645,9 @@ export interface RecaptchaKey {

/**
* The request interface for updating a reCAPTCHA Config.
* 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 {
/**
1 change: 0 additions & 1 deletion src/auth/project-config-manager.ts
Original file line number Diff line number Diff line change
@@ -27,7 +27,6 @@ import {
*/
export class ProjectConfigManager {
private readonly authRequestHandler: AuthRequestHandler;

/**
* Initializes a ProjectConfigManager instance for a specified FirebaseApp.
*
43 changes: 40 additions & 3 deletions src/auth/project-config.ts
Original file line number Diff line number Diff line change
@@ -18,6 +18,8 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
import {
SmsRegionsAuthConfig,
SmsRegionConfig,
RecaptchaConfig,
RecaptchaAuthConfig,
} from './auth-config';
import { deepCopy } from '../utils/deep-copy';

@@ -29,6 +31,13 @@ export interface UpdateProjectConfigRequest {
* The SMS configuration to update on the project.
*/
smsRegionConfig?: SmsRegionConfig;
/**
* The recaptcha configuration to update on the project.
* By enabling reCAPTCHA Enterprise Integration you are
* agreeing to reCAPTCHA Enterprise
* {@link https://cloud.google.com/terms/service-terms | Term of Service}.
*/
recaptchaConfig?: RecaptchaConfig;
}

/**
@@ -37,6 +46,7 @@ export interface UpdateProjectConfigRequest {
*/
export interface ProjectConfigServerResponse {
smsRegionConfig?: SmsRegionConfig;
recaptchaConfig?: RecaptchaConfig;
}

/**
@@ -45,6 +55,7 @@ export interface ProjectConfigServerResponse {
*/
export interface ProjectConfigClientRequest {
smsRegionConfig?: SmsRegionConfig;
recaptchaConfig?: RecaptchaConfig;
}

/**
@@ -57,6 +68,13 @@ export class ProjectConfig {
* This is based on the calling code of the destination phone number.
*/
public readonly smsRegionConfig?: SmsRegionConfig;
/**
* The recaptcha configuration to update on the project 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;

/**
* Validates a project config options object. Throws an error on failure.
@@ -72,6 +90,7 @@ export class ProjectConfig {
}
const validKeys = {
smsRegionConfig: true,
recaptchaConfig: true,
}
// Check for unsupported top level attributes.
for (const key in request) {
@@ -86,20 +105,31 @@ export class ProjectConfig {
if (typeof request.smsRegionConfig !== 'undefined') {
SmsRegionsAuthConfig.validate(request.smsRegionConfig);
}

// Validate reCAPTCHA config attribute.
if (typeof request.recaptchaConfig !== 'undefined') {
RecaptchaAuthConfig.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);
ProjectConfig.validate(configOptions);
return configOptions as ProjectConfigClientRequest;
}


/**
* The recaptcha configuration.
*/
get recaptchaConfig(): RecaptchaConfig | undefined {
return this.recaptchaConfig_;
}
/**
* The Project Config object constructor.
*
@@ -111,6 +141,9 @@ export class ProjectConfig {
if (typeof response.smsRegionConfig !== 'undefined') {
this.smsRegionConfig = response.smsRegionConfig;
}
if (typeof response.recaptchaConfig !== 'undefined') {
this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig);
}
}
/**
* Returns a JSON-serializable representation of this object.
@@ -121,10 +154,14 @@ export class ProjectConfig {
// JSON serialization
const json = {
smsRegionConfig: deepCopy(this.smsRegionConfig),
recaptchaConfig: this.recaptchaConfig_?.toJSON(),
};
if (typeof json.smsRegionConfig === 'undefined') {
delete json.smsRegionConfig;
}
if (typeof json.recaptchaConfig === 'undefined') {
delete json.recaptchaConfig;
}
return json;
}
}
12 changes: 9 additions & 3 deletions src/auth/tenant.ts
Original file line number Diff line number Diff line change
@@ -62,6 +62,9 @@ export interface UpdateTenantRequest {

/**
* The recaptcha configuration to update on the tenant.
* By enabling reCAPTCHA Enterprise Integration you are
* agreeing to reCAPTCHA Enterprise
* {@link https://cloud.google.com/terms/service-terms | Term of Service}.
*/
recaptchaConfig?: RecaptchaConfig;
}
@@ -137,9 +140,12 @@ export class Tenant {
private readonly emailSignInConfig_?: EmailSignInConfig;
private readonly multiFactorConfig_?: MultiFactorAuthConfig;

/*
* The map conatining the reCAPTCHA config.
*/
/**
* 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;
/**
* The SMS Regions Config to update a tenant.
12 changes: 12 additions & 0 deletions src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -737,6 +737,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 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".'
}
}

/**
@@ -996,6 +1004,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. */
20 changes: 19 additions & 1 deletion test/unit/auth/project-config-manager.spec.ts
Original file line number Diff line number Diff line change
@@ -51,6 +51,17 @@ describe('ProjectConfigManager', () => {
allowedRegions: [ 'AC', 'AD' ],
},
},
recaptchaConfig: {
emailPasswordEnforcementState: 'AUDIT',
managedRules: [ {
endScore: 0.2,
action: 'BLOCK'
} ],
recaptchaKeys: [ {
type: 'WEB',
key: 'test-key-1' }
],
}
};

before(() => {
@@ -131,6 +142,13 @@ describe('ProjectConfigManager', () => {
disallowedRegions: [ 'AC', 'AD' ],
},
},
recaptchaConfig: {
emailPasswordEnforcementState: 'AUDIT',
managedRules: [ {
endScore: 0.2,
action: 'BLOCK'
} ],
}
};
const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE);
const expectedError = new FirebaseAuthError(
@@ -193,4 +211,4 @@ describe('ProjectConfigManager', () => {
});
});
});
});
});
111 changes: 107 additions & 4 deletions test/unit/auth/project-config.spec.ts
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ import * as sinonChai from 'sinon-chai';
import * as chaiAsPromised from 'chai-as-promised';

import { deepCopy } from '../../../src/utils/deep-copy';
import { RecaptchaAuthConfig } from '../../../src/auth/auth-config';
import {
ProjectConfig,
ProjectConfigServerResponse,
@@ -66,6 +67,27 @@ describe('ProjectConfig', () => {
disallowedRegions: ['AC', 'AD'],
},
},
recaptchaConfig: {
emailPasswordEnforcementState: 'AUDIT',
managedRules: [ {
endScore: 0.2,
action: 'BLOCK'
} ],
recaptchaKeys: [ {
type: 'WEB',
key: 'test-key-1' }
],
}
};

const updateProjectConfigRequest: UpdateProjectConfigRequest = {
recaptchaConfig: {
emailPasswordEnforcementState: 'AUDIT',
managedRules: [ {
endScore: 0.2,
action: 'BLOCK'
} ]
}
};

describe('buildServerRequest()', () => {
@@ -136,6 +158,64 @@ describe('ProjectConfig', () => {
ProjectConfig.buildServerRequest(configOptionsClientRequest2);
}).not.to.throw;
});
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 emailPasswordEnforcementState attribute', () => {
const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
configOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null;
expect(() => {
ProjectConfig.buildServerRequest(configOptionsClientRequest);
}).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.');
});

it('should throw on invalid emailPasswordEnforcementState attribute', () => {
const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
configOptionsClientRequest.recaptchaConfig
.emailPasswordEnforcementState = 'INVALID';
expect(() => {
ProjectConfig.buildServerRequest(configOptionsClientRequest);
}).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".');
});

it('should throw on non-array managedRules attribute', () => {
const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
configOptionsClientRequest.recaptchaConfig.managedRules = 'non-array';
expect(() => {
ProjectConfig.buildServerRequest(configOptionsClientRequest);
}).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".');
});

it('should throw on invalid managedRules attribute', () => {
const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
configOptionsClientRequest.recaptchaConfig.managedRules =
[{ 'score': 0.1, 'action': 'BLOCK' }];
expect(() => {
ProjectConfig.buildServerRequest(configOptionsClientRequest);
}).to.throw('"score" is not a valid RecaptchaManagedRule parameter.');
});

it('should throw on invalid RecaptchaManagedRule.action attribute', () => {
const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
configOptionsClientRequest.recaptchaConfig.managedRules =
[{ 'endScore': 0.1, 'action': 'ALLOW' }];
expect(() => {
ProjectConfig.buildServerRequest(configOptionsClientRequest);
}).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".');
});

const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop];
nonObjects.forEach((request) => {
@@ -147,7 +227,7 @@ describe('ProjectConfig', () => {
});

it('should throw on unsupported attribute for update request', () => {
const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any;
const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any;
configOptionsClientRequest.unsupported = 'value';
expect(() => {
ProjectConfig.buildServerRequest(configOptionsClientRequest);
@@ -172,21 +252,44 @@ describe('ProjectConfig', () => {
};
expect(projectConfig.smsRegionConfig).to.deep.equal(expectedSmsRegionConfig);
});
it('should set readonly property recaptchaConfig', () => {
const expectedRecaptchaConfig = new RecaptchaAuthConfig(
{
emailPasswordEnforcementState: 'AUDIT',
managedRules: [ {
endScore: 0.2,
action: 'BLOCK'
} ],
recaptchaKeys: [ {
type: 'WEB',
key: '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({
smsRegionConfig: deepCopy(serverResponse.smsRegionConfig)
smsRegionConfig: deepCopy(serverResponse.smsRegionConfig),
recaptchaConfig: deepCopy(serverResponse.recaptchaConfig)
});
});

it('should not populate optional fields if not available', () => {
const serverResponseOptionalCopy: ProjectConfigServerResponse = deepCopy(serverResponse);
delete serverResponseOptionalCopy.smsRegionConfig;
delete serverResponseOptionalCopy.recaptchaConfig?.emailPasswordEnforcementState;
delete serverResponseOptionalCopy.recaptchaConfig?.managedRules;

expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({});
expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({
recaptchaConfig: {
recaptchaKeys: deepCopy(serverResponse.recaptchaConfig?.recaptchaKeys),
}
});
});
});
});
});
3 changes: 2 additions & 1 deletion test/unit/auth/tenant.spec.ts
Original file line number Diff line number Diff line change
@@ -116,7 +116,8 @@ describe('Tenant', () => {
type: 'WEB',
key: 'test-key-1' }
],
}
},
smsRegionConfig: smsAllowByDefault,
};

const clientRequestWithRecaptcha: UpdateTenantRequest = {

0 comments on commit c39c189

Please sign in to comment.