--- description: "Task list for feature 063 — Entra Sign-in (Tenant Panel) v1" --- # Tasks: 063 — Entra Sign-in (Tenant Panel) v1 **Input**: Design documents from `/specs/063-entra-signin/` **Prerequisites**: `plan.md`, `spec.md` **Tests**: REQUIRED (Pest) **Notes / Guardrails** - 063 covers **/admin** Entra sign-in only. - No `/system` changes and **no break-glass UX** on `/admin/login`. - Login is synchronous and must not require a queue worker. - DB-only render/hydration scope: `/admin/login`, `/admin/no-access`, `/admin/choose-tenant`. - Outbound HTTP is allowed only for the OIDC exchange on `/auth/entra/*` endpoints. --- ## Phase 1: Setup (Shared Infrastructure) - [x] T001 Configure Microsoft (Entra) OIDC provider in `config/services.php` under key `microsoft` using env vars `ENTRA_CLIENT_ID`, `ENTRA_CLIENT_SECRET`, `ENTRA_REDIRECT_URI`, and optional `ENTRA_AUTHORITY_TENANT` (default `organizations`). - [x] T002 Add `ENTRA_CLIENT_ID`, `ENTRA_CLIENT_SECRET`, `ENTRA_REDIRECT_URI`, and `ENTRA_AUTHORITY_TENANT` to `.env.example` (no break-glass vars in 063). --- ## Phase 2: Foundational (Blocking Prerequisites) - [x] T003 Verify `users` has `entra_tenant_id` and `entra_object_id` (prefer existing types; if missing, add nullable `string(255)` columns) + a unique index on (entra_tenant_id, entra_object_id). Keep columns nullable in v1; enforce non-null only after explicit backfill/migration. - [x] T004 Apply migrations via Sail: `./vendor/bin/sail artisan migrate`. - [x] T005 Create `app/Http/Controllers/Auth/EntraController.php` to handle `/auth/entra/redirect` and `/auth/entra/callback` only (NoAccess and Chooser are Filament pages). - [x] T006 Add routes for `/auth/entra/redirect` and `/auth/entra/callback` in `routes/web.php` with route names `auth.entra.redirect` and `auth.entra.callback`. - [x] T007 Define a dedicated rate limiter for `auth.entra.callback` in `app/Providers/AppServiceProvider.php` using `RateLimiter::for('entra-callback', ...)` and apply `->middleware('throttle:entra-callback')` to the callback route; add a small feature test asserting 429 after excessive hits. - [x] T008 Ensure `app/Models/User.php` allows writing `entra_tenant_id` and `entra_object_id` (fillable/guarded), without refactoring membership relationships. --- ## Phase 3: US1 — Entra-only login UI on `/admin/login` (P1) **Goal**: A tenant user can only start Microsoft sign-in from `/admin/login`. - [x] T009 [US1] Override the Filament login page for the `/admin` panel to show only a "Sign in with Microsoft" action linking to `route('auth.entra.redirect')` (no email/password inputs). - [x] T010 [US1] Create feature test `tests/Feature/Auth/AdminLoginIsEntraOnlyTest.php` verifying the Microsoft button exists and password inputs do not. --- ## Phase 4: US2 — OIDC callback upserts tenant identity safely (P1) **Goal**: The callback upserts a tenant user using Entra claims. - [x] T011 [US2] Implement `EntraController@callback` upsert keyed by `(entra_tenant_id=tid, entra_object_id=oid)`; regenerate session on success; never store tokens. - [x] T012 [US2] Block login for disabled/soft-deleted users (Option B): redirect to `/admin/login` with generic error; log `reason_code=user_disabled`. - [x] T013 [US2] Handle missing/invalid `tid` or `oid` (or invalid state) by redirecting back to `/admin/login` with a generic error + reason_code log (no claims/tokens dumped). - [x] T014 [US2] Implement privacy-safe logging for login successes and failures (minimal identity; include `correlation_id`; never dump raw claims/tokens). - [x] T015 [US2] Create feature test `tests/Feature/Auth/EntraCallbackUpsertByTidOidTest.php` to test upsert and session regeneration. - [x] T016 [US2] Create feature test `tests/Feature/Auth/DisabledUserLoginIsBlockedTest.php`. - [x] T017 [US2] Create feature test `tests/Feature/Auth/OidcFailureRedirectsSafelyTest.php`. --- ## Phase 5: US3 — Post-login routing is membership-based (P1) **Goal**: After login, routing depends on Suite tenant memberships. - [x] T018 [P] [US3] Create `app/Services/Auth/PostLoginRedirectResolver.php` that resolves redirect targets for 0/1/>1 memberships (0 → `/admin/no-access`, 1 → `TenantDashboard::getUrl(tenant: $tenant)`, >1 → `/admin/choose-tenant`). IMPORTANT: do not hardcode `/admin/t/...`; always use Filament page URL helpers so routing stays stable if prefixes/slugs change. - [x] T019 [US3] In `EntraController@callback`, call `PostLoginRedirectResolver` after upsert to determine redirect. - [x] T020 [US3] Create a Filament page `app/Filament/Pages/ChooseTenant.php` mounted under the `/admin` panel at `/admin/choose-tenant`. - [x] T021 [US3] Implement tenant selection action on the chooser page: selecting a tenant sets Filament tenant context (session) and redirects to `App\\Filament\\Pages\\TenantDashboard::getUrl(tenant: $tenant)`; persist `users.last_tenant_id` if present. IMPORTANT: use Filament URL helpers (no hardcoded paths). - [x] T022 [US3] Create feature test `tests/Feature/Auth/PostLoginRoutingByMembershipTest.php` to validate 0/1/>1 routing. - [x] T023 [US3] Create feature test `tests/Feature/Auth/TenantChooserSelectionTest.php` ensuring chooser selection sets tenant context and redirects (and stores last_tenant_id if present). --- ## Phase 6: US4 — Filament-native “No access” page (P2) - [x] T024 [US4] Create a Filament page `app/Filament/Pages/NoAccess.php` under the `/admin` panel with route `/admin/no-access`. - [x] T025 [US4] Add feature test `tests/Feature/Auth/NoAccessPageRendersTest.php`. --- ## Phase 7: Cross-cutting (Polish & Guards) - [x] T026 Create `tests/Feature/Auth/SessionSeparationSmokeTest.php` to ensure tenant session cannot access `/system`; platform session cannot access `/admin` tenant routes. - [x] T027 Run formatting: `./vendor/bin/sail bin pint --dirty`. - [x] T028 Run feature tests: `./vendor/bin/sail artisan test tests/Feature/Auth --stop-on-failure`. - [ ] T029 Manual verification: run through `/admin/login` → OIDC → 0/1/>1 membership outcomes, chooser selection, and confirm no break-glass UI appears on `/admin/login`. - [x] T030 Add `tests/Feature/Auth/DbOnlyPagesDoNotMakeHttpRequestsTest.php` to enforce DB-only render/hydration for `/admin/login`, `/admin/no-access`, and `/admin/choose-tenant` using `Http::preventStrayRequests()` + render each page + assert no exceptions; optional hardening: `Queue::fake()` + `Bus::fake()` (and/or `Event::fake()`) so render paths can’t silently dispatch. --- ## Dependencies & Execution Order - Phase 1–2 must complete before US1–US4. - Implement in order: US1 → US2 → US3 → US4. - Phase 7 is last.