Authentication & Security¶
This section covers everything about who can do what in an arvel app — authentication (proving who a user is), authorization (deciding what they may do), and the cryptographic primitives (password hashing, value encryption) underneath. Start here: this page gives you the mental model, helps you pick the right auth mechanism, gets you protecting a route in five minutes, and points at the deep-dive page for each mechanism.
Almost every "auth bug" you've ever debugged comes from quietly conflating three different questions:
- Who is this user? — their durable identity (the row in your
userstable). - How did they prove it? — a password, a Google login, a Keycloak SSO token, an API key.
- What are they allowed to do? — their roles and permissions.
Smush those together and you get the classic messes: a user who can't log in with Google because they signed up with a password; an SSO migration that means re-creating every account; an admin role that silently came from an identity provider nobody audited. arvel keeps the three separate, and joins them with two small, well-defined seams — so each can change without disturbing the others.
The mental model¶
How they prove it Who they are What they may do
┌───────────────────┐ ┌──────────────┐ ┌────────────────────┐
│ AUTHENTICATION │ │ IDENTITY │ │ AUTHORIZATION │
│ guard drivers │──────▶│ User │──────────▶│ roles/permissions │
│ password · OIDC │ │ (your table)│ │ Gate · policies │
│ token · session │ └──────────────┘ └────────────────────┘
└───────────────────┘ ▲ ▲
│ Principal │ AuthIdentity │ claim→role map
│ (provider, subject, │ (provider, subject) │ (group/role claim
│ claims) │ → user │ → arvel Role)
└──────────────────────────┘ │
translate identity translate authorization
Two rules make this work, and they're the whole philosophy of the section:
- Identity is decoupled from authentication. Your
Userrecord never stores how someone logs in. A person can have a password and a Google login and a Keycloak account — all three resolve to the sameUser. Adding or removing a login method is a row, not a migration. - The identity provider's vocabulary is translated at the door, never modeled inside arvel. An
external
subclaim becomes anAuthIdentitylink; an externalgroupsclaim becomes arvelRoles through a mapping. There is exactly one authorization vocabulary —RoleandPermission— and everything an IdP asserts is converted into it.
Everything else is an application of those two rules.
Choosing an auth mechanism¶
"Authentication" isn't one thing — it's a family of mechanisms for different clients. Pick by who's calling and how they prove it:
| Your situation | Use | Why | Deep dive |
|---|---|---|---|
| Server-rendered web app, users log in with a form in a browser | Session (SessionGuard) |
A cookie the browser sends automatically; server tracks the session. Add remember-me + 2FA. | Authentication |
| API, SPA, mobile, or CLI — a non-browser client that puts a credential in a header | API token (personal access token) | An opaque bearer token you issue per user, sent as Authorization: Bearer …, with per-token abilities + expiry. |
API Tokens |
| Machine-to-machine / service accounts | API token with abilities | Same tokens, scoped to just the abilities that service needs. | API Tokens |
| "Log in with Google / GitHub" (social login) | OAuth 2.0 (OAuthProvider) |
The redirect→callback→code-exchange flow that logs the user in through the provider and gets their profile. | OAuth2 |
| "Log in with our company account" — enterprise SSO (Keycloak, Okta, Entra/Azure AD) | OIDC (OAuthProvider to obtain + OidcGuard to validate) |
OIDC = OAuth2 + a standard identity layer; you get a signed id_token with identity claims. |
SSO / OIDC |
| Your API validates a JWT an upstream gateway/IdP already issued | OIDC guard (OidcGuard, validate mode) |
Verifies the token's signature against the IdP's JWKS and turns it into a Principal — no redirect. |
SSO / OIDC |
Two decisions capture most of it:
- Browser vs non-browser → session for browsers, API token for everything else. (A SPA served from your own domain can use either — sessions if it shares the domain, tokens if it's cross-origin.)
- Do you run the login, or does someone else? → you run it (password / API token) vs a provider
runs it (OAuth for social, OIDC for enterprise SSO). These compose: a session app can offer
"log in with Google" and a password form, both resolving to the same
User.
You rarely pick just one. A typical app uses sessions for its web UI, API tokens for its
public API, and OAuth/OIDC as extra login buttons — all landing on the same User and the same
authorization checks.
Protect your first route in 5 minutes¶
Make your user model authenticatable, log someone in, protect a route, and read the user back.
1. The user model — mix in Authenticatable (login) and HasRoles (permissions):
# app/models/user.py
from arvel import Model
from arvel.auth import Authenticatable, HasRoles
class User(Model, Authenticatable, HasRoles):
__fields__ = {"email": str, "name": str, "password": str}
__fillable__ = ["email", "name"]
__casts__ = {"password": "hashed"} # a raw password is argon2-hashed on assignment
2a. Session login (browser app) — verify the password the timing-safe way, then start a session:
from arvel.auth import verify_credentials
from arvel.auth.guards import SessionGuard
async def login(request):
data = await request.form()
user = await User.where("email", data["email"]).first()
if not await verify_credentials(user, data["password"]): # dummy-hash on a missing user; off-loop
abort(401, "Invalid credentials")
await SessionGuard().login(user, request) # rotates the session id, persists the user
return redirect("/dashboard")
2b. Or token login (API) — verify, then mint a bearer token (shown once):
from arvel.auth import verify_credentials
from arvel.auth.tokens import create_token
async def token_login(request):
data = await request.json()
user = await User.where("email", data["email"]).first()
if not await verify_credentials(user, data["password"]):
abort(401, "Invalid credentials")
plaintext, _ = await create_token(user, name="mobile-app") # the client stores `plaintext`
return {"token": plaintext}
3. Protect a route with the auth middleware (401 if not authenticated), and read the user:
from arvel import Route
from arvel.auth import current_user
async def me(request):
user = current_user.get() # the authenticated User, established by the guard middleware
return {"email": user.email}
Route.get("/me", me, middleware=["auth"]) # ENFORCES: 401 unless authenticated
Route.get("/api/me", me, middleware=["auth"]).secure("bearer") # …and documents the scheme in OpenAPI
That's a working, secure login. verify_credentials closes the two classic password-check bugs (user
enumeration by timing + blocking the event loop); SessionGuard.login rotates the session id (fixation
defense); tokens are hashed at rest. Everything below layers depth onto this.
What each mechanism is (in one paragraph each)¶
- Sessions — the browser stores an opaque session-id cookie; the server keeps the session state. Stateful, revocable instantly (drop the server record), and the natural fit for a server-rendered app. Comes with CSRF protection and session-fixation defense. See Authentication.
- API tokens — high-entropy random strings you issue per user; the client sends one as a bearer header. arvel stores only a SHA-256 hash, so a DB leak exposes nothing usable, and each token carries abilities (scopes) + an optional expiry. Deliberately opaque, not JWTs — so you can revoke one instantly by deleting its row (a self-contained JWT can't be un-issued). See API Tokens.
- OAuth 2.0 — a delegation protocol. "Log in with Google" redirects the user to Google, who sends back an authorization code you exchange for tokens + the user's profile. You never see their Google password. See OAuth2.
- OIDC — OAuth 2.0 plus a standardized identity layer. Where plain OAuth gets you access,
OIDC gets you a signed
id_tokenwith identity claims (sub,email,groups). This is what enterprise SSO (Keycloak/Okta/Entra) speaks. arvel splits it into obtain (OAuthProviderruns the redirect) and validate (OidcGuardverifies the JWT). See SSO / OIDC. - Guards — the pluggable engine under all of the above. Every mechanism is a guard driver that
turns a request into a
Principal; you can register your own. See Guards & Drivers.
The confusing parts, cleared up¶
- Token vs session — which and why. A session is server-side state keyed by a cookie: great for browsers, instantly revocable, but the server must hold the state. A token is a self-describing credential the client sends on every request: great for APIs and cross-origin clients, no cookie needed. Rule: browser → session, everything else → token. arvel's tokens are opaque (hashed in the DB), so they keep the session's best trait — instant revocation — which a stateless JWT gives up.
- OAuth vs OIDC — they're not alternatives. OAuth answers "can this app act on the user's
behalf?" (authorization). OIDC adds "…and here is who the user is, signed" (authentication). For a
social/SSO login you usually want both:
OAuthProviderruns the OAuth redirect and hands you anid_token; the OIDC verifier validates it. If a page says "use OAuth to log in," it almost always means the OIDC-flavored flow. Don't build identity on plain OAuth access tokens — they're not proof of who logged in. - "Obtain a token" vs "validate a token." Two different jobs people conflate. Obtaining is the
browser redirect dance (
OAuthProvider,[oauth]extra). Validating is an API checking a bearer JWT it was handed (OidcGuard,[jwt]extra). You need the first to log users in, the second to protect an API behind an existing IdP. - Expiry & rotation. Access tokens should be short-lived; pair them with rotating refresh tokens (the refresh flow in Routes & Flows) so a leaked access token dies quickly and each refresh invalidates the last. Sessions expire via cookie lifetime + server TTL.
- Callbacks & redirects. The OAuth
redirect_uriyou send must exactly match one registered with the provider — scheme, host, port, path, trailing slash. A mismatch is the #1 OAuth error (see Troubleshooting).
Security¶
Auth is where a small mistake becomes a real vulnerability. arvel's defaults are safe; these are the things to get right and the anti-patterns to never ship.
Secrets & storage
- Never commit secrets. APP_KEY, OAuth client secrets, and IdP credentials go in the environment,
not the repo. APP_KEY keys signed URLs, remember-me cookies, and encryption — rotate it with a
plan, never casually.
- Passwords are argon2id-hashed (never stored or logged in plaintext); cast the column "hashed"
so a raw password is hashed on assignment and can never reach the DB. See Hashing.
- API tokens and reset tokens are stored hashed — the plaintext exists only in the client's hand /
the emailed link. A DB leak exposes nothing usable.
Verifying credentials
- Use verify_credentials / Auth.attempt, never an inline Hasher().check. The naive
if user is None or not Hasher().check(pw, user.password) leaks two things: it skips the hash when
the user is missing (an unknown email answers faster — a user-enumeration timing oracle), and the
sync check blocks the event loop. verify_credentials burns a dummy hash for a missing user
and runs off-loop. Never compare passwords with ==.
Tokens & JWTs
- Short-lived access tokens + rotating refresh tokens. Don't issue a long-lived access token you
can't revoke.
- JWT algorithm safety. arvel rejects a JWT whose algorithm list mixes symmetric and asymmetric
families (the alg confusion attack, where an attacker signs an HS256 token with your public key
as the HMAC secret). Configure a single, explicit algorithm family for your IdP.
- Validate signatures against the IdP's JWKS, and check iss/aud/exp. OidcGuard does this.
Sessions & cookies
- CSRF is on for the web group. State-changing session requests are checked (double-submit token +
origin/referer). Don't disable it except for a genuine webhook endpoint. See CSRF.
- Session fixation is defended — SessionGuard.login rotates the session id before persisting the
user id. Do the login through the guard, not by hand-setting the session.
- The session cookie is HttpOnly + Secure; the CSRF cookie is intentionally not HttpOnly (it's
the double-submit token, not a secret).
OAuth / OIDC
- Exact redirect_uri match — an over-broad or mismatched redirect is an open-redirect / token-theft
vector. Register the precise URI.
- The OAuth state parameter is CSRF protection for the flow — arvel handles it; don't strip it.
- Vet what an IdP claim maps to. A groups claim that maps to an admin Role is a privilege path;
never map an unvetted claim to * or an admin role. See IdP → Roles.
Anti-patterns — never do these
- ❌ Never store google_id/how-they-log-in on the User row — use identities.
- ❌ Never hand out the * (super-admin) permission to anything an IdP claim can reach unvetted.
- ❌ Never put a bearer token or password in a URL, a log line, or a trace attribute.
- ❌ Never "document" a route as protected without the actual guard middleware — a .secure()
marker documents the scheme; the enforcement is the middleware.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 on a route you thought was protected/open | The guard middleware isn't applied, or the user_resolver/token guard isn't wired, or no Authorization: Bearer header was sent. |
Add the guard middleware (middleware=["auth"]) — .secure("bearer") only documents the scheme, it doesn't enforce; confirm the request carries the credential; check the guard is registered. |
Authorize("posts.updte") silently 403s everything |
A typo'd ability never matches, so the Gate always denies. | Assert your abilities exist at boot (Gate.has_ability); see providers-and-middleware. |
| JWT "invalid signature" / verification fails | Wrong signing key or JWKS URL; an algorithm mismatch between the IdP and your guard; or the guard rejecting a mixed HS+asymmetric alg list. |
Point the guard at the IdP's JWKS endpoint; pin a single algorithm family; confirm iss/aud. See SSO / OIDC. |
| "Token expired" | Access token past its exp. |
Refresh with the rotating refresh token (don't just re-issue); if there's no refresh, re-authenticate. See Routes & Flows. |
OAuth redirect_uri_mismatch / invalid redirect |
The redirect_uri you send doesn't exactly match one registered with the provider (scheme/host/port/path/trailing slash). |
Register the exact callback URL with the provider and use the identical string. |
OAuth state mismatch / CSRF error on callback |
The state didn't round-trip (session dropped between redirect and callback, or a proxy stripped it). |
Ensure the session cookie survives the redirect; don't remove the state param. |
| A password user can't "log in with Google" (or vice-versa) | The Google identity was never linked to the existing User. |
Link login methods as identities on the same user — don't create a second account. |
| 419 "page expired" on a form POST | Missing/invalid CSRF token on a session request. | Include the CSRF token (csrf_field() / the XSRF-TOKEN cookie for a SPA). See CSRF. |
| Login is slow / unknown emails respond faster | An inline Hasher().check (blocking + enumeration oracle). |
Switch to verify_credentials. |
Full reference — the deep-dive pages¶
Start at the top and read down, or jump to what you need:
| Page | What you'll learn |
|---|---|
| Authentication | Passwords, sessions, SessionGuard, the local guard, brute-force lockout |
| Guards & Drivers | The Principal contract, the guard manager, registering your own guard |
| API Tokens | Issuing/validating bearer tokens, abilities, expiry, TokenGuard |
| SSO / OIDC | Obtaining vs validating a token; the oidc guard; claims; offline testing |
| OAuth2 Social Login | The redirect/callback flow; configuring Google/GitHub/Keycloak |
| Identities & Account Linking | One person, many login methods — safely linked |
| Mapping IdP Groups to Roles | Let Keycloak/Okta groups drive arvel roles |
| Authorization | Roles, permissions, gates, policies, impersonation |
| Two-Factor Authentication | TOTP second factor + single-use recovery codes |
| Password Reset | The reset-link flow; pruning expired tokens |
| Email Verification | Verify-email flow + the verified route guard |
| Routes & Flows | Wiring login/logout, refresh-token rotation, reset, verification |
| Providers & Middleware | Registering auth services + the auth/guest/verified guards |
| Configuration | Every auth config key + the security audit-log channel |
| Hashing · Encryption | The crypto primitives underneath |
See also¶
- Service Container — guards and the user provider are resolved from the container.
- Middleware — where the active user is established for each request.
- Validation — validating login and registration input.
- CSRF — the cross-site-request-forgery protection on the web group.