TenantAtlas/specs/063-entra-signin/tasks.md
ahmido c5fbcaa692 063-entra-signin (#76)
Key changes

Adds Entra OIDC redirect + callback endpoints under /auth/entra/* (token exchange only there).
Upserts tenant users keyed by (entra_tenant_id = tid, entra_object_id = oid); regenerates session; never stores tokens.
Blocks disabled / soft-deleted users with a generic error and safe logging.
Membership-based post-login routing:
0 memberships → /admin/no-access
1 membership → tenant dashboard (via Filament URL helpers)
>1 memberships → /admin/choose-tenant
Adds Filament pages:
/admin/choose-tenant (tenant selection + redirect)
/admin/no-access (tenantless-safe)
Both use simple layout to avoid tenant-required UI.
Guards / tests

Adds DbOnlyPagesDoNotMakeHttpRequestsTest to enforce DB-only render/hydration for:
/admin/login, /admin/no-access, /admin/choose-tenant
with Http::preventStrayRequests()
Adds session separation smoke coverage to ensure tenant session doesn’t access system and vice versa.
Runs: vendor/bin/sail artisan test --compact tests/Feature/Auth

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #76
2026-01-27 16:38:53 +00:00

96 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 cant silently dispatch.
---
## Dependencies & Execution Order
- Phase 12 must complete before US1US4.
- Implement in order: US1 → US2 → US3 → US4.
- Phase 7 is last.