Do the simplest thing that works. From my perspective, this would be a HTTP Basic-Auth challenge, which the server compares with expected credentials. The expected credentials could e.g. be provided via an environment variable.
HTTP has a built-in authentication mechanism with the Authorization
header. If you use a proxy server, it's probably easiest to configure there (e.g. via .htaccess
files in Apache). But it's also easy to implement via a middleware function that performs the following:
- if the client has sent a header
Authorization: Basic CREDENTIALS
:
- decode and check the credentials
- if the credentials match, continue processing the request
- otherwise, respond immediately with
401 Unauthorized
, including a WWW-Authenticate: Basic realm="Access to the web app"
header.
From the client perspective, this leads to the following flow:
- the first time the web app is visited, the site responds with an error, and the browser shows a popup asking for username+password. The browser dialog may or may not show the realm string to the user.
- upon entering the correct username+password, the page is re-loaded
- the browser will remember these credentials for all subsequent requests on that origin
- the Authorization header will also be sent for subresource and fetch() requests on that origin
Since the user interface for this is provided by the browser, it can be super easy to develop.
Disadvantages:
- only username+password supported
- can't log out directly (but can forget credentials by deleting “site data” for that origin, or whatever your browser calls it)
Providing the expected credentials to the app through environment variables is likely to be perfectly fine. This allows you to easily rotate credentials, if necessary. For software that is supposed to run on localhost, it's also feasible to generate a secure password during app startup and to print the password to be used to the log. You can easily generate cryptographically secure passwords on Linux via a shell command like head -c 32 /dev/urandom | base64
, corresponding to the NodeJS JavaScript code:
import { open } from "fs/promises";
const f = await open("/dev/urandom");
const { buffer } = await f.read({ buffer: Buffer.alloc(32) });
const password = buffer.toString('base64');
console.log(`your password is ${password}`);
Or equivalently in a cross-platform manner:
import { randomBytes } from "crypto";
const password = randomBytes(32).toString('base64');
console.log(`your password is ${password}`);