---
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.