# Browser authentication and the accounts starter

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.7/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:

```sh
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`.

| Route | GET | POST |
| --- | --- | --- |
| `/accounts` | Signed-in account and session actions | — |
| `/accounts/login` | Login form | Rotate session and sign in |
| `/accounts/recover` | Recovery form | Deliver a reset link to the verified destination |
| `/accounts/reset` | Reset form with token | Consume the token once and replace the password |
| `/accounts/change-password` | Current/new password form | Replace 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>.css` | Embedded 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](https://cicadel.fauzaanu.com/en/0.2.7/howto/request-limits/index.md)). 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](https://cicadel.fauzaanu.com/en/0.2.7/howto/htmx/index.md#csrf-for-htmx-requests)).

## 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](https://cicadel.fauzaanu.com/en/0.2.7/howto/events/index.md#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:

| Variable | Purpose |
| --- | --- |
| `CICADEL_PUBLIC_URL` | HTTPS origin used to construct reset links |
| `CICADEL_MAIL_FROM` | Sender mailbox |
| `CICADEL_SMTP_ADDRESS` | SMTP `host:port` with STARTTLS |
| `CICADEL_SMTP_USERNAME`, `CICADEL_SMTP_PASSWORD` | Optional SMTP credentials |
| `CICADEL_LOG_FORMAT`, `CICADEL_LOG_LEVEL` | JSON/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](https://cicadel.fauzaanu.com/en/0.2.7/topics/email/index.md), [logging](https://cicadel.fauzaanu.com/en/0.2.7/topics/logging/index.md) and
[operations](https://cicadel.fauzaanu.com/en/0.2.7/howto/operations/index.md) for configuration details.
