Skip to content

Authentication

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.

Public password registration is disabled by default. Set SECURITY_REGISTERABLE=true only if your app needs it. Create users from the CLI instead:

Terminal window
uv run stk install # admin user with the admin role
uv run stk create -e user@example.com # prompts for a password
uv run stk add-role -e user@example.com -r editor
uv run stk reset -e user@example.com # prompts for a new password

New 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).

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:

Terminal window
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:

Terminal window
GOOGLE_AUTH_ENABLED=true
GOOGLE_OAUTH_CLIENT_ID=your_client_id
GOOGLE_OAUTH_CLIENT_SECRET=your_client_secret
GITHUB_AUTH_ENABLED=true
GITHUB_OAUTH_CLIENT_ID=your_client_id
GITHUB_OAUTH_CLIENT_SECRET=your_client_secret

Callbacks 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.

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():
pass

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.

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.

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.