Configuration¶
arvel's auth reads its settings from the auth.* config namespace — define them in config/auth.py
(a config = {...} mapping merged into the dotted-key config repository, read via config("auth.…")).
Every knob has a safe built-in default, so configuration is optional; set only what you want to change.
Precedence¶
For the tunable security knobs, the value is resolved in this order:
- an explicit constructor argument (e.g.
StartSession(secure=False)) — always wins; - otherwise the
auth.*config value, when an app is running; - otherwise the built-in default.
So a component constructed in a test (no app) uses its built-in default; wired through an app it honours your config; and an explicit argument overrides both.
config/auth.py¶
config = {
# --- identity / providers -------------------------------------------------
"default_guard": "session", # which guard guard() returns (session | local | oidc | …)
"user_model": "app.models.User", # model for identity resolution (required for the user provider)
"jit": False, # just-in-time provision a user on first SSO login
"trusted_email_providers": [], # providers whose verified email may link an existing account
"oidc": { # OIDC guard (required keys; fails loud if incomplete)
"jwks_uri": "https://idp.example.com/.well-known/jwks.json",
"issuer": "https://idp.example.com/",
"audience": "my-client-id",
"provider": "oidc", # optional label on the resulting Principal
},
# --- session cookie (StartSession) ---------------------------------------
"session": {
"lifetime": 120, # MINUTES; cookie max-age / TTL = x60 (default 2h)
"secure": True, # mark the cookie Secure (HTTPS-only); False for plain-HTTP dev
"host_prefix": True, # __Host- cookie prefix (default = secure; requires Secure)
},
# --- login lockout (LoginRateLimiter) ------------------------------------
"lockout": {
"max_attempts": 5, # failed logins before lockout
"decay_seconds": 900, # lockout window, seconds (default 15m)
"fail_open": True, # on a cache outage: True = allow (alert!), False = deny
},
# --- remember-me (RememberMe / remember()) -------------------------------
"remember": {
"ttl": 60 * 60 * 24 * 30, # remember-cookie lifetime, seconds (default 30d)
"secure": True, # Secure flag on the remember cookie
},
# --- sudo / password-confirm (EnsurePasswordConfirmed) -------------------
"password_timeout": 10800, # how long a password confirmation stays fresh, seconds (3h)
# --- impersonation -------------------------------------------------------
"impersonation": {
"ability": "impersonate", # the Gate ability that authorizes "login as"
},
}
Notes¶
- Security defaults are fail-closed.
session.secureandremember.securedefault toTrue; only set themFalsefor local plain-HTTP development. lockout.fail_openis the one availability-over-security default — on a cache outage logins are allowed (so an outage doesn't lock everyone out) but throttling is off, so alert on cache downtime or setfail_open = falseto fail closed.oidckeys are required when you use the OIDC guard — a missingjwks_uri/issuer/audienceraises at construction rather than silently rejecting every token.- These keys are read where the corresponding component is built; passing the argument explicitly at the call site always overrides the config value.
Framework integration¶
Auth doesn't reinvent infrastructure — it resolves the framework's own services from the container, so it honours your app-wide configuration automatically:
| Capability | How auth uses it |
|---|---|
Config (config("auth.…")) |
All the keys above are read as defaults via the config repository — explicit constructor args still win. |
Log (Log facade) |
Security events are emitted on the security channel (see the table below). Route that channel to durable storage. |
Hash (app("hash")) |
Password verification + rehash-on-login resolve the container's hasher (resolve_hasher()), so an app-configured Argon2 cost is honoured. |
Http (app("http")) |
fetch_userinfo calls the OIDC userinfo endpoint through the framework HTTP client. |
Security audit events (security log channel)¶
Identifier-only, best-effort (a logging failure never breaks an auth decision):
| Event | Level | Fields |
|---|---|---|
auth.login.succeeded |
info | user_id |
auth.login.failed |
warning | identifier |
auth.login.blocked |
warning | identifier, reason=locked_out |
auth.login.locked_out |
warning | identifier (the failure that trips the lockout) |
auth.refresh.reused |
warning | tokenable_id (reuse → family revoked) |
auth.remember.theft_detected |
warning | selector, tokenable_id |
auth.impersonation.started |
info | impersonator_id, target_id, ability |
auth.impersonation.denied |
warning | impersonator_id, target_id, reason (+ ability when unauthorized) |
auth.impersonation.stopped |
info | impersonator_id |
auth.logout_everywhere |
info | tokenable_id, failures |
Common mistakes & gotchas¶
- Not routing the
securitychannel anywhere durable. The audit events above are emitted best-effort on thesecuritylog channel — if you don't route that channel to persistent storage, a login-lockout or refresh-token-reuse signal is logged and gone. Wire it up before you need it. - Leaving the hash cost at the default in production. Password verification resolves the container's hasher, so an app-configured Argon2 cost is honoured — but only if you set one. Tune it to your hardware (see Hashing).
- Rotating
APP_KEYwithout a plan. Signed URLs and remember-me cookies are keyed off it; rotate it and outstanding tokens stop verifying. Use the encrypter'sprevious_keyspath (see Encryption) when a value must survive a rotation. - Flipping
session.secure/remember.secureoff and forgetting. They default toTrue(fail-closed) for a reason —Falseis for local plain-HTTP only; shipping it sends session cookies over the clear. - Ignoring
lockout.fail_open. On a cache outage it lets logins through with throttling off (availability over security). Alert on cache downtime, or setfail_open = falseto fail closed.
See also¶
- Authentication · Guards & Drivers · Routes & Flows
- Providers & Middleware — where these components are wired into the kernel.