e8e89686f5
5 plans across 4 waves covering all 9 requirements (AUTH-01..06, TNNT-01..03) and 18 locked decisions (D-01..D-18): - Plan 01 (W1): Backend auth foundation with Prisma schema, RLS, JWT, admin seed - Plan 02 (W2): Frontend auth flow, login page, user/tenant CRUD admin pages - Plan 03 (W2): SUS package verification, password reset, force-change interceptor - Plan 04 (W3): LDAP sync service, per-tenant config, field mapping, admin UI - Plan 05 (W4): Visual verification of complete auth and multi-tenancy system Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
357 lines
24 KiB
Markdown
357 lines
24 KiB
Markdown
---
|
|
phase: 02-authentication-multi-tenancy
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- apps/api/prisma/schema.prisma
|
|
- apps/api/prisma/migrations/*
|
|
- apps/api/src/prisma/prisma.module.ts
|
|
- apps/api/src/prisma/prisma.service.ts
|
|
- apps/api/src/prisma/prisma-tenant.extension.ts
|
|
- apps/api/src/auth/auth.module.ts
|
|
- apps/api/src/auth/auth.controller.ts
|
|
- apps/api/src/auth/auth.service.ts
|
|
- apps/api/src/auth/strategies/local.strategy.ts
|
|
- apps/api/src/auth/strategies/jwt.strategy.ts
|
|
- apps/api/src/auth/guards/jwt-auth.guard.ts
|
|
- apps/api/src/auth/guards/roles.guard.ts
|
|
- apps/api/src/auth/decorators/public.decorator.ts
|
|
- apps/api/src/auth/decorators/roles.decorator.ts
|
|
- apps/api/src/auth/decorators/current-user.decorator.ts
|
|
- apps/api/src/auth/dto/login.dto.ts
|
|
- apps/api/src/user/user.module.ts
|
|
- apps/api/src/user/user.service.ts
|
|
- apps/api/src/user/admin-seed.service.ts
|
|
- apps/api/src/tenant/tenant.module.ts
|
|
- apps/api/src/tenant/tenant.service.ts
|
|
- apps/api/src/tenant/tenant.middleware.ts
|
|
- apps/api/src/app.module.ts
|
|
- apps/api/src/main.ts
|
|
- apps/api/package.json
|
|
- docker-compose.yml
|
|
- docker-compose.dev.yml
|
|
autonomous: true
|
|
requirements:
|
|
- AUTH-01
|
|
- AUTH-03
|
|
- AUTH-04
|
|
- AUTH-05
|
|
- TNNT-01
|
|
- TNNT-03
|
|
must_haves:
|
|
truths:
|
|
- "Initial admin account is created from Docker ENV on first startup (D-05, D-07, D-13)"
|
|
- "POST /auth/login with valid credentials returns httpOnly JWT cookie with 30-day expiry (D-02)"
|
|
- "POST /auth/logout clears the session cookie"
|
|
- "All non-public API routes reject requests without valid JWT (returns 401)"
|
|
- "RoleGuard enforces SUPER_ADMIN / ADMIN / USER access levels on protected routes (D-12)"
|
|
- "Every tenant-scoped database query is filtered by RLS using app.current_tenant session variable (D-11)"
|
|
- "Tenant ID is extracted from JWT claim and set per request via TenantMiddleware (D-08)"
|
|
artifacts:
|
|
- path: "apps/api/prisma/schema.prisma"
|
|
provides: "User, Tenant, PasswordResetToken, LdapConfig, LdapFieldMapping models with Role enum"
|
|
contains: "model User"
|
|
- path: "apps/api/src/auth/auth.service.ts"
|
|
provides: "Login validation, JWT issuance, cookie management"
|
|
exports: ["AuthService"]
|
|
- path: "apps/api/src/prisma/prisma-tenant.extension.ts"
|
|
provides: "Prisma Client Extension that sets app.current_tenant per query"
|
|
exports: ["forTenant"]
|
|
- path: "apps/api/src/tenant/tenant.middleware.ts"
|
|
provides: "Extracts tenantId from JWT, creates tenant-scoped Prisma client"
|
|
exports: ["TenantMiddleware"]
|
|
- path: "apps/api/src/user/admin-seed.service.ts"
|
|
provides: "Creates initial Super-Admin from ENV on bootstrap"
|
|
exports: ["AdminSeedService"]
|
|
key_links:
|
|
- from: "apps/api/src/auth/strategies/jwt.strategy.ts"
|
|
to: "apps/api/src/auth/guards/jwt-auth.guard.ts"
|
|
via: "Passport JWT strategy registered as global APP_GUARD"
|
|
pattern: "APP_GUARD.*JwtAuthGuard"
|
|
- from: "apps/api/src/tenant/tenant.middleware.ts"
|
|
to: "apps/api/src/prisma/prisma-tenant.extension.ts"
|
|
via: "Middleware calls forTenant() to create scoped client"
|
|
pattern: "forTenant"
|
|
- from: "apps/api/src/user/admin-seed.service.ts"
|
|
to: "apps/api/src/prisma/prisma.service.ts"
|
|
via: "onApplicationBootstrap lifecycle hook creates admin"
|
|
pattern: "onApplicationBootstrap"
|
|
---
|
|
|
|
<objective>
|
|
Backend authentication foundation: Prisma schema with User/Tenant/Role models, JWT httpOnly cookie auth via Passport strategies, global AuthGuard + RoleGuard, RLS tenant isolation via Prisma Client Extension, TenantMiddleware for per-request tenant context, and initial Super-Admin seed from Docker ENV.
|
|
|
|
This plan delivers AUTH-01 (admin seed per D-05/D-07/D-13), AUTH-03 (login/logout), AUTH-04 (30-day session per D-02), AUTH-05 (RBAC per D-12), TNNT-01 (RLS per D-11), TNNT-03 (tenant per request per D-08).
|
|
|
|
Purpose: Establish the entire backend auth and multi-tenancy infrastructure that all subsequent plans build on. Without this, no frontend auth or user management is possible.
|
|
|
|
Output: Working API with login/logout endpoints, global JWT protection, role-based guards, tenant-scoped database queries, and auto-seeded Super-Admin account.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/02-authentication-multi-tenancy/02-CONTEXT.md
|
|
@.planning/phases/02-authentication-multi-tenancy/02-RESEARCH.md
|
|
@.planning/phases/01-foundation-portal-shell/01-01-SUMMARY.md
|
|
@apps/api/prisma/schema.prisma
|
|
@apps/api/src/app.module.ts
|
|
@apps/api/src/main.ts
|
|
@apps/api/src/health/health.controller.ts
|
|
@apps/api/package.json
|
|
@docker-compose.yml
|
|
</context>
|
|
|
|
## Artifacts this phase produces
|
|
|
|
| Artifact | Plan | Purpose |
|
|
|----------|------|---------|
|
|
| Expanded Prisma schema (User, Tenant, Role, PasswordResetToken, LdapConfig, LdapFieldMapping) | 02-01 | Database foundation for all auth/tenant features |
|
|
| RLS migration SQL (policies, app role, current_tenant_id function) | 02-01 | Tenant data isolation at database level |
|
|
| PrismaModule + PrismaService (singleton) | 02-01 | Shared database access across all modules |
|
|
| AuthModule (Passport local + JWT strategies, guards, decorators) | 02-01 | Login/logout, JWT cookie auth, RBAC |
|
|
| TenantModule (middleware, service) | 02-01 | Per-request tenant context injection |
|
|
| UserModule (service, admin seed) | 02-01 | User operations and initial admin creation |
|
|
| Login page with split-screen layout | 02-02 | User-facing login UI per D-01 |
|
|
| Next.js middleware for route protection | 02-02 | Frontend auth gate per D-04 |
|
|
| User CRUD endpoints + frontend admin pages | 02-02 | User management per AUTH-02 |
|
|
| Tenant CRUD endpoints + admin pages | 02-02 | Tenant management per TNNT-02 |
|
|
| Auth-wired header and sidebar-footer | 02-02 | Show logged-in user info |
|
|
| MailModule + password reset flow | 02-03 | Self-service password reset per D-03 |
|
|
| Force-password-change interceptor | 02-03 | Per D-06 |
|
|
| LDAP sync service + config UI | 02-04 | LDAP import per AUTH-06, D-14..D-18 |
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Prisma schema expansion, RLS migration, and PrismaModule</name>
|
|
<read_first>
|
|
- apps/api/prisma/schema.prisma (current Tenant-only schema)
|
|
- apps/api/src/app.module.ts (current module imports)
|
|
- apps/api/package.json (current dependencies)
|
|
- .planning/phases/02-authentication-multi-tenancy/02-RESEARCH.md (Prisma schema section, RLS SQL section, Package Legitimacy Audit)
|
|
</read_first>
|
|
<files>
|
|
apps/api/prisma/schema.prisma
|
|
apps/api/prisma/migrations/[timestamp]_auth_multi_tenancy/migration.sql
|
|
apps/api/src/prisma/prisma.module.ts
|
|
apps/api/src/prisma/prisma.service.ts
|
|
apps/api/src/prisma/prisma-tenant.extension.ts
|
|
apps/api/package.json
|
|
docker-compose.yml
|
|
docker-compose.dev.yml
|
|
</files>
|
|
<action>
|
|
Install backend auth dependencies per RESEARCH.md standard stack. In apps/api run:
|
|
pnpm add @nestjs/jwt @nestjs/passport passport passport-jwt passport-local argon2 class-validator class-transformer
|
|
pnpm add -D @types/passport-jwt @types/passport-local
|
|
|
|
NOTE: Do NOT install ldapts, @nestjs-modules/mailer, or nodemailer yet -- those are SUS-flagged packages that require human verification in Plan 02-03.
|
|
|
|
Expand apps/api/prisma/schema.prisma with the full Phase 2 data model per RESEARCH.md code example:
|
|
- Role enum with SUPER_ADMIN, ADMIN, USER values
|
|
- Expand existing Tenant model: add isActive Boolean default true, add users User[] relation, add ldapConfig LdapConfig? relation
|
|
- User model with fields: id (uuid), username (unique), email (unique), passwordHash (String optional for LDAP users per A6), displayName (optional), role (Role default USER), isActive (Boolean default true), mustChangePassword (Boolean default false per D-06), ldapDn (optional), tenantId (String), tenant relation, createdAt, updatedAt, lastLoginAt (optional), passwordResetTokens relation. Indexes on tenantId, username, email.
|
|
- PasswordResetToken model: id (uuid), token (unique), userId, user relation with onDelete Cascade, expiresAt, usedAt (optional), createdAt
|
|
- LdapConfig model: id (uuid), tenantId (unique), tenant relation, serverUrl, baseDn, bindDn, bindPassword, searchFilter (default "(objectClass=person)"), syncIntervalMin (Int default 60), isActive (Boolean default true), lastSyncAt (optional), createdAt, updatedAt, fieldMappings relation
|
|
- LdapFieldMapping model: id (uuid), ldapConfigId, ldapConfig relation with onDelete Cascade, ldapField, tesseraField, isDefault (Boolean default false), createdAt. Unique constraint on [ldapConfigId, ldapField].
|
|
|
|
Add TESSERA_ADMIN_USER, TESSERA_ADMIN_EMAIL, TESSERA_ADMIN_PASSWORD, TESSERA_FORCE_CHANGE, and JWT_SECRET environment variables to docker-compose.yml api service per D-05.
|
|
Set JWT_SECRET to a development-only value like "tessera-dev-jwt-secret-change-in-production".
|
|
Add TESSERA_ADMIN_USER=admin, TESSERA_ADMIN_EMAIL=admin@tessera.local, TESSERA_ADMIN_PASSWORD=admin123 as defaults.
|
|
Add TESSERA_FORCE_CHANGE=false as default.
|
|
|
|
Run prisma migrate dev --name auth_multi_tenancy to generate migration.
|
|
|
|
After migration is generated, create a SECOND manual SQL migration for RLS setup. Create a new migration directory manually (prisma/migrations/[timestamp]_rls_policies/migration.sql) with:
|
|
- CREATE OR REPLACE FUNCTION current_tenant_id() that returns current_setting('app.current_tenant', true)
|
|
- ALTER TABLE "User" ENABLE ROW LEVEL SECURITY and FORCE ROW LEVEL SECURITY
|
|
- CREATE POLICY tenant_isolation_policy ON "User" USING (tenant_id = current_tenant_id()::uuid) -- note: Prisma maps tenantId to "tenantId" column, verify the actual column name from the generated migration and use that exact name
|
|
- Same RLS for PasswordResetToken (via user join or direct tenant_id -- use the approach that matches schema)
|
|
- Same RLS for LdapConfig and LdapFieldMapping (via tenantId)
|
|
|
|
IMPORTANT for RLS SQL: Use parameterized set_config, never string interpolation per RESEARCH.md anti-pattern guidance. The current_tenant_id() function uses current_setting which is safe.
|
|
|
|
Create apps/api/src/prisma/prisma.module.ts as a Global NestJS module exporting PrismaService.
|
|
Create apps/api/src/prisma/prisma.service.ts extending PrismaClient, implementing OnModuleInit (call this.$connect in onModuleInit). Register as injectable singleton.
|
|
Create apps/api/src/prisma/prisma-tenant.extension.ts with the forTenant function per RESEARCH.md Pattern 2: accepts PrismaClient and tenantId string, returns extended client that wraps $allOperations in a transaction calling SET app.current_tenant via $executeRawUnsafe with set_config. Use parameterized query: SELECT set_config('app.current_tenant', $1, true) with the tenantId as parameter to avoid SQL injection.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/vicolab/projects/tessera-ctl/apps/api && npx prisma validate && npx prisma migrate status</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- schema.prisma contains all 6 models (Tenant, User, Role enum, PasswordResetToken, LdapConfig, LdapFieldMapping)
|
|
- Migration files exist under prisma/migrations/
|
|
- RLS migration SQL contains ENABLE ROW LEVEL SECURITY for User table
|
|
- PrismaModule is @Global() and exports PrismaService
|
|
- prisma-tenant.extension.ts exports forTenant function
|
|
- docker-compose.yml api service has TESSERA_ADMIN_USER, TESSERA_ADMIN_EMAIL, TESSERA_ADMIN_PASSWORD, JWT_SECRET env vars
|
|
</acceptance_criteria>
|
|
<done>Prisma schema has all Phase 2 models, migrations are generated, RLS policies are in migration SQL, PrismaModule/PrismaService/forTenant extension exist, Docker env vars configured.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: AuthModule with Passport strategies, guards, and admin seed</name>
|
|
<read_first>
|
|
- apps/api/src/prisma/prisma.module.ts (from Task 1)
|
|
- apps/api/src/prisma/prisma.service.ts (from Task 1)
|
|
- apps/api/src/prisma/prisma-tenant.extension.ts (from Task 1)
|
|
- apps/api/prisma/schema.prisma (expanded schema from Task 1)
|
|
- .planning/phases/02-authentication-multi-tenancy/02-RESEARCH.md (Pattern 1: JWT auth, Pattern 3: Global guard, Pattern 5: Tenant middleware, admin seed example, security domain)
|
|
</read_first>
|
|
<files>
|
|
apps/api/src/auth/auth.module.ts
|
|
apps/api/src/auth/auth.controller.ts
|
|
apps/api/src/auth/auth.service.ts
|
|
apps/api/src/auth/strategies/local.strategy.ts
|
|
apps/api/src/auth/strategies/jwt.strategy.ts
|
|
apps/api/src/auth/guards/jwt-auth.guard.ts
|
|
apps/api/src/auth/guards/roles.guard.ts
|
|
apps/api/src/auth/decorators/public.decorator.ts
|
|
apps/api/src/auth/decorators/roles.decorator.ts
|
|
apps/api/src/auth/decorators/current-user.decorator.ts
|
|
apps/api/src/auth/dto/login.dto.ts
|
|
apps/api/src/user/user.module.ts
|
|
apps/api/src/user/user.service.ts
|
|
apps/api/src/user/admin-seed.service.ts
|
|
apps/api/src/tenant/tenant.module.ts
|
|
apps/api/src/tenant/tenant.service.ts
|
|
apps/api/src/tenant/tenant.middleware.ts
|
|
apps/api/src/app.module.ts
|
|
apps/api/src/main.ts
|
|
</files>
|
|
<action>
|
|
Create the AuthModule per RESEARCH.md architecture:
|
|
|
|
auth/dto/login.dto.ts: LoginDto class with username (string, @IsNotEmpty) and password (string, @IsNotEmpty) validated via class-validator decorators.
|
|
|
|
auth/decorators/public.decorator.ts: Export IS_PUBLIC_KEY constant and Public() decorator using SetMetadata per RESEARCH.md Pattern 3.
|
|
auth/decorators/roles.decorator.ts: Export ROLES_KEY constant and Roles(...roles: Role[]) decorator using SetMetadata. Import Role enum from @prisma/client.
|
|
auth/decorators/current-user.decorator.ts: Export CurrentUser parameter decorator using createParamDecorator that extracts user from request object.
|
|
|
|
auth/strategies/local.strategy.ts: PassportLocalStrategy extending PassportStrategy(Strategy) from passport-local. The validate method receives username and password, calls AuthService.validateUser, throws UnauthorizedException if null.
|
|
|
|
auth/strategies/jwt.strategy.ts: JwtStrategy extending PassportStrategy(Strategy) from passport-jwt. Configure to extract JWT from cookie named "session" (use a custom extractor function that reads req.cookies.session). The secretOrKey comes from ConfigService JWT_SECRET. The validate method receives the decoded payload and returns the user object (sub, username, role, tenantId).
|
|
|
|
auth/guards/jwt-auth.guard.ts: JwtAuthGuard extending AuthGuard('jwt'). Override canActivate to check IS_PUBLIC_KEY metadata via Reflector -- if @Public(), return true, otherwise delegate to super.canActivate. Per RESEARCH.md Pattern 3.
|
|
auth/guards/roles.guard.ts: RolesGuard implementing CanActivate. Use Reflector to read ROLES_KEY. If no roles set, allow. Otherwise check if request.user.role is in the required roles array. Per D-12, roles are SUPER_ADMIN, ADMIN, USER.
|
|
|
|
auth/auth.service.ts: AuthService injectable. Dependencies: PrismaService, JwtService, ConfigService. Methods:
|
|
- validateUser(username, password): Find user by username (use unscoped prisma -- admin seed runs before tenant context exists), check isActive, verify password with argon2.verify, return user or null. Per Pitfall 6, always check isActive.
|
|
- login(user, response): Build JWT payload with sub=user.id, username=user.username, role=user.role, tenantId=user.tenantId. Sign with JwtService using expiresIn '30d' per D-02. Set httpOnly cookie named "session" on response: httpOnly true, secure only in production, sameSite 'lax', maxAge 30*24*60*60*1000, path '/'. Return user info object (id, username, role, displayName, tenantId, mustChangePassword).
|
|
- logout(response): Clear the "session" cookie with same path and domain settings.
|
|
|
|
auth/auth.controller.ts: AuthController with prefix 'auth'.
|
|
- POST /auth/login: @Public() decorated, @UseGuards(AuthGuard('local')). Receives LoginDto body and @Req() request, @Res({ passthrough: true }) response. Calls authService.login(request.user, response). Returns the user info. Uses @HttpCode(200).
|
|
- POST /auth/logout: Calls authService.logout(response). Returns { message: 'Logged out' }.
|
|
- GET /auth/me: Returns request.user from JWT (for session check).
|
|
|
|
auth/auth.module.ts: Imports JwtModule.registerAsync with useFactory reading JWT_SECRET from ConfigService, signOptions expiresIn '30d'. Imports PassportModule. Imports UserModule. Providers: AuthService, LocalStrategy, JwtStrategy. Controllers: AuthController. Exports: AuthService.
|
|
|
|
user/user.service.ts: UserService injectable. Dependency: PrismaService. Methods:
|
|
- findByUsername(username): prisma.user.findUnique where username. Uses UNSCOPED prisma (not tenant-scoped) because login must work across tenants.
|
|
- findById(id): prisma.user.findUnique where id.
|
|
- create(data): prisma.user.create with argon2.hash for password.
|
|
- update(id, data): prisma.user.update. If data includes password, hash it.
|
|
- deactivate(id): prisma.user.update set isActive false.
|
|
- delete(id): prisma.user.delete.
|
|
|
|
user/admin-seed.service.ts: AdminSeedService implementing OnApplicationBootstrap per RESEARCH.md code example. In onApplicationBootstrap: read TESSERA_ADMIN_USER, TESSERA_ADMIN_EMAIL, TESSERA_ADMIN_PASSWORD, TESSERA_FORCE_CHANGE from ConfigService. If any of user/email/password missing, skip. Check if user exists by username. If exists, skip. Upsert default tenant (slug 'default', name 'Default'). Create user with role SUPER_ADMIN, password hashed with argon2, mustChangePassword from TESSERA_FORCE_CHANGE (default false), tenantId from default tenant. Per D-05, D-07, D-13.
|
|
|
|
user/user.module.ts: Providers: UserService, AdminSeedService. Exports: UserService.
|
|
|
|
tenant/tenant.service.ts: TenantService injectable. Dependency: PrismaService. Methods:
|
|
- findAll(): prisma.tenant.findMany.
|
|
- findById(id): prisma.tenant.findUnique.
|
|
- create(data): prisma.tenant.create.
|
|
- update(id, data): prisma.tenant.update.
|
|
|
|
tenant/tenant.middleware.ts: TenantMiddleware implementing NestMiddleware per RESEARCH.md Pattern 5. Dependency: PrismaService. In use(req, res, next): extract user from req.user (set by JWT auth). If no user, call next (public routes). Determine tenantId: if user.role is SUPER_ADMIN and x-tenant-id header exists, use header value; otherwise use user.tenantId. If no tenantId and not SUPER_ADMIN, throw ForbiddenException. Attach tenant-scoped prisma client to req (req.tenantPrisma = forTenant(prisma, tenantId) when tenantId exists, unscoped prisma for Super-Admin without header). Attach req.tenantId. Call next(). Per D-08, D-10.
|
|
|
|
tenant/tenant.module.ts: Providers: TenantService. Exports: TenantService.
|
|
|
|
Update app.module.ts: Import PrismaModule, AuthModule, UserModule, TenantModule. Register JwtAuthGuard as global APP_GUARD provider. Apply TenantMiddleware to all routes via configure method (implement NestModule, apply TenantMiddleware forRoutes('*')). Note: TenantMiddleware runs AFTER the AuthGuard, so req.user is available.
|
|
|
|
Update main.ts: Enable ValidationPipe globally with whitelist true and transform true. Update CORS to specify origin from ConfigService (default 'http://localhost:3000') and credentials true per Pitfall 4. Add cookie-parser middleware (install cookie-parser: pnpm add cookie-parser && pnpm add -D @types/cookie-parser). Call app.use(cookieParser()) before listen.
|
|
|
|
Mark health controller's check method with @Public() decorator so it remains accessible without auth.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm turbo type-check --filter=@tessera/api</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- AuthModule registers JwtAuthGuard as global APP_GUARD
|
|
- POST /auth/login endpoint exists with @Public() decorator
|
|
- POST /auth/logout endpoint clears session cookie
|
|
- GET /auth/me returns user from JWT
|
|
- AdminSeedService creates Super-Admin from ENV on bootstrap per D-05/D-07/D-13
|
|
- TenantMiddleware extracts tenantId from JWT and supports Super-Admin tenant switching via x-tenant-id header per D-08/D-10
|
|
- RolesGuard checks SUPER_ADMIN/ADMIN/USER roles per D-12
|
|
- UserService.findByUsername uses unscoped Prisma (not tenant-scoped) for cross-tenant login
|
|
- ValidationPipe with whitelist and transform enabled globally
|
|
- CORS configured with credentials true per Pitfall 4
|
|
- Health endpoint has @Public() decorator
|
|
- Type-check passes cleanly
|
|
</acceptance_criteria>
|
|
<done>Full backend auth stack operational: login returns JWT cookie, global guard protects all routes except @Public(), admin auto-seeded from ENV, tenant context injected per request via RLS.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| Browser -> Next.js | Untrusted user input (login form, admin forms) |
|
|
| Next.js -> NestJS API | Optimistic frontend auth check; API must validate independently |
|
|
| NestJS API -> PostgreSQL | RLS policies enforce tenant isolation at DB level |
|
|
| JWT cookie | Session token crosses browser/server boundary on every request |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-02-01 | Spoofing | auth/login endpoint | mitigate | argon2 password verification; never reveal whether username or password is wrong (generic "Invalid credentials") |
|
|
| T-02-02 | Tampering | JWT cookie | mitigate | httpOnly + secure (prod) + sameSite=lax; signed with HS256 via @nestjs/jwt |
|
|
| T-02-03 | Information Disclosure | Cross-tenant data | mitigate | PostgreSQL RLS policies on all tenant-scoped tables; forTenant extension sets session variable per query |
|
|
| T-02-04 | Elevation of Privilege | RolesGuard | mitigate | Role checked from JWT claim; guard validates against required roles per route |
|
|
| T-02-05 | Tampering | SQL injection via RLS set_config | mitigate | Use parameterized $executeRawUnsafe with $1 placeholder, never string interpolation |
|
|
| T-02-06 | Denial of Service | login endpoint brute force | accept | Rate limiting deferred -- low risk in internal deployment; add @nestjs/throttler in future phase if needed |
|
|
| T-02-07 | Spoofing | Deactivated user JWT still valid | mitigate | AuthGuard checks user.isActive in DB on every request per Pitfall 6; short cache TTL acceptable |
|
|
| T-02-SC | Tampering | npm installs | mitigate | Package Legitimacy Audit in RESEARCH.md; blocking human checkpoint for SUS packages (ldapts, @nestjs-modules/mailer, nodemailer) in Plan 02-03 |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
After both tasks complete:
|
|
1. docker compose up -d and verify API starts without errors
|
|
2. Check logs for "Admin seeded" or equivalent bootstrap message
|
|
3. curl -X POST http://localhost:3001/auth/login -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123"}' -c cookies.txt returns 200 with user info and Set-Cookie header
|
|
4. curl http://localhost:3001/auth/me -b cookies.txt returns admin user data
|
|
5. curl http://localhost:3001/health returns 200 (public route)
|
|
6. curl http://localhost:3001/auth/me (no cookie) returns 401
|
|
7. pnpm turbo type-check passes for all packages
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- API boots and auto-seeds Super-Admin from Docker ENV (AUTH-01 per D-05/D-07/D-13)
|
|
- Login endpoint validates credentials and returns 30-day httpOnly JWT cookie (AUTH-03, AUTH-04 per D-02)
|
|
- All non-@Public() routes require valid JWT (AUTH-05)
|
|
- RolesGuard enforces SUPER_ADMIN/ADMIN/USER levels (AUTH-05 per D-12)
|
|
- Tenant-scoped queries use RLS via Prisma Client Extension (TNNT-01 per D-11)
|
|
- TenantMiddleware sets tenant context from JWT per request (TNNT-03 per D-08)
|
|
- Type-check passes across all workspace packages
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/02-authentication-multi-tenancy/02-01-SUMMARY.md` when done
|
|
</output>
|