diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 3723abd051..80df3be641 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -498,7 +498,7 @@ export interface UidIdentifier { export type UpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; // @public -export type UpdateMultiFactorInfoRequest = UpdatePhoneMultiFactorInfoRequest; +export type UpdateMultiFactorInfoRequest = UpdatePhoneMultiFactorInfoRequest | UpdateTotpMultiFactorInfoRequest; // @public export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactorInfoRequest { @@ -543,6 +543,14 @@ export interface UpdateTenantRequest { } | null; } +// @public (undocumented) +export interface UpdateTotpMultiFactorInfoRequest extends BaseUpdateMultiFactorInfoRequest { + // Warning: (ae-forgotten-export) The symbol "TotpInfo" needs to be exported by the entry point index.d.ts + // + // (undocumented) + totpInfo: TotpInfo; +} + // @public export type UserIdentifier = UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 9fd535777c..bb15f960bd 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -28,7 +28,7 @@ import * as utils from '../utils/index'; import { UserImportOptions, UserImportRecord, UserImportResult, - UserImportBuilder, AuthFactorInfo, convertMultiFactorInfoToServerFormat, + UserImportBuilder, AuthFactorInfo, convertMultiFactorInfoToServerFormat, TotpInfoResponse, } from './user-import-builder'; import { ActionCodeSettings, ActionCodeSettingsBuilder } from './action-code-settings-builder'; import { Tenant, TenantServerResponse, CreateTenantRequest, UpdateTenantRequest } from './tenant'; @@ -251,6 +251,7 @@ function validateAuthFactorInfo(request: AuthFactorInfo): void { displayName: true, phoneInfo: true, enrolledAt: true, + totpInfo: true, }; // Remove unsupported keys from the original request. for (const key in request) { @@ -260,7 +261,7 @@ function validateAuthFactorInfo(request: AuthFactorInfo): void { } // No enrollment ID is available for signupNewUser. Use another identifier. const authFactorInfoIdentifier = - request.mfaEnrollmentId || request.phoneInfo || JSON.stringify(request); + request.mfaEnrollmentId || request.phoneInfo || request.totpInfo || JSON.stringify(request); // Enrollment uid may or may not be specified for update operations. if (typeof request.mfaEnrollmentId !== 'undefined' && !validator.isNonEmptyString(request.mfaEnrollmentId)) { @@ -293,6 +294,8 @@ function validateAuthFactorInfo(request: AuthFactorInfo): void { `The second factor "phoneNumber" for "${authFactorInfoIdentifier}" must be a non-empty ` + 'E.164 standard compliant identifier string.'); } + } else if (typeof request.totpInfo !== 'undefined') { + validateTotpInfo(request.totpInfo); } else { // Invalid second factor. For example, a phone second factor may have been provided without // a phone number. A TOTP based second factor may require a secret key, etc. @@ -302,6 +305,32 @@ function validateAuthFactorInfo(request: AuthFactorInfo): void { } } +/** + * Validates an TotpInfoResponse object. All unsupported parameters + * are removed from the original request. If an invalid field is passed + * an error is thrown. + * + * @param request - The TotpInfoResponse request object. + */ +function validateTotpInfo(request: TotpInfoResponse): void { + const validKeys = { + sharedSecretKey: true, + }; + // Remove unsupported keys from the original request. + for (const key in request) { + if (!(key in validKeys)) { + delete request[key]; + } + } + if (typeof request.sharedSecretKey !== 'undefined' && + !validator.isString(request.sharedSecretKey)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_SHARED_SECRET_KEY, + '"totpInfo.sharedSecretKey" must be a valid string.', + ); + } +} + /** * Validates a providerUserInfo object. All unsupported parameters diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 28ee595c46..e0e3fb3b16 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -93,11 +93,18 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor phoneNumber: string; } +export interface UpdateTotpMultiFactorInfoRequest extends BaseUpdateMultiFactorInfoRequest { + totpInfo: TotpInfo; +} + +export interface TotpInfo { + sharedSecretKey: string; +} /** * Type representing the properties of a user-enrolled second factor * for an `UpdateRequest`. */ -export type UpdateMultiFactorInfoRequest = | UpdatePhoneMultiFactorInfoRequest; +export type UpdateMultiFactorInfoRequest = | UpdatePhoneMultiFactorInfoRequest | UpdateTotpMultiFactorInfoRequest; /** * The multi-factor related user settings for create operations. diff --git a/src/auth/index.ts b/src/auth/index.ts index a559a706f8..3e94499304 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -103,6 +103,7 @@ export { PasswordPolicyEnforcementState, CustomStrengthOptionsConfig, EmailPrivacyConfig, + UpdateTotpMultiFactorInfoRequest, } from './auth-config'; export { diff --git a/src/auth/user-import-builder.ts b/src/auth/user-import-builder.ts index 23e4e5aba3..c311153a8c 100644 --- a/src/auth/user-import-builder.ts +++ b/src/auth/user-import-builder.ts @@ -20,7 +20,8 @@ import * as utils from '../utils'; import * as validator from '../utils/validator'; import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { - UpdateMultiFactorInfoRequest, UpdatePhoneMultiFactorInfoRequest, MultiFactorUpdateSettings + UpdateMultiFactorInfoRequest, UpdatePhoneMultiFactorInfoRequest, MultiFactorUpdateSettings, + UpdateTotpMultiFactorInfoRequest } from './auth-config'; export type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | @@ -262,9 +263,14 @@ export interface AuthFactorInfo { displayName?: string; phoneInfo?: string; enrolledAt?: string; + totpInfo?: TotpInfoResponse; [key: string]: any; } +export interface TotpInfoResponse { + sharedSecretKey?: string; + [key: string]: any; +} /** UploadAccount endpoint request user interface. */ interface UploadAccountUser { @@ -321,7 +327,8 @@ export type ValidatorFunction = (data: UploadAccountUser) => void; * @param multiFactorInfo - The client format second factor. * @returns The corresponding AuthFactorInfo server request format. */ -export function convertMultiFactorInfoToServerFormat(multiFactorInfo: UpdateMultiFactorInfoRequest): AuthFactorInfo { +export function convertMultiFactorInfoToServerFormat(multiFactorInfo: UpdateMultiFactorInfoRequest, + isUploadRequest = false): AuthFactorInfo { let enrolledAt; if (typeof multiFactorInfo.enrollmentTime !== 'undefined') { if (validator.isUTCDateString(multiFactorInfo.enrollmentTime)) { @@ -350,6 +357,23 @@ export function convertMultiFactorInfoToServerFormat(multiFactorInfo: UpdateMult } } return authFactorInfo; + } else if (isUploadRequest && isTotpFactor(multiFactorInfo)) { + // If any required field is missing or invalid, validation will still fail later. + const authFactorInfo: AuthFactorInfo = { + mfaEnrollmentId: multiFactorInfo.uid, + displayName: multiFactorInfo.displayName, + // Required for all phone second factors. + totpInfo: { + sharedSecretKey: multiFactorInfo.totpInfo.sharedSecretKey, + }, + enrolledAt, + }; + for (const objKey in authFactorInfo) { + if (typeof authFactorInfo[objKey] === 'undefined') { + delete authFactorInfo[objKey]; + } + } + return authFactorInfo; } else { // Unsupported second factor. throw new FirebaseAuthError( @@ -363,6 +387,11 @@ function isPhoneFactor(multiFactorInfo: UpdateMultiFactorInfoRequest): return multiFactorInfo.factorId === 'phone'; } +function isTotpFactor(multiFactorInfo: UpdateMultiFactorInfoRequest): + multiFactorInfo is UpdateTotpMultiFactorInfoRequest { + return multiFactorInfo.factorId === 'totp'; +} + /** * @param {any} obj The object to check for number field within. * @param {string} key The entry key. @@ -385,6 +414,7 @@ function getNumberField(obj: any, key: string): number { */ function populateUploadAccountUser( user: UserImportRecord, userValidator?: ValidatorFunction): UploadAccountUser { + console.log('USER_IN_PROGRESS= ', user.uid); const result: UploadAccountUser = { localId: user.uid, email: user.email, @@ -433,12 +463,11 @@ function populateUploadAccountUser( }); }); } - // Convert user.multiFactor.enrolledFactors to server format. if (validator.isNonNullObject(user.multiFactor) && validator.isNonEmptyArray(user.multiFactor.enrolledFactors)) { user.multiFactor.enrolledFactors.forEach((multiFactorInfo) => { - result.mfaInfo!.push(convertMultiFactorInfoToServerFormat(multiFactorInfo)); + result.mfaInfo!.push(convertMultiFactorInfoToServerFormat(multiFactorInfo, true)); }); } @@ -460,7 +489,9 @@ function populateUploadAccountUser( if (typeof userValidator === 'function') { userValidator(result); } + console.log('RESULT=$=', JSON.stringify(result)) return result; + } @@ -490,7 +521,7 @@ export class UserImportBuilder { this.validatedUsers = []; this.userImportResultErrors = []; this.indexMap = {}; - + console.log('USERS_ppl = ', JSON.stringify(users)); this.validatedUsers = this.populateUsers(users, userRequestValidator); this.validatedOptions = this.populateOptions(options, this.requiresHashOptions); } diff --git a/src/utils/error.ts b/src/utils/error.ts index cdb7faef05..48365275aa 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -439,6 +439,10 @@ export class AuthClientErrorCode { code: 'invalid-display-name', message: 'The displayName field must be a valid string.', }; + public static INVALID_SHARED_SECRET_KEY = { + code: 'invalid-shared-secret-key', + message: 'The sharedSecretKey field must be a valid string.', + }; public static INVALID_DYNAMIC_LINK_DOMAIN = { code: 'invalid-dynamic-link-domain', message: 'The provided dynamic link domain is not configured or authorized ' + diff --git a/test/unit/auth/user-import-builder.spec.ts b/test/unit/auth/user-import-builder.spec.ts index 859265a03a..34e8704931 100644 --- a/test/unit/auth/user-import-builder.spec.ts +++ b/test/unit/auth/user-import-builder.spec.ts @@ -110,6 +110,22 @@ describe('UserImportBuilder', () => { phoneNumber: '+16505551000', factorId: 'phone', }, + { + uid: 'enrolledSecondFactor3', + enrollmentTime: now.toUTCString(), + displayName: 'displayNameTotp', + totpInfo: { + sharedSecretKey: 'VIAAQYSO37EKAWB2KAXEQ7EGUMLWI3P4' + }, + factorId: 'totp', + }, + { + uid: 'enrolledSecondFactor4', + totpInfo: { + sharedSecretKey: 'WSUKMEVTQ62EUBF37F2R466ZVLNFL3IF' + }, + factorId: 'totp', + }, ], }, }, @@ -163,6 +179,20 @@ describe('UserImportBuilder', () => { mfaEnrollmentId: 'enrolledSecondFactor2', phoneInfo: '+16505551000', }, + { + mfaEnrollmentId: 'enrolledSecondFactor3', + enrolledAt: now.toISOString(), + displayName: 'displayNameTotp', + totpInfo: { + sharedSecretKey: 'VIAAQYSO37EKAWB2KAXEQ7EGUMLWI3P4' + } + }, + { + mfaEnrollmentId: 'enrolledSecondFactor4', + totpInfo: { + sharedSecretKey: 'WSUKMEVTQ62EUBF37F2R466ZVLNFL3IF' + } + }, ], }, ]; @@ -825,7 +855,7 @@ describe('UserImportBuilder', () => { uid: 'enrollmentId2', secret: 'SECRET', displayName: 'Google Authenticator on personal phone', - factorId: 'totp', + factorId: 'unsupportedFactorId', } as any, ], },