Authentication
A generated v2 application includes registration, sessions, email confirmation, password reset, and an services.Identity workflow. Controllers own HTTP. Identity owns peppered passwords, tokens, and queue inserts for mail. Plural model APIs such as models.Users and models.Tokens own persistence. Session state and flashes use kiks.
Account routes live under /users (router/routes/users.go). Controllers register as Fx constructors with injected services.Identity and, for Inertia apps, *inertia.Renderer.
Fx wiring
Generated roots compose these pieces:
| Piece | Role |
|---|---|
config.NewAuth / config.NewSession |
Pepper, token signing key, session cookie keys |
cookies.Module (NewJar) |
kiks jar with NewSession[*cookies.App] |
jar.EchoMiddleware |
Loads bag and flashes onto context.Context |
services.NewIdentity |
Users, tokens, queue insert, auth/mail config |
Controllers (Sessions, Registrations, Confirmations, ResetPasswords) |
HTTP adapters |
1func NewIdentity(
2 db storage.Connection,
3 users models.Users,
4 tokens models.Tokens,
5 insertOnly storage.InsertQueue,
6 appCfg config.App,
7 authCfg config.Auth,
8 mailCfg config.Mail,
9) Identity
Identity never reads environment variables. Controllers never store models or sessions on Echo context keys. See Cookies & Sessions and Configuration.
Sessions: login and logout
Inertia session controller (generated shape):
1type Sessions struct {
2 identity services.Identity
3 renderer *inertia.Renderer
4}
5
6func NewSessions(identity services.Identity, renderer *inertia.Renderer) Sessions {
7 return Sessions{identity: identity, renderer: renderer}
8}
9
10func (s Sessions) New(etx *echo.Context) error {
11 return s.renderer.Page(etx, "Auth/Login", inertia.Props{}).Render()
12}
13
14func (s Sessions) Create(etx *echo.Context) error {
15 ctx, span := telemetry.From(etx, "sessions.create")
16 defer span.End()
17
18 var payload struct {
19 Email string `json:"email"`
20 Password string `json:"password"`
21 }
22 if err := etx.Bind(&payload); err != nil {
23 return s.renderer.Page(etx, "Errors/BadRequest", inertia.Props{}).Render()
24 }
25
26 user, err := s.identity.AuthenticateUser(ctx, services.LoginData{
27 Email: payload.Email,
28 Password: payload.Password,
29 })
30 if err != nil {
31 if validationErrors, ok := validation.As(err); ok {
32 return s.renderer.Page(etx, "Auth/Login", inertia.Props{}).
33 ValidationErrors(validationErrors.ToMap()).
34 Render()
35 }
36 return s.renderer.Redirect(etx, routes.SessionNew.URL(), http.StatusSeeOther)
37 }
38
39 if err := kiks.Set(etx.Request().Context(), &cookies.App{
40 UserID: user.ID.String(),
41 IsAdmin: user.IsAdmin,
42 IsAuthenticated: true,
43 }); err != nil {
44 return err
45 }
46
47 return s.renderer.Location(etx, routes.HomePage.URL())
48}
49
50func (s Sessions) Destroy(etx *echo.Context) error {
51 ctx, span := telemetry.From(etx, "sessions.destroy")
52 defer span.End()
53
54 if err := kiks.Destroy[*cookies.App](ctx); err != nil {
55 return err
56 }
57 return s.renderer.Redirect(etx, routes.SessionNew.URL(), http.StatusSeeOther)
58}
Read the session later with the same pointer type:
1app, err := kiks.Get[*cookies.App](c.Request().Context())
2if err != nil {
3 return err
4}
middleware.AuthOnly redirects guests to routes.SessionNew when app == nil or !app.IsAuthenticated.
Flash after a successful write (for example a password update) with kiks, not a custom flash API:
1kiks.AddFlash(etx.Request().Context(), kiks.FlashSuccess, "Password updated.")
Inertia surfaces those messages through WithFlashProvider reading kiks.Flashes. See Shared Data and Redirects.
Registration, confirmation, and reset
| Flow | Routes (prefix /users) |
Service entry | Outcome |
|---|---|---|---|
| Register | GET /sign-up, POST /users |
RegisterUser |
Redirect to confirmation; email job queued |
| Confirm | GET /confirmation/new, POST /confirmation |
VerifyEmail |
Sets cookies.App, Location home |
| Reset request | GET /password/new, POST /password |
request token + email | Redirect back with flash |
| Reset apply | GET /password/:token/edit, PUT/PATCH /password |
update password | Session optional; flash success |
Registration Create binds email/password/confirmPassword, maps validation.As errors onto Auth/Registration, and on success redirects to confirmation without signing the user in. Confirmation Create verifies the code, then kiks.Sets the session the same way login does.
Password reset issues a signed token (TOKEN_SIGNING_KEY), queues transactional mail with a reset URL, and consumes the token when the user submits a new password. Keep pepper rotation (PEPPER / PREVIOUS_PEPPERS) in deployment secrets. See Configuration.
CSRF and CORS
Unsafe cookie-authenticated requests stay CSRF protected.
| Setting | Behavior |
|---|---|
CSRF_STRATEGY=header_only (default) |
Requires Fetch Metadata (Sec-Fetch-Site / related) for unsafe methods |
CSRF_STRATEGY=header_or_legacy_token |
Also accepts _csrf form fields or X-CSRF-Token |
CSRF_TRUSTED_ORIGINS |
Extra trusted origins beyond the app base URL |
CORS_ALLOWED_ORIGINS |
Explicit credentialed origins; wildcards rejected |
Inertia and fetch clients send credentials with same-origin requests. Legacy HTML forms that post without Fetch Metadata need the compatibility strategy or a token field.
Credentialed CORS trusts the application base URL (PROTOCOL + DOMAIN) plus CORS_ALLOWED_ORIGINS. Do not enable credentialed *.
Authorization
Authentication only proves identity. Gate every protected resource with middleware (AuthOnly) and explicit checks (ownership, admin) before model or service calls.