auth.md 3.6 KB

Auth

Overview

Authentication is stateless: the server issues a signed JWT on login and never stores session state. Every request carries the token in an HTTP-only cookie; the server verifies the signature and expiry on each request without any database or cache lookup. This makes horizontal scaling trivial — any replica can handle any request.

Credential storage is pluggable. The initial implementation reads from a local file mounted by Kubernetes from a ConfigMap, which is sufficient for dev and early deployments. The interface is designed so the file-based backend can be swapped for an OIDC/SSO provider without touching the JWT layer or any route handler.

Flow

POST /auth/login
  │
  ├─ read userId + password from form body
  ├─ load credentials from backend (file or SSO)
  ├─ verify password (bcrypt compare)
  ├─ invalid → 401, re-render login form with error message
  └─ valid   → sign JWT → set HttpOnly cookie → redirect to /

Subsequent requests
  │
  ├─ hooks.server.ts reads `session` cookie
  ├─ verifyToken() checks signature + expiry (no I/O)
  ├─ valid   → populate event.locals.user → proceed
  └─ invalid → redirect to /auth/login

GET /auth/logout
  │
  └─ clear `session` cookie → redirect to /auth/login

JWT

  • Algorithm: HS256 with a secret from AUTH_JWT_SECRET env var
  • TTL: 8 hours; encoded in the exp claim
  • Payload: { username: string } where username is the user's id field
  • Transport: HttpOnly; SameSite=Lax; Secure; Path=/ cookie named session
  • Verification: pure in-memory — jose.jwtVerify() with no external calls

Credential Backend

Local File

A JSON file mounted into the pod from a Kubernetes ConfigMap:

[
	{
		"id": "admin",
		"passwordHash": "$2b$10$...",
		"name": "Admin User",
		"email": "admin@example.com",
		"workspaces": ["default"]
	}
]

Fields: id (required, used as login identifier and stored in JWT), passwordHash (required, bcrypt), name (optional, display name), email (optional), workspaces (optional, list of workspace IDs the user may access; defaults to []).

  • Path: AUTH_USERS_FILE env var
  • Read once at server startup in Auth.init() and cached in memory; pod restart required to pick up changes
  • Passwords are bcrypt hashes (cost factor 10). Generate with: htpasswd -nbB admin mypassword | cut -d: -f2

ConfigMap example:

apiVersion: v1
kind: ConfigMap
metadata:
  name: agent-paas-users
  namespace: agent-paas
data:
  users.json: |
    [
      { "id": "admin", "passwordHash": "$2b$10$...", "workspaces": ["default"] }
    ]

Mount it as a volume at /etc/agent-paas/users.json in the pod spec.

SSO / OIDC

When SSO is required, replace the Auth class with an OIDC-backed implementation that:

  1. Redirects unauthenticated users to the IdP authorization endpoint
  2. Handles the callback, exchanges the code for tokens, validates the ID token
  3. Issues the same internal JWT (same { username: id } payload) so the rest of the system is unaffected

The Server class wires in the concrete Auth implementation; no route handler or hook needs to change.

Scaling Properties

  • No shared state — JWT verification is pure CPU; no Redis, no DB, no sticky sessions needed
  • Credential cache — the users list is loaded once at startup per replica; all replicas read the same ConfigMap at pod start, so they stay consistent without coordination
  • Secret rotation — rotating AUTH_JWT_SECRET invalidates all existing sessions (users must re-login); this is acceptable for a developer tool with an 8-hour TTL