How Basic Authentication Works with PI_WEB_PASSWORD and Allowed Hosts in Pi-Web
Pi-web activates HTTP Basic Authentication when the PIWEBPASSWORD environment variable is set, optionally restricting access to specific hosts via PIWEBALLOWED_HOSTS.
The agegr/pi-web repository implements a lightweight, environment-driven authentication layer that protects the Next.js UI and API without modifying application code. This guide explains the complete flow from environment configuration to request validation, referencing the actual source implementation.
How Authentication Is Triggered
Authentication is gated entirely by environment variables. The server startup script checks for PIWEBPASSWORD and, if present, injects a middleware that intercepts all incoming requests.
| Variable | Required | Purpose |
|---|---|---|
PIWEBPASSWORD |
Yes (to enable auth) | The clear-text password clients must provide via Authorization: Basic … header |
PIWEBALLOWED_HOSTS |
No | Comma-separated list of permitted hostnames/IPs; requests from other hosts receive 401 Unauthorized even with valid credentials |
If PIWEBPASSWORD is unset, the middleware is skipped entirely and the UI runs without authentication.
Where Authentication Lives: bin/pi-web.js
The core logic resides in bin/pi-web.js, the server bootstrap file. This script:
- Creates an Express-compatible middleware stack
- Conditionally registers the authentication middleware based on
process.env.PIWEBPASSWORD - Falls through to the Next.js request handler after successful validation
// bin/pi-web.js — simplified structure showing middleware registration
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true);
// Authentication middleware injected here when PIWEBPASSWORD is set
if (process.env.PIWEBPASSWORD) {
const allowedHosts = (process.env.PIWEBALLOWED_HOSTS || '')
.split(',')
.map(h => h.trim())
.filter(Boolean);
const host = req.headers.host?.split(':')[0] ?? '';
if (allowedHosts.length && !allowedHosts.includes(host)) {
res.statusCode = 401;
res.end('Host not allowed');
return;
}
const auth = req.headers.authorization ?? '';
const match = auth.match(/^Basic\s+(.+)$/i);
if (!match) {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Pi-Web"');
res.end('Authentication required');
return;
}
const decoded = Buffer.from(match[1], 'base64').toString('utf8');
const [, password] = decoded.split(':'); // username ignored
if (password !== process.env.PIWEBPASSWORD) {
res.statusCode = 401;
res.setHeader('WWW-Authenticate', 'Basic realm="Pi-Web"');
res.end('Invalid credentials');
return;
}
}
handle(req, res, parsedUrl);
}).listen(30141, (err) => {
if (err) throw err;
console.log('> Ready on http://localhost:30141');
});
});
Step-by-Step Request Flow
Understanding how a request moves through the authentication layer helps debug access issues.
Step 1: Host Validation
When PIWEBALLOWED_HOSTS is defined, the middleware extracts the Host header, strips any port suffix, and validates against the allowlist.
// Host normalization: "localhost:30141" → "localhost"
const host = req.headers.host?.split(':')[0] ?? '';
Mismatch behavior: Immediate 401 response with body Host not allowed. No password prompt is issued.
Step 2: Basic Auth Header Parsing
Valid hosts proceed to password verification. The middleware expects:
Authorization: Basic <base64>
Where <base64> encodes username:password. The username is discarded — only the password segment matters.
Step 3: Password Comparison
The decoded password is compared to PIWEBPASSWORD using strict equality (case-sensitive). There is no hashing; the password is stored and compared in plaintext.
Success: Request continues to handle(req, res, parsedUrl), entering Next.js routing.
Failure: 401 response with WWW-Authenticate: Basic realm="Pi-Web" header, triggering browser credential dialogs.
Practical Configuration Examples
Enable authentication with a single environment variable:
# Unix/Linux
export PIWEBPASSWORD=superSecret123
npm start
Restrict to specific hosts:
export PIWEBPASSWORD=superSecret123
export PIWEBALLOWED_HOSTS=localhost,192.168.1.100,pi-web.mydomain.com
npm start
Docker deployment:
# Dockerfile excerpt
ENV PIWEBPASSWORD=${PIWEB_PASSWORD}
ENV PIWEBALLOWED_HOSTS=${PIWEB_ALLOWED_HOSTS}
CMD ["node", "bin/pi-web.js"]
Making Authenticated Requests
Browser access triggers the native credential dialog. Programmatic access requires proper header construction.
Using curl
Empty username (password-only):
curl -u :superSecret123 http://localhost:30141/api/sessions
With explicit header:
curl -H "Authorization: Basic $(echo -n ':superSecret123' | base64)" \
http://localhost:30141/api/sessions
Using JavaScript (fetch)
const password = 'superSecret123';
const credentials = btoa(`:${password}`);
const response = await fetch('http://localhost:30141/api/sessions', {
headers: {
'Authorization': `Basic ${credentials}`
}
});
Testing Host Restrictions
From an unauthorized host:
# Machine at 10.0.0.5 attempts access
curl http://10.0.0.5:30141/api/sessions
# → HTTP/1.1 401 Unauthorized
# → Body: "Host not allowed"
From an authorized host with wrong password:
curl -u :wrongPassword http://localhost:30141/api/sessions
# → HTTP/1.1 401 Unauthorized
# → WWW-Authenticate: Basic realm="Pi-Web"
Integration with Next.js Architecture
The authentication layer sits outside the Next.js application code. This design provides several benefits:
- Zero application changes: Routes in
app/api/...and pages inapp/...require no auth logic - Universal protection: Static assets, API endpoints, and server-side rendered pages are all covered
- Environment portability: Same codebase runs with or without authentication based on deployment context
Related authentication files (for alternative methods):
| File | Purpose |
|---|---|
app/api/auth/login/[provider]/route.ts |
OAuth-based login flows |
app/api/auth/api-key/[provider]/route.ts |
API key generation and validation |
next.config.ts |
Server-side environment variable exposure |
Basic auth operates independently of these OAuth/API-key systems. They can coexist: basic auth protects the UI entry point, while API keys authenticate programmatic API access.
Security Considerations
Based on the agegr/pi-web implementation:
- Plaintext storage:
PIWEBPASSWORDis compared directly without hashing. Protect the environment variable accordingly. - No rate limiting: Failed attempts are not throttled; consider network-level protection for exposed deployments.
- HTTP by default: The development server runs unencrypted. Use a reverse proxy (nginx, traefik) with TLS for production.
- Host header trust:
PIWEBALLOWED_HOSTSrelies on theHostheader, which can be manipulated by clients. Use it for convenience, not as a security boundary.
Summary
- Enable auth: Set
PIWEBPASSWORDenvironment variable - Restrict hosts: Optionally add
PIWEBALLOWED_HOSTScomma-separated list - Entry point:
bin/pi-web.jsregisters the middleware before Next.js handling - Header format:
Authorization: Basic <base64(:password)> - Username ignored: Only password segment is validated
- Fails closed: Missing/invalid credentials yield
401withWWW-Authenticateheader
Frequently Asked Questions
How do I disable authentication entirely?
Unset PIWEBPASSWORD or remove it from your environment. The middleware is conditionally registered only when this variable exists.
Why is my password not working with special characters?
Ensure proper URL-encoding or shell-escaping when setting PIWEBPASSWORD. The comparison is literal — special characters must match exactly, including case.
Can I use PIWEBALLOWED_HOSTS without setting a password?
No. The host validation logic exists inside the password-guarded middleware block. Without PIWEBPASSWORD, the entire authentication layer is bypassed.
Does basic auth protect WebSocket connections?
The middleware in bin/pi-web.js wraps the HTTP request handler. WebSocket upgrade requests first pass through this handler, so yes — unauthenticated WebSocket upgrade attempts are rejected with 401.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →