| 14 | * user or trusted-network deployments where you just want a simple password. |
| 15 | */ |
| 16 | export class TokenAuthAdapter implements AuthAdapter { |
| 17 | private readonly token: string | undefined; |
| 18 | private readonly adminUsers: ReadonlySet<string>; |
| 19 | |
| 20 | constructor() { |
| 21 | this.token = process.env.AUTH_TOKEN; |
| 22 | this.adminUsers = new Set( |
| 23 | (process.env.ADMIN_USERS ?? "") |
| 24 | .split(",") |
| 25 | .map((s) => s.trim()) |
| 26 | .filter(Boolean), |
| 27 | ); |
| 28 | } |
| 29 | |
| 30 | authenticate(req: IncomingMessage): AuthUser | null { |
| 31 | if (!this.token) { |
| 32 | return { id: "default", isAdmin: true }; |
| 33 | } |
| 34 | if (this.extractToken(req) === this.token) { |
| 35 | return { |
| 36 | id: "default", |
| 37 | isAdmin: this.adminUsers.size === 0 || this.adminUsers.has("default"), |
| 38 | }; |
| 39 | } |
| 40 | return null; |
| 41 | } |
| 42 | |
| 43 | setupRoutes(_app: Application): void { |
| 44 | // Token auth needs no login/callback/logout routes. |
| 45 | } |
| 46 | |
| 47 | requireAuth(req: Request, res: Response, next: NextFunction): void { |
| 48 | const user = this.authenticate(req as unknown as IncomingMessage); |
| 49 | if (!user) { |
| 50 | res.status(401).json({ error: "Unauthorized" }); |
| 51 | return; |
| 52 | } |
| 53 | (req as AuthenticatedRequest).user = user; |
| 54 | next(); |
| 55 | } |
| 56 | |
| 57 | // ── Internals ───────────────────────────────────────────────────────────── |
| 58 | |
| 59 | private extractToken(req: IncomingMessage): string | null { |
| 60 | // 1. ?token= query param (used by WebSocket clients and simple links) |
| 61 | const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`); |
| 62 | const qp = url.searchParams.get("token"); |
| 63 | if (qp) return qp; |
| 64 | |
| 65 | // 2. Authorization: Bearer <token> |
| 66 | const auth = req.headers["authorization"]; |
| 67 | if (auth?.startsWith("Bearer ")) return auth.slice(7); |
| 68 | |
| 69 | // 3. Cookie cc_token (set by token-auth login page if any) |
| 70 | const cookieHeader = req.headers.cookie ?? ""; |
| 71 | for (const part of cookieHeader.split(";")) { |
| 72 | const eq = part.indexOf("="); |
| 73 | if (eq === -1) continue; |
nothing calls this directly
no outgoing calls
no test coverage detected