Local auth: users, sessions, register/login/logout/me #4

Closed
opened 2026-07-18 18:15:26 +00:00 by steve · 0 comments
Owner

Part of epic #20 · Design: DESIGN.md

Context

Pansy is multi-user: users own gardens and share them with others. This issue implements local (email + password) authentication and the session layer that OIDC (#5) also reuses. Auth is OIDC-first in deployment, but local auth is the fallback and the simplest path to a working login for development.

Scope

  • internal/store/users.go, internal/store/sessions.go: queries for the existing users/sessions tables from migration 0001.
  • internal/service/auth.go:
    • Register — argon2id hashing (golang.org/x/crypto/argon2, id variant, ~64 MiB / 1 iteration / 4 threads, random 16-byte salt; encode params+salt+hash in the stored string). First registered user gets is_admin=true. PANSY_REGISTRATION=closed blocks signup except when no users exist (bootstrap).
    • Login — verify with constant-time comparison; identical error for unknown email vs wrong password.
    • Sessions — random 32-byte token; store sha256(token); 30-day sliding expiry (touch on use); logout deletes the row.
  • internal/api: POST /api/v1/auth/register, POST /auth/login, POST /auth/logout, GET /auth/me. Session cookie: HttpOnly, SameSite=Lax, Secure when PANSY_BASE_URL is https, path /.
  • Auth middleware: resolves the cookie to a user, injects actor user ID into the request context; /api/v1 routes except healthz/auth require it.
  • GET /api/v1/auth/providers: {local: bool, oidc: bool, oidc_label: string} from config — the login page (#6) renders from this. oidc stays false until #5.
  • Expired-session cleanup (on access or a periodic sweep — keep it simple).

Out of scope

  • OIDC (#5). Login UI (#6). Password reset/change flows, email verification, rate limiting (post-v1).

Key files & patterns

  • Constant-time compare habit: subtle.ConstantTimeCompare as in /Users/steve/Projects/mort/cmd/mort/agentkit_api.go.
  • Service-layer convention (all methods take (ctx, actor, args); handlers stay thin): DESIGN.md § Backend layout.

Dependencies

Blocked by #1.

Acceptance criteria

  • curl flow: register → login → cookie → GET /auth/me returns the user; logout invalidates; restart of the server keeps valid sessions (DB-backed).
  • Second registered user has is_admin=false; PANSY_REGISTRATION=closed returns 403 for signup when users exist but allows the very first user.
  • Wrong-password and unknown-email responses are indistinguishable.
  • go test ./internal/service/... covers register/login/session expiry.
Part of epic #20 · Design: [DESIGN.md](https://gitea.stevedudenhoeffer.com/steve/pansy/src/branch/main/DESIGN.md) ## Context Pansy is multi-user: users own gardens and share them with others. This issue implements local (email + password) authentication and the session layer that OIDC (#5) also reuses. Auth is OIDC-first in deployment, but local auth is the fallback and the simplest path to a working login for development. ## Scope - [ ] `internal/store/users.go`, `internal/store/sessions.go`: queries for the existing `users`/`sessions` tables from migration 0001. - [ ] `internal/service/auth.go`: - `Register` — argon2id hashing (`golang.org/x/crypto/argon2`, id variant, ~64 MiB / 1 iteration / 4 threads, random 16-byte salt; encode params+salt+hash in the stored string). First registered user gets `is_admin=true`. `PANSY_REGISTRATION=closed` blocks signup **except when no users exist** (bootstrap). - `Login` — verify with constant-time comparison; identical error for unknown email vs wrong password. - Sessions — random 32-byte token; store `sha256(token)`; 30-day sliding expiry (touch on use); logout deletes the row. - [ ] `internal/api`: `POST /api/v1/auth/register`, `POST /auth/login`, `POST /auth/logout`, `GET /auth/me`. Session cookie: HttpOnly, SameSite=Lax, Secure when `PANSY_BASE_URL` is https, path `/`. - [ ] Auth middleware: resolves the cookie to a user, injects actor user ID into the request context; `/api/v1` routes except healthz/auth require it. - [ ] `GET /api/v1/auth/providers`: `{local: bool, oidc: bool, oidc_label: string}` from config — the login page (#6) renders from this. `oidc` stays false until #5. - [ ] Expired-session cleanup (on access or a periodic sweep — keep it simple). ## Out of scope - OIDC (#5). Login UI (#6). Password reset/change flows, email verification, rate limiting (post-v1). ## Key files & patterns - Constant-time compare habit: `subtle.ConstantTimeCompare` as in `/Users/steve/Projects/mort/cmd/mort/agentkit_api.go`. - Service-layer convention (all methods take `(ctx, actor, args)`; handlers stay thin): `DESIGN.md` § Backend layout. ## Dependencies Blocked by #1. ## Acceptance criteria - curl flow: register → login → cookie → `GET /auth/me` returns the user; logout invalidates; restart of the server keeps valid sessions (DB-backed). - Second registered user has `is_admin=false`; `PANSY_REGISTRATION=closed` returns 403 for signup when users exist but allows the very first user. - Wrong-password and unknown-email responses are indistinguishable. - `go test ./internal/service/...` covers register/login/session expiry.
steve added the backendauth labels 2026-07-18 18:15:26 +00:00
steve closed this issue 2026-07-18 21:05:01 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/pansy#4