persona-community-5/.claude/guides/auth.md
jordan bd2f591b98
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-24 07:39:46 +00:00

5.2 KiB

Authentication & User Management

Complete auth system with registration, login, sessions, and verification flows.

Architecture

Frontend (AuthProvider) → HTTP → Auth Handlers → AuthService → Repositories (Memory | Postgres)
  • pkg/auth/ — JWT validation, middleware, password hashing, session checking (shared)
  • service/internal/domain/ — User, Session, AuthCode domain models
  • service/internal/port/ — Repository interfaces (UserRepository, SessionRepository, AuthCodeRepository)
  • service/internal/adapter/memory/ — In-memory implementations for standalone dev
  • service/internal/adapter/postgres/ — PostgreSQL/CockroachDB implementations for production
  • service/internal/service/auth.go — Business logic (AuthService)
  • service/internal/api/handlers/auth.go — Core HTTP handlers (login, register, profile)
  • service/internal/api/handlers/auth_flows.go — Flow handlers (OTP, magic link, sessions, reset)

Standalone Mode (No DATABASE_URL)

When DATABASE_URL is not set, the service runs with in-memory adapters:

  • Two demo users seeded: test@example.com / Password123, admin@example.com / Admin1234
  • Auth codes (OTP, magic links, reset tokens) logged to stdout (no notify/email needed)
  • Sessions stored in memory (lost on restart)
  • No external dependencies required

Token Lifecycle

  • Access token: 15 minutes, JWT with embedded session ID (sid claim)
  • Refresh: POST /auth/refresh with valid token returns new token (same session)
  • Session: 30-day lifetime, tracked in sessions table
  • Revocation: Revoking a session invalidates all tokens for that session

Environment Variables

Variable Default Description
JWT_SECRET "" Secret for signing JWT tokens
REGISTRATION_ENABLED true Allow new user registration
DATABASE_URL "" If set, use Postgres repos; otherwise in-memory
NOTIFY_URL "" Notify service URL. If set, emails sent via notify; otherwise logged to stdout
NOTIFY_API_KEY "" Per-project notify send key (notify_send_xxx)
NOTIFY_HOST "" Sending domain (e.g. myapp.threesix.ai)
NOTIFY_FROM noreply@{project}.com Registered sender address

Auth Flows

Password Login

POST /auth/login { email, password } → { token, user }

Registration

POST /auth/register { email, password, name } → { token, user }

OTP Login

POST /auth/otp/send { email } → 200 (code logged to stdout in dev)
POST /auth/otp/verify { email, code } → { token, user }
POST /auth/magic-link { email } → 200 (token logged to stdout in dev)
POST /auth/magic-link/verify { email, token } → { token, user }

Password Reset

POST /auth/forgot-password { email } → 200 (token logged to stdout in dev)
POST /auth/reset-password { email, token, newPassword } → 200

Email Verification (requires auth)

POST /auth/verify-email/send → 200 (code logged to stdout in dev)
POST /auth/verify-email { code } → 200

Session Management (requires auth)

GET  /auth/sessions → [{ id, deviceLabel, ipAddress, lastActiveAt, isCurrent }]
DELETE /auth/sessions/{id} → 204
DELETE /auth/sessions → 204 (revoke all except current)

Profile (requires auth)

GET  /auth/me → { user }
PUT  /auth/me { name, avatarUrl } → { user }
POST /auth/change-password { currentPassword, newPassword } → 200
POST /auth/logout → 204

Frontend Integration

The @persona-community-5/auth package provides AuthProvider and useAuth() hook:

// In App.tsx
<AuthProvider authBaseUrl={`${apiBaseUrl}/api/service-name`}>
  <App />
</AuthProvider>

// In components
const { user, login, register, logout, sendOTP, loginWithOTP } = useAuth();

Auto-refresh schedules token renewal at 80% of token lifetime.

Adding Session Revocation Middleware

To enforce session revocation on every request (opt-in):

import "git.threesix.ai/jordan/persona-community-5/pkg/auth"

checker := func(ctx context.Context, sid string) (bool, error) {
    session, err := sessionRepo.Get(ctx, domain.SessionID(sid))
    if err != nil { return false, nil }
    return session.IsActive(), nil
}

r.Use(auth.SessionCheck(checker))

Password Requirements

  • Minimum 8 characters, maximum 72 (bcrypt limit)
  • Must contain uppercase, lowercase, and digit
  • Hashed with bcrypt cost 12

Database Tables

When DATABASE_URL is set, these tables are auto-created:

  • users — Core identity (email, name, status)
  • user_passwords — Bcrypt hashes (separate for OAuth-only users)
  • sessions — Login sessions with IP/device tracking
  • auth_codes — OTP, magic link, reset, and verification codes
  • user_roles — Many-to-many user roles
  • oauth_connections — Schema placeholder for future OAuth provider links (table exists but no handlers/adapters yet)

Note: The oauth_connections table is created by the migration but has no corresponding handlers, service methods, or adapters. It's a schema placeholder — implementing OAuth requires building the full handler → service → adapter chain. See the composable monorepo templates guide for adding new auth providers.