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
- You configure a bcrypt password hash and a secret key in
config.yaml. - A user visits the frontend. The frontend calls
GET /api/auth/meto determine whether authentication is required. - If required, the login page is shown. The user submits their password to
POST /api/auth/login. - The backend verifies the password against the stored bcrypt hash. On success, it sets a signed JWT in an HTTP-only
pensieve_tokencookie (7-day expiry). - Every subsequent request to a protected route includes the cookie automatically. The
require_authFastAPI dependency validates the JWT on each request. - 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:
secretmust be at least 32 characters long. Shorter values are rejected with a configuration error.password_hashmust 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_tokencookie 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.yaml—cors_originsmust include your frontend URL and allow credentials. - Verify the
pensieve_tokencookie is being set by inspecting the browser’s DevTools → Application → Cookies.
Next Steps
- Global Configuration — full
config.yamlreference including theauthsection - Environment Variables — store secrets outside
config.yaml - Local Setup — run Pensieve locally
- Docker Setup — deploy with Docker