UNPKG

express-tailscale-auth

Version:

Express middleware for Tailscale authentication

1 lines 12.3 kB
{"version":3,"sources":["../src/tailscale/schema.ts","../src/utils/checkTrustProxyConfig.ts","../src/index.ts"],"names":["tailscaleCapabilityMethods","z","tailscaleCapabilitySchema","checkTrustProxyConfiguration","req","trustProxy","trustProxyWarningShown","createTailscaleAuthMw","options","localApi","TailscaleLocalApi","debug","getIp","all","trust","compile","finalAddress","proxyAddr","res","next","rawClientIp","gatedClientIp","parsedCapabilities","whois","capabilities","parseResult","e","route","parsed","method","cap","pm"],"mappings":"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","file":"index.cjs","sourcesContent":["import { z } from \"zod\"\n\nexport const tailscaleCapabilityMethods = z.enum([\"GET\", \"POST\", \"PUT\", \"DELETE\", \"*\"])\n\nexport type TailscaleCapabilityMethods = z.infer<typeof tailscaleCapabilityMethods>\nexport const tailscaleCapabilitySchema = z.object({\n routes: z\n .array(\n z.object({\n route: z.string(),\n methods: z.array(tailscaleCapabilityMethods),\n })\n )\n .optional(),\n})\n\nexport type TailscaleCapabilitySchema = z.infer<typeof tailscaleCapabilitySchema>\n","import type { Request } from \"express\"\n\nexport const checkTrustProxyConfiguration = (req: Request) => {\n const trustProxy = req.app.get('trust proxy')\n const hasProxyHeaders = !!(\n req.headers['x-forwarded-for'] || \n req.headers['x-forwarded-host'] || \n req.headers['x-forwarded-proto']\n )\n\n if (hasProxyHeaders && (trustProxy === false || trustProxy === undefined)) {\n console.warn(`\n⚠️ Tailscale Auth Middleware Warning ⚠️\n\nDetected X-Forwarded-* headers but Express 'trust proxy' is not configured.\nThis may result in incorrect IP address detection and authentication failures.\n\nTo fix this, add one of the following to your Express app:\n\n // For most reverse proxy setups (recommended)\n app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal'])\n \n\nSee: https://expressjs.com/en/guide/behind-proxies.html\n`)\nreturn true\n }\n return false\n }","import { NextFunction, Request, Response } from \"express\"\nimport pm from \"picomatch\"\nimport proxyAddr, { all, compile } from \"proxy-addr\"\nimport { TailscaleLocalApi, WhoIsResponse as TailscaleWhoisResponse } from \"tailscale-local-api\"\nimport { tailscaleCapabilityMethods, TailscaleCapabilitySchema, tailscaleCapabilitySchema } from \"./tailscale/schema.js\"\nimport { checkTrustProxyConfiguration } from \"./utils/checkTrustProxyConfig.js\"\n\n\ndeclare module \"express-serve-static-core\" {\n interface Request {\n tailscaleUser?: TailscaleWhoisResponse[\"userProfile\"] & { capabilities: TailscaleCapabilitySchema }\n }\n}\nexport type RequestWithTailscaleUser = Request & {\n tailscaleUser?: TailscaleWhoisResponse[\"userProfile\"] & { capabilities: TailscaleCapabilitySchema }\n}\n\nlet trustProxyWarningShown = false\n\n\nexport interface TailscaleAuthMiddlewareOptions {\n /**\n * The path to the tailscaled unix socket. \n * By default will look for common location on specific platforms.\n * \n * Unless you're running tailscale in a special way, you probably don't need to set this.\n * @example '/var/run/tailscale/tailscaled.sock'\n */\n socketPath?: string\n \n /**\n * If true, will only use the unix socket to connect to tailscaled.\n * If false, will use the localhost TCP port to connect to tailscaled.\n * \n * Unless you're running tailscale in a special way, you probably don't need to set this.\n * \n * @default false\n */\n useSocketOnly?: boolean\n \n /**\n * Whether to enable debug mode.\n * If enabled, the middleware will log debug information to the console.\n * \n * @default false\n */\n debug?: boolean\n \n /**\n * The capabilities namespace to use for the Tailscale Grants.\n * If not set, the middleware will assume the user has access to every method on every route.\n * \n * @example\n * Set capabilitiesNamespace as \"test.com/cap/express\" for grants:\n * ```json\n * \"grants\": [{\n * \"src\": [\"user@example.com\"],\n * \"dst\": [\"*\"],\n * \"app\": {\n * \"test.com/cap/express\": [\n * {\n * \"routes\": [\n * {\"route\": \"/api\", \"methods\": [\"*\"]},\n * {\"route\": \"/api/**\", \"methods\": [\"GET\", \"POST\"]}\n * ]\n * }\n * ]\n * }\n * }]\n * ```\n */\n capabilitiesNamespace?: string\n}\n\nexport const createTailscaleAuthMw = (\n /**\n * Configuration options for the Tailscale authentication middleware.\n * \n * @example\n * ```typescript\n * const authMiddleware = createTailscaleAuthMw({\n * socketPath: '/var/run/tailscale/tailscaled.sock',\n * useSocketOnly: true,\n * debug: true,\n * capabilitiesNamespace: 'test.com/cap/express'\n * })\n * ```\n * \n * @example\n * Set capabilitiesNamespace as \"test.com/cap/express\" for grants:\n * \n * ```json\n * \"grants\": [{\n * \"src\": [\"user@example.com\"],\n * \"dst\": [\"*\"],\n * \"app\": {\n * \"test.com/cap/express\": [\n * {\n * \"routes\": [\n * {\"route\": \"/api\", \"methods\": [\"*\"]},\n * {\"route\": \"/api/**\", \"methods\": [\"GET\", \"POST\"]}\n * ]\n * }\n * ]\n * }\n * }]\n * ```\n */\n options: TailscaleAuthMiddlewareOptions = {}\n) => {\n \n const localApi = new TailscaleLocalApi(options)\n const debug = options.debug || false\n\n \n\n const getIp = (req: Request) => {\n if (debug) console.debug(\"all addrs of req\", all(req))\n\n if(!trustProxyWarningShown && checkTrustProxyConfiguration(req)){\n trustProxyWarningShown = true\n }\n\n const trustProxy = req.app.get('trust proxy')\n \n if (trustProxy === false || trustProxy === undefined) {\n if (debug) console.debug(\"Not trusting reverse proxy, returning socket address\", req.socket.remoteAddress)\n return req.socket.remoteAddress\n }\n \n if (typeof trustProxy === \"boolean\" && trustProxy === true) {\n return all(req).at(-1)\n }\n\n\n const trust = compile(trustProxy)\n const finalAddress = proxyAddr(req, trust)\n if (debug) console.debug(\"trusting remote address\", finalAddress)\n return finalAddress\n }\n\n return async (req: Request, res: Response, next: NextFunction) => {\n const rawClientIp = getIp(req)\n if (!rawClientIp) {\n if (debug) console.debug(\"No client IP found\", rawClientIp)\n res.status(404).send(`Cannot ${req.method} ${req.path}`)\n return\n }\n const gatedClientIp = localApi.isInTailscaleIpRange(rawClientIp)\n if (!gatedClientIp) {\n if (debug) console.debug(\"Request IP not from Tailscale range\", rawClientIp)\n res.status(404).send(`Cannot ${req.method} ${req.path}`)\n return\n }\n let parsedCapabilities\n try {\n const whois = await localApi.whoIs(gatedClientIp)\n\n // If a capabilities namespace is set, read the capabilities from the Tailscale ACL\n // If not, assume the user has access to every method on every route\n const capabilities = options.capabilitiesNamespace\n ? whois.capMap[options.capabilitiesNamespace]?.[0]\n : { routes: [{ route: \"**\", methods: [\"*\"] }] }\n\n const parseResult = tailscaleCapabilitySchema.safeParse(capabilities)\n parsedCapabilities = parseResult.data\n req.tailscaleUser = {...whois.userProfile, capabilities: parsedCapabilities ?? {}}\n\n if (!parseResult.success) {\n if (debug) console.debug(\"Couldn't find or parse capabilities\", parseResult.error)\n res.status(401).send(`Cannot ${req.method} ${req.path}`)\n return\n }\n } catch (e) {\n console.error(e)\n res.status(401).send(`Cannot ${req.method} ${req.path}`)\n return\n }\n\n const route = req.originalUrl\n const parsed = tailscaleCapabilityMethods.safeParse(req.method)\n if (!parsed.success) {\n if (debug) console.debug(\"Invalid method\", parsed.error)\n res.status(401).send(`Cannot ${req.method} ${req.path}`)\n return\n }\n \n const method = parsed.data\n const hasCapability = parsedCapabilities?.routes?.some((cap) => {\n const isMatch = pm(cap.route)\n const routeMatch = isMatch(route)\n if (!routeMatch) return false\n return cap.methods.includes(\"*\") || cap.methods.includes(method)\n })\n if (!hasCapability) {\n if (debug) console.debug(\"Requester has no capability for route\", req.path, req.method)\n res.status(401).send(`Cannot ${req.method} ${req.path}`)\n return\n }\n next()\n }\n}\n"]}