cicadel

Authentication

The accounts app signs users in with server-side sessions and CSRF checks. Every generated project includes it.

#How it fits together

Cicadel has one core accounts service and two HTTP adapters:

  • Browser adapter. Server-rendered HTML, server-side SCS sessions and CSRF checks.
  • JSON bearer adapter. For API clients that send Authorization: Bearer ….

Both adapters read the same PostgreSQL users, account sessions, recovery tokens, groups and permissions. Disabling an account, resetting its password or revoking its sessions affects both. A browser never receives the account bearer token in HTML or JSON.

#Ask your agent

Text
Read https://cicadel.fauzaanu.com/en/0.2.8/topics/authentication/index.md.
Add browser sign-in to my application using the accounts starter or adapter.
Use PostgreSQL sessions, CSRF protection and verified recovery addresses.
Show the registration and route protection code. Test sign-in, sign-out,
and rejection of revoked sessions; keep secrets outside source code.

#Create the starter

From the framework checkout:

Terminal
go build -o bin/cicadel ./cmd/cicadel
./bin/cicadel startproject /tmp/myapp example.com/myapp
cd /tmp/myapp
go mod edit -replace=github.com/fauzaanu/cicadel=/absolute/path/to/Cicadel
go mod edit -replace=github.com/fauzaanu/cicadel/contrib/accounts=/absolute/path/to/Cicadel/contrib/accounts
go mod edit -replace=github.com/fauzaanu/cicadel/contrib/formsets=/absolute/path/to/Cicadel/contrib/formsets
go mod edit -replace=github.com/fauzaanu/cicadel/contrib/migrations=/absolute/path/to/Cicadel/contrib/migrations
go mod edit -replace=github.com/fauzaanu/cicadel/contrib/querygen=/absolute/path/to/Cicadel/contrib/querygen
go mod tidy
export DATABASE_URL='postgres://USER:PASSWORD@localhost/myapp?sslmode=disable'
export CICADEL_DEBUG=true
export CICADEL_SECURE_COOKIES=false
go run manage.go migrate
CICADEL_USER_PASSWORD='a local account password' go run manage.go createuser owner@example.com
go run manage.go runserver

startproject always includes accounts, formsets and the standard migration workflow. Runtime commands need PostgreSQL.

The accounts module ships with the framework at the same version. Keep all core modules on one version, and commit your resolved Go dependencies.

#Who can have an account

  • The stored username is the account’s verified recovery address. Create accounts only for email addresses whose ownership you have verified.
  • Requests cannot supply a separate delivery address.
  • There is no public registration endpoint and no administrator panel.
  • Use the service’s Create API for your own provisioning and invitation workflows.

#Screens and actions

The default prefix is /accounts. Change it with accounts.Pages.Prefix.

RouteGETPOST
/accountsSigned-in account and session actions—
/accounts/loginLogin formRotate session and sign in
/accounts/recoverRecovery formDeliver a reset link to the verified destination
/accounts/resetReset form with tokenConsume the token once and replace the password
/accounts/change-passwordCurrent/new password formReplace password and revoke all sessions
/accounts/logout—Revoke current account session and destroy its cookie session
/accounts/revoke—Revoke all account sessions and sign out
/static/accounts/app.<hash>.cssEmbedded compiled stylesheet, published through App.Static—

To customize the screens, compose your own pages with contrib/accounts/accountui.

#Integrate an existing application

Create the service and the browser adapter in your project setup. Then register the returned app:

Go
service, err := accounts.New(accounts.Options{
    DB: db,
    DeliverRecovery: DeliverRecovery,
})
if err != nil {
    return err
}
browser, err := service.Browser(security.NewSessions(store, true))
if err != nil {
    return err
}
authApp, err := browser.HTMLApp(accounts.Pages{
    Brand: "My application",
    Logger: logger,
})
if err != nil {
    return err
}
installed = append(installed, authApp)

This fragment uses your PostgreSQL db, shared session store, logger, recovery callback and existing []cicadel.App. Pass installed to the project’s cicadel.New call.

  1. Register the adapter once.
  2. Run migrate before serving.

The generated starter has the full setup in project/application.go.

#Protect a view

Send anonymous visitors to the sign-in page, like Django’s login_required. Install browser.Middleware and browser.LoginRequired on the app that owns the views:

Go
notes := cicadel.App{
    Name: "notes",
    Middleware: []cicadel.Middleware{
        browser.Middleware,
        browser.LoginRequired("/accounts/login"),
    },
    Routes: []urls.Route{
        urls.Path("GET", "/notes/{id}", "notes:detail", detail),
    },
}

An anonymous GET /notes/42 is redirected to /accounts/login?next=%2Fnotes%2F42. The sign-in page carries next in its form and returns there after a successful sign-in.

  • Unsafe requests (POST, DELETE …) are redirected without next, so signing in never repeats an action.
  • htmx requests get HX-Redirect, so the whole page navigates. next is the page the request came from (HX-Current-URL) when it is on the same host.
  • next is only ever a local path. The sign-in page ignores //evil.test, https://…, backslashes and control characters, and returns to the account page instead.
  • LoginRequired panics at startup when its login URL is not a local path.

For JSON routes, use browser.RequireLogin instead. It answers 401.

  • Apply browser.Middleware first. It loads the session, checks CSRF and resolves the user.
  • Read the user with accounts.UserFromContext inside handlers.
  • Both guards check a freshly resolved account, not only a stored SCS user ID.

Warning

Login is not authorization. Object permissions still need explicit application policy and scoped database queries. Check them inside the handler.

HTMLApp already applies LoadAndSave → CSRF → account resolution to its own routes. Do not wrap those routes with the same middleware again.

#Sessions

  • The starter stores sessions in the PostgreSQL table accounts_browser_sessions, using SCS’s configurable table name. You can supply a custom store instead.
  • Migration 002 adds that table without rewriting existing accounts history.
  • Sessions are HttpOnly, SameSite=Lax and secure by default.
  • Sessions rotate on login and clear at logout.
  • Account expiry and revocation are checked on every browser request.
  • SCS expiry can end a session before its account token expires. accounts:cleanup removes expired tokens.
  • Every unsafe method requires CSRF.
  • Bodies under browser.Middleware keep the application’s limit, settings.MaxBodyBytes, or a route’s own limit (see Limit request bodies). The account pages bound their own forms to 8 KiB.
  • htmx requests send the CSRF token as a header when the page sets layout.Props.CSRFToken (see htmx).

#API clients

For API-only clients, use service.HTTPApp(prefix) and service.Middleware with an explicit Authorization: Bearer … header.

Register either adapter once, under the accounts app name. If a host needs both, combine their routes into that one app. Scope the cookie middleware to the HTML routes.

Warning

Never apply cookie CSRF middleware to a bearer-only API by accident.

#React to account changes

The accounts service emits typed events such as accounts.UserCreated, accounts.LoggedIn and accounts.PasswordChanged. See accounts events.

#Recovery delivery

project.DeliverRecovery is the application-owned callback that sends reset links. It:

  • sends to the stored verified address;
  • builds a local reset route from the configured origin;
  • passes the token only to the mail transport.

Recovery reveals nothing about which usernames exist:

  • Known and unknown users get the same recovery response.
  • Missing delivery configuration is handled without revealing whether a username exists.
  • Delivery errors are logged without tokens, addresses or server response text.

Passwords must be 12–72 bytes. Applications may add their own password policy.

Configure these starter environment variables:

VariablePurpose
CICADEL_PUBLIC_URLHTTPS origin used to construct reset links
CICADEL_MAIL_FROMSender mailbox
CICADEL_SMTP_ADDRESSSMTP host:port with STARTTLS
CICADEL_SMTP_USERNAME, CICADEL_SMTP_PASSWORDOptional SMTP credentials
CICADEL_LOG_FORMAT, CICADEL_LOG_LEVELJSON/text format and log threshold

#Deploy

  1. Use HTTPS and secure cookies.
  2. Configure allowed hosts.
  3. Apply gateway rate limits to the login and recovery endpoints.
  4. Schedule manage.go accounts:cleanup.
  5. Do not log query strings on reset routes at the proxy.

Reset pages send no-referrer, no-store and a restrictive content security policy. See email, logging and operations for configuration details.

Type to search this version.