Skip to content

Commit

Permalink
feat(auth): Support sms region config change on Tenant and Project le…
Browse files Browse the repository at this point in the history
…vel (#1921)

* api proposal sms draft

* support SMS regions config update for project and tenant

* fix lint issues

* remove reCAPTCHA related docs and variable names

* Address PR feedbacks

* removed the unnecessary optional tag

* updated the validation parameter type

* update the doc

* Fixing some problems that failed build 14.x and 16.x

* Undoing package-lock.json changes

* Reverting package-lock.json changes

Co-authored-by: Liubin Jiang <liubinj@google.com>
Co-authored-by: Liubin Jiang <56564857+Xiaoshouzi-gh@users.noreply.github.com>
  • Loading branch information
3 people committed Oct 7, 2022
1 parent ee60cd1 commit 294bffd
Show file tree
Hide file tree
Showing 13 changed files with 1,163 additions and 60 deletions.
47 changes: 47 additions & 0 deletions etc/firebase-admin.auth.api.md
Expand Up @@ -23,10 +23,35 @@ export interface ActionCodeSettings {
url: string;
}

// @public
export interface AllowByDefault {
disallowedRegions: string[];
}

// @public
export interface AllowByDefaultWrap {
allowByDefault: AllowByDefault;
// @alpha (undocumented)
allowlistOnly?: never;
}

// @public
export interface AllowlistOnly {
allowedRegions: string[];
}

// @public
export interface AllowlistOnlyWrap {
// @alpha (undocumented)
allowByDefault?: never;
allowlistOnly: AllowlistOnly;
}

// @public
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;
}

Expand Down Expand Up @@ -309,6 +334,18 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {
toJSON(): object;
}

// @public
export class ProjectConfig {
readonly smsRegionConfig?: SmsRegionConfig;
toJSON(): object;
}

// @public
export class ProjectConfigManager {
getProjectConfig(): Promise<ProjectConfig>;
updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise<ProjectConfig>;
}

// @public
export interface ProviderIdentifier {
// (undocumented)
Expand Down Expand Up @@ -342,13 +379,17 @@ export interface SessionCookieOptions {
expiresIn: number;
}

// @public
export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap;

// @public
export class Tenant {
// (undocumented)
readonly anonymousSignInEnabled: boolean;
readonly displayName?: string;
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
get multiFactorConfig(): MultiFactorConfig | undefined;
readonly smsRegionConfig?: SmsRegionConfig;
readonly tenantId: string;
readonly testPhoneNumbers?: {
[phoneNumber: string]: string;
Expand Down Expand Up @@ -391,6 +432,11 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor
phoneNumber: string;
}

// @public
export interface UpdateProjectConfigRequest {
smsRegionConfig?: SmsRegionConfig;
}

// @public
export interface UpdateRequest {
disabled?: boolean;
Expand All @@ -411,6 +457,7 @@ export interface UpdateTenantRequest {
displayName?: string;
emailSignInConfig?: EmailSignInProviderConfig;
multiFactorConfig?: MultiFactorConfig;
smsRegionConfig?: SmsRegionConfig;
testPhoneNumbers?: {
[phoneNumber: string]: string;
} | null;
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 62 additions & 10 deletions src/auth/auth-api-request.ts
Expand Up @@ -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 = {
Expand Down Expand Up @@ -102,7 +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}');


/** Maximum allowed number of tenants to download at one time. */
const MAX_LIST_TENANT_PAGE_SIZE = 1000;

Expand Down Expand Up @@ -1981,6 +1981,29 @@ export abstract class AbstractAuthRequestHandler {
}
}

/** Instantiates the getConfig endpoint settings. */
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)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INTERNAL_ERROR,
'INTERNAL ASSERT FAILED: Unable to get project config',
);
}
});

/** Instantiates the updateConfig endpoint settings. */
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)) {
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')
Expand Down Expand Up @@ -2049,13 +2072,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 tenantMgmtResourceBuilder: AuthResourceUrlBuilder;
protected readonly authResourceUrlBuilder: AuthResourceUrlBuilder;

/**
* The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp.
Expand All @@ -2065,7 +2088,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
*/
constructor(app: App) {
super(app);
this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(app, 'v2');
this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2');
}

/**
Expand All @@ -2082,6 +2105,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 getProjectConfig(): Promise<ProjectConfigServerResponse> {
return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_PROJECT_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 updateProjectConfig(options: UpdateProjectConfigRequest): Promise<ProjectConfigServerResponse> {
try {
const request = ProjectConfig.buildServerRequest(options);
const updateMask = utils.generateUpdateMask(request);
return this.invokeRequestHandler(
this.authResourceUrlBuilder, UPDATE_PROJECT_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.
*
Expand All @@ -2092,7 +2144,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.authResourceUrlBuilder, GET_TENANT, {}, { tenantId })
.then((response: any) => {
return response as TenantServerResponse;
});
Expand Down Expand Up @@ -2122,7 +2174,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.authResourceUrlBuilder, LIST_TENANTS, request)
.then((response: any) => {
if (!response.tenants) {
response.tenants = [];
Expand All @@ -2142,7 +2194,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.authResourceUrlBuilder, DELETE_TENANT, undefined, { tenantId })
.then(() => {
// Return nothing.
});
Expand All @@ -2158,7 +2210,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.authResourceUrlBuilder, CREATE_TENANT, request)
.then((response: any) => {
return response as TenantServerResponse;
});
Expand All @@ -2184,7 +2236,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.authResourceUrlBuilder, UPDATE_TENANT, request,
{ tenantId, updateMask: updateMask.join(',') })
.then((response: any) => {
return response as TenantServerResponse;
Expand Down
143 changes: 143 additions & 0 deletions src/auth/auth-config.ts
Expand Up @@ -1451,3 +1451,146 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
};
}
}

/**
* The request interface for updating a SMS Region Config.
* Configures the regions where users are allowed to send verification SMS.
* This is based on the calling code of the destination phone number.
*/
export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap;

/**
* Mutual exclusive SMS Region Config of AllowByDefault interface
*/
export interface AllowByDefaultWrap {
/**
* Allow every region by default.
*/
allowByDefault: AllowByDefault;
/** @alpha */
allowlistOnly?: never;
}

/**
* Mutually exclusive SMS Region Config of AllowlistOnly interface
*/
export interface AllowlistOnlyWrap {
/**
* Only allowing regions by explicitly adding them to an
* allowlist.
*/
allowlistOnly: AllowlistOnly;
/** @alpha */
allowByDefault?: never;
}

/**
* Defines a policy of allowing every region by default and adding disallowed
* regions to a disallow list.
*/
export interface AllowByDefault {
/**
* Two letter unicode region codes to disallow as defined by
* https://cldr.unicode.org/
* The full list of these region codes is here:
* https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json
*/
disallowedRegions: string[];
}

/**
* Defines a policy of only allowing regions by explicitly adding them to an
* allowlist.
*/
export interface AllowlistOnly {
/**
* Two letter unicode region codes to allow as defined by
* https://cldr.unicode.org/
* The full list of these region codes is here:
* https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json
*/
allowedRegions: string[];
}

/**
* Defines the SMSRegionConfig class used for validation.
*
* @internal
*/
export class SmsRegionsAuthConfig {
public static validate(options: SmsRegionConfig): void {
if (!validator.isNonNullObject(options)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"SmsRegionConfig" must be a non-null object.',
);
}

const validKeys = {
allowlistOnly: true,
allowByDefault: true,
};

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

// validate mutual exclusiveness of allowByDefault and allowlistOnly
if (typeof options.allowByDefault !== 'undefined' && typeof options.allowlistOnly !== 'undefined') {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.',
);
}
// validation for allowByDefault type
if (typeof options.allowByDefault !== 'undefined') {
const allowByDefaultValidKeys = {
disallowedRegions: true,
}
for (const key in options.allowByDefault) {
if (!(key in allowByDefaultValidKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid SmsRegionConfig.allowByDefault parameter.`,
);
}
}
// disallowedRegion can be empty.
if (typeof options.allowByDefault.disallowedRegions !== 'undefined'
&& !validator.isArray(options.allowByDefault.disallowedRegions)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.',
);
}
}

if (typeof options.allowlistOnly !== 'undefined') {
const allowListOnlyValidKeys = {
allowedRegions: true,
}
for (const key in options.allowlistOnly) {
if (!(key in allowListOnlyValidKeys)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
`"${key}" is not a valid SmsRegionConfig.allowlistOnly parameter.`,
);
}
}

// allowedRegions can be empty
if (typeof options.allowlistOnly.allowedRegions !== 'undefined'
&& !validator.isArray(options.allowlistOnly.allowedRegions)) {
throw new FirebaseAuthError(
AuthClientErrorCode.INVALID_CONFIG,
'"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.',
);
}
}
}
}

0 comments on commit 294bffd

Please sign in to comment.