firebase-tools
Version:
Command-Line Interface for Firebase
1,095 lines • 104 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseBlockingFunctionJwt = exports.setAccountInfoImpl = exports.resetPassword = exports.SESSION_COOKIE_MAX_VALID_DURATION = exports.CUSTOM_TOKEN_AUDIENCE = exports.authOperations = void 0;
const url_1 = require("url");
const jsonwebtoken_1 = require("jsonwebtoken");
const node_fetch_1 = require("node-fetch");
const abort_controller_1 = require("abort-controller");
const utils_1 = require("./utils");
const errors_1 = require("./errors");
const types_1 = require("../types");
const emulatorLogger_1 = require("../emulatorLogger");
const state_1 = require("./state");
exports.authOperations = {
identitytoolkit: {
getProjects,
getRecaptchaParams,
accounts: {
createAuthUri,
delete: deleteAccount,
lookup,
resetPassword,
sendOobCode,
sendVerificationCode,
signInWithCustomToken,
signInWithEmailLink,
signInWithIdp,
signInWithPassword,
signInWithPhoneNumber,
signUp,
update: setAccountInfo,
mfaEnrollment: {
finalize: mfaEnrollmentFinalize,
start: mfaEnrollmentStart,
withdraw: mfaEnrollmentWithdraw,
},
mfaSignIn: {
start: mfaSignInStart,
finalize: mfaSignInFinalize,
},
},
projects: {
createSessionCookie,
queryAccounts,
getConfig,
updateConfig,
accounts: {
_: signUp,
delete: deleteAccount,
lookup,
query: queryAccounts,
sendOobCode,
update: setAccountInfo,
batchCreate,
batchDelete,
batchGet,
},
tenants: {
create: createTenant,
delete: deleteTenant,
get: getTenant,
list: listTenants,
patch: updateTenant,
createSessionCookie,
accounts: {
_: signUp,
batchCreate,
batchDelete,
batchGet,
delete: deleteAccount,
lookup,
query: queryAccounts,
sendOobCode,
update: setAccountInfo,
},
},
},
},
securetoken: {
token: grantToken,
},
emulator: {
projects: {
accounts: {
delete: deleteAllAccountsInProject,
},
config: {
get: getEmulatorProjectConfig,
update: updateEmulatorProjectConfig,
},
oobCodes: {
list: listOobCodesInProject,
},
verificationCodes: {
list: listVerificationCodesInProject,
},
},
},
};
const PASSWORD_MIN_LENGTH = 6;
exports.CUSTOM_TOKEN_AUDIENCE = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
const MFA_INELIGIBLE_PROVIDER = new Set([
state_1.PROVIDER_ANONYMOUS,
state_1.PROVIDER_PHONE,
state_1.PROVIDER_CUSTOM,
state_1.PROVIDER_GAME_CENTER,
]);
async function signUp(state, reqBody, ctx) {
var _a, _b, _c, _d;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
let provider;
const timestamp = new Date();
let updates = {
lastLoginAt: timestamp.getTime().toString(),
};
if ((_a = ctx.security) === null || _a === void 0 ? void 0 : _a.Oauth2) {
if (reqBody.idToken) {
(0, errors_1.assert)(!reqBody.localId, "UNEXPECTED_PARAMETER : User ID");
}
if (reqBody.localId) {
(0, errors_1.assert)(!state.getUserByLocalId(reqBody.localId), "DUPLICATE_LOCAL_ID");
}
updates.displayName = reqBody.displayName;
updates.photoUrl = reqBody.photoUrl;
updates.emailVerified = reqBody.emailVerified || false;
if (reqBody.phoneNumber) {
(0, errors_1.assert)((0, utils_1.isValidPhoneNumber)(reqBody.phoneNumber), "INVALID_PHONE_NUMBER : Invalid format.");
(0, errors_1.assert)(!state.getUserByPhoneNumber(reqBody.phoneNumber), "PHONE_NUMBER_EXISTS");
updates.phoneNumber = reqBody.phoneNumber;
}
if (reqBody.disabled) {
updates.disabled = true;
}
}
else {
(0, errors_1.assert)(!reqBody.localId, "UNEXPECTED_PARAMETER : User ID");
if (reqBody.idToken || reqBody.password || reqBody.email) {
updates.displayName = reqBody.displayName;
updates.emailVerified = false;
(0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
(0, errors_1.assert)(reqBody.password, "MISSING_PASSWORD");
provider = state_1.PROVIDER_PASSWORD;
(0, errors_1.assert)(state.allowPasswordSignup, "OPERATION_NOT_ALLOWED");
}
else {
provider = state_1.PROVIDER_ANONYMOUS;
(0, errors_1.assert)(state.enableAnonymousUser, "ADMIN_ONLY_OPERATION");
}
}
if (reqBody.email || (reqBody.email === "" && provider)) {
(0, errors_1.assert)((0, utils_1.isValidEmailAddress)(reqBody.email), "INVALID_EMAIL");
const email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
(0, errors_1.assert)(!state.getUserByEmail(email), "EMAIL_EXISTS");
updates.email = email;
}
if (reqBody.password) {
(0, errors_1.assert)(reqBody.password.length >= PASSWORD_MIN_LENGTH, `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters`);
updates.salt = "fakeSalt" + (0, utils_1.randomId)(20);
updates.passwordHash = hashPassword(reqBody.password, updates.salt);
updates.passwordUpdatedAt = Date.now();
updates.validSince = (0, utils_1.toUnixTimestamp)(new Date()).toString();
}
if (reqBody.mfaInfo) {
updates.mfaInfo = getMfaEnrollmentsFromRequest(state, reqBody.mfaInfo, {
generateEnrollmentIds: true,
});
}
if (state instanceof state_1.TenantProjectState) {
updates.tenantId = state.tenantId;
}
let user;
if (reqBody.idToken) {
({ user } = parseIdToken(state, reqBody.idToken));
}
let extraClaims;
if (!user) {
updates.createdAt = timestamp.getTime().toString();
const localId = (_b = reqBody.localId) !== null && _b !== void 0 ? _b : state.generateLocalId();
if (reqBody.email && !((_c = ctx.security) === null || _c === void 0 ? void 0 : _c.Oauth2)) {
const userBeforeCreate = Object.assign({ localId }, updates);
const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_CREATE, userBeforeCreate, { signInMethod: "password" });
updates = Object.assign(Object.assign({}, updates), blockingResponse.updates);
}
user = state.createUserWithLocalId(localId, updates);
(0, errors_1.assert)(user, "DUPLICATE_LOCAL_ID");
if (reqBody.email && !((_d = ctx.security) === null || _d === void 0 ? void 0 : _d.Oauth2)) {
if (!user.disabled) {
const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, user, { signInMethod: "password" });
updates = blockingResponse.updates;
extraClaims = blockingResponse.extraClaims;
user = state.updateUserByLocalId(user.localId, updates);
}
(0, errors_1.assert)(!user.disabled, "USER_DISABLED");
}
}
else {
user = state.updateUserByLocalId(user.localId, updates);
}
return Object.assign({ kind: "identitytoolkit#SignupNewUserResponse", localId: user.localId, displayName: user.displayName, email: user.email }, (provider ? issueTokens(state, user, provider, { extraClaims }) : {}));
}
function lookup(state, reqBody, ctx) {
var _a, _b, _c, _d, _e;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
const seenLocalIds = new Set();
const users = [];
function tryAddUser(maybeUser) {
if (maybeUser && !seenLocalIds.has(maybeUser.localId)) {
users.push(maybeUser);
seenLocalIds.add(maybeUser.localId);
}
}
if ((_a = ctx.security) === null || _a === void 0 ? void 0 : _a.Oauth2) {
if (reqBody.initialEmail) {
throw new errors_1.NotImplementedError("Lookup by initialEmail is not implemented.");
}
for (const localId of (_b = reqBody.localId) !== null && _b !== void 0 ? _b : []) {
tryAddUser(state.getUserByLocalId(localId));
}
for (const email of (_c = reqBody.email) !== null && _c !== void 0 ? _c : []) {
const canonicalizedEmail = (0, utils_1.canonicalizeEmailAddress)(email);
tryAddUser(state.getUserByEmail(canonicalizedEmail));
}
for (const phoneNumber of (_d = reqBody.phoneNumber) !== null && _d !== void 0 ? _d : []) {
tryAddUser(state.getUserByPhoneNumber(phoneNumber));
}
for (const { providerId, rawId } of (_e = reqBody.federatedUserId) !== null && _e !== void 0 ? _e : []) {
if (!providerId || !rawId) {
continue;
}
tryAddUser(state.getUserByProviderRawId(providerId, rawId));
}
}
else {
(0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
const { user } = parseIdToken(state, reqBody.idToken);
users.push(redactPasswordHash(user));
}
return {
kind: "identitytoolkit#GetAccountInfoResponse",
users: users.length ? users : undefined,
};
}
function batchCreate(state, reqBody) {
var _a, _b;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
(0, errors_1.assert)((_a = reqBody.users) === null || _a === void 0 ? void 0 : _a.length, "MISSING_USER_ACCOUNT");
if (reqBody.sanityCheck) {
if (state.oneAccountPerEmail) {
const existingEmails = new Set();
for (const userInfo of reqBody.users) {
if (userInfo.email) {
(0, errors_1.assert)(!existingEmails.has(userInfo.email), `DUPLICATE_EMAIL : ${userInfo.email}`);
existingEmails.add(userInfo.email);
}
}
}
const existingProviderAccounts = new Set();
for (const userInfo of reqBody.users) {
for (const { providerId, rawId } of (_b = userInfo.providerUserInfo) !== null && _b !== void 0 ? _b : []) {
const key = `${providerId}:${rawId}`;
(0, errors_1.assert)(!existingProviderAccounts.has(key), `DUPLICATE_RAW_ID : Provider id(${providerId}), Raw id(${rawId})`);
existingProviderAccounts.add(key);
}
}
}
if (!reqBody.allowOverwrite) {
const existingLocalIds = new Set();
for (const userInfo of reqBody.users) {
const localId = userInfo.localId || "";
(0, errors_1.assert)(!existingLocalIds.has(localId), `DUPLICATE_LOCAL_ID : ${localId}`);
existingLocalIds.add(localId);
}
}
const errors = [];
for (let index = 0; index < reqBody.users.length; index++) {
const userInfo = reqBody.users[index];
try {
(0, errors_1.assert)(userInfo.localId, "localId is missing");
const uploadTime = new Date();
const fields = {
displayName: userInfo.displayName,
photoUrl: userInfo.photoUrl,
lastLoginAt: userInfo.lastLoginAt,
};
if (userInfo.tenantId) {
(0, errors_1.assert)(state instanceof state_1.TenantProjectState && state.tenantId === userInfo.tenantId, "Tenant id in userInfo does not match the tenant id in request.");
}
if (state instanceof state_1.TenantProjectState) {
fields.tenantId = state.tenantId;
}
if (userInfo.passwordHash) {
fields.passwordHash = userInfo.passwordHash;
fields.salt = userInfo.salt;
fields.passwordUpdatedAt = uploadTime.getTime();
}
else if (userInfo.rawPassword) {
fields.salt = userInfo.salt || "fakeSalt" + (0, utils_1.randomId)(20);
fields.passwordHash = hashPassword(userInfo.rawPassword, fields.salt);
fields.passwordUpdatedAt = uploadTime.getTime();
}
if (userInfo.customAttributes) {
validateSerializedCustomClaims(userInfo.customAttributes);
fields.customAttributes = userInfo.customAttributes;
}
if (userInfo.providerUserInfo) {
fields.providerUserInfo = [];
for (const providerUserInfo of userInfo.providerUserInfo) {
const { providerId, rawId, federatedId } = providerUserInfo;
if (providerId === state_1.PROVIDER_PASSWORD || providerId === state_1.PROVIDER_PHONE) {
continue;
}
if (!rawId || !providerId) {
if (!federatedId) {
(0, errors_1.assert)(false, "federatedId or (providerId & rawId) is required");
}
else {
(0, errors_1.assert)(false, "((Parsing federatedId is not implemented in Auth Emulator; please specify providerId AND rawId as a workaround.))");
}
}
const existingUserWithRawId = state.getUserByProviderRawId(providerId, rawId);
(0, errors_1.assert)(!existingUserWithRawId || existingUserWithRawId.localId === userInfo.localId, "raw id exists in other account in database");
fields.providerUserInfo.push(Object.assign(Object.assign({}, providerUserInfo), { providerId, rawId }));
}
}
if (userInfo.phoneNumber) {
(0, errors_1.assert)((0, utils_1.isValidPhoneNumber)(userInfo.phoneNumber), "phone number format is invalid");
fields.phoneNumber = userInfo.phoneNumber;
}
fields.validSince = (0, utils_1.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;
(0, errors_1.assert)((0, utils_1.isValidEmailAddress)(email), "email is invalid");
const existingUserWithEmail = state.getUserByEmail(email);
(0, errors_1.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 = (0, utils_1.canonicalizeEmailAddress)(email);
}
fields.emailVerified = !!userInfo.emailVerified;
fields.disabled = !!userInfo.disabled;
if (userInfo.mfaInfo && userInfo.mfaInfo.length > 0) {
fields.mfaInfo = [];
(0, errors_1.assert)(fields.email, "Second factor account requires email to be presented.");
(0, errors_1.assert)(fields.emailVerified, "Second factor account requires email to be verified.");
const existingIds = new Set();
for (const enrollment of userInfo.mfaInfo) {
if (enrollment.mfaEnrollmentId) {
(0, errors_1.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();
(0, errors_1.assert)(enrollment.phoneInfo, "Second factor not supported.");
(0, errors_1.assert)((0, utils_1.isValidPhoneNumber)(enrollment.phoneInfo), "Phone number format is invalid");
enrollment.unobfuscatedPhoneInfo = enrollment.phoneInfo;
fields.mfaInfo.push(enrollment);
}
}
if (state.getUserByLocalId(userInfo.localId)) {
(0, errors_1.assert)(reqBody.allowOverwrite, "localId belongs to an existing account - can not overwrite.");
}
state.overwriteUserWithLocalId(userInfo.localId, fields);
}
catch (e) {
if (e instanceof errors_1.BadRequestError) {
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,
};
}
function batchDelete(state, reqBody) {
var _a;
const errors = [];
const localIds = (_a = reqBody.localIds) !== null && _a !== void 0 ? _a : [];
(0, errors_1.assert)(localIds.length > 0 && localIds.length <= 1000, "LOCAL_ID_LIST_EXCEEDS_LIMIT");
for (let index = 0; index < localIds.length; index++) {
const localId = localIds[index];
const user = state.getUserByLocalId(localId);
if (!user) {
continue;
}
else if (!user.disabled && !reqBody.force) {
errors.push({
index,
localId,
message: "NOT_DISABLED : Disable the account before batch deletion.",
});
}
else {
state.deleteUser(user);
}
}
return { errors: errors.length ? errors : undefined };
}
function batchGet(state, reqBody, ctx) {
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
const maxResults = Math.min(Math.floor(ctx.params.query.maxResults) || 20, 1000);
const users = state.queryUsers({}, { sortByField: "localId", order: "ASC", startToken: ctx.params.query.nextPageToken });
let newPageToken = undefined;
if (maxResults >= 0 && users.length >= maxResults) {
users.length = maxResults;
if (users.length) {
newPageToken = users[users.length - 1].localId;
}
}
return {
kind: "identitytoolkit#DownloadAccountResponse",
users,
nextPageToken: newPageToken,
};
}
function createAuthUri(state, reqBody) {
var _a;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
const sessionId = reqBody.sessionId || (0, utils_1.randomId)(27);
if (reqBody.providerId) {
throw new errors_1.NotImplementedError("Sign-in with IDP is not yet supported.");
}
(0, errors_1.assert)(reqBody.identifier, "MISSING_IDENTIFIER");
(0, errors_1.assert)(reqBody.continueUri, "MISSING_CONTINUE_URI");
(0, errors_1.assert)((0, utils_1.isValidEmailAddress)(reqBody.identifier), "INVALID_IDENTIFIER");
const email = (0, utils_1.canonicalizeEmailAddress)(reqBody.identifier);
(0, errors_1.assert)((0, utils_1.parseAbsoluteUri)(reqBody.continueUri), "INVALID_CONTINUE_URI");
const allProviders = [];
const signinMethods = [];
let registered = false;
const users = state.getUsersByEmailOrProviderEmail(email);
if (state.oneAccountPerEmail) {
if (users.length) {
registered = true;
(_a = users[0].providerUserInfo) === null || _a === void 0 ? void 0 : _a.forEach(({ providerId }) => {
if (providerId === state_1.PROVIDER_PASSWORD) {
allProviders.push(providerId);
if (users[0].passwordHash) {
signinMethods.push(state_1.PROVIDER_PASSWORD);
}
if (users[0].emailLinkSignin) {
signinMethods.push(state_1.SIGNIN_METHOD_EMAIL_LINK);
}
}
else if (providerId !== state_1.PROVIDER_PHONE) {
allProviders.push(providerId);
signinMethods.push(providerId);
}
});
}
}
else {
const user = users.find((u) => u.email);
if (user) {
registered = true;
if (user.passwordHash || user.emailLinkSignin) {
allProviders.push(state_1.PROVIDER_PASSWORD);
if (users[0].passwordHash) {
signinMethods.push(state_1.PROVIDER_PASSWORD);
}
if (users[0].emailLinkSignin) {
signinMethods.push(state_1.SIGNIN_METHOD_EMAIL_LINK);
}
}
}
}
if (state.enableImprovedEmailPrivacy) {
return {
kind: "identitytoolkit#CreateAuthUriResponse",
sessionId,
};
}
else {
return {
kind: "identitytoolkit#CreateAuthUriResponse",
registered,
allProviders,
sessionId,
signinMethods,
};
}
}
const SESSION_COOKIE_MIN_VALID_DURATION = 5 * 60;
exports.SESSION_COOKIE_MAX_VALID_DURATION = 14 * 24 * 60 * 60;
function createSessionCookie(state, reqBody) {
(0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
const validDuration = Number(reqBody.validDuration) || exports.SESSION_COOKIE_MAX_VALID_DURATION;
(0, errors_1.assert)(validDuration >= SESSION_COOKIE_MIN_VALID_DURATION &&
validDuration <= exports.SESSION_COOKIE_MAX_VALID_DURATION, "INVALID_DURATION");
const { payload } = parseIdToken(state, reqBody.idToken);
const issuedAt = (0, utils_1.toUnixTimestamp)(new Date());
const expiresAt = issuedAt + validDuration;
const sessionCookie = (0, jsonwebtoken_1.sign)(Object.assign(Object.assign({}, payload), { iat: issuedAt, exp: expiresAt, iss: `https://session.firebase.google.com/${payload.aud}` }), "fake-secret", {
algorithm: "none",
});
return { sessionCookie };
}
function deleteAccount(state, reqBody, ctx) {
var _a;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
let user;
if ((_a = ctx.security) === null || _a === void 0 ? void 0 : _a.Oauth2) {
(0, errors_1.assert)(reqBody.localId, "MISSING_LOCAL_ID");
const maybeUser = state.getUserByLocalId(reqBody.localId);
(0, errors_1.assert)(maybeUser, "USER_NOT_FOUND");
user = maybeUser;
}
else {
(0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
user = parseIdToken(state, reqBody.idToken).user;
}
state.deleteUser(user);
return {
kind: "identitytoolkit#DeleteAccountResponse",
};
}
function getProjects(state) {
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
(0, errors_1.assert)(state instanceof state_1.AgentProjectState, "UNSUPPORTED_TENANT_OPERATION");
return {
projectId: state.projectNumber,
authorizedDomains: [
"localhost",
],
};
}
function getRecaptchaParams(state) {
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
return {
kind: "identitytoolkit#GetRecaptchaParamResponse",
recaptchaStoken: "This-is-a-fake-token__Dont-send-this-to-the-Recaptcha-service__The-Auth-Emulator-does-not-support-Recaptcha",
recaptchaSiteKey: "Fake-key__Do-not-send-this-to-Recaptcha_",
};
}
function queryAccounts(state, reqBody) {
var _a;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
if ((_a = reqBody.expression) === null || _a === void 0 ? void 0 : _a.length) {
throw new errors_1.NotImplementedError("expression is not implemented.");
}
if (reqBody.returnUserInfo === false) {
return {
recordsCount: state.getUserCount().toString(),
};
}
if (reqBody.limit) {
throw new errors_1.NotImplementedError("limit is not implemented.");
}
reqBody.offset = reqBody.offset || "0";
if (reqBody.offset !== "0") {
throw new errors_1.NotImplementedError("offset is not implemented.");
}
if (!reqBody.order || reqBody.order === "ORDER_UNSPECIFIED") {
reqBody.order = "ASC";
}
if (!reqBody.sortBy || reqBody.sortBy === "SORT_BY_FIELD_UNSPECIFIED") {
reqBody.sortBy = "USER_ID";
}
let sortByField;
if (reqBody.sortBy === "USER_ID") {
sortByField = "localId";
}
else {
throw new errors_1.NotImplementedError("Only sorting by USER_ID is implemented.");
}
const users = state.queryUsers({}, { order: reqBody.order, sortByField });
return {
recordsCount: users.length.toString(),
userInfo: users,
};
}
function resetPassword(state, reqBody) {
var _a;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
(0, errors_1.assert)(state.allowPasswordSignup, "PASSWORD_LOGIN_DISABLED");
(0, errors_1.assert)(reqBody.oobCode, "MISSING_OOB_CODE");
const oob = state.validateOobCode(reqBody.oobCode);
(0, errors_1.assert)(oob, "INVALID_OOB_CODE");
if (reqBody.newPassword) {
(0, errors_1.assert)(oob.requestType === "PASSWORD_RESET", "INVALID_OOB_CODE");
(0, errors_1.assert)(reqBody.newPassword.length >= PASSWORD_MIN_LENGTH, `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters`);
state.deleteOobCode(reqBody.oobCode);
let user = state.getUserByEmail(oob.email);
(0, errors_1.assert)(user, "INVALID_OOB_CODE");
const salt = "fakeSalt" + (0, utils_1.randomId)(20);
const passwordHash = hashPassword(reqBody.newPassword, salt);
user = state.updateUserByLocalId(user.localId, {
emailVerified: true,
passwordHash,
salt,
passwordUpdatedAt: Date.now(),
validSince: (0, utils_1.toUnixTimestamp)(new Date()).toString(),
}, { deleteProviders: (_a = user.providerUserInfo) === null || _a === void 0 ? void 0 : _a.map((info) => info.providerId) });
}
return {
kind: "identitytoolkit#ResetPasswordResponse",
requestType: oob.requestType,
email: oob.requestType === "EMAIL_SIGNIN" ? undefined : oob.email,
newEmail: oob.newEmail,
};
}
exports.resetPassword = resetPassword;
function sendOobCode(state, reqBody, ctx) {
var _a;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
(0, errors_1.assert)(reqBody.requestType && reqBody.requestType !== "OOB_REQ_TYPE_UNSPECIFIED", "MISSING_REQ_TYPE");
if (reqBody.returnOobLink) {
(0, errors_1.assert)((_a = ctx.security) === null || _a === void 0 ? void 0 : _a.Oauth2, "INSUFFICIENT_PERMISSION");
}
if (reqBody.continueUrl) {
(0, errors_1.assert)((0, utils_1.parseAbsoluteUri)(reqBody.continueUrl), "INVALID_CONTINUE_URI : ((expected an absolute URI with valid scheme and host))");
}
let email;
let newEmail;
let mode;
switch (reqBody.requestType) {
case "EMAIL_SIGNIN":
(0, errors_1.assert)(state.enableEmailLinkSignin, "OPERATION_NOT_ALLOWED");
mode = "signIn";
(0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
break;
case "PASSWORD_RESET":
mode = "resetPassword";
(0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
const maybeUser = state.getUserByEmail(email);
if (state.enableImprovedEmailPrivacy && !maybeUser) {
return {
kind: "identitytoolkit#GetOobConfirmationCodeResponse",
email,
};
}
(0, errors_1.assert)(maybeUser, "EMAIL_NOT_FOUND");
break;
case "VERIFY_EMAIL":
mode = "verifyEmail";
if (reqBody.returnOobLink && !reqBody.idToken) {
(0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
const maybeUser = state.getUserByEmail(email);
(0, errors_1.assert)(maybeUser, "USER_NOT_FOUND");
}
else {
const user = parseIdToken(state, reqBody.idToken || "").user;
(0, errors_1.assert)(user.email, "MISSING_EMAIL");
email = user.email;
}
break;
case "VERIFY_AND_CHANGE_EMAIL":
mode = "verifyAndChangeEmail";
(0, errors_1.assert)(reqBody.newEmail, "MISSING_NEW_EMAIL");
newEmail = (0, utils_1.canonicalizeEmailAddress)(reqBody.newEmail);
if (reqBody.returnOobLink && !reqBody.idToken) {
(0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
const maybeUser = state.getUserByEmail(email);
(0, errors_1.assert)(maybeUser, "USER_NOT_FOUND");
}
else {
(0, errors_1.assert)(reqBody.idToken, "MISSING_ID_TOKEN");
const user = parseIdToken(state, reqBody.idToken).user;
(0, errors_1.assert)(user.email, "MISSING_EMAIL");
email = user.email;
}
(0, errors_1.assert)(!state.getUserByEmail(newEmail), "EMAIL_EXISTS");
break;
default:
throw new errors_1.NotImplementedError(reqBody.requestType);
}
if (reqBody.canHandleCodeInApp) {
emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("WARN", "canHandleCodeInApp is unsupported in Auth Emulator. All OOB operations will complete via web.");
}
const url = (0, utils_1.authEmulatorUrl)(ctx.req);
const oobRecord = createOobRecord(state, email, url, {
requestType: reqBody.requestType,
mode,
continueUrl: reqBody.continueUrl,
newEmail,
});
if (reqBody.returnOobLink) {
return {
kind: "identitytoolkit#GetOobConfirmationCodeResponse",
email,
oobCode: oobRecord.oobCode,
oobLink: oobRecord.oobLink,
};
}
else {
logOobMessage(oobRecord);
return {
kind: "identitytoolkit#GetOobConfirmationCodeResponse",
email,
};
}
}
function sendVerificationCode(state, reqBody) {
var _a;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
(0, errors_1.assert)(state instanceof state_1.AgentProjectState, "UNSUPPORTED_TENANT_OPERATION");
(0, errors_1.assert)(reqBody.phoneNumber && (0, utils_1.isValidPhoneNumber)(reqBody.phoneNumber), "INVALID_PHONE_NUMBER : Invalid format.");
const user = state.getUserByPhoneNumber(reqBody.phoneNumber);
(0, errors_1.assert)(!((_a = user === null || user === void 0 ? void 0 : user.mfaInfo) === null || _a === void 0 ? void 0 : _a.length), "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user.");
const { sessionInfo, phoneNumber, code } = state.createVerificationCode(reqBody.phoneNumber);
emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("BULLET", `To verify the phone number ${phoneNumber}, use the code ${code}.`);
return {
sessionInfo,
};
}
function setAccountInfo(state, reqBody, ctx) {
var _a;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
const url = (0, utils_1.authEmulatorUrl)(ctx.req);
return setAccountInfoImpl(state, reqBody, {
privileged: !!((_a = ctx.security) === null || _a === void 0 ? void 0 : _a.Oauth2),
emulatorUrl: url,
});
}
function setAccountInfoImpl(state, reqBody, { privileged = false, emulatorUrl = undefined } = {}) {
var _a, _b;
const unimplementedFields = ["provider", "upgradeToFederatedLogin"];
for (const field of unimplementedFields) {
if (field in reqBody) {
throw new errors_1.NotImplementedError(`${field} is not implemented yet.`);
}
}
if (!privileged) {
(0, errors_1.assert)(reqBody.idToken || reqBody.oobCode, "INVALID_REQ_TYPE : Unsupported request parameters.");
(0, errors_1.assert)(reqBody.customAttributes == null, "INSUFFICIENT_PERMISSION");
}
else {
(0, errors_1.assert)(reqBody.localId, "MISSING_LOCAL_ID");
}
if (reqBody.customAttributes) {
validateSerializedCustomClaims(reqBody.customAttributes);
}
reqBody.deleteAttribute = reqBody.deleteAttribute || [];
for (const attr of reqBody.deleteAttribute) {
if (attr === "PROVIDER" || attr === "RAW_USER_INFO") {
throw new errors_1.NotImplementedError(`deleteAttribute: ${attr}`);
}
}
const updates = {};
let user;
let signInProvider;
let isEmailUpdate = false;
let newEmail;
if (reqBody.oobCode) {
const oob = state.validateOobCode(reqBody.oobCode);
(0, errors_1.assert)(oob, "INVALID_OOB_CODE");
switch (oob.requestType) {
case "VERIFY_EMAIL": {
state.deleteOobCode(reqBody.oobCode);
signInProvider = state_1.PROVIDER_PASSWORD;
const maybeUser = state.getUserByEmail(oob.email);
(0, errors_1.assert)(maybeUser, "INVALID_OOB_CODE");
user = maybeUser;
updates.emailVerified = true;
if (oob.email !== user.email) {
updates.email = oob.email;
}
break;
}
case "VERIFY_AND_CHANGE_EMAIL":
state.deleteOobCode(reqBody.oobCode);
const maybeUser = state.getUserByEmail(oob.email);
(0, errors_1.assert)(maybeUser, "INVALID_OOB_CODE");
(0, errors_1.assert)(oob.newEmail, "INVALID_OOB_CODE");
(0, errors_1.assert)(!state.getUserByEmail(oob.newEmail), "EMAIL_EXISTS");
user = maybeUser;
if (oob.newEmail !== user.email) {
updates.email = oob.newEmail;
updates.emailVerified = true;
newEmail = oob.newEmail;
}
break;
case "RECOVER_EMAIL": {
state.deleteOobCode(reqBody.oobCode);
const maybeUser = state.getUserByInitialEmail(oob.email);
(0, errors_1.assert)(maybeUser, "INVALID_OOB_CODE");
(0, errors_1.assert)(!state.getUserByEmail(oob.email), "EMAIL_EXISTS");
user = maybeUser;
if (oob.email !== user.email) {
updates.email = oob.email;
updates.emailVerified = true;
}
break;
}
default:
throw new errors_1.NotImplementedError(oob.requestType);
}
}
else {
if (reqBody.idToken) {
({ user, signInProvider } = parseIdToken(state, reqBody.idToken));
(0, errors_1.assert)(reqBody.disableUser == null, "OPERATION_NOT_ALLOWED");
}
else {
(0, errors_1.assert)(reqBody.localId, "MISSING_LOCAL_ID");
const maybeUser = state.getUserByLocalId(reqBody.localId);
(0, errors_1.assert)(maybeUser, "USER_NOT_FOUND");
user = maybeUser;
}
if (reqBody.email) {
(0, errors_1.assert)((0, utils_1.isValidEmailAddress)(reqBody.email), "INVALID_EMAIL");
newEmail = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
if (newEmail !== user.email) {
(0, errors_1.assert)(!state.getUserByEmail(newEmail), "EMAIL_EXISTS");
updates.email = newEmail;
updates.emailVerified = false;
isEmailUpdate = true;
if (signInProvider !== state_1.PROVIDER_ANONYMOUS && user.email && !user.initialEmail) {
updates.initialEmail = user.email;
}
}
}
if (reqBody.password) {
(0, errors_1.assert)(reqBody.password.length >= PASSWORD_MIN_LENGTH, `WEAK_PASSWORD : Password should be at least ${PASSWORD_MIN_LENGTH} characters`);
updates.salt = "fakeSalt" + (0, utils_1.randomId)(20);
updates.passwordHash = hashPassword(reqBody.password, updates.salt);
updates.passwordUpdatedAt = Date.now();
signInProvider = state_1.PROVIDER_PASSWORD;
}
if (reqBody.password || reqBody.validSince || updates.email) {
updates.validSince = (0, utils_1.toUnixTimestamp)(new Date()).toString();
}
if (reqBody.mfa) {
if (reqBody.mfa.enrollments && reqBody.mfa.enrollments.length > 0) {
updates.mfaInfo = getMfaEnrollmentsFromRequest(state, reqBody.mfa.enrollments);
}
else {
updates.mfaInfo = undefined;
}
}
const fieldsToCopy = [
"displayName",
"photoUrl",
];
if (privileged) {
if (reqBody.disableUser != null) {
updates.disabled = reqBody.disableUser;
}
if (reqBody.phoneNumber && reqBody.phoneNumber !== user.phoneNumber) {
(0, errors_1.assert)((0, utils_1.isValidPhoneNumber)(reqBody.phoneNumber), "INVALID_PHONE_NUMBER : Invalid format.");
(0, errors_1.assert)(!state.getUserByPhoneNumber(reqBody.phoneNumber), "PHONE_NUMBER_EXISTS");
updates.phoneNumber = reqBody.phoneNumber;
}
fieldsToCopy.push("emailVerified", "customAttributes", "createdAt", "lastLoginAt", "validSince");
}
for (const field of fieldsToCopy) {
if (reqBody[field] != null) {
(0, utils_1.mirrorFieldTo)(updates, field, reqBody);
}
}
for (const attr of reqBody.deleteAttribute) {
switch (attr) {
case "USER_ATTRIBUTE_NAME_UNSPECIFIED":
continue;
case "DISPLAY_NAME":
updates.displayName = undefined;
break;
case "PHOTO_URL":
updates.photoUrl = undefined;
break;
case "PASSWORD":
updates.passwordHash = undefined;
updates.salt = undefined;
break;
case "EMAIL":
updates.email = undefined;
updates.emailVerified = undefined;
updates.emailLinkSignin = undefined;
break;
}
}
if ((_a = reqBody.deleteProvider) === null || _a === void 0 ? void 0 : _a.includes(state_1.PROVIDER_PASSWORD)) {
updates.email = undefined;
updates.emailVerified = undefined;
updates.emailLinkSignin = undefined;
updates.passwordHash = undefined;
updates.salt = undefined;
}
if ((_b = reqBody.deleteProvider) === null || _b === void 0 ? void 0 : _b.includes(state_1.PROVIDER_PHONE)) {
updates.phoneNumber = undefined;
}
}
if (reqBody.linkProviderUserInfo) {
(0, errors_1.assert)(reqBody.linkProviderUserInfo.providerId, "MISSING_PROVIDER_ID");
(0, errors_1.assert)(reqBody.linkProviderUserInfo.rawId, "MISSING_RAW_ID");
}
user = state.updateUserByLocalId(user.localId, updates, {
deleteProviders: reqBody.deleteProvider,
upsertProviders: reqBody.linkProviderUserInfo
? [reqBody.linkProviderUserInfo]
: undefined,
});
if (signInProvider !== state_1.PROVIDER_ANONYMOUS && user.initialEmail && isEmailUpdate) {
if (!emulatorUrl) {
throw new Error("Internal assertion error: missing emulatorUrl param");
}
sendOobForEmailReset(state, user.initialEmail, emulatorUrl);
}
return redactPasswordHash(Object.assign({ kind: "identitytoolkit#SetAccountInfoResponse", localId: user.localId, emailVerified: user.emailVerified, providerUserInfo: user.providerUserInfo, email: user.email, displayName: user.displayName, photoUrl: user.photoUrl, newEmail, passwordHash: user.passwordHash }, (updates.validSince && signInProvider ? issueTokens(state, user, signInProvider) : {})));
}
exports.setAccountInfoImpl = setAccountInfoImpl;
function sendOobForEmailReset(state, initialEmail, url) {
const oobRecord = createOobRecord(state, initialEmail, url, {
requestType: "RECOVER_EMAIL",
mode: "recoverEmail",
});
logOobMessage(oobRecord);
}
function createOobRecord(state, email, url, params) {
const oobRecord = state.createOob(email, params.newEmail, params.requestType, (oobCode) => {
url.pathname = "/emulator/action";
url.searchParams.set("mode", params.mode);
url.searchParams.set("lang", "en");
url.searchParams.set("oobCode", oobCode);
url.searchParams.set("apiKey", "fake-api-key");
if (params.continueUrl) {
url.searchParams.set("continueUrl", params.continueUrl);
}
if (state instanceof state_1.TenantProjectState) {
url.searchParams.set("tenantId", state.tenantId);
}
return url.toString();
});
return oobRecord;
}
function logOobMessage(oobRecord) {
const oobLink = oobRecord.oobLink;
const email = oobRecord.email;
let maybeMessage;
switch (oobRecord.requestType) {
case "EMAIL_SIGNIN":
maybeMessage = `To sign in as ${email}, follow this link: ${oobLink}`;
break;
case "PASSWORD_RESET":
maybeMessage = `To reset the password for ${email}, follow this link: ${oobLink}&newPassword=NEW_PASSWORD_HERE`;
break;
case "VERIFY_EMAIL":
maybeMessage = `To verify the email address ${email}, follow this link: ${oobLink}`;
break;
case "VERIFY_AND_CHANGE_EMAIL":
maybeMessage = `To verify and change the email address from ${email} to ${oobRecord.newEmail}, follow this link: ${oobLink}`;
break;
case "RECOVER_EMAIL":
maybeMessage = `To reset your email address to ${email}, follow this link: ${oobLink}`;
break;
}
if (maybeMessage) {
emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("BULLET", maybeMessage);
}
}
function signInWithCustomToken(state, reqBody) {
var _a;
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
(0, errors_1.assert)(reqBody.token, "MISSING_CUSTOM_TOKEN");
let payload;
if (reqBody.token.startsWith("{")) {
try {
payload = JSON.parse(reqBody.token);
}
catch (_b) {
throw new errors_1.BadRequestError("INVALID_CUSTOM_TOKEN : ((Auth Emulator only accepts strict JSON or JWTs as fake custom tokens.))");
}
}
else {
const decoded = (0, jsonwebtoken_1.decode)(reqBody.token, { complete: true });
if (state instanceof state_1.TenantProjectState) {
(0, errors_1.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.payload.tenant_id) === state.tenantId, "TENANT_ID_MISMATCH");
}
(0, errors_1.assert)(decoded, "INVALID_CUSTOM_TOKEN : Invalid assertion format");
if (decoded.header.alg !== "none") {
emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("WARN", "Received a signed custom token. Auth Emulator does not validate JWTs and IS NOT SECURE");
}
(0, errors_1.assert)(decoded.payload.aud === exports.CUSTOM_TOKEN_AUDIENCE, `INVALID_CUSTOM_TOKEN : ((Invalid aud (audience): ${decoded.payload.aud} ` +
"Note: Firebase ID Tokens / third-party tokens cannot be used with signInWithCustomToken.))");
payload = decoded.payload;
}
const localId = (_a = coercePrimitiveToString(payload.uid)) !== null && _a !== void 0 ? _a : coercePrimitiveToString(payload.user_id);
(0, errors_1.assert)(localId, "MISSING_IDENTIFIER");
let extraClaims = {};
if ("claims" in payload) {
validateCustomClaims(payload.claims);
extraClaims = payload.claims;
}
let user = state.getUserByLocalId(localId);
const isNewUser = !user;
const timestamp = new Date();
const updates = {
customAuth: true,
lastLoginAt: timestamp.getTime().toString(),
tenantId: state instanceof state_1.TenantProjectState ? state.tenantId : undefined,
};
if (user) {
(0, errors_1.assert)(!user.disabled, "USER_DISABLED");
user = state.updateUserByLocalId(localId, updates);
}
else {
updates.createdAt = timestamp.getTime().toString();
user = state.createUserWithLocalId(localId, updates);
if (!user) {
throw new Error(`Internal assertion error: trying to create duplicate localId: ${localId}`);
}
}
return Object.assign({ kind: "identitytoolkit#VerifyCustomTokenResponse", isNewUser }, issueTokens(state, user, state_1.PROVIDER_CUSTOM, { extraClaims }));
}
async function signInWithEmailLink(state, reqBody) {
(0, errors_1.assert)(!state.disableAuth, "PROJECT_DISABLED");
(0, errors_1.assert)(state.enableEmailLinkSignin, "OPERATION_NOT_ALLOWED");
const userFromIdToken = reqBody.idToken ? parseIdToken(state, reqBody.idToken).user : undefined;
(0, errors_1.assert)(reqBody.email, "MISSING_EMAIL");
const email = (0, utils_1.canonicalizeEmailAddress)(reqBody.email);
(0, errors_1.assert)(reqBody.oobCode, "MISSING_OOB_CODE");
const oob = state.validateOobCode(reqBody.oobCode);
(0, errors_1.assert)(oob && oob.requestType === "EMAIL_SIGNIN", "INVALID_OOB_CODE");
(0, errors_1.assert)(email === oob.email, "INVALID_EMAIL : The email provided does not match the sign-in email address.");
state.deleteOobCode(reqBody.oobCode);
const userFromEmail = state.getUserByEmail(email);
let user = userFromIdToken || userFromEmail;
const isNewUser = !user;
const timestamp = new Date();
let updates = {
email,
emailVerified: true,
emailLinkSignin: true,
};
if (state instanceof state_1.TenantProjectState) {
updates.tenantId = state.tenantId;
}
let extraClaims;
if (!user) {
updates.createdAt = timestamp.getTime().toString();
const localId = state.generateLocalId();
const userBeforeCreate = Object.assign({ localId }, updates);
const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_CREATE, userBeforeCreate, { signInMethod: "emailLink" });
updates = Object.assign(Object.assign({}, updates), blockingResponse.updates);
user = state.createUserWithLocalId(localId, updates);
if (!user.disabled && !isMfaEnabled(state, user)) {
const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, user, { signInMethod: "emailLink" });
updates = blockingResponse.updates;
extraClaims = blockingResponse.extraClaims;
user = state.updateUserByLocalId(user.localId, updates);
}
}
else {
(0, errors_1.assert)(!user.disabled, "USER_DISABLED");
if (userFromIdToken && userFromEmail) {
(0, errors_1.assert)(userFromIdToken.localId === userFromEmail.localId, "EMAIL_EXISTS");
}
if (!user.disabled && !isMfaEnabled(state, user)) {
const blockingResponse = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, Object.assign(Object.assign({}, user), updates), { signInMethod: "emailLink" });
updates = Object.assign(Object.assign({}, updates), blockingResponse.updates);
extraClaims = blockingResponse.extraClaims;
}
user = state.updateUserByLocalId(user.localId, updates);
}
const response = {
kind: "identitytoolkit#EmailLinkSigninResponse",
email,
localId: user.localId,
isNewUser,
};
(0, errors_1.assert)(!user.disabled, "USER_DISABLED");
if (isMfaEnabled(state, user)) {
return Object.assign(Object.assign({}, response), mfaPending(state, user, state_1.PROVIDER_PASSWORD));
}
else {
user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() });
return Object.assign(Object.assign({}, response), issueTokens(state, user, state_1.PROVIDER_PASSWORD, { extraClaims }));
}
}
async f