Authentication

Authentication

By default Pensieve runs in open-access mode — no login is required. When you add an auth section to config.yaml, every API route and the WebSocket endpoint are protected by a password and JWT-based session.


How It Works

  1. You configure a bcrypt password hash and a secret key in config.yaml.
  2. A user visits the frontend. The frontend calls GET /api/auth/me to determine whether authentication is required.
  3. If required, the login page is shown. The user submits their password to POST /api/auth/login.
  4. The backend verifies the password against the stored bcrypt hash. On success, it sets a signed JWT in an HTTP-only pensieve_token cookie (7-day expiry).
  5. Every subsequent request to a protected route includes the cookie automatically. The require_auth FastAPI dependency validates the JWT on each request.
  6. To end the session, the frontend calls POST /api/auth/logout, which clears the cookie.

Configuration

Add an auth section to your config.yaml:

auth:
  secret: "your-secret-key-must-be-at-least-32-chars"
  password_hash: "$2b$12$..."   # generated via: python -m pensieve.auth hash-password

To disable authentication (open-access mode), omit the auth section entirely.

Fields

Field Required Description
secret Yes Secret key used to sign JWTs. Must be at least 32 characters long. Keep this value private — anyone with the secret can forge tokens.
password_hash Yes Bcrypt hash of the login password. Must start with $2 (bcrypt format). Generate with python -m pensieve.auth hash-password.

Security note: Store your secret in an environment variable and use interpolation to keep it out of the config file:

> auth:
>   secret: "${PENSIEVE_AUTH_SECRET}"
>   password_hash: "${PENSIEVE_PASSWORD_HASH}"
> ```

---

## Generating a Password Hash

Pensieve provides a CLI helper to generate a bcrypt hash from a plain-text password:

```bash
python -m pensieve.auth hash-password

You will be prompted to enter your password (input is hidden). The command prints the bcrypt hash to stdout:

Enter password to hash:
$2b$12$eW8y3QkZvR1mNpLxO7dHuOq3R7dWvE1LkJzFQ4HtM5VnP2sXaG8Ky

Copy the printed hash into password_hash in your config.yaml.

Note: The hash is one-way — you cannot recover the original password from it. If you forget your password, generate a new hash with a new password and update config.yaml.


Validation

At startup, Pensieve validates the auth configuration:

  • secret must be at least 32 characters long. Shorter values are rejected with a configuration error.
  • password_hash must be a valid bcrypt hash (starts with $2). If you paste a plain-text password by mistake, the server will refuse to start.

API Endpoints

These endpoints are always public (no authentication required):

POST /api/auth/login

Verify a password and start a session.

Request body:

{ "password": "your-password" }

Response (success — 200):

{ "status": "ok" }

The response also sets a pensieve_token HTTP-only cookie (valid for 7 days). The browser sends this cookie automatically on every subsequent request.

Error responses:

Status Condition
401 Unauthorized Password is incorrect.
404 Not Found Authentication is not configured (no auth section in config.yaml).

POST /api/auth/logout

End the current session by clearing the pensieve_token cookie.

Response (200):

{ "status": "ok" }

GET /api/auth/me

Check whether authentication is required and whether the current request is authenticated. The frontend calls this on startup to decide whether to show the login page.

Response (200):

{
  "auth_required": true,
  "authenticated": false
}
Field Description
auth_required true if an auth section is present in config.yaml; false in open-access mode.
authenticated true if the request carries a valid, non-expired pensieve_token cookie.

Protected Routes

When authentication is configured, all routes under /api/ — except the three auth endpoints above — require a valid session cookie. This includes:

  • All workspace and project CRUD endpoints
  • Artifact downloads
  • Chat history
  • Identity and topic memory endpoints
  • Workspace configuration endpoints
  • The WebSocket endpoint (/ws)

Requests without a valid pensieve_token cookie receive a 401 Unauthorized response.


Security Details

Property Value
Password hashing bcrypt (via the bcrypt library)
Token format JWT (HS256)
Token expiry 7 days
Cookie flags HttpOnly, SameSite=Lax
Secret algorithm HMAC-SHA256
  • HTTP-only cookies — the pensieve_token cookie cannot be read by JavaScript, which mitigates XSS-based token theft.
  • SameSite=Lax — the cookie is sent with same-site requests and top-level cross-site navigations, but not with cross-site subrequests (e.g., embedded images), which reduces CSRF risk.
  • bcrypt — password hashes are computationally expensive to crack. Plain-text passwords are never stored.

Recommendation: Use a randomly generated secret of at least 64 characters. You can generate one with:

> python -c "import secrets; print(secrets.token_hex(32))"
> ```

---

## Complete Example

### 1. Generate a password hash

```bash
python -m pensieve.auth hash-password
# Enter password to hash: (enter your password)
# $2b$12$eW8y3QkZvR1mNpLxO7dHuOq3R7dWvE1LkJzFQ4HtM5VnP2sXaG8Ky

2. Generate a secret key

python -c "import secrets; print(secrets.token_hex(32))"
# a3f8c2d14e7b9056f1a2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4

3. Add to config.yaml (using environment variables)

# config.yaml

auth:
  secret: "${PENSIEVE_AUTH_SECRET}"
  password_hash: "${PENSIEVE_PASSWORD_HASH}"

4. Add to .env

# .env
PENSIEVE_AUTH_SECRET=a3f8c2d14e7b9056f1a2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4
PENSIEVE_PASSWORD_HASH=$2b$12$eW8y3QkZvR1mNpLxO7dHuOq3R7dWvE1LkJzFQ4HtM5VnP2sXaG8Ky

5. Start Pensieve

pensieve

The login page is now shown when you open the frontend.


Troubleshooting

“auth.secret must be at least 32 characters long”

The value of secret is too short. Use a randomly generated key of at least 32 characters (64 recommended).

“auth.password_hash must be a bcrypt hash”

The password_hash value does not start with $2. Make sure you ran python -m pensieve.auth hash-password and copied the full output (including the leading $2b$...). Do not paste your plain-text password here.

Frontend shows login page even after correct password

  • Check that the backend is reachable at the URL configured in VITE_API_URL.
  • Ensure CORS is configured correctly in config.yamlcors_origins must include your frontend URL and allow credentials.
  • Verify the pensieve_token cookie is being set by inspecting the browser’s DevTools → Application → Cookies.

Next Steps