UNPKG

firebase-tools

Version:
1,095 lines 104 kB
"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