Phase 06: Backend Authentication - 3 plan(s) in 2 wave(s) - Wave 1: 06-01 (login/logout foundation) - Wave 2: 06-02 (password reset), 06-03 (RBAC + 2FA) - parallel - Ready for execution
301 lines
12 KiB
Markdown
301 lines
12 KiB
Markdown
---
|
|
phase: 06-backend-authentication
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- summercms/resources/db/migration/V6__backend_auth.sql
|
|
- summercms/src/auth/AdminUser.scala
|
|
- summercms/src/auth/Role.scala
|
|
- summercms/src/auth/AdminSession.scala
|
|
- summercms/src/auth/LoginAttempt.scala
|
|
- summercms/src/auth/PasswordService.scala
|
|
- summercms/src/auth/SessionService.scala
|
|
- summercms/src/auth/LoginService.scala
|
|
- summercms/src/auth/AuthError.scala
|
|
- summercms/src/auth/middleware/AuthMiddleware.scala
|
|
- summercms/src/auth/routes/LoginRoutes.scala
|
|
- summercms/src/api/Routes.scala
|
|
- build.mill
|
|
autonomous: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Admin can log in with valid email/password and receive a session token"
|
|
- "Admin receives generic error message on invalid credentials (no hints about which field is wrong)"
|
|
- "Admin can log out and the session is revoked"
|
|
- "Progressive lockout prevents brute force (10 min after 5 failures, etc.)"
|
|
- "Sessions can be listed and revoked individually"
|
|
artifacts:
|
|
- path: "summercms/resources/db/migration/V6__backend_auth.sql"
|
|
provides: "Database schema for admin_users, roles, admin_sessions, login_attempts"
|
|
contains: "CREATE TABLE admin_users"
|
|
- path: "summercms/src/auth/PasswordService.scala"
|
|
provides: "Argon2id password hashing"
|
|
exports: ["PasswordService", "hash", "verify"]
|
|
- path: "summercms/src/auth/SessionService.scala"
|
|
provides: "JWT + DB hybrid session management"
|
|
exports: ["SessionService", "createSession", "validateToken", "revokeSession"]
|
|
- path: "summercms/src/auth/LoginService.scala"
|
|
provides: "Login logic with progressive lockout"
|
|
exports: ["LoginService", "authenticate"]
|
|
- path: "summercms/src/auth/routes/LoginRoutes.scala"
|
|
provides: "POST /admin/auth/login, POST /admin/auth/logout, GET/DELETE /admin/auth/sessions"
|
|
min_lines: 50
|
|
key_links:
|
|
- from: "LoginRoutes.scala"
|
|
to: "LoginService"
|
|
via: "ZIO service dependency"
|
|
pattern: "ZIO\\.service\\[LoginService\\]"
|
|
- from: "LoginService"
|
|
to: "PasswordService"
|
|
via: "password verification"
|
|
pattern: "passwordService\\.verify"
|
|
- from: "SessionService"
|
|
to: "admin_sessions table"
|
|
via: "Quill queries"
|
|
pattern: "querySchema\\[AdminSession\\]"
|
|
---
|
|
|
|
<objective>
|
|
Create the foundation for admin backend authentication with login/logout functionality.
|
|
|
|
Purpose: Enable admin users to authenticate securely with JWT sessions backed by database for revocation support. This is the prerequisite for all protected admin functionality.
|
|
|
|
Output:
|
|
- Database migration with admin_users, roles, admin_sessions, login_attempts tables
|
|
- PasswordService with Argon2id hashing
|
|
- SessionService with JWT + DB hybrid for revocation support
|
|
- LoginService with progressive lockout protection
|
|
- Login/logout HTTP endpoints with auth middleware
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/jin/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/06-backend-authentication/06-CONTEXT.md
|
|
@.planning/phases/06-backend-authentication/06-RESEARCH.md
|
|
|
|
# Existing codebase patterns
|
|
@summercms/src/repository/UserRepository.scala
|
|
@summercms/src/repository/RepositoryError.scala
|
|
@summercms/src/db/QuillContext.scala
|
|
@summercms/src/api/Routes.scala
|
|
@build.mill
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create database migration and domain models</name>
|
|
<files>
|
|
summercms/resources/db/migration/V6__backend_auth.sql
|
|
summercms/src/auth/AdminUser.scala
|
|
summercms/src/auth/Role.scala
|
|
summercms/src/auth/AdminSession.scala
|
|
summercms/src/auth/LoginAttempt.scala
|
|
summercms/src/auth/AuthError.scala
|
|
build.mill
|
|
</files>
|
|
<action>
|
|
1. Add new dependencies to build.mill:
|
|
- mvn"com.password4j:password4j:1.8.4" (Argon2id hashing)
|
|
- mvn"com.github.jwt-scala::jwt-zio-json:11.0.3" (JWT with ZIO JSON)
|
|
|
|
2. Create V6__backend_auth.sql migration with tables:
|
|
- roles (id, code, name, permissions TEXT[], is_system, created_at)
|
|
- admin_users (id, email, password_hash, first_name, last_name, role_id FK, totp_secret, totp_enabled, recovery_codes TEXT[], created_at, updated_at, last_login_at)
|
|
- admin_sessions (id VARCHAR(64) PK, user_id FK, created_at, expires_at, ip_address INET, user_agent, revoked, revoked_at)
|
|
- login_attempts (id, email, ip_address INET, attempted_at, successful)
|
|
- Insert default Super Admin role with permissions = '{"*"}'
|
|
- Add indexes on admin_sessions(user_id), admin_sessions(expires_at), login_attempts(email, attempted_at)
|
|
|
|
3. Create domain models in summercms/src/auth/:
|
|
- AdminUser case class mapping to admin_users table (use Instant for timestamps)
|
|
- Role case class mapping to roles table
|
|
- AdminSession case class mapping to admin_sessions table
|
|
- LoginAttempt case class mapping to login_attempts table
|
|
|
|
4. Create AuthError sealed trait ADT:
|
|
- InvalidCredentials (generic - no hints)
|
|
- AccountLocked(until: Instant)
|
|
- TotpRequired
|
|
- InvalidTotp
|
|
- SessionExpired
|
|
- SessionRevoked
|
|
- InvalidToken(reason: String)
|
|
|
|
Follow existing patterns from RepositoryError.scala and model/User.scala.
|
|
</action>
|
|
<verify>
|
|
- ./mill summercms.compile succeeds
|
|
- Migration file exists at summercms/resources/db/migration/V6__backend_auth.sql
|
|
- All case classes compile with proper Instant types
|
|
</verify>
|
|
<done>
|
|
Database schema defined for auth tables, domain models created matching schema, build compiles with new dependencies
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create PasswordService, SessionService, and LoginService</name>
|
|
<files>
|
|
summercms/src/auth/PasswordService.scala
|
|
summercms/src/auth/SessionService.scala
|
|
summercms/src/auth/LoginService.scala
|
|
summercms/src/auth/repository/AdminUserRepository.scala
|
|
summercms/src/auth/repository/AdminSessionRepository.scala
|
|
summercms/src/auth/repository/LoginAttemptRepository.scala
|
|
summercms/src/config/AppConfig.scala
|
|
</files>
|
|
<action>
|
|
1. Create PasswordService (ZIO service pattern):
|
|
- trait PasswordService with hash(password: String): Task[String] and verify(password: String, hash: String): Task[Boolean]
|
|
- Implementation using Password4j with Argon2id (default settings are OWASP compliant)
|
|
- Use ZIO.attemptBlocking for Password4j calls
|
|
|
|
2. Create repository traits following existing UserRepository pattern:
|
|
- AdminUserRepository: findById, findByEmail, create, update
|
|
- AdminSessionRepository: create, findById, findByUserId, revoke, revokeAllForUser
|
|
- LoginAttemptRepository: recordAttempt, countRecentFailures(email, ip, since)
|
|
All should use RepositoryError for errors.
|
|
|
|
3. Add auth config to AppConfig.scala:
|
|
- jwtSecret: String (env: JWT_SECRET, default: generate-a-secure-secret-in-production)
|
|
- sessionDuration: Duration (default: 24 hours)
|
|
- rememberMeDuration: Duration (default: 30 days)
|
|
|
|
4. Create SessionService (ZIO service pattern):
|
|
- createSession(userId, role, rememberMe, ip, userAgent): Task[String] - returns JWT token
|
|
- validateToken(token: String): IO[AuthError, SessionClaims]
|
|
- revokeSession(sessionId: String): Task[Unit]
|
|
- listSessions(userId: Long): Task[List[AdminSession]]
|
|
- revokeAllSessions(userId: Long): Task[Unit]
|
|
- Use jwt-zio-json for JWT encoding/decoding with HS256
|
|
- Store session in DB and include sessionId in JWT claims
|
|
- On validate: decode JWT, check expiry, check DB for revocation
|
|
|
|
5. Create LoginService (ZIO service pattern):
|
|
- authenticate(email, password, totpCode: Option, ip): IO[AuthError, AdminUser]
|
|
- Progressive lockout thresholds (per RESEARCH.md):
|
|
- 5 failures: 10 min lockout
|
|
- 10 failures: 20 min lockout
|
|
- 15 failures: 1 hour lockout
|
|
- 20 failures: 24 hour lockout
|
|
- IMPORTANT: Always call passwordService.verify even for non-existent users (timing attack prevention)
|
|
- Record all login attempts (success and failure)
|
|
- Return generic InvalidCredentials error for both wrong email and wrong password
|
|
- For now, skip TOTP validation (will be added in plan 06-03)
|
|
|
|
Use ZLayer pattern like existing services. Follow _root_.config import pattern to avoid shadowing.
|
|
</action>
|
|
<verify>
|
|
- ./mill summercms.compile succeeds
|
|
- PasswordService.hash returns Argon2id hash (starts with $argon2)
|
|
- SessionService.createSession returns a JWT string
|
|
- LoginService has lockout thresholds defined
|
|
</verify>
|
|
<done>
|
|
PasswordService hashes with Argon2id, SessionService creates/validates JWT tokens with DB backing, LoginService implements progressive lockout, all repositories created
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Create auth routes and middleware</name>
|
|
<files>
|
|
summercms/src/auth/middleware/AuthMiddleware.scala
|
|
summercms/src/auth/routes/LoginRoutes.scala
|
|
summercms/src/api/Routes.scala
|
|
summercms/src/Main.scala
|
|
</files>
|
|
<action>
|
|
1. Create AuthMiddleware.scala:
|
|
- Extract JWT from Authorization: Bearer header OR __Host-SID cookie
|
|
- Call SessionService.validateToken
|
|
- On success, make SessionClaims available to handlers
|
|
- On failure, return 401 Unauthorized with JSON error
|
|
|
|
2. Create LoginRoutes.scala with endpoints:
|
|
- POST /admin/auth/login
|
|
- Body: { email, password, totpCode?, rememberMe? }
|
|
- On success: Return { token, user: { id, email, firstName, lastName } }
|
|
- Set __Host-SID cookie (httpOnly, secure, sameSite=Strict, path=/admin)
|
|
- On lockout: Return 429 with Retry-After header
|
|
- On invalid: Return 401 with generic "Invalid email or password"
|
|
|
|
- POST /admin/auth/logout (requires auth)
|
|
- Revoke current session
|
|
- Clear __Host-SID cookie
|
|
- Return 200 OK
|
|
|
|
- GET /admin/auth/sessions (requires auth)
|
|
- List all sessions for current user
|
|
- Return array with id, createdAt, expiresAt, ipAddress, userAgent, current: boolean
|
|
|
|
- DELETE /admin/auth/sessions/:id (requires auth)
|
|
- Revoke specific session by ID
|
|
- Only allow revoking own sessions
|
|
- Return 204 No Content
|
|
|
|
3. Update Routes.scala to compose LoginRoutes (public) and protected routes (with AuthMiddleware)
|
|
|
|
4. Update Main.scala to provide all new service layers:
|
|
- PasswordService.live
|
|
- SessionService.live (needs AdminSessionRepository, AuthConfig)
|
|
- LoginService.live (needs AdminUserRepository, LoginAttemptRepository, PasswordService)
|
|
- AdminUserRepository.live, AdminSessionRepository.live, LoginAttemptRepository.live
|
|
|
|
Use zio.json for JSON encoding/decoding. Follow existing HealthRoutes.scala pattern for route structure.
|
|
|
|
Cookie configuration (per RESEARCH.md):
|
|
- name: __Host-SID (prefix for additional security)
|
|
- httpOnly: true
|
|
- secure: true (in production, allow http in dev)
|
|
- sameSite: Strict
|
|
- path: /admin
|
|
- maxAge: 24 hours (or 30 days with rememberMe)
|
|
</action>
|
|
<verify>
|
|
- ./mill summercms.compile succeeds
|
|
- curl -X POST localhost:8080/admin/auth/login with valid credentials returns token
|
|
- curl -X POST localhost:8080/admin/auth/logout with valid token returns 200
|
|
- curl -X GET localhost:8080/admin/auth/sessions with valid token returns session list
|
|
</verify>
|
|
<done>
|
|
Login returns JWT token and sets cookie, logout revokes session, session listing works, auth middleware protects routes
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
After all tasks complete:
|
|
1. Run ./mill summercms.compile - should succeed
|
|
2. Run migrations against database
|
|
3. Create a test admin user manually in DB (with hashed password from PasswordService)
|
|
4. Test login flow: POST /admin/auth/login with credentials
|
|
5. Test session listing: GET /admin/auth/sessions with token
|
|
6. Test logout: POST /admin/auth/logout
|
|
7. Verify token no longer works after logout
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Admin can log in with valid email/password and receive JWT token
|
|
- Login returns generic error for invalid credentials (no hints)
|
|
- Logout revokes the session in database
|
|
- Revoked token returns 401 on subsequent requests
|
|
- Progressive lockout kicks in after 5 failed attempts
|
|
- Sessions can be listed and individually revoked
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/06-backend-authentication/06-01-SUMMARY.md`
|
|
</output>
|