123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.createApp = void 0;
- const cors = require("cors");
- const express = require("express");
- const exegesisExpress = require("exegesis-express");
- const errors_1 = require("exegesis/lib/errors");
- const _ = require("lodash");
- const index_1 = require("./index");
- const emulatorLogger_1 = require("../emulatorLogger");
- const types_1 = require("../types");
- const operations_1 = require("./operations");
- const state_1 = require("./state");
- const apiSpec_1 = require("./apiSpec");
- const errors_2 = require("./errors");
- const utils_1 = require("./utils");
- const lodash_1 = require("lodash");
- const handlers_1 = require("./handlers");
- const bodyParser = require("body-parser");
- const url_1 = require("url");
- const jsonwebtoken_1 = require("jsonwebtoken");
- const apiSpec = apiSpec_1.default;
- const API_SPEC_PATH = "/emulator/openapi.json";
- const AUTH_HEADER_PREFIX = "bearer ";
- const SERVICE_ACCOUNT_TOKEN_PREFIX = "ya29.";
- function specForRouter() {
- const paths = {};
- Object.entries(apiSpec.paths).forEach(([path, pathObj]) => {
- var _a;
- const servers = (_a = pathObj.servers) !== null && _a !== void 0 ? _a : apiSpec.servers;
- if (!servers || !servers.length) {
- throw new Error("No servers defined in API spec.");
- }
- const pathWithNamespace = servers[0].url.replace("https://", "/") + path;
- paths[pathWithNamespace] = pathObj;
- });
- return Object.assign(Object.assign({}, apiSpec), { paths, servers: undefined, "x-exegesis-controller": "auth" });
- }
- function specWithEmulatorServer(protocol, host) {
- const paths = {};
- Object.entries(apiSpec.paths).forEach(([path, pathObj]) => {
- const servers = pathObj.servers;
- if (servers) {
- pathObj = Object.assign(Object.assign({}, pathObj), { servers: serversWithEmulators(servers) });
- }
- paths[path] = pathObj;
- });
- if (!apiSpec.servers) {
- throw new Error("No servers defined in API spec.");
- }
- return Object.assign(Object.assign({}, apiSpec), { servers: serversWithEmulators(apiSpec.servers), paths });
- function serversWithEmulators(servers) {
- const result = [];
- for (const server of servers) {
- result.push({
- url: server.url ? server.url.replace("https://", "{EMULATOR}/") : "{EMULATOR}",
- variables: {
- EMULATOR: {
- default: host ? `${protocol}://${host}` : "",
- description: "The protocol, hostname, and port of Firebase Auth Emulator.",
- },
- },
- });
- if (server.url) {
- result.push(server);
- }
- }
- return result;
- }
- }
- async function createApp(defaultProjectId, singleProjectMode = index_1.SingleProjectMode.NO_WARNING, projectStateForId = new Map()) {
- const app = express();
- app.set("json spaces", 2);
- app.use("/", (req, res, next) => {
- if (req.headers["access-control-request-private-network"]) {
- res.setHeader("access-control-allow-private-network", "true");
- }
- next();
- });
- app.use(cors({ origin: true }));
- app.delete("*", (req, _, next) => {
- delete req.headers["content-type"];
- next();
- });
- app.get("/", (req, res) => {
- return res.json({
- authEmulator: {
- ready: true,
- docs: "https://firebase.google.com/docs/emulator-suite",
- apiSpec: API_SPEC_PATH,
- },
- });
- });
- app.get(API_SPEC_PATH, (req, res) => {
- res.json(specWithEmulatorServer(req.protocol, req.headers.host));
- });
- registerLegacyRoutes(app);
- (0, handlers_1.registerHandlers)(app, (apiKey, tenantId) => getProjectStateById(getProjectIdByApiKey(apiKey), tenantId));
- const apiKeyAuthenticator = (ctx, info) => {
- if (!info.name) {
- throw new Error("apiKey param/header name is undefined in API spec.");
- }
- let key;
- const req = ctx.req;
- switch (info.in) {
- case "header":
- key = req.get(info.name);
- break;
- case "query": {
- const q = req.query[info.name];
- key = typeof q === "string" ? q : undefined;
- break;
- }
- default:
- throw new Error('apiKey must be defined as in: "query" or "header" in API spec.');
- }
- if (key) {
- return { type: "success", user: getProjectIdByApiKey(key) };
- }
- else {
- return undefined;
- }
- };
- const oauth2Authenticator = (ctx) => {
- const authorization = ctx.req.headers["authorization"];
- if (!authorization || !authorization.toLowerCase().startsWith(AUTH_HEADER_PREFIX)) {
- return undefined;
- }
- const scopes = Object.keys(ctx.api.openApiDoc.components.securitySchemes.Oauth2.flows.authorizationCode.scopes);
- const token = authorization.substr(AUTH_HEADER_PREFIX.length);
- if (token.toLowerCase() === "owner") {
- return { type: "success", user: defaultProjectId, scopes };
- }
- else if (token.startsWith(SERVICE_ACCOUNT_TOKEN_PREFIX)) {
- emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("WARN", `Received service account token ${token}. Assuming that it owns project "${defaultProjectId}".`);
- return { type: "success", user: defaultProjectId, scopes };
- }
- throw new errors_2.UnauthenticatedError("Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", [
- {
- message: "Invalid Credentials",
- domain: "global",
- reason: "authError",
- location: "Authorization",
- locationType: "header",
- },
- ]);
- };
- const apis = await exegesisExpress.middleware(specForRouter(), {
- controllers: { auth: toExegesisController(operations_1.authOperations, getProjectStateById) },
- authenticators: {
- apiKeyQuery: apiKeyAuthenticator,
- apiKeyHeader: apiKeyAuthenticator,
- Oauth2: oauth2Authenticator,
- },
- autoHandleHttpErrors(err) {
- if (err.type === "entity.parse.failed") {
- const message = `Invalid JSON payload received. ${err.message}`;
- err = new errors_2.InvalidArgumentError(message, [
- {
- message,
- domain: "global",
- reason: "parseError",
- },
- ]);
- }
- if (err instanceof errors_1.ValidationError) {
- const firstError = err.errors[0];
- let details;
- if (firstError.location) {
- details = `${firstError.location.path} ${firstError.message}`;
- }
- else {
- details = firstError.message;
- }
- err = new errors_2.InvalidArgumentError(`Invalid JSON payload received. ${details}`);
- }
- if (err.name === "HttpBadRequestError") {
- err = new errors_2.BadRequestError(err.message, "unknown");
- }
- throw err;
- },
- defaultMaxBodySize: 1024 * 1024 * 1024,
- validateDefaultResponses: true,
- onResponseValidationError({ errors }) {
- (0, utils_1.logError)(new Error(`An internal error occured when generating response. Details:\n${JSON.stringify(errors)}`));
- throw new errors_2.InternalError("An internal error occured when generating response.", "emulator-response-validation");
- },
- customFormats: {
- "google-datetime"() {
- return true;
- },
- "google-fieldmask"() {
- return true;
- },
- "google-duration"() {
- return true;
- },
- uint64() {
- return true;
- },
- uint32() {
- return true;
- },
- byte() {
- return true;
- },
- },
- plugins: [
- {
- info: { name: "test" },
- makeExegesisPlugin() {
- return {
- postSecurity(pluginContext) {
- wrapValidateBody(pluginContext);
- return Promise.resolve();
- },
- postController(ctx) {
- if (ctx.res.statusCode === 401) {
- const requirements = ctx.api.operationObject.security;
- if (requirements === null || requirements === void 0 ? void 0 : requirements.some((req) => req.apiKeyQuery || req.apiKeyHeader)) {
- throw new errors_2.PermissionDeniedError("The request is missing a valid API key.");
- }
- else {
- throw new errors_2.UnauthenticatedError("Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", [
- {
- message: "Login Required.",
- domain: "global",
- reason: "required",
- location: "Authorization",
- locationType: "header",
- },
- ]);
- }
- }
- },
- };
- },
- },
- ],
- });
- app.use(apis);
- app.use(() => {
- throw new errors_2.NotFoundError();
- });
- app.use(((err, req, res, next) => {
- let apiError;
- if (err instanceof errors_2.ApiError) {
- apiError = err;
- }
- else if (!err.status || err.status === 500) {
- apiError = new errors_2.UnknownError(err.message || "Unknown error", err.name || "unknown");
- }
- else {
- return res.status(err.status).json(err);
- }
- if (apiError.code === 500) {
- (0, utils_1.logError)(err);
- }
- return res.status(apiError.code).json({ error: apiError });
- }));
- return app;
- function getProjectIdByApiKey(apiKey) {
- apiKey;
- return defaultProjectId;
- }
- function getProjectStateById(projectId, tenantId) {
- let agentState = projectStateForId.get(projectId);
- if (singleProjectMode !== index_1.SingleProjectMode.NO_WARNING &&
- projectId &&
- defaultProjectId !== projectId) {
- const errorString = `Multiple projectIds are not recommended in single project mode. ` +
- `Requested project ID ${projectId}, but the emulator is configured for ` +
- `${defaultProjectId}. To opt-out of single project mode add/set the ` +
- `\'"singleProjectMode"\' false' property in the firebase.json emulators config.`;
- emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("WARN", errorString);
- if (singleProjectMode === index_1.SingleProjectMode.ERROR) {
- throw new errors_2.BadRequestError(errorString);
- }
- }
- if (!agentState) {
- agentState = new state_1.AgentProjectState(projectId);
- projectStateForId.set(projectId, agentState);
- }
- if (!tenantId) {
- return agentState;
- }
- return agentState.getTenantProject(tenantId);
- }
- }
- exports.createApp = createApp;
- function registerLegacyRoutes(app) {
- const relyingPartyPrefix = "/www.googleapis.com/identitytoolkit/v3/relyingparty/";
- const v1Prefix = "/identitytoolkit.googleapis.com/v1/";
- for (const [oldPath, newPath] of [
- ["createAuthUri", "accounts:createAuthUri"],
- ["deleteAccount", "accounts:delete"],
- ["emailLinkSignin", "accounts:signInWithEmailLink"],
- ["getAccountInfo", "accounts:lookup"],
- ["getOobConfirmationCode", "accounts:sendOobCode"],
- ["getProjectConfig", "projects"],
- ["getRecaptchaParam", "recaptchaParams"],
- ["publicKeys", "publicKeys"],
- ["resetPassword", "accounts:resetPassword"],
- ["sendVerificationCode", "accounts:sendVerificationCode"],
- ["setAccountInfo", "accounts:update"],
- ["setProjectConfig", "setProjectConfig"],
- ["signupNewUser", "accounts:signUp"],
- ["verifyAssertion", "accounts:signInWithIdp"],
- ["verifyCustomToken", "accounts:signInWithCustomToken"],
- ["verifyPassword", "accounts:signInWithPassword"],
- ["verifyPhoneNumber", "accounts:signInWithPhoneNumber"],
- ]) {
- app.all(`${relyingPartyPrefix}${oldPath}`, (req, _, next) => {
- req.url = `${v1Prefix}${newPath}`;
- next();
- });
- }
- app.post(`${relyingPartyPrefix}signOutUser`, () => {
- throw new errors_2.NotImplementedError(`signOutUser is not implemented in the Auth Emulator.`);
- });
- for (const [oldPath, newMethod, newPath] of [
- ["downloadAccount", "GET", "accounts:batchGet"],
- ["uploadAccount", "POST", "accounts:batchCreate"],
- ]) {
- app.post(`${relyingPartyPrefix}${oldPath}`, bodyParser.json(), (req, res, next) => {
- req.body = convertKeysToCamelCase(req.body || {});
- const targetProjectId = req.body.targetProjectId;
- if (!targetProjectId) {
- return next(new errors_2.BadRequestError("INSUFFICIENT_PERMISSION"));
- }
- delete req.body.targetProjectId;
- req.method = newMethod;
- let qs = req.url.split("?", 2)[1] || "";
- if (newMethod === "GET") {
- Object.assign(req.query, req.body);
- const bodyAsQuery = new url_1.URLSearchParams(req.body).toString();
- qs = qs ? `${qs}&${bodyAsQuery}` : bodyAsQuery;
- delete req.body;
- delete req.headers["content-type"];
- }
- req.url = `${v1Prefix}projects/${encodeURIComponent(targetProjectId)}/${newPath}?${qs}`;
- next();
- });
- }
- }
- function toExegesisController(ops, getProjectStateById) {
- const result = {};
- processNested(ops, "");
- return new Proxy(result, {
- get: (obj, prop) => {
- if (typeof prop !== "string" || prop in obj) {
- return obj[prop];
- }
- const stub = () => {
- throw new errors_2.NotImplementedError(`${prop} is not implemented in the Auth Emulator.`);
- };
- return stub;
- },
- });
- function processNested(nested, prefix) {
- Object.entries(nested).forEach(([key, value]) => {
- if (typeof value === "function") {
- result[`${prefix}${key}`] = toExegesisOperation(value);
- }
- else {
- processNested(value, `${prefix}${key}.`);
- if (typeof value._ === "function") {
- result[`${prefix}${key}`] = toExegesisOperation(value._);
- }
- }
- });
- }
- function toExegesisOperation(operation) {
- return (ctx) => {
- var _a, _b, _c, _d, _e, _f, _g, _h;
- let targetProjectId = ctx.params.path.targetProjectId || ((_a = ctx.requestBody) === null || _a === void 0 ? void 0 : _a.targetProjectId);
- if (targetProjectId) {
- if ((_b = ctx.api.operationObject.security) === null || _b === void 0 ? void 0 : _b.some((sec) => sec.Oauth2)) {
- (0, errors_2.assert)((_c = ctx.security) === null || _c === void 0 ? void 0 : _c.Oauth2, "INSUFFICIENT_PERMISSION : Only authenticated requests can specify target_project_id.");
- }
- }
- else {
- targetProjectId = ctx.user;
- }
- let targetTenantId = undefined;
- if (ctx.params.path.tenantId && ((_d = ctx.requestBody) === null || _d === void 0 ? void 0 : _d.tenantId)) {
- (0, errors_2.assert)(ctx.params.path.tenantId === ctx.requestBody.tenantId, "TENANT_ID_MISMATCH");
- }
- targetTenantId = ctx.params.path.tenantId || ((_e = ctx.requestBody) === null || _e === void 0 ? void 0 : _e.tenantId);
- if ((_f = ctx.requestBody) === null || _f === void 0 ? void 0 : _f.idToken) {
- const idToken = (_g = ctx.requestBody) === null || _g === void 0 ? void 0 : _g.idToken;
- const decoded = (0, jsonwebtoken_1.decode)(idToken, { complete: true });
- if ((decoded === null || decoded === void 0 ? void 0 : decoded.payload.firebase.tenant) && targetTenantId) {
- (0, errors_2.assert)((decoded === null || decoded === void 0 ? void 0 : decoded.payload.firebase.tenant) === targetTenantId, "TENANT_ID_MISMATCH");
- }
- targetTenantId = targetTenantId || (decoded === null || decoded === void 0 ? void 0 : decoded.payload.firebase.tenant);
- }
- if ((_h = ctx.requestBody) === null || _h === void 0 ? void 0 : _h.refreshToken) {
- const refreshTokenRecord = (0, state_1.decodeRefreshToken)(ctx.requestBody.refreshToken);
- if (refreshTokenRecord.tenantId && targetTenantId) {
- (0, errors_2.assert)(refreshTokenRecord.tenantId === targetTenantId, "TENANT_ID_MISMATCH: ((Refresh token tenant ID does not match target tenant ID.))");
- }
- targetTenantId = targetTenantId || refreshTokenRecord.tenantId;
- }
- return operation(getProjectStateById(targetProjectId, targetTenantId), ctx.requestBody, ctx);
- };
- }
- }
- function wrapValidateBody(pluginContext) {
- const op = pluginContext._operation;
- if (op.validateBody && !op._authEmulatorValidateBodyWrapped) {
- const validateBody = op.validateBody.bind(op);
- op.validateBody = (body) => {
- return validateAndFixRestMappingRequestBody(validateBody, body);
- };
- op._authEmulatorValidateBodyWrapped = true;
- }
- }
- function validateAndFixRestMappingRequestBody(validate, body) {
- var _a;
- body = convertKeysToCamelCase(body);
- let result;
- let keepFixing = false;
- const fixedPaths = new Set();
- do {
- result = validate(body);
- if (!result.errors)
- return result;
- keepFixing = false;
- for (const error of result.errors) {
- const path = (_a = error.location) === null || _a === void 0 ? void 0 : _a.path;
- const ajvError = error.ajvError;
- if (!path || fixedPaths.has(path) || !ajvError) {
- continue;
- }
- const dataPath = jsonPointerToPath(path);
- const value = _.get(body, dataPath);
- if (ajvError.keyword === "type" && ajvError.params.type === "string") {
- if (typeof value === "number") {
- _.set(body, dataPath, value.toString());
- keepFixing = true;
- }
- }
- else if (ajvError.keyword === "enum") {
- const params = ajvError.params;
- const enumValue = params.allowedValues[value];
- if (enumValue) {
- _.set(body, dataPath, enumValue);
- keepFixing = true;
- }
- }
- }
- } while (keepFixing);
- return result;
- }
- function convertKeysToCamelCase(body) {
- if (body == null || typeof body !== "object")
- return body;
- if (Array.isArray(body)) {
- return body.map(convertKeysToCamelCase);
- }
- const result = Object.create(null);
- for (const key of Object.keys(body)) {
- result[(0, lodash_1.camelCase)(key)] = convertKeysToCamelCase(body[key]);
- }
- return result;
- }
- function jsonPointerToPath(pointer) {
- const path = pointer.split("/").map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
- if (path[0] === "#" || path[0] === "") {
- path.shift();
- }
- return path;
- }
|