From c066a7d46b476f3b7552638d67807fc6d40100b1 Mon Sep 17 00:00:00 2001 From: Nopm Date: Tue, 3 Jun 2025 21:44:43 -0300 Subject: [PATCH] password based service info auth (better than the first one we had) --- .env.example | 9 +++++++++ src/config.ts | 26 ++++++++++++++++++++++++++ src/info-page.ts | 43 +++++++++++++++++++++++++++++-------------- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index cc0fbd5..2d64415 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,15 @@ NODE_ENV=production # Defaults to true. Set to false to disable login and make the info page public. # ENABLE_INFO_PAGE_LOGIN=true +# Authentication mode for the service info page. (token | password) +# If 'token', any valid user token (via `getUser()`) is used (requires GATEKEEPER='user_token' mode). +# If 'password', SERVICE_INFO_PASSWORD is used. +# Defaults to 'token' if ENABLE_INFO_PAGE_LOGIN is true. +# SERVICE_INFO_AUTH_MODE=token + +# Password for the service info page if SERVICE_INFO_AUTH_MODE is 'password'. +# SERVICE_INFO_PASSWORD=your-service-info-password + # The route name used to proxy requests to APIs, relative to the Web site root. # PROXY_ENDPOINT_ROUTE=/proxy diff --git a/src/config.ts b/src/config.ts index 24253a3..d688f43 100644 --- a/src/config.ts +++ b/src/config.ts @@ -435,6 +435,10 @@ type Config = { loginImageUrl?: string; /** Whether to enable the token-based login page for the service info page. Defaults to true. */ enableInfoPageLogin?: boolean; + /** Authentication mode for the service info page. (token | password) */ + serviceInfoAuthMode: "token" | "password"; + /** Password for the service info page if serviceInfoAuthMode is 'password'. */ + serviceInfoPassword?: string; }; // To change configs, create a file called .env in the root directory. @@ -554,6 +558,8 @@ export const config: Config = { }, loginImageUrl: getEnvWithDefault("LOGIN_IMAGE_URL", ""), enableInfoPageLogin: getEnvWithDefault("ENABLE_INFO_PAGE_LOGIN", true), + serviceInfoAuthMode: getEnvWithDefault("SERVICE_INFO_AUTH_MODE", "token") as Config["serviceInfoAuthMode"], + serviceInfoPassword: getEnvWithDefault("SERVICE_INFO_PASSWORD", undefined), } as const; function generateSigningKey() { @@ -691,6 +697,25 @@ export async function assertConfigIsValid() { } } + if (config.enableInfoPageLogin) { + if (!["token", "password"].includes(config.serviceInfoAuthMode)) { + throw new Error( + `Invalid SERVICE_INFO_AUTH_MODE: ${config.serviceInfoAuthMode}. Must be 'token' or 'password'.` + ); + } + if (config.serviceInfoAuthMode === "password" && !config.serviceInfoPassword) { + throw new Error( + "SERVICE_INFO_AUTH_MODE is 'password' but SERVICE_INFO_PASSWORD is not set." + ); + } + // If service info login is token-based, gatekeeper must be 'user_token' mode for getUser() to be effective. + if (config.serviceInfoAuthMode === "token" && config.gatekeeper !== "user_token") { + throw new Error( + "SERVICE_INFO_AUTH_MODE is 'token' for info page login, but GATEKEEPER is not 'user_token'. User token authentication will not work." + ); + } + } + // Ensure forks which add new secret-like config keys don't unwittingly expose // them to users. for (const key of getKeys(config)) { @@ -765,6 +790,7 @@ export const OMITTED_KEYS = [ "powTokenPurgeHours", "loginImageUrl", "enableInfoPageLogin", + "serviceInfoPassword", ] satisfies (keyof Config)[]; type OmitKeys = (typeof OMITTED_KEYS)[number]; diff --git a/src/info-page.ts b/src/info-page.ts index b3dee0a..ffa2edf 100644 --- a/src/info-page.ts +++ b/src/info-page.ts @@ -110,7 +110,7 @@ function renderLoginPage(csrf: string, error?: string) { padding:30px;width:100%;max-width:400px;text-align:center;} .logo-image{max-width:200px;margin-bottom:20px;} .form-group{margin-bottom:20px;} - input[type=text]{width:100%;padding:10px;border:1px solid #ddd;border-radius:4px; + input[type=text], input[type=password]{width:100%;padding:10px;border:1px solid #ddd;border-radius:4px; box-sizing:border-box;font-size:16px;} button{background:#4caf50;color:#fff;border:none;padding:12px 20px;border-radius:4px; cursor:pointer;font-size:16px;width:100%;} @@ -120,8 +120,8 @@ function renderLoginPage(csrf: string, error?: string) { @media (prefers-color-scheme: dark) { body { background: #2c2c2c; color: #e0e0e0; } .login-container { background: #383838; box-shadow: 0 4px 12px rgba(0,0,0,0.4); border: 1px solid #4a4a4a; } - input[type=text] { background: #4a4a4a; color: #e0e0e0; border: 1px solid #5a5a5a; } - input[type=text]::placeholder { color: #999; } + input[type=text], input[type=password] { background: #4a4a4a; color: #e0e0e0; border: 1px solid #5a5a5a; } + input[type=text]::placeholder, input[type=password]::placeholder { color: #999; } button { background: #007bff; } /* Using a blue for dark mode button */ button:hover { background: #0056b3; } .error-message { color: #ff8a80; } /* Lighter red for errors in dark mode */ @@ -134,7 +134,9 @@ function renderLoginPage(csrf: string, error?: string) { ${errBlock}
- + ${config.serviceInfoAuthMode === "password" + ? `` + : ``}
@@ -343,17 +345,30 @@ infoPageRouter.use( /* login attempt */ infoPageRouter.post(LOGIN_ROUTE, (req, res) => { - const token = (req.body.token || "").trim(); - - const user = getUser(token); // returns undefined if invalid - if (!user) { - return res - .status(401) - .send(renderLoginPage(res.locals.csrfToken, "Invalid token. Please try again.")); + if (config.serviceInfoAuthMode === "password") { + const password = (req.body.password || "").trim(); + // Simple string comparison; for production, consider a timing-safe comparison library + if (config.serviceInfoPassword && password === config.serviceInfoPassword) { + req.session!.infoPageAuthed = true; + return res.redirect("/"); + } else { + return res + .status(401) + .send(renderLoginPage(res.locals.csrfToken, "Invalid password. Please try again.")); + } + } else { + // Token-based authentication (using any valid user token) + const token = (req.body.token || "").trim(); + const user = getUser(token); // returns undefined if invalid + if (user) { + req.session!.infoPageAuthed = true; + return res.redirect("/"); + } else { + return res + .status(401) + .send(renderLoginPage(res.locals.csrfToken, "Invalid token. Please try again.")); + } } - - req.session!.infoPageAuthed = true; - res.redirect("/"); }); /* GET / – either login form or info page */