Authentication
A scaffolded application ships working registration, sign-in, email confirmation, password reset, and session handling. HTTP decisions live in handlers/auth.go, the workflows in services/, and the persisted state in the users and tokens domains.
Routes
Auth pages and actions live under /users:
| Method | Path | Handler |
|---|---|---|
| GET | /users/sign-in |
SessionNew |
| POST | /users/sign-in |
SessionCreate |
| DELETE | /users/sign-out |
SessionDestroy |
| GET | /users/sign-up |
RegistrationNew |
| POST | /users |
RegistrationCreate |
| GET | /users/confirmation/new |
ConfirmationNew |
| POST | /users/confirmation |
ConfirmationCreate |
| GET | /users/password/new |
PasswordNew |
| POST | /users/password |
PasswordCreate |
| GET | /users/password/:token/edit |
PasswordEdit |
| PUT | /users/password |
PasswordUpdate |
Every route is declared as a value in routes/users.go and registered by handlers.NewAuthHandler(...). Unsafe actions are rate limited per client IP.
Registration and confirmation
services.Identity.RegisterUser commits the user and the queued verification email in one transaction. The handler stores the new user id as the session's PendingUserID and redirects to the confirmation page.
VerifyEmail then:
- finds the outstanding verification token for that user,
- increments its attempt count before checking the code,
- marks
email_validated_atwhen the code is valid, - destroys the token and signs the user in.
A verification code is six digits and expires after 24 hours. Five failed attempts destroy the token. Invalid attempts commit the attempt counter before the handler returns a field error; otherwise brute-force protection would roll back with the failed transaction.
Signing in with an unverified address returns services.ErrEmailNotVerified together with the user, so the handler can resend a code and send the visitor back to confirmation instead of silently rejecting the login.
Sign in
services.Identity.AuthenticateUser normalises the email, looks the user up, and verifies the password with pkg/auth. Password hashes are argon2id PHC strings. PEPPER is the current pepper; PREVIOUS_PEPPERS keeps older peppers verifiable, and a successful login rehashes the password with the current one.
An unknown address still spends the same work through auth.DummyVerify, so response time does not reveal which addresses have accounts. On success the handler writes the session:
1if err := kiks.Set(ctx, &middleware.Session{
2 UserID: entity.ID.String(),
3 SessionVersion: entity.SessionVersion,
4 IsAuthenticated: true,
5}); err != nil {
6 return err
7}
Sessions
The cookie jar is built in internal/runtime from middleware.NewSessionCookie:
1func NewSessionCookie(cfg config.Session, secure bool) kiks.Definition {
2 return kiks.NewSession[*Session](
3 cfg.Name,
4 kiks.HTTPOnly(),
5 kiks.Secure(secure),
6 kiks.MaxAge(cfg.MaxAge),
7 )
8}
SESSION_KEY and SESSION_ENCRYPTION_KEY are hexadecimal keys; invalid hex fails startup. The cookie is encrypted, HttpOnly, SameSite=Lax, and Path=/; production adds Secure. SESSION_MAX_AGE sets the lifetime in seconds and defaults to seven days.
middleware.Session carries the user id and session_version, not authorization flags. middleware.AuthOnly(users) reloads the user from the database on every request and rejects a missing user or a stale version, so a password change, admin demotion, or deletion takes effect immediately. ChangePassword bumps the version inside the same transaction, revoking every existing session.
Password reset
RequestResetPassword looks up the address and enqueues a reset job. An unknown address succeeds silently so the endpoint cannot be used to enumerate accounts. The queue worker mints a one-hour token and renders the email with the link to /users/password/:token/edit.
ResetPassword resolves the token by its hash, rejects expired or missing tokens, changes the password, and destroys the user's outstanding reset tokens in one transaction. The password change also bumps the session version.
CSRF, CORS, and rate limits
anduin.DefaultCSRF protects unsafe cookie-authenticated requests. CSRF_STRATEGY accepts:
header_only(default): requires Fetch Metadata (Sec-Fetch-Site);header_or_legacy_token: also accepts_csrforX-CSRF-Tokenfor legacy forms.
Bearer-token API requests skip CSRF only when they do not carry the application session cookie. CSRF_TRUSTED_ORIGINS adds exact origins.
CORS trusts the configured host origins plus CORS_ALLOWED_ORIGINS. Origins are exact; wildcard origins are rejected when credentials are allowed.
Auth endpoints allow five attempts per client IP per route in ten minutes. anduin.RateLimit stores counters in the injected cache.Store; use the Redis driver so replicas share counters. The client IP comes from the peer address unless HTTP_TRUSTED_PROXIES names the reverse proxies whose X-Forwarded-For header is trusted.
Authorization
Authentication establishes identity only. AuthOnly proves the session is current; each protected resource still needs its own authorization check. middleware.ViewerFrom(ctx) returns the database-backed Viewer (ID and IsAdmin) that AuthOnly attached, so admin checks read the database rather than a cookie.