Search documentation

Find a page in the Andurel docs.

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.