in src/emulator/auth/operations.ts [294:489]
function batchCreate(
state: ProjectState,
reqBody: Schemas["GoogleCloudIdentitytoolkitV1UploadAccountRequest"]
): Schemas["GoogleCloudIdentitytoolkitV1UploadAccountResponse"] {
assert(!state.disableAuth, "PROJECT_DISABLED");
assert(state.usageMode !== UsageMode.PASSTHROUGH, "UNSUPPORTED_PASSTHROUGH_OPERATION");
assert(reqBody.users?.length, "MISSING_USER_ACCOUNT");
if (reqBody.sanityCheck) {
if (state.oneAccountPerEmail) {
const existingEmails = new Set<string>();
for (const userInfo of reqBody.users) {
if (userInfo.email) {
assert(!existingEmails.has(userInfo.email), `DUPLICATE_EMAIL : ${userInfo.email}`);
existingEmails.add(userInfo.email);
}
}
}
// Check that there is no duplicate (providerId, rawId) tuple.
const existingProviderAccounts = new Set<string>();
for (const userInfo of reqBody.users) {
for (const { providerId, rawId } of userInfo.providerUserInfo ?? []) {
const key = `${providerId}:${rawId}`;
assert(
!existingProviderAccounts.has(key),
`DUPLICATE_RAW_ID : Provider id(${providerId}), Raw id(${rawId})`
);
existingProviderAccounts.add(key);
}
}
}
if (!reqBody.allowOverwrite) {
const existingLocalIds = new Set<string>();
for (const userInfo of reqBody.users) {
const localId = userInfo.localId || "";
assert(!existingLocalIds.has(localId), `DUPLICATE_LOCAL_ID : ${localId}`);
existingLocalIds.add(localId);
}
}
const errors: { index: number; message: string }[] = [];
for (let index = 0; index < reqBody.users.length; index++) {
const userInfo = reqBody.users[index];
try {
assert(userInfo.localId, "localId is missing");
const uploadTime = new Date();
const fields: Omit<Partial<UserInfo>, "localId"> = {
displayName: userInfo.displayName,
photoUrl: userInfo.photoUrl,
lastLoginAt: userInfo.lastLoginAt,
};
if (userInfo.tenantId) {
assert(
state instanceof TenantProjectState && state.tenantId === userInfo.tenantId,
"Tenant id in userInfo does not match the tenant id in request."
);
}
if (state instanceof TenantProjectState) {
fields.tenantId = state.tenantId;
}
// password
if (userInfo.passwordHash) {
// TODO: Check and block non-emulator hashes.
fields.passwordHash = userInfo.passwordHash;
fields.salt = userInfo.salt;
fields.passwordUpdatedAt = uploadTime.getTime();
} else if (userInfo.rawPassword) {
fields.salt = userInfo.salt || "fakeSalt" + randomId(20);
fields.passwordHash = hashPassword(userInfo.rawPassword, fields.salt);
fields.passwordUpdatedAt = uploadTime.getTime();
}
// custom attrs
if (userInfo.customAttributes) {
validateSerializedCustomClaims(userInfo.customAttributes);
fields.customAttributes = userInfo.customAttributes;
}
// federated
if (userInfo.providerUserInfo) {
fields.providerUserInfo = [];
for (const providerUserInfo of userInfo.providerUserInfo) {
const { providerId, rawId, federatedId } = providerUserInfo;
if (providerId === PROVIDER_PASSWORD || providerId === PROVIDER_PHONE) {
// These providers are handled automatically by create / update.
continue;
}
if (!rawId || !providerId) {
if (!federatedId) {
assert(false, "federatedId or (providerId & rawId) is required");
} else {
// TODO
assert(
false,
"((Parsing federatedId is not implemented in Auth Emulator; please specify providerId AND rawId as a workaround.))"
);
}
}
const existingUserWithRawId = state.getUserByProviderRawId(providerId, rawId);
assert(
!existingUserWithRawId || existingUserWithRawId.localId === userInfo.localId,
"raw id exists in other account in database"
);
fields.providerUserInfo.push({ ...providerUserInfo, providerId, rawId });
}
}
// phone number
if (userInfo.phoneNumber) {
assert(isValidPhoneNumber(userInfo.phoneNumber), "phone number format is invalid");
fields.phoneNumber = userInfo.phoneNumber;
}
fields.validSince = toUnixTimestamp(uploadTime).toString();
fields.createdAt = uploadTime.getTime().toString();
if (fields.createdAt && !isNaN(Number(userInfo.createdAt))) {
fields.createdAt = userInfo.createdAt;
}
if (userInfo.email) {
const email = userInfo.email;
assert(isValidEmailAddress(email), "email is invalid");
// For simplicity, Auth Emulator performs this check in all cases
// (unlike production which checks only if (reqBody.sanityCheck && state.oneAccountPerEmail)).
// We return a non-standard error message in other cases to clarify.
const existingUserWithEmail = state.getUserByEmail(email);
assert(
!existingUserWithEmail || existingUserWithEmail.localId === userInfo.localId,
reqBody.sanityCheck && state.oneAccountPerEmail
? "email exists in other account in database"
: `((Auth Emulator does not support importing duplicate email: ${email}))`
);
fields.email = canonicalizeEmailAddress(email);
}
fields.emailVerified = !!userInfo.emailVerified;
fields.disabled = !!userInfo.disabled;
// MFA
if (userInfo.mfaInfo) {
fields.mfaInfo = [];
assert(fields.email, "Second factor account requires email to be presented.");
assert(fields.emailVerified, "Second factor account requires email to be verified.");
const existingIds = new Set<string>();
for (const enrollment of userInfo.mfaInfo) {
if (enrollment.mfaEnrollmentId) {
assert(!existingIds.has(enrollment.mfaEnrollmentId), "Enrollment id already exists.");
existingIds.add(enrollment.mfaEnrollmentId);
}
}
for (const enrollment of userInfo.mfaInfo) {
enrollment.mfaEnrollmentId = enrollment.mfaEnrollmentId || newRandomId(28, existingIds);
enrollment.enrolledAt = enrollment.enrolledAt || new Date().toISOString();
assert(enrollment.phoneInfo, "Second factor not supported.");
assert(isValidPhoneNumber(enrollment.phoneInfo), "Phone number format is invalid");
enrollment.unobfuscatedPhoneInfo = enrollment.phoneInfo;
fields.mfaInfo.push(enrollment);
}
}
if (state.getUserByLocalId(userInfo.localId)) {
assert(
reqBody.allowOverwrite,
"localId belongs to an existing account - can not overwrite."
);
}
state.overwriteUserWithLocalId(userInfo.localId, fields);
} catch (e: any) {
if (e instanceof BadRequestError) {
// Use friendlier messages for some codes, consistent with production.
let message = e.message;
if (message === "INVALID_CLAIMS") {
message = "Invalid custom claims provided.";
} else if (message === "CLAIMS_TOO_LARGE") {
message = "Custom claims provided are too large.";
} else if (message.startsWith("FORBIDDEN_CLAIM")) {
message = "Custom claims provided include a reserved claim.";
}
errors.push({
index,
message,
});
} else {
throw e;
}
}
}
return {
kind: "identitytoolkit#UploadAccountResponse",
error: errors,
};
}