Skip to content

Commit ef33c3c

Browse files
feat(auth): Adds ability to enable MFA on a Google Cloud Identity Platform tenant (#930)
* feat(auth): Adds ability to enable MFA on a tenant. This includes the following capabilities: - Ability to enable disable MFA on a tenant. - Configure the MFA supported type. - Configure the test phone number / code pairs on the tenant.
1 parent be4b539 commit ef33c3c

File tree

12 files changed

+807
-48
lines changed

12 files changed

+807
-48
lines changed

docgen/content-sources/node/toc.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ toc:
4040
path: /docs/reference/admin/node/admin.auth.ListProviderConfigResults
4141
- title: "ListTenantsResult"
4242
path: /docs/reference/admin/node/admin.auth.ListTenantsResult
43+
- title: "MultiFactorConfig"
44+
path: /docs/reference/admin/node/admin.auth.MultiFactorConfig
4345
- title: "MultiFactorCreateSettings"
4446
path: /docs/reference/admin/node/admin.auth.MultiFactorCreateSettings
4547
- title: "MultiFactorInfo"

src/auth.d.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,12 +971,50 @@ export namespace admin.auth {
971971
passwordRequired?: boolean;
972972
};
973973

974+
/**
975+
* The multi-factor auth configuration on the current tenant.
976+
*/
977+
multiFactorConfig?: admin.auth.MultiFactorConfig;
978+
979+
/**
980+
* The map containing the test phone number / code pairs for the tenant.
981+
*/
982+
testPhoneNumbers?: {[phoneNumber: string]: string};
983+
974984
/**
975985
* @return A JSON-serializable representation of this object.
976986
*/
977987
toJSON(): Object;
978988
}
979989

990+
/**
991+
* Identifies a second factor type.
992+
*/
993+
type AuthFactorType = 'phone';
994+
995+
/**
996+
* Identifies a multi-factor configuration state.
997+
*/
998+
type MultiFactorConfigState = 'ENABLED' | 'DISABLED';
999+
1000+
/**
1001+
* Interface representing a multi-factor configuration.
1002+
* This can be used to define whether multi-factor authentication is enabled
1003+
* or disabled and the list of second factor challenges that are supported.
1004+
*/
1005+
interface MultiFactorConfig {
1006+
/**
1007+
* The multi-factor config state.
1008+
*/
1009+
state: admin.auth.MultiFactorConfigState;
1010+
1011+
/**
1012+
* The list of identifiers for enabled second factors.
1013+
* Currently only ‘phone’ is supported.
1014+
*/
1015+
factorIds?: admin.auth.AuthFactorType[];
1016+
}
1017+
9801018
/**
9811019
* Interface representing the properties to update on the provided tenant.
9821020
*/
@@ -1003,6 +1041,17 @@ export namespace admin.auth {
10031041
*/
10041042
passwordRequired?: boolean;
10051043
};
1044+
1045+
/**
1046+
* The multi-factor auth configuration to update on the tenant.
1047+
*/
1048+
multiFactorConfig?: admin.auth.MultiFactorConfig;
1049+
1050+
/**
1051+
* The updated map containing the test phone number / code pairs for the tenant.
1052+
* Passing null clears the previously save phone number / code pairs.
1053+
*/
1054+
testPhoneNumbers?: {[phoneNumber: string]: string} | null;
10061055
}
10071056

10081057
/**

src/auth/auth-api-request.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1801,7 +1801,7 @@ const DELETE_TENANT = new ApiSettings('/tenants/{tenantId}', 'DELETE');
18011801

18021802
/** Instantiates the updateTenant endpoint settings. */
18031803
const UPDATE_TENANT = new ApiSettings('/tenants/{tenantId}?updateMask={updateMask}', 'PATCH')
1804-
// Set response validator.
1804+
// Set response validator.
18051805
.setResponseValidator((response: any) => {
18061806
// Response should always contain at least the tenant name.
18071807
if (!validator.isNonEmptyString(response.name) ||
@@ -1982,7 +1982,9 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler {
19821982
try {
19831983
// Construct backend request.
19841984
const request = Tenant.buildServerRequest(tenantOptions, false);
1985-
const updateMask = utils.generateUpdateMask(request);
1985+
// Do not traverse deep into testPhoneNumbers. The entire content should be replaced
1986+
// and not just specific phone numbers.
1987+
const updateMask = utils.generateUpdateMask(request, ['testPhoneNumbers']);
19861988
return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, UPDATE_TENANT, request,
19871989
{ tenantId, updateMask: updateMask.join(',') })
19881990
.then((response: any) => {

src/auth/auth-config.ts

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import * as validator from '../utils/validator';
1818
import { deepCopy } from '../utils/deep-copy';
1919
import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error';
2020

21+
/** A maximum of 10 test phone number / code pairs can be configured. */
22+
export const MAXIMUM_TEST_PHONE_NUMBERS = 10;
2123

2224
/** The filter interface used for listing provider configurations. */
2325
export interface AuthProviderConfigFilter {
@@ -160,6 +162,212 @@ export interface EmailSignInConfigServerRequest {
160162
enableEmailLinkSignin?: boolean;
161163
}
162164

165+
/** Identifies the public second factor type. */
166+
export type AuthFactorType = 'phone';
167+
168+
/** Identifies the server side second factor type. */
169+
export type AuthFactorServerType = 'PHONE_SMS';
170+
171+
/** Client Auth factor type to server auth factor type mapping. */
172+
export const AUTH_FACTOR_CLIENT_TO_SERVER_TYPE: {[key: string]: AuthFactorServerType} = {
173+
phone: 'PHONE_SMS',
174+
};
175+
176+
/** Server Auth factor type to client auth factor type mapping. */
177+
export const AUTH_FACTOR_SERVER_TO_CLIENT_TYPE: {[key: string]: AuthFactorType} =
178+
Object.keys(AUTH_FACTOR_CLIENT_TO_SERVER_TYPE)
179+
.reduce((res: {[key: string]: AuthFactorType}, key) => {
180+
res[AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[key]] = key as AuthFactorType;
181+
return res;
182+
}, {});
183+
184+
/** Identifies a multi-factor configuration state. */
185+
export type MultiFactorConfigState = 'ENABLED' | 'DISABLED';
186+
187+
/**
188+
* Public API interface representing a multi-factor configuration.
189+
*/
190+
export interface MultiFactorConfig {
191+
/**
192+
* The multi-factor config state.
193+
*/
194+
state: MultiFactorConfigState;
195+
196+
/**
197+
* The list of identifiers for enabled second factors.
198+
* Currently only ‘phone’ is supported.
199+
*/
200+
factorIds?: AuthFactorType[];
201+
}
202+
203+
/** Server side multi-factor configuration. */
204+
export interface MultiFactorAuthServerConfig {
205+
state?: MultiFactorConfigState;
206+
enabledProviders?: AuthFactorServerType[];
207+
}
208+
209+
210+
/**
211+
* Defines the multi-factor config class used to convert client side MultiFactorConfig
212+
* to a format that is understood by the Auth server.
213+
*/
214+
export class MultiFactorAuthConfig implements MultiFactorConfig {
215+
public readonly state: MultiFactorConfigState;
216+
public readonly factorIds: AuthFactorType[];
217+
218+
/**
219+
* Static method to convert a client side request to a MultiFactorAuthServerConfig.
220+
* Throws an error if validation fails.
221+
*
222+
* @param options The options object to convert to a server request.
223+
* @return The resulting server request.
224+
*/
225+
public static buildServerRequest(options: MultiFactorConfig): MultiFactorAuthServerConfig {
226+
const request: MultiFactorAuthServerConfig = {};
227+
MultiFactorAuthConfig.validate(options);
228+
if (Object.prototype.hasOwnProperty.call(options, 'state')) {
229+
request.state = options.state;
230+
}
231+
if (Object.prototype.hasOwnProperty.call(options, 'factorIds')) {
232+
(options.factorIds || []).forEach((factorId) => {
233+
if (typeof request.enabledProviders === 'undefined') {
234+
request.enabledProviders = [];
235+
}
236+
request.enabledProviders.push(AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[factorId]);
237+
});
238+
// In case an empty array is passed. Ensure it gets populated so the array is cleared.
239+
if (options.factorIds && options.factorIds.length === 0) {
240+
request.enabledProviders = [];
241+
}
242+
}
243+
return request;
244+
}
245+
246+
/**
247+
* Validates the MultiFactorConfig options object. Throws an error on failure.
248+
*
249+
* @param options The options object to validate.
250+
*/
251+
private static validate(options: MultiFactorConfig): void {
252+
const validKeys = {
253+
state: true,
254+
factorIds: true,
255+
};
256+
if (!validator.isNonNullObject(options)) {
257+
throw new FirebaseAuthError(
258+
AuthClientErrorCode.INVALID_CONFIG,
259+
'"MultiFactorConfig" must be a non-null object.',
260+
);
261+
}
262+
// Check for unsupported top level attributes.
263+
for (const key in options) {
264+
if (!(key in validKeys)) {
265+
throw new FirebaseAuthError(
266+
AuthClientErrorCode.INVALID_CONFIG,
267+
`"${key}" is not a valid MultiFactorConfig parameter.`,
268+
);
269+
}
270+
}
271+
// Validate content.
272+
if (typeof options.state !== 'undefined' &&
273+
options.state !== 'ENABLED' &&
274+
options.state !== 'DISABLED') {
275+
throw new FirebaseAuthError(
276+
AuthClientErrorCode.INVALID_CONFIG,
277+
'"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".',
278+
);
279+
}
280+
281+
if (typeof options.factorIds !== 'undefined') {
282+
if (!validator.isArray(options.factorIds)) {
283+
throw new FirebaseAuthError(
284+
AuthClientErrorCode.INVALID_CONFIG,
285+
'"MultiFactorConfig.factorIds" must be an array of valid "AuthFactorTypes".',
286+
);
287+
}
288+
289+
// Validate content of array.
290+
options.factorIds.forEach((factorId) => {
291+
if (typeof AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[factorId] === 'undefined') {
292+
throw new FirebaseAuthError(
293+
AuthClientErrorCode.INVALID_CONFIG,
294+
`"${factorId}" is not a valid "AuthFactorType".`,
295+
);
296+
}
297+
});
298+
}
299+
}
300+
301+
/**
302+
* The MultiFactorAuthConfig constructor.
303+
*
304+
* @param response The server side response used to initialize the
305+
* MultiFactorAuthConfig object.
306+
* @constructor
307+
*/
308+
constructor(response: MultiFactorAuthServerConfig) {
309+
if (typeof response.state === 'undefined') {
310+
throw new FirebaseAuthError(
311+
AuthClientErrorCode.INTERNAL_ERROR,
312+
'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response');
313+
}
314+
this.state = response.state;
315+
this.factorIds = [];
316+
(response.enabledProviders || []).forEach((enabledProvider) => {
317+
// Ignore unsupported types. It is possible the current admin SDK version is
318+
// not up to date and newer backend types are supported.
319+
if (typeof AUTH_FACTOR_SERVER_TO_CLIENT_TYPE[enabledProvider] !== 'undefined') {
320+
this.factorIds.push(AUTH_FACTOR_SERVER_TO_CLIENT_TYPE[enabledProvider]);
321+
}
322+
})
323+
}
324+
325+
/** @return The plain object representation of the multi-factor config instance. */
326+
public toJSON(): object {
327+
return {
328+
state: this.state,
329+
factorIds: this.factorIds,
330+
};
331+
}
332+
}
333+
334+
335+
/**
336+
* Validates the provided map of test phone number / code pairs.
337+
* @param testPhoneNumbers The phone number / code pairs to validate.
338+
*/
339+
export function validateTestPhoneNumbers(
340+
testPhoneNumbers: {[phoneNumber: string]: string},
341+
): void {
342+
if (!validator.isObject(testPhoneNumbers)) {
343+
throw new FirebaseAuthError(
344+
AuthClientErrorCode.INVALID_ARGUMENT,
345+
'"testPhoneNumbers" must be a map of phone number / code pairs.',
346+
);
347+
}
348+
if (Object.keys(testPhoneNumbers).length > MAXIMUM_TEST_PHONE_NUMBERS) {
349+
throw new FirebaseAuthError(AuthClientErrorCode.MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED);
350+
}
351+
for (const phoneNumber in testPhoneNumbers) {
352+
// Validate phone number.
353+
if (!validator.isPhoneNumber(phoneNumber)) {
354+
throw new FirebaseAuthError(
355+
AuthClientErrorCode.INVALID_TESTING_PHONE_NUMBER,
356+
`"${phoneNumber}" is not a valid E.164 standard compliant phone number.`
357+
);
358+
}
359+
360+
// Validate code.
361+
if (!validator.isString(testPhoneNumbers[phoneNumber]) ||
362+
!/^[\d]{6}$/.test(testPhoneNumbers[phoneNumber])) {
363+
throw new FirebaseAuthError(
364+
AuthClientErrorCode.INVALID_TESTING_PHONE_NUMBER,
365+
`"${testPhoneNumbers[phoneNumber]}" is not a valid 6 digit code string.`
366+
);
367+
}
368+
}
369+
}
370+
163371

164372
/**
165373
* Defines the email sign-in config class used to convert client side EmailSignInConfig

0 commit comments

Comments
 (0)