@@ -18,6 +18,8 @@ import * as validator from '../utils/validator';
1818import { deepCopy } from '../utils/deep-copy' ;
1919import { 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. */
2325export 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