Password Management
Configure PBKDF2-SHA256 password hashing, admin CLI and API resets, self-service token reset links, and SQLite or PostgreSQL credential storage in Potato.
New in v2.4.0
Potato's authentication system uses PBKDF2-SHA256 with 100,000 iterations and per-user salts — the same approach recommended by NIST for secure password storage. This page covers how passwords are stored, how to reset them, and how to persist credentials across server restarts.
It applies to the in_memory and database methods. Under OAuth the identity provider owns the password, so none of this applies except to the local accounts in mixed mode.
Security Implementation
Passwords are stored in salt$hash format:
- 32-character hex salt, unique per user
- 64-character hex digest, derived by PBKDF2-HMAC-SHA256 over the password with that salt and 100,000 iterations
- Constant-time comparison via
hmac.compare_digestto prevent timing attacks
A single pass of SHA-256 over a salted password is fast enough to brute-force at scale. The 100,000 iterations are what make each guess expensive.
Existing plaintext passwords in user_config.json files are automatically re-hashed with unique salts when Potato loads them — no manual migration needed.
Default Configuration
By default, Potato uses in-memory authentication. require_password is a top-level key rather than part of the authentication block:
require_password: true
authentication:
method: in_memory
user_config:
users:
- username: "annotator1"
password: "initial-password" # will be hashed on first load
- username: "annotator2"
password: "initial-password"Persistent Credentials
Accounts created through the sign-up form are written to a JSONL file and read back at the next boot. Under in_memory that file is user_config.json beside the output directory unless you name another one.
File-Based Persistence
authentication:
method: in_memory
user_config_path: /shared/path/to/user_config.jsonlName a path when several server instances share one roster, or when you want the roster somewhere other than beside the annotations. New registrations and password changes are written as they happen.
Keep the roster with the annotation output. An annotator's directory is named after their username, so a roster that has gone missing while the output directory survives is a study where the next person to type that username would inherit someone else's work. Potato refuses the registration instead, which means the annotator cannot get back in until you restore the file.
Before v2.8.3, in_memory wrote nothing unless user_config_path was set explicitly, so accounts did not survive a restart. If you ran a study on an earlier build, those accounts are gone; the annotations are not.
Database Backend
SQLite (no additional dependencies):
authentication:
method: database
database_url: "sqlite:///auth/users.db"PostgreSQL (requires psycopg2-binary):
authentication:
method: database
database_url: "postgresql://user:password@localhost:5432/potato_auth"Potato creates the users table on first startup and runs SQLite in WAL mode for better concurrent reads. database_url must start with sqlite:/// or postgresql://. You can also set POTATO_DB_CONNECTION in the environment, which database_url overrides when both are present.
Note: method: database and user_config_path are mutually exclusive — choose one persistence strategy. Potato raises an error if both are configured.
Resetting Passwords
Admin CLI
Reset a password from the command line:
# Prompts for username and password
potato reset-password config.yaml
# Prompts for the password only
potato reset-password config.yaml --username annotator1Admin API
Reset programmatically with the admin API key:
curl -X POST http://localhost:8000/admin/reset_password \
-H "X-API-Key: $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"username": "annotator1", "new_password": "new-secure-password"}'The key comes from the top-level admin_api_key setting, the POTATO_ADMIN_API_KEY environment variable, or a generated admin_api_key.txt in the task directory, in that order.
Self-Service Token Reset
Annotators who forget a password can reset it themselves. They visit /forgot-password and enter their username, and Potato generates a single-use token valid for 24 hours. The reset link is shown on screen for an admin to pass on; Potato sends no email. The annotator opens /reset/<token> and sets a new password, which consumes the token.
The flow needs no configuration. When require_password: true is set, a "Forgot Password?" link appears on the login page.
Admins can also mint a token directly:
curl -X POST http://localhost:8000/admin/create_reset_token \
-H "X-API-Key: $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"username": "annotator1"}'
# {"reset_link": "http://localhost:8000/reset/abc123...",
# "token": "abc123...", "expires_in_hours": 24}Pass "ttl_hours": 48 in the request body for a different expiry.
Passwordless Mode
For classroom demos, quick studies, or tasks using external authentication (Prolific, SONA), you can disable passwords entirely:
require_password: false
authentication:
method: in_memoryAnnotators enter any username to log in — no password prompt appears. Not recommended for sensitive data or tasks requiring verified identity.
See Passwordless Login for details.
Complete Reference
# Top-level: require a password at login (default: true)
require_password: true
# Top-level: key for the admin endpoints
admin_api_key: ${POTATO_ADMIN_API_KEY}
authentication:
# in_memory (default) | database | clerk | oauth
method: in_memory
# in_memory only, mutually exclusive with method: database
user_config_path: users.jsonl
# database only; sqlite:/// or postgresql://
# database_url: "sqlite:///auth.db"
user_config:
users:
- username: "researcher"
password: "secure-passphrase"
- username: "annotator1"
password: "initial-pass"| Endpoint | Method | Auth | Description |
|---|---|---|---|
/forgot-password | GET, POST | none | Request a reset token |
/reset/<token> | GET, POST | none | Set a new password |
/admin/reset_password | POST | API key | Admin password reset |
/admin/create_reset_token | POST | API key | Generate a reset token |
Further Reading
- SSO & OAuth Authentication — sign in with Google, GitHub, or institutional SSO
- Passwordless Login — username-only access for open tasks
- Production Setup — HTTPS and reverse proxy configuration
- Admin Dashboard — managing annotator accounts
For implementation details, see the source documentation.