Skip to content

Commit 35df364

Browse files
feat(auth): Support sms region config change on Tenant and Project level. (#1673)
* Supported SMS regions config update on a project and a tenant level. * Added integration tests.
1 parent b2a28ae commit 35df364

12 files changed

+1115
-12
lines changed

etc/firebase-admin.auth.api.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,35 @@ export interface ActionCodeSettings {
2323
url: string;
2424
}
2525

26+
// @public
27+
export interface AllowByDefault {
28+
disallowedRegions: string[];
29+
}
30+
31+
// @public
32+
export interface AllowByDefaultWrap {
33+
allowByDefault: AllowByDefault;
34+
// @alpha (undocumented)
35+
allowlistOnly?: never;
36+
}
37+
38+
// @public
39+
export interface AllowlistOnly {
40+
allowedRegions: string[];
41+
}
42+
43+
// @public
44+
export interface AllowlistOnlyWrap {
45+
// @alpha (undocumented)
46+
allowByDefault?: never;
47+
allowlistOnly: AllowlistOnly;
48+
}
49+
2650
// @public
2751
export class Auth extends BaseAuth {
2852
// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts
2953
get app(): App;
54+
projectConfigManager(): ProjectConfigManager;
3055
tenantManager(): TenantManager;
3156
}
3257

@@ -309,6 +334,18 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo {
309334
toJSON(): object;
310335
}
311336

337+
// @public
338+
export class ProjectConfig {
339+
readonly smsRegionConfig?: SmsRegionConfig;
340+
toJSON(): object;
341+
}
342+
343+
// @public
344+
export class ProjectConfigManager {
345+
getProjectConfig(): Promise<ProjectConfig>;
346+
updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise<ProjectConfig>;
347+
}
348+
312349
// @public
313350
export interface ProviderIdentifier {
314351
// (undocumented)
@@ -342,13 +379,17 @@ export interface SessionCookieOptions {
342379
expiresIn: number;
343380
}
344381

382+
// @public
383+
export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap;
384+
345385
// @public
346386
export class Tenant {
347387
// (undocumented)
348388
readonly anonymousSignInEnabled: boolean;
349389
readonly displayName?: string;
350390
get emailSignInConfig(): EmailSignInProviderConfig | undefined;
351391
get multiFactorConfig(): MultiFactorConfig | undefined;
392+
readonly smsRegionConfig?: SmsRegionConfig;
352393
readonly tenantId: string;
353394
readonly testPhoneNumbers?: {
354395
[phoneNumber: string]: string;
@@ -391,6 +432,11 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor
391432
phoneNumber: string;
392433
}
393434

435+
// @public
436+
export interface UpdateProjectConfigRequest {
437+
smsRegionConfig?: SmsRegionConfig;
438+
}
439+
394440
// @public
395441
export interface UpdateRequest {
396442
disabled?: boolean;
@@ -411,6 +457,7 @@ export interface UpdateTenantRequest {
411457
displayName?: string;
412458
emailSignInConfig?: EmailSignInProviderConfig;
413459
multiFactorConfig?: MultiFactorConfig;
460+
smsRegionConfig?: SmsRegionConfig;
414461
testPhoneNumbers?: {
415462
[phoneNumber: string]: string;
416463
} | null;

src/auth/auth-api-request.ts

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest,
4343
SAMLUpdateAuthProviderRequest
4444
} from './auth-config';
45+
import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config';
4546

4647
/** Firebase Auth request header. */
4748
const FIREBASE_AUTH_HEADER = {
@@ -102,7 +103,6 @@ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace(
102103
const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace(
103104
'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}');
104105

105-
106106
/** Maximum allowed number of tenants to download at one time. */
107107
const MAX_LIST_TENANT_PAGE_SIZE = 1000;
108108

@@ -1981,6 +1981,29 @@ export abstract class AbstractAuthRequestHandler {
19811981
}
19821982
}
19831983

1984+
/** Instantiates the getConfig endpoint settings. */
1985+
const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET')
1986+
.setResponseValidator((response: any) => {
1987+
// Response should always contain at least the config name.
1988+
if (!validator.isNonEmptyString(response.name)) {
1989+
throw new FirebaseAuthError(
1990+
AuthClientErrorCode.INTERNAL_ERROR,
1991+
'INTERNAL ASSERT FAILED: Unable to get project config',
1992+
);
1993+
}
1994+
});
1995+
1996+
/** Instantiates the updateConfig endpoint settings. */
1997+
const UPDATE_PROJECT_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH')
1998+
.setResponseValidator((response: any) => {
1999+
// Response should always contain at least the config name.
2000+
if (!validator.isNonEmptyString(response.name)) {
2001+
throw new FirebaseAuthError(
2002+
AuthClientErrorCode.INTERNAL_ERROR,
2003+
'INTERNAL ASSERT FAILED: Unable to update project config',
2004+
);
2005+
}
2006+
});
19842007

19852008
/** Instantiates the getTenant endpoint settings. */
19862009
const GET_TENANT = new ApiSettings('/tenants/{tenantId}', 'GET')
@@ -2049,13 +2072,13 @@ const CREATE_TENANT = new ApiSettings('/tenants', 'POST')
20492072

20502073

20512074
/**
2052-
* Utility for sending requests to Auth server that are Auth instance related. This includes user and
2053-
* tenant management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines
2075+
* Utility for sending requests to Auth server that are Auth instance related. This includes user, tenant,
2076+
* and project config management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines
20542077
* additional tenant management related APIs.
20552078
*/
20562079
export class AuthRequestHandler extends AbstractAuthRequestHandler {
20572080

2058-
protected readonly tenantMgmtResourceBuilder: AuthResourceUrlBuilder;
2081+
protected readonly authResourceUrlBuilder: AuthResourceUrlBuilder;
20592082

20602083
/**
20612084
* The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp.
@@ -2065,7 +2088,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
20652088
*/
20662089
constructor(app: App) {
20672090
super(app);
2068-
this.tenantMgmtResourceBuilder = new AuthResourceUrlBuilder(app, 'v2');
2091+
this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2');
20692092
}
20702093

20712094
/**
@@ -2082,6 +2105,35 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
20822105
return new AuthResourceUrlBuilder(this.app, 'v2');
20832106
}
20842107

2108+
/**
2109+
* Get the current project's config
2110+
* @returns A promise that resolves with the project config information.
2111+
*/
2112+
public getProjectConfig(): Promise<ProjectConfigServerResponse> {
2113+
return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_PROJECT_CONFIG, {}, {})
2114+
.then((response: any) => {
2115+
return response as ProjectConfigServerResponse;
2116+
});
2117+
}
2118+
2119+
/**
2120+
* Update the current project's config.
2121+
* @returns A promise that resolves with the project config information.
2122+
*/
2123+
public updateProjectConfig(options: UpdateProjectConfigRequest): Promise<ProjectConfigServerResponse> {
2124+
try {
2125+
const request = ProjectConfig.buildServerRequest(options);
2126+
const updateMask = utils.generateUpdateMask(request);
2127+
return this.invokeRequestHandler(
2128+
this.authResourceUrlBuilder, UPDATE_PROJECT_CONFIG, request, { updateMask: updateMask.join(',') })
2129+
.then((response: any) => {
2130+
return response as ProjectConfigServerResponse;
2131+
});
2132+
} catch (e) {
2133+
return Promise.reject(e);
2134+
}
2135+
}
2136+
20852137
/**
20862138
* Looks up a tenant by tenant ID.
20872139
*
@@ -2092,7 +2144,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
20922144
if (!validator.isNonEmptyString(tenantId)) {
20932145
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID));
20942146
}
2095-
return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, GET_TENANT, {}, { tenantId })
2147+
return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT, {}, { tenantId })
20962148
.then((response: any) => {
20972149
return response as TenantServerResponse;
20982150
});
@@ -2122,7 +2174,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
21222174
if (typeof request.pageToken === 'undefined') {
21232175
delete request.pageToken;
21242176
}
2125-
return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, LIST_TENANTS, request)
2177+
return this.invokeRequestHandler(this.authResourceUrlBuilder, LIST_TENANTS, request)
21262178
.then((response: any) => {
21272179
if (!response.tenants) {
21282180
response.tenants = [];
@@ -2142,7 +2194,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
21422194
if (!validator.isNonEmptyString(tenantId)) {
21432195
return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID));
21442196
}
2145-
return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, DELETE_TENANT, undefined, { tenantId })
2197+
return this.invokeRequestHandler(this.authResourceUrlBuilder, DELETE_TENANT, undefined, { tenantId })
21462198
.then(() => {
21472199
// Return nothing.
21482200
});
@@ -2158,7 +2210,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
21582210
try {
21592211
// Construct backend request.
21602212
const request = Tenant.buildServerRequest(tenantOptions, true);
2161-
return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, CREATE_TENANT, request)
2213+
return this.invokeRequestHandler(this.authResourceUrlBuilder, CREATE_TENANT, request)
21622214
.then((response: any) => {
21632215
return response as TenantServerResponse;
21642216
});
@@ -2184,7 +2236,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
21842236
// Do not traverse deep into testPhoneNumbers. The entire content should be replaced
21852237
// and not just specific phone numbers.
21862238
const updateMask = utils.generateUpdateMask(request, ['testPhoneNumbers']);
2187-
return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, UPDATE_TENANT, request,
2239+
return this.invokeRequestHandler(this.authResourceUrlBuilder, UPDATE_TENANT, request,
21882240
{ tenantId, updateMask: updateMask.join(',') })
21892241
.then((response: any) => {
21902242
return response as TenantServerResponse;

src/auth/auth-config.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,3 +1451,146 @@ export class OIDCConfig implements OIDCAuthProviderConfig {
14511451
};
14521452
}
14531453
}
1454+
1455+
/**
1456+
* The request interface for updating a SMS Region Config.
1457+
* Configures the regions where users are allowed to send verification SMS.
1458+
* This is based on the calling code of the destination phone number.
1459+
*/
1460+
export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap;
1461+
1462+
/**
1463+
* Mutual exclusive SMS Region Config of AllowByDefault interface
1464+
*/
1465+
export interface AllowByDefaultWrap {
1466+
/**
1467+
* Allow every region by default.
1468+
*/
1469+
allowByDefault: AllowByDefault;
1470+
/** @alpha */
1471+
allowlistOnly?: never;
1472+
}
1473+
1474+
/**
1475+
* Mutually exclusive SMS Region Config of AllowlistOnly interface
1476+
*/
1477+
export interface AllowlistOnlyWrap {
1478+
/**
1479+
* Only allowing regions by explicitly adding them to an
1480+
* allowlist.
1481+
*/
1482+
allowlistOnly: AllowlistOnly;
1483+
/** @alpha */
1484+
allowByDefault?: never;
1485+
}
1486+
1487+
/**
1488+
* Defines a policy of allowing every region by default and adding disallowed
1489+
* regions to a disallow list.
1490+
*/
1491+
export interface AllowByDefault {
1492+
/**
1493+
* Two letter unicode region codes to disallow as defined by
1494+
* https://cldr.unicode.org/
1495+
* The full list of these region codes is here:
1496+
* https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json
1497+
*/
1498+
disallowedRegions: string[];
1499+
}
1500+
1501+
/**
1502+
* Defines a policy of only allowing regions by explicitly adding them to an
1503+
* allowlist.
1504+
*/
1505+
export interface AllowlistOnly {
1506+
/**
1507+
* Two letter unicode region codes to allow as defined by
1508+
* https://cldr.unicode.org/
1509+
* The full list of these region codes is here:
1510+
* https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json
1511+
*/
1512+
allowedRegions: string[];
1513+
}
1514+
1515+
/**
1516+
* Defines the SMSRegionConfig class used for validation.
1517+
*
1518+
* @internal
1519+
*/
1520+
export class SmsRegionsAuthConfig {
1521+
public static validate(options: SmsRegionConfig): void {
1522+
if (!validator.isNonNullObject(options)) {
1523+
throw new FirebaseAuthError(
1524+
AuthClientErrorCode.INVALID_CONFIG,
1525+
'"SmsRegionConfig" must be a non-null object.',
1526+
);
1527+
}
1528+
1529+
const validKeys = {
1530+
allowlistOnly: true,
1531+
allowByDefault: true,
1532+
};
1533+
1534+
for (const key in options) {
1535+
if (!(key in validKeys)) {
1536+
throw new FirebaseAuthError(
1537+
AuthClientErrorCode.INVALID_CONFIG,
1538+
`"${key}" is not a valid SmsRegionConfig parameter.`,
1539+
);
1540+
}
1541+
}
1542+
1543+
// validate mutual exclusiveness of allowByDefault and allowlistOnly
1544+
if (typeof options.allowByDefault !== 'undefined' && typeof options.allowlistOnly !== 'undefined') {
1545+
throw new FirebaseAuthError(
1546+
AuthClientErrorCode.INVALID_CONFIG,
1547+
'SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.',
1548+
);
1549+
}
1550+
// validation for allowByDefault type
1551+
if (typeof options.allowByDefault !== 'undefined') {
1552+
const allowByDefaultValidKeys = {
1553+
disallowedRegions: true,
1554+
}
1555+
for (const key in options.allowByDefault) {
1556+
if (!(key in allowByDefaultValidKeys)) {
1557+
throw new FirebaseAuthError(
1558+
AuthClientErrorCode.INVALID_CONFIG,
1559+
`"${key}" is not a valid SmsRegionConfig.allowByDefault parameter.`,
1560+
);
1561+
}
1562+
}
1563+
// disallowedRegion can be empty.
1564+
if (typeof options.allowByDefault.disallowedRegions !== 'undefined'
1565+
&& !validator.isArray(options.allowByDefault.disallowedRegions)) {
1566+
throw new FirebaseAuthError(
1567+
AuthClientErrorCode.INVALID_CONFIG,
1568+
'"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.',
1569+
);
1570+
}
1571+
}
1572+
1573+
if (typeof options.allowlistOnly !== 'undefined') {
1574+
const allowListOnlyValidKeys = {
1575+
allowedRegions: true,
1576+
}
1577+
for (const key in options.allowlistOnly) {
1578+
if (!(key in allowListOnlyValidKeys)) {
1579+
throw new FirebaseAuthError(
1580+
AuthClientErrorCode.INVALID_CONFIG,
1581+
`"${key}" is not a valid SmsRegionConfig.allowlistOnly parameter.`,
1582+
);
1583+
}
1584+
}
1585+
1586+
// allowedRegions can be empty
1587+
if (typeof options.allowlistOnly.allowedRegions !== 'undefined'
1588+
&& !validator.isArray(options.allowlistOnly.allowedRegions)) {
1589+
throw new FirebaseAuthError(
1590+
AuthClientErrorCode.INVALID_CONFIG,
1591+
'"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.',
1592+
);
1593+
}
1594+
}
1595+
}
1596+
}

0 commit comments

Comments
 (0)