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

feat(auth): Support sms region config change on Tenant and Project level #1921

Merged
merged 15 commits into from Oct 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.',
);
}
}
}
}