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

Add support for importing users with totp second factor #2363

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion etc/firebase-admin.auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down
33 changes: 31 additions & 2 deletions src/auth/auth-api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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)) {
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
9 changes: 8 additions & 1 deletion src/auth/auth-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export {
PasswordPolicyEnforcementState,
CustomStrengthOptionsConfig,
EmailPrivacyConfig,
UpdateTotpMultiFactorInfoRequest,
} from './auth-config';

export {
Expand Down
41 changes: 36 additions & 5 deletions src/auth/user-import-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' |
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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(
Expand All @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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));
});
}

Expand All @@ -460,7 +489,9 @@ function populateUploadAccountUser(
if (typeof userValidator === 'function') {
userValidator(result);
}
console.log('RESULT=$=', JSON.stringify(result))
return result;

}


Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions src/utils/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ' +
Expand Down
32 changes: 31 additions & 1 deletion test/unit/auth/user-import-builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
],
},
},
Expand Down Expand Up @@ -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'
}
},
],
},
];
Expand Down Expand Up @@ -825,7 +855,7 @@ describe('UserImportBuilder', () => {
uid: 'enrollmentId2',
secret: 'SECRET',
displayName: 'Google Authenticator on personal phone',
factorId: 'totp',
factorId: 'unsupportedFactorId',
} as any,
],
},
Expand Down
Loading