docs(02): create phase 2 authentication & multi-tenancy plans
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>
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
---
|
||||
phase: 02-authentication-multi-tenancy
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 02-01
|
||||
files_modified:
|
||||
- apps/api/src/mail/mail.module.ts
|
||||
- apps/api/src/mail/mail.service.ts
|
||||
- apps/api/src/auth/auth.service.ts
|
||||
- apps/api/src/auth/auth.controller.ts
|
||||
- apps/api/src/auth/dto/reset-password.dto.ts
|
||||
- apps/api/src/auth/dto/change-password.dto.ts
|
||||
- apps/api/src/auth/interceptors/force-password-change.interceptor.ts
|
||||
- apps/api/src/app.module.ts
|
||||
- apps/api/package.json
|
||||
- apps/web/src/app/(auth)/reset-password/page.tsx
|
||||
- apps/web/src/app/(auth)/reset-password/[token]/page.tsx
|
||||
- apps/web/src/app/(portal)/change-password/page.tsx
|
||||
- apps/web/src/messages/de.json
|
||||
- apps/web/src/messages/en.json
|
||||
- docker-compose.yml
|
||||
- docker-compose.dev.yml
|
||||
autonomous: false
|
||||
requirements:
|
||||
- AUTH-03
|
||||
- AUTH-04
|
||||
user_setup:
|
||||
- service: smtp
|
||||
why: "Password reset emails require SMTP configuration"
|
||||
env_vars:
|
||||
- name: TESSERA_SMTP_HOST
|
||||
source: "Your SMTP server hostname"
|
||||
- name: TESSERA_SMTP_PORT
|
||||
source: "SMTP port (587 for STARTTLS, 465 for SSL)"
|
||||
- name: TESSERA_SMTP_USER
|
||||
source: "SMTP authentication username"
|
||||
- name: TESSERA_SMTP_PASSWORD
|
||||
source: "SMTP authentication password"
|
||||
- name: TESSERA_SMTP_FROM
|
||||
source: "Sender email address"
|
||||
dashboard_config:
|
||||
- task: "For development, MailHog is added to docker-compose.dev.yml automatically (no config needed)"
|
||||
location: "http://localhost:8025 for MailHog web UI"
|
||||
must_haves:
|
||||
truths:
|
||||
- "SUS-flagged packages (ldapts, @nestjs-modules/mailer, nodemailer) are verified by human before installation"
|
||||
- "User can request password reset via email with a time-limited token (D-03)"
|
||||
- "Admin can manually reset a user's password (D-03)"
|
||||
- "User with mustChangePassword=true is forced to change password before accessing any other page (D-06)"
|
||||
- "Password reset token expires after 1 hour and can only be used once"
|
||||
- "MailHog is available in development docker-compose for email testing"
|
||||
artifacts:
|
||||
- path: "apps/api/src/mail/mail.module.ts"
|
||||
provides: "NestJS mailer module with SMTP transport from ENV"
|
||||
exports: ["MailModule"]
|
||||
- path: "apps/api/src/mail/mail.service.ts"
|
||||
provides: "Email sending for password reset"
|
||||
exports: ["MailService"]
|
||||
- path: "apps/api/src/auth/interceptors/force-password-change.interceptor.ts"
|
||||
provides: "Global interceptor checking mustChangePassword"
|
||||
exports: ["ForcePasswordChangeInterceptor"]
|
||||
- path: "apps/web/src/app/(auth)/reset-password/page.tsx"
|
||||
provides: "Password reset request form"
|
||||
min_lines: 30
|
||||
- path: "apps/web/src/app/(auth)/reset-password/[token]/page.tsx"
|
||||
provides: "Password reset form with token validation"
|
||||
min_lines: 40
|
||||
key_links:
|
||||
- from: "apps/api/src/auth/auth.service.ts"
|
||||
to: "apps/api/src/mail/mail.service.ts"
|
||||
via: "Send password reset email with token link"
|
||||
pattern: "mailService.*sendPasswordReset"
|
||||
- from: "apps/api/src/auth/interceptors/force-password-change.interceptor.ts"
|
||||
to: "apps/api/src/auth/auth.controller.ts"
|
||||
via: "Returns 403 FORCE_PASSWORD_CHANGE on all routes except change-password"
|
||||
pattern: "FORCE_PASSWORD_CHANGE"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Package legitimacy verification for SUS-flagged packages, then password reset flow (self-service via email and admin manual reset per D-03), force-password-change interceptor (D-06), and MailModule with SMTP configuration.
|
||||
|
||||
This plan addresses security-critical password management: D-03 (password reset via email + admin reset), D-06 (force change on first login), and the package legitimacy gate for SUS packages identified in RESEARCH.md.
|
||||
|
||||
Purpose: Complete the authentication lifecycle -- users who forget passwords can self-recover, admins can force password changes, and the first login experience respects D-06.
|
||||
|
||||
Output: Working password reset email flow, force-change interceptor, MailHog in dev compose, verified SUS packages installed.
|
||||
</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/02-authentication-multi-tenancy/02-01-SUMMARY.md
|
||||
@apps/api/src/auth/auth.service.ts
|
||||
@apps/api/src/auth/auth.controller.ts
|
||||
@apps/api/src/app.module.ts
|
||||
@apps/api/package.json
|
||||
@docker-compose.yml
|
||||
@docker-compose.dev.yml
|
||||
</context>
|
||||
|
||||
## Artifacts this phase produces
|
||||
|
||||
See Plan 02-01 for the full artifacts table.
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 1: Verify SUS-flagged npm packages before installation</name>
|
||||
<what-built>
|
||||
RESEARCH.md Package Legitimacy Audit flagged three packages as SUS (too-new publish dates on well-established packages):
|
||||
- ldapts (6+ yrs, 400K/wk downloads) -- flagged for recent publish
|
||||
- @nestjs-modules/mailer (5+ yrs, 350K/wk downloads) -- flagged for recent publish
|
||||
- nodemailer (14+ yrs, 17M/wk downloads) -- flagged for recent publish
|
||||
|
||||
All three are well-established packages with millions of cumulative downloads. The SUS flag is due to routine version updates, not suspicious origin. However, per the supply chain security protocol, human verification is required before installation.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
Please verify each package is legitimate by checking their npm pages:
|
||||
|
||||
1. **ldapts**: Visit https://www.npmjs.com/package/ldapts
|
||||
- Verify publisher is the expected maintainer (ldapts org)
|
||||
- Verify recent publish is a routine version update (not a hijack)
|
||||
- Check for any security advisories
|
||||
|
||||
2. **@nestjs-modules/mailer**: Visit https://www.npmjs.com/package/@nestjs-modules/mailer
|
||||
- Verify publisher is nest-modules org
|
||||
- Verify recent publish is routine
|
||||
- Check for any security advisories
|
||||
|
||||
3. **nodemailer**: Visit https://www.npmjs.com/package/nodemailer
|
||||
- Verify publisher is andris (Andris Reinman, original author)
|
||||
- Verify recent publish is routine
|
||||
- Check for any security advisories
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to proceed with installation, or describe any concerns found</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: MailModule, password reset flow, force-change interceptor, and frontend pages</name>
|
||||
<read_first>
|
||||
- apps/api/src/auth/auth.service.ts (existing auth service from Plan 02-01)
|
||||
- apps/api/src/auth/auth.controller.ts (existing auth controller)
|
||||
- apps/api/src/app.module.ts (current module configuration)
|
||||
- apps/api/prisma/schema.prisma (PasswordResetToken model)
|
||||
- apps/web/src/app/(auth)/layout.tsx (auth layout from Plan 02-02)
|
||||
- .planning/phases/02-authentication-multi-tenancy/02-RESEARCH.md (SMTP ENV vars, security domain, Pitfall 5)
|
||||
</read_first>
|
||||
<files>
|
||||
apps/api/src/mail/mail.module.ts
|
||||
apps/api/src/mail/mail.service.ts
|
||||
apps/api/src/auth/auth.service.ts
|
||||
apps/api/src/auth/auth.controller.ts
|
||||
apps/api/src/auth/dto/reset-password.dto.ts
|
||||
apps/api/src/auth/dto/change-password.dto.ts
|
||||
apps/api/src/auth/interceptors/force-password-change.interceptor.ts
|
||||
apps/api/src/app.module.ts
|
||||
apps/api/package.json
|
||||
apps/web/src/app/(auth)/reset-password/page.tsx
|
||||
apps/web/src/app/(auth)/reset-password/[token]/page.tsx
|
||||
apps/web/src/app/(portal)/change-password/page.tsx
|
||||
apps/web/src/messages/de.json
|
||||
apps/web/src/messages/en.json
|
||||
docker-compose.yml
|
||||
docker-compose.dev.yml
|
||||
</files>
|
||||
<action>
|
||||
First, install the SUS-flagged packages (approved by human in Task 1):
|
||||
cd apps/api && pnpm add @nestjs-modules/mailer nodemailer ldapts
|
||||
cd apps/api && pnpm add -D @types/nodemailer
|
||||
|
||||
Add MailHog service to docker-compose.dev.yml for development email testing:
|
||||
mailhog service using mailhog/mailhog image, ports 1025:1025 (SMTP) and 8025:8025 (web UI), on backend-net.
|
||||
Add TESSERA_SMTP_HOST, TESSERA_SMTP_PORT, TESSERA_SMTP_USER, TESSERA_SMTP_PASSWORD, TESSERA_SMTP_FROM, TESSERA_APP_URL env vars to docker-compose.yml api service with defaults pointing to MailHog: host=mailhog, port=1025, user empty, password empty, from="Tessera <tessera@tessera.local>", app_url=http://localhost:3000.
|
||||
|
||||
Create apps/api/src/mail/mail.module.ts:
|
||||
- Import MailerModule.forRootAsync from @nestjs-modules/mailer
|
||||
- Configure with useFactory reading TESSERA_SMTP_HOST, TESSERA_SMTP_PORT, TESSERA_SMTP_SECURE, TESSERA_SMTP_USER, TESSERA_SMTP_PASSWORD from ConfigService
|
||||
- Set defaults.from from TESSERA_SMTP_FROM env var
|
||||
- Export the module globally
|
||||
|
||||
Create apps/api/src/mail/mail.service.ts:
|
||||
- Injectable service wrapping MailerService from @nestjs-modules/mailer
|
||||
- Method sendPasswordResetEmail(email, token, locale): Compose email with subject (i18n-aware: "Passwort zuruecksetzen" / "Reset your password"), body containing the reset link: TESSERA_APP_URL/reset-password/TOKEN. Use plain text email (no HTML template needed for MVP). Include expiry notice (1 hour).
|
||||
- Method sendWelcomeEmail(email, username, locale): Optional welcome email for newly created users.
|
||||
|
||||
Create apps/api/src/auth/dto/reset-password.dto.ts:
|
||||
- RequestResetDto: email (@IsEmail, @IsNotEmpty)
|
||||
- ResetPasswordDto: token (@IsString, @IsNotEmpty), newPassword (@IsString, @MinLength(8))
|
||||
|
||||
Create apps/api/src/auth/dto/change-password.dto.ts:
|
||||
- ChangePasswordDto: currentPassword (@IsString), newPassword (@IsString, @MinLength(8))
|
||||
|
||||
Extend apps/api/src/auth/auth.service.ts with new methods:
|
||||
- requestPasswordReset(email): Find user by email. If not found, return success anyway (prevent email enumeration). Generate a random UUID token, create PasswordResetToken record with 1-hour expiry. Send email via MailService. Per D-03 self-service reset.
|
||||
- resetPassword(token, newPassword): Find PasswordResetToken by token. Validate not expired and not used. Hash new password with argon2, update user. Mark token as used (set usedAt). Set mustChangePassword to false.
|
||||
- changePassword(userId, currentPassword, newPassword): Verify current password with argon2. Hash new password. Update user. Set mustChangePassword to false.
|
||||
- adminResetPassword(userId, newPassword): Per D-03 admin reset. Hash new password, update user, optionally set mustChangePassword to true.
|
||||
|
||||
Extend apps/api/src/auth/auth.controller.ts with new endpoints:
|
||||
- POST /auth/request-reset: @Public(). Body: RequestResetDto. Calls requestPasswordReset. Always returns 200 (no email enumeration).
|
||||
- POST /auth/reset-password: @Public(). Body: ResetPasswordDto. Calls resetPassword. Returns success or error.
|
||||
- POST /auth/change-password: Body: ChangePasswordDto + @CurrentUser. Calls changePassword.
|
||||
- POST /auth/admin-reset-password/:userId: @Roles(Role.ADMIN, Role.SUPER_ADMIN) @UseGuards(RolesGuard). Calls adminResetPassword.
|
||||
|
||||
Create apps/api/src/auth/interceptors/force-password-change.interceptor.ts per Pitfall 5:
|
||||
- Global NestJS interceptor implementing NestInterceptor
|
||||
- In intercept(): Check if request.user exists and user.mustChangePassword is true
|
||||
- If true AND the current route is NOT /auth/change-password and NOT /auth/logout, throw ForbiddenException with body { statusCode: 403, message: 'FORCE_PASSWORD_CHANGE', error: 'Must change password before continuing' }
|
||||
- Register as global APP_INTERCEPTOR in app.module.ts
|
||||
|
||||
Update app.module.ts: Import MailModule. Register ForcePasswordChangeInterceptor as global APP_INTERCEPTOR.
|
||||
|
||||
Create apps/web/src/app/(auth)/reset-password/page.tsx:
|
||||
- 'use client' form page in the (auth) route group (no sidebar/header per D-04)
|
||||
- Simple form with email input and submit button
|
||||
- On submit, POST to /auth/request-reset
|
||||
- Show success message: "Falls ein Konto mit dieser E-Mail existiert, wurde ein Link gesendet" (prevents email enumeration)
|
||||
- Link back to /login
|
||||
- All strings via useTranslations('auth')
|
||||
|
||||
Create apps/web/src/app/(auth)/reset-password/[token]/page.tsx:
|
||||
- 'use client' form page
|
||||
- Form with new password and confirm password fields
|
||||
- On submit, POST to /auth/reset-password with token from URL params and new password
|
||||
- Show success or error message
|
||||
- On success, redirect to /login after 3 seconds
|
||||
- All strings via useTranslations('auth')
|
||||
|
||||
Create apps/web/src/app/(portal)/change-password/page.tsx:
|
||||
- 'use client' form page inside (portal) route group (has sidebar/header)
|
||||
- Form with current password, new password, confirm password
|
||||
- On submit, POST to /auth/change-password
|
||||
- On success, update auth store (clear mustChangePassword) and redirect to dashboard
|
||||
- All strings via useTranslations('auth')
|
||||
|
||||
Add i18n keys to de.json and en.json:
|
||||
- auth.resetPassword.title, auth.resetPassword.email, auth.resetPassword.submit, auth.resetPassword.success, auth.resetPassword.newPassword, auth.resetPassword.confirmPassword, auth.resetPassword.tokenExpired, auth.resetPassword.tokenInvalid, auth.resetPassword.success
|
||||
- auth.changePassword.title, auth.changePassword.currentPassword, auth.changePassword.newPassword, auth.changePassword.confirmPassword, auth.changePassword.submit, auth.changePassword.success, auth.changePassword.forceChangeNotice
|
||||
- auth.forgotPassword (link text on login page)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm turbo type-check</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- ldapts, @nestjs-modules/mailer, and nodemailer installed in apps/api/package.json
|
||||
- MailModule configures SMTP from ENV variables
|
||||
- POST /auth/request-reset sends password reset email (testable via MailHog)
|
||||
- POST /auth/reset-password validates token and resets password
|
||||
- POST /auth/change-password validates current password and changes to new
|
||||
- POST /auth/admin-reset-password/:userId allows ADMIN/SUPER_ADMIN to reset user passwords per D-03
|
||||
- ForcePasswordChangeInterceptor returns 403 FORCE_PASSWORD_CHANGE on all routes except change-password and logout per D-06
|
||||
- MailHog service added to docker-compose.dev.yml on port 8025
|
||||
- Reset password page exists at /reset-password with email form
|
||||
- Token reset page exists at /reset-password/[token] with new password form
|
||||
- Change password page exists at /change-password with current + new password form
|
||||
- All strings internationalized in de.json and en.json
|
||||
- Type-check passes for all packages
|
||||
</acceptance_criteria>
|
||||
<done>Password reset via email works end-to-end (D-03), admin can manually reset passwords (D-03), force-password-change interceptor enforces D-06, MailHog available for dev email testing.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Email link -> reset form | Token in URL must be validated server-side |
|
||||
| Public reset endpoint | Must not reveal whether email exists (enumeration prevention) |
|
||||
| npm registry -> project | SUS packages verified by human before install |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-02-12 | Information Disclosure | /auth/request-reset | mitigate | Always return 200 regardless of email existence; prevents enumeration |
|
||||
| T-02-13 | Repudiation | Password reset token | mitigate | Single-use token (mark usedAt after use); 1-hour expiry; UUID token not guessable |
|
||||
| T-02-14 | Elevation of Privilege | Force password change bypass | mitigate | Global interceptor on ALL routes except /auth/change-password and /auth/logout per Pitfall 5 |
|
||||
| T-02-15 | Tampering | Admin password reset | mitigate | Only ADMIN/SUPER_ADMIN via RolesGuard; ADMIN cannot reset SUPER_ADMIN passwords |
|
||||
| T-02-SC | Tampering | npm installs (ldapts, mailer, nodemailer) | mitigate | Blocking human checkpoint verifies package legitimacy before installation |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After both tasks complete:
|
||||
1. docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
||||
2. Login as admin, navigate to /change-password, change password -- should succeed
|
||||
3. With TESSERA_FORCE_CHANGE=true, new login should redirect to /change-password
|
||||
4. Request password reset via /reset-password, check MailHog at http://localhost:8025 for email
|
||||
5. Click reset link in email, set new password, login with new password
|
||||
6. Admin resets another user's password via API
|
||||
7. pnpm turbo type-check passes
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- SUS packages verified and installed (supply chain security gate passed)
|
||||
- Password reset via email works end-to-end with MailHog in dev (D-03)
|
||||
- Admin can manually reset user passwords (D-03)
|
||||
- Force-password-change interceptor blocks all routes until password changed (D-06)
|
||||
- Reset tokens are single-use with 1-hour expiry
|
||||
- No email enumeration possible via reset endpoint
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/02-authentication-multi-tenancy/02-03-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user