Authentication
Overview
Section titled “Overview”stk builds authentication on quart-security: session auth, roles, TOTP two-factor, WebAuthn passkeys, recovery codes, and opt-in Google and GitHub OAuth. Production requirements are in SECURITY.md; the deployment guide covers the settings you must set.
Accounts and passwords
Section titled “Accounts and passwords”Public password registration is disabled by default. Set SECURITY_REGISTERABLE=true only if your app needs it. Create users from the CLI instead:
uv run stk install # admin user with the admin roleuv run stk create -e user@example.com # prompts for a passworduv run stk add-role -e user@example.com -r editoruv run stk reset -e user@example.com # prompts for a new passwordNew passwords are hashed with Argon2id and must be at least 12 characters. Legacy hashes still verify and are upgraded on login. Password creation and reset also check the password against the Have I Been Pwned breach service; if that service is unreachable the password is accepted, so monitor for those failures.
After 5 failed logins an account is locked for 15 minutes (quart-security’s SECURITY_LOGIN_MAX_ATTEMPTS and SECURITY_LOCKOUT_MINUTES defaults).
Two-factor and passkeys
Section titled “Two-factor and passkeys”TOTP authenticator apps are the enabled second factor. Three single-use recovery codes are generated per user, hashed at rest, and shown only once. WebAuthn passkeys work as a first factor or as a second factor. Managing 2FA, recovery codes, or passkeys requires a fresh session: a login within the last 60 minutes, otherwise the request gets a 401.
TOTP seeds are encrypted at rest. By default the key is derived from SECRET_KEY, so changing SECRET_KEY breaks existing 2FA unless you keep the old key. For independent rotation, set SECURITY_TOTP_ENCRYPTION_KEYS to a comma-separated list of Fernet keys, newest first:
uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"The first key encrypts new data and every listed key can decrypt. After upgrading or rotating keys, run uv run stk protect-mfa to encrypt legacy seeds and re-encrypt existing ones with the current key. Store the keys separately from database backups. A wrong key fails closed.
Passkeys need SECURITY_WAN_RP_ID set to your DNS name and SECURITY_WAN_EXPECTED_ORIGIN set to your HTTPS origin.
Google and GitHub login are off until enabled:
GOOGLE_AUTH_ENABLED=trueGOOGLE_OAUTH_CLIENT_ID=your_client_idGOOGLE_OAUTH_CLIENT_SECRET=your_client_secret
GITHUB_AUTH_ENABLED=trueGITHUB_OAUTH_CLIENT_ID=your_client_idGITHUB_OAUTH_CLIENT_SECRET=your_client_secretCallbacks are /login/google/callback and /login/github/callback, built from STK_PUBLIC_URL when it is set. Requested scopes are openid profile email for Google and read:user user:email for GitHub. Each login uses single-use state and PKCE. Only verified provider emails are accepted, a matching local email is never linked automatically, and a user with local 2FA must still complete it. Locked accounts cannot sign in through OAuth either.
Protecting routes
Section titled “Protecting routes”from quart_security import auth_required, roles_required
@app.route("/admin")@auth_required("session")@roles_required("admin")async def admin_dashboard(): return "Admin only content"To protect a whole blueprint, apply the decorator to its before_request hook, as stk/portal/views.py does:
@portal.before_request@auth_required("session")async def before_request(): passCSRF and logout
Section titled “CSRF and logout”Every POST, PUT, PATCH, and DELETE request needs the session CSRF token, in a form field or the X-CSRFToken header. The shared layout configures Axios for you. Third-party webhooks that have no session can opt out per view with csrf_exempt from stk.security, placed under the route decorator; such a view must authenticate the caller itself, for example with an HMAC signature.
Logout is POST-only. Use layoutMixin.logout(); a plain link to /logout returns 405.
Sessions and rate limits
Section titled “Sessions and rate limits”PERMANENT_SESSION_LIFETIME is 3600 seconds. Cookies default to Secure, HttpOnly, and SameSite=Lax, controlled by SESSION_COOKIE_SECURE, SESSION_COOKIE_HTTPONLY, and SESSION_COOKIE_SAMESITE. Sessions are stored in Redis when REDIS_URL is set and the full extra is installed, otherwise in the cookie. Each login is recorded with IP and device in the Session table. DISABLE_MULTIPLE_SESSIONS=true logs out other sessions on every login.
Auth endpoints are rate limited per IP with fixed one-minute windows stored in the database, so limits hold across workers. The defaults are 10 authentication attempts and 120 authentication requests per minute, set by the STK_AUTH_ATTEMPT_LIMIT and STK_AUTH_REQUEST_LIMIT app config values. Limits depend on a trustworthy client IP, so behind a proxy trust forwarded headers only from that proxy.
Troubleshooting
Section titled “Troubleshooting”OAuth failing: check that the callback URL registered with the provider matches STK_PUBLIC_URL plus the callback path, and that the provider account has a verified email.
TOTP codes rejected: check the server clock, and that SECRET_KEY or SECURITY_TOTP_ENCRYPTION_KEYS still holds the key the seeds were encrypted with.
Passkeys failing: SECURITY_WAN_RP_ID and SECURITY_WAN_EXPECTED_ORIGIN must match the host in the browser.
Logged out too soon: check PERMANENT_SESSION_LIFETIME, and for Redis sessions, REDIS_URL.