Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create/Update tenant with ReCAPTCHA Config #1586

Merged
merged 14 commits into from
Mar 7, 2022
Merged
30 changes: 30 additions & 0 deletions etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,34 @@ export interface ProviderIdentifier {
providerUid: string;
}

// @public
export type RecaptchaAction = 'BLOCK';

// @public
export interface RecaptchaConfig {
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
managedRules?: RecaptchaManagedRule[];
recaptchaKeys?: RecaptchaKey[];
}

// @public
export interface RecaptchaKey {
key: string;
type?: RecaptchaKeyClientType;
}

// @public
export type RecaptchaKeyClientType = 'WEB';

// @public
export interface RecaptchaManagedRule {
action?: RecaptchaAction;
endScore: number;
}

// @public
export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE';

// @public
export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig {
callbackURL?: string;
Expand Down Expand Up @@ -296,6 +324,7 @@ export class Tenant {
readonly displayName?: string;
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
get multiFactorConfig(): MultiFactorConfig | undefined;
get recaptchaConfig(): RecaptchaConfig | undefined;
readonly tenantId: string;
readonly testPhoneNumbers?: {
[phoneNumber: string]: string;
Expand Down Expand Up @@ -358,6 +387,7 @@ export interface UpdateTenantRequest {
displayName?: string;
emailSignInConfig?: EmailSignInProviderConfig;
multiFactorConfig?: MultiFactorConfig;
recaptchaConfig?: RecaptchaConfig;
testPhoneNumbers?: {
[phoneNumber: string]: string;
} | null;
Expand Down
155 changes: 144 additions & 11 deletions src/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1497,22 +1497,155 @@ export interface RecaptchaKey {
/**
* The reCAPTCHA site key.
*/
key: string;
key: string;
}

/**
* The request interface for updating a reCAPTCHA Config.
*/
export interface RecaptchaConfig {
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
/**
/**
* The enforcement state of email password provider.
*/
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
lahirumaramba marked this conversation as resolved.
Show resolved Hide resolved
/**
* The reCAPTCHA managed rules.
*/
managedRules?: RecaptchaManagedRule[];

/**
* The reCAPTCHA managed rules.
*/
managedRules: RecaptchaManagedRule[];
/**
* The reCAPTCHA keys.
*/
recaptchaKeys?: RecaptchaKey[];
}

/**
* The reCAPTCHA keys.
*/
recaptchaKeys?: RecaptchaKey[];
export class RecaptchaAuthConfig implements RecaptchaConfig {
public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState;
public readonly managedRules?: RecaptchaManagedRule[];
public readonly recaptchaKeys?: RecaptchaKey[];

constructor(recaptchaConfig: RecaptchaConfig) {
this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState;
this.managedRules = recaptchaConfig.managedRules;
this.recaptchaKeys = recaptchaConfig.recaptchaKeys;
}

/**
* Validates the RecaptchaConfig options object. Throws an error on failure.
* @param options - The options object to validate.
*/
public static validate(options: RecaptchaConfig): void {
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
const validKeys = {
emailPasswordEnforcementState: true,
managedRules: true,
recaptchaKeys: true,
};

if (!validator.isNonNullObject(options)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaConfig" must be a non-null object.',
);
}

for (const key in options) {
if (!(key in validKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid RecaptchaConfig parameter.`,
);
}
}

// Validation
if (options.emailPasswordEnforcementState !== undefined){
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_ARGUMENT,
'"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.',)
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
}

if (options.emailPasswordEnforcementState !== 'OFF' &&
options.emailPasswordEnforcementState !== 'AUDIT' &&
options.emailPasswordEnforcementState !== 'ENFORCE') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".',
);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since emailPasswordEnforcementState values are restricted, I think you can combine all this to a single if statement.

if (typeof options. emailPasswordEnforcementState !== 'undefined' &&
        options. emailPasswordEnforcementState !== 'OFF' &&
        options. emailPasswordEnforcementState !== 'AUDIT' &&
        options. emailPasswordEnforcementState !== 'ENFORCE') {
      throw new FirebaseAuthError(
....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for this separation is to align with the backend error-code and keep consistency with the existing error message. E.g. empty string is an INVALID_ARGUMENT but invalid string is an INVALID_CONFIG.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SDK shouldn't allow empty strings in the request to the BE service so this error technically should not occur (from the backend). I felt like the check is redundant because you check for a restricted set of strings here regardless. Not a big issue so I will leave it up to you to decide the best path here.


if (typeof options.managedRules !== 'undefined') {
// Validate array
if (!validator.isArray(options.managedRules)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".',
);
}
// Validate each rule of the array
options.managedRules.forEach((managedRule) => {
RecaptchaAuthConfig.validateManagedRule(managedRule);
});
lahirumaramba marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* Validate each element in ManagedRule array
* @param options - The options object to validate.
*/
private static validateManagedRule(options: RecaptchaManagedRule): void {
const validKeys = {
endScore: true,
action: true,
}
if (!validator.isNonNullObject(options)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaManagedRule" must be a non-null object.',
);
}
// Check for unsupported top level attributes.
for (const key in options) {
if (!(key in validKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid RecaptchaManagedRule parameter.`,
);
}
}

// Validate content.
if (typeof options.action !== 'undefined' &&
options.action !== 'BLOCK') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"RecaptchaManagedRule.action" must be "BLOCK".',
);
}
}

/**
* Returns a JSON-serializable representation of this object.
* @returns The JSON-serializable object representation of the ReCaptcha config instance
*/
public toJSON(): object {
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
const json: any = {
emailPasswordEnforcementState: this.emailPasswordEnforcementState,
managedRules: deepCopy(this.managedRules),
recaptchaKeys: deepCopy(this.recaptchaKeys)
}

if (typeof json.emailPasswordEnforcementState === 'undefined') {
delete json.emailPasswordEnforcementState;
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
}
if (typeof json.managedRules === 'undefined') {
delete json.managedRules;
}
if (typeof json.recaptchaKeys === 'undefined') {
delete json.recaptchaKeys;
}

return json;
}
}
6 changes: 6 additions & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ export {
OAuthResponseType,
OIDCAuthProviderConfig,
OIDCUpdateAuthProviderRequest,
RecaptchaAction,
RecaptchaConfig,
RecaptchaKey,
RecaptchaKeyClientType,
RecaptchaManagedRule,
RecaptchaProviderEnforcementState,
SAMLAuthProviderConfig,
SAMLUpdateAuthProviderRequest,
UserProvider,
Expand Down
35 changes: 34 additions & 1 deletion src/auth/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
import {
EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig,
MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig,
MultiFactorAuthConfig,
MultiFactorAuthConfig, RecaptchaAuthConfig, RecaptchaConfig
} from './auth-config';

/**
Expand Down Expand Up @@ -54,6 +54,11 @@ export interface UpdateTenantRequest {
* Passing null clears the previously save phone number / code pairs.
*/
testPhoneNumbers?: { [phoneNumber: string]: string } | null;

/**
* The recaptcha configuration to update on the tenant.
*/
recaptchaConfig?: RecaptchaConfig;
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -68,6 +73,7 @@ export interface TenantOptionsServerRequest extends EmailSignInConfigServerReque
enableAnonymousUser?: boolean;
mfaConfig?: MultiFactorAuthServerConfig;
testPhoneNumbers?: {[key: string]: string};
recaptchaConfig? : RecaptchaConfig;
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
}

/** The tenant server response interface. */
Expand All @@ -79,6 +85,7 @@ export interface TenantServerResponse {
enableAnonymousUser?: boolean;
mfaConfig?: MultiFactorAuthServerConfig;
testPhoneNumbers?: {[key: string]: string};
recaptchaConfig? : RecaptchaConfig;
}

/**
Expand Down Expand Up @@ -123,6 +130,10 @@ export class Tenant {
private readonly emailSignInConfig_?: EmailSignInConfig;
private readonly multiFactorConfig_?: MultiFactorAuthConfig;

/*
* The map conatining the reCAPTCHA config.
*/
private readonly recaptchaConfig_?: RecaptchaAuthConfig;
/**
* Builds the corresponding server request for a TenantOptions object.
*
Expand Down Expand Up @@ -152,6 +163,9 @@ export class Tenant {
// null will clear existing test phone numbers. Translate to empty object.
request.testPhoneNumbers = tenantOptions.testPhoneNumbers ?? {};
}
if (typeof tenantOptions.recaptchaConfig !== 'undefined') {
request.recaptchaConfig = tenantOptions.recaptchaConfig;
lahirumaramba marked this conversation as resolved.
Show resolved Hide resolved
}
return request;
}

Expand Down Expand Up @@ -185,6 +199,7 @@ export class Tenant {
anonymousSignInEnabled: true,
multiFactorConfig: true,
testPhoneNumbers: true,
recaptchaConfig: true
Xiaoshouzi-gh marked this conversation as resolved.
Show resolved Hide resolved
};
const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest';
if (!validator.isNonNullObject(request)) {
Expand Down Expand Up @@ -231,6 +246,10 @@ export class Tenant {
// This will throw an error if invalid.
MultiFactorAuthConfig.buildServerRequest(request.multiFactorConfig);
}
// Validate reCAPTCHAConfig type if provided.
if (typeof request.recaptchaConfig !== 'undefined') {
RecaptchaAuthConfig.validate(request.recaptchaConfig);
lahirumaramba marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
Expand Down Expand Up @@ -265,6 +284,9 @@ export class Tenant {
if (typeof response.testPhoneNumbers !== 'undefined') {
this.testPhoneNumbers = deepCopy(response.testPhoneNumbers || {});
}
if (typeof response.recaptchaConfig !== 'undefined') {
this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig);
}
}

/**
Expand All @@ -281,6 +303,13 @@ export class Tenant {
return this.multiFactorConfig_;
}

/**
* The recaptcha config auth configuration of the current tenant.
*/
get recaptchaConfig(): RecaptchaConfig | undefined {
return this.recaptchaConfig_;
}

/**
* Returns a JSON-serializable representation of this object.
*
Expand All @@ -294,13 +323,17 @@ export class Tenant {
multiFactorConfig: this.multiFactorConfig_?.toJSON(),
anonymousSignInEnabled: this.anonymousSignInEnabled,
testPhoneNumbers: this.testPhoneNumbers,
recaptchaConfig: this.recaptchaConfig_?.toJSON(),
};
if (typeof json.multiFactorConfig === 'undefined') {
delete json.multiFactorConfig;
}
if (typeof json.testPhoneNumbers === 'undefined') {
delete json.testPhoneNumbers;
}
if (typeof json.recaptchaConfig === 'undefined') {
delete json.recaptchaConfig;
}
return json;
}
}
Expand Down
Loading