Sign-in flows
Every way in is its own switch, and they combine. The tag next to each heading is the config that turns it on — nothing here is mandatory, and the whole front end is replaceable.
Password
password_enabled=TrueThe classic route. One first factor is enough unless you configure a chain.
- identifier + password
/auth/login- session
- protected route
The identifier field follows login_identifier: username, email or both.
PIN as a first factor
pin_enabled=True, pin_login=TrueSign in with a short PIN instead of a password — with its own, stricter lockout.
- identifier + PIN
/auth/pin- session
With pin_login=False the PIN still exists but is no way in — only an extra factor or a step-up (see below).
Second factor (TOTP)
totp_enabled=TrueAfter the first factor, a one-time code from an authenticator app.
- first factor
/auth/totp- 6-digit code
- session mfa_ok
Users without 2FA skip the step; recovery codes work too. login_chain=['password','totp'] makes it mandatory — accounts without TOTP are then walked through setup on their next sign-in.
Step-up for sensitive areas
require(mfa=True), stepup_methods=["pin"]You are signed in — the area still demands a fresh confirmation.
- sensitive route
- freshness expired?
/auth/reauth- PIN / TOTP / password
- area unlocked
stepup_methods names what should confirm. Empty = the strongest method the user has set up (TOTP → PIN → password). A user with none of the listed methods also falls back to their best one — so it is a wish, not a barrier. Add stepup_strict=True for the barrier. Asked again after stepup_max_age_sec.
Factor chain per route
require(factors=["password", "pin"])A route demands several factors in a fixed order.
- password
- chain incomplete
/auth/pin- PIN
- route unlocked
Someone already signed in only gets the missing field — not the whole login page again. The same works globally via login_chain.
Shared secret (no account)
resource_locks_enabled=TrueOne area, one PIN or passphrase. No sign-up, no user.
- protected area
/auth/resource/<name>- PIN
- unlocked, time-limited
Depends(auth.require_resource("guests")) — backed by its own cookie, not by a session.
Sign-in link by email
magiclink_enabled=True + MailerType your address, click the one-time link, you are in.
- mailer
- click link
- session
The same mechanism carries invitations, forgot-password and the sign-up email confirmation.
External identity provider
oidc_enabled=True / saml_enabled=True / ldap_enabled=TrueOIDC, SAML or LDAP/AD. TinySesam is the client — not the provider.
- button
- IdP (PocketID, Entra, ADFS …)
- callback
- session + roles from groups
Map IdP groups onto local roles — after that the same require_role(...) guards apply everywhere.
Forward-auth (other apps)
forward_auth_enabled=TrueThe reverse proxy asks before every request — this is how you guard apps you did not build.
- request to other app
- proxy →
/auth/forward - 200 + Remote-User
- app responds
Without a session: 401 + X-TinySesam-Location → the proxy redirects to login. Caddy/nginx/Traefik examples live in deploy/forward-auth/.
Curious how it feels? The showcase in examples/showcase.py renders these same diagrams — but marks what its own config actually has on.
