# How Basic Authentication Works with PI_WEB_PASSWORD and Allowed Hosts in Pi-Web

> Learn how Pi-web enables basic authentication with PIWEBPASSWORD and restricts access using PIWEBALLOWED_HOSTS. Secure your Pi-web instance effectively.

- Repository: [Alex Yang/pi-web](https://github.com/agegr/pi-web)
- Tags: how-to-guide
- Published: 2026-08-16

---

**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`](https://github.com/agegr/pi-web/blob/main/bin/pi-web.js)

The core logic resides in **[`bin/pi-web.js`](https://github.com/agegr/pi-web/blob/main/bin/pi-web.js)**, the server bootstrap file. This script:

1. Creates an Express-compatible middleware stack
2. Conditionally registers the authentication middleware based on `process.env.PIWEBPASSWORD`
3. Falls through to the Next.js request handler after successful validation

```javascript
// 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.

```javascript
// 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:

```bash

# Unix/Linux

export PIWEBPASSWORD=superSecret123
npm start

```

Restrict to specific hosts:

```bash
export PIWEBPASSWORD=superSecret123
export PIWEBALLOWED_HOSTS=localhost,192.168.1.100,pi-web.mydomain.com
npm start

```

Docker deployment:

```dockerfile

# 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):

```bash
curl -u :superSecret123 http://localhost:30141/api/sessions

```

With explicit header:

```bash
curl -H "Authorization: Basic $(echo -n ':superSecret123' | base64)" \
  http://localhost:30141/api/sessions

```

### Using JavaScript (fetch)

```javascript
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:

```bash

# 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:

```bash
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 in `app/...` 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`](https://github.com/agegr/pi-web/blob/main/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:** `PIWEBPASSWORD` is 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_HOSTS` relies on the `Host` header, which can be manipulated by clients. Use it for convenience, not as a security boundary.

---

## Summary

- **Enable auth:** Set `PIWEBPASSWORD` environment variable
- **Restrict hosts:** Optionally add `PIWEBALLOWED_HOSTS` comma-separated list
- **Entry point:** [`bin/pi-web.js`](https://github.com/agegr/pi-web/blob/main/bin/pi-web.js) registers 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 `401` with `WWW-Authenticate` header

---

## 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`](https://github.com/agegr/pi-web/blob/main/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`.