063-entra-signin #76

Merged
ahmido merged 5 commits from 063-entra-signin into dev 2026-01-27 16:38:54 +00:00
Showing only changes of commit 4acb86d49d - Show all commits

View File

@ -0,0 +1,104 @@
## Technical Plan: 063 — Entra Sign-in (Tenant Panel) v1
**Feature Branch**: `063-entra-signin`
**Created**: 2026-01-26
**Status**: Draft (v1)
**Spec**: [specs/063-entra-signin/spec.md](specs/063-entra-signin/spec.md)
---
### 1. Overview & Goal Alignment
* **Goal**: Implement a secure, Entra ID-only sign-in flow for the Tenant Admin panel (`/admin`), safely onboarding users and routing them based on tenant memberships.
* **Key Constraints**: No `/system` panel modification, no Graph calls during render/hydration, DB-only at render time for login/no-access pages, synchronous login flow.
* **Clarifications Incorporated**:
* Multi-tenant users redirect to a dedicated chooser page.
* Disabled users are blocked from login, redirected to `/admin/login` with generic error.
* `entra_tenant_id` and `entra_object_id` columns are `VARCHAR(36)` (or UUID for Postgres).
### 2. Architecture & High-Level Design
* **Authentication Flow**:
1. User navigates to `/admin/login`.
2. Presented with "Sign in with Microsoft" button (no local login fields).
3. Clicking button initiates OIDC flow via Laravel Socialite (Entra ID provider).
4. Redirects to `/auth/entra/redirect`.
5. Entra ID authenticates user and redirects back to `/auth/entra/callback` with OIDC claims.
6. Callback handler:
* Validates claims (`tid`, `oid` present).
* Performs upsert on `users` table based on `(entra_tenant_id, entra_object_id)`.
* Checks user status (active/disabled). If disabled, blocks login.
* Regenerates session.
* Determines post-login route based on tenant memberships.
* **User Provisioning (Upsert)**:
* Use `User::updateOrCreate` with `['entra_tenant_id' => $tid, 'entra_object_id' => $oid]` as unique key.
* Populate `name`, `email` from claims.
* `entra_tenant_id`, `entra_object_id` columns as `VARCHAR(36)` (or UUID).
* **Post-Login Routing**:
* **0 Memberships**: Redirect to `/admin/no-access` (Filament page).
* **1 Membership**: Redirect to that tenants dashboard.
* **N Memberships**: Redirect to `/admin/choose-tenant` (dedicated Filament chooser page).
* **Session Separation**: Leverage Laravel's guard system for clear separation between `platform` (system) and `tenant` (admin) panels.
### 3. Key Components & Implementation Details
* **Laravel Socialite**:
* Configure Entra ID provider in `config/services.php`.
* Create `SocialiteController` (or similar) to handle `/auth/entra/redirect` and `/auth/entra/callback`.
* **User Model (`app/Models/User.php`)**:
* Add `entra_tenant_id` and `entra_object_id` as fillable properties.
* Implement logic for checking `tenants()` relationship.
* **Migrations**:
* Add `entra_tenant_id` (`string('entra_tenant_id', 36)->nullable()` or `uuid('entra_tenant_id')`) and `entra_object_id` (`string('entra_object_id', 36)->nullable()` or `uuid('entra_object_id')`) columns to `users` table.
* Add unique index `unique(['entra_tenant_id', 'entra_object_id'])`.
* **Note**: Initial migration can add as nullable, then a follow-up migration can make non-nullable if all existing users are migrated or it's a new system. Given it's a new system for `admin` sign-in, it should probably be non-nullable from the start.
* **Filament Panel Customization**:
* Override default Filament login page for `/admin` panel to remove email/password fields and add "Sign in with Microsoft" button.
* Create `NoAccessPage` (`/admin/no-access`) and `TenantChooserPage` (`/admin/choose-tenant`) as Filament pages.
* **Error Handling & Logging**:
* Implement robust OIDC failure handling as per `FR-004`.
* Utilize Laravel's logging facilities for privacy-safe audit logs (`FR-005`).
* Define stable `reason_code` examples (e.g., `oidc_missing_claims`, `user_disabled`).
* **Service Layer (Optional but Recommended)**:
* Consider a `EntraLoginService` to encapsulate OIDC callback logic, user upsert, and routing decisions. This keeps controllers lean and business logic testable.
### 4. Database Schema Changes
* **`users` table**:
* `entra_tenant_id` `string('entra_tenant_id', 36)->nullable()` (or `uuid('entra_tenant_id')`)
* `entra_object_id` `string('entra_object_id', 36)->nullable()` (or `uuid('entra_object_id')`)
* Add `unique(['entra_tenant_id', 'entra_object_id'])` index.
* **Note**: Initial migration can add as nullable, then a follow-up migration can make non-nullable if all existing users are migrated or it's a new system. Given it's a new system for `admin` sign-in, it should probably be non-nullable from the start.
### 5. Test Plan (Building on Spec Acceptance Tests)
* **Unit Tests (Pest)**:
* Socialite callback handler logic (claim validation, upsert logic, disabled user check).
* User model methods related to Entra ID and tenant memberships.
* Routing service/resolver.
* **Feature Tests (Pest)**:
* `AdminLoginIsEntraOnlyTest` (GET `/admin/login` renders correctly, no password inputs).
* `EntraCallbackUpsertByTidOidTest` (callback upserts, session regenerated).
* `PostLoginRoutingByMembershipTest` (0, 1, N memberships routing).
* `OidcFailureRedirectsSafelyTest` (missing claims, generic error, logs).
* `SessionSeparationSmokeTest` (guard separation works).
* `DisabledUserLoginIsBlockedTest` (disabled user login attempt blocked and logged).
* **Browser Tests (Pest v4)**:
* End-to-end flow for successful Entra login.
* Verify the `/admin/choose-tenant` page renders correctly and allows selection.
### 6. Deployment Considerations
* **Environment Variables**:
* `ENTRA_CLIENT_ID`, `ENTRA_CLIENT_SECRET`, `ENTRA_REDIRECT_URI` for Socialite.
* These must be managed securely (Dokploy environment variables).
* **Migrations**: Ensure database migrations are run (`sail artisan migrate`).
* **Filament Assets**: `php artisan filament:assets` must be run on deployment.
* **Dokploy**: Ensure Dokploy config includes any new routes/pages.
### 7. Open Questions / Potential Risks
* **Entra ID Setup**: Assumed Entra ID application registration and configuration are handled externally.
* **`last_tenant_id`**: Decision on whether to implement `last_tenant_id` for multi-membership users is deferred. It's an optimization.
* **User Provisioning**: What if required claims like `email` or `name` are missing from Entra ID? Current spec implies they *should* be present, but fallback behavior is not explicitly defined for `name` (email is nullable).
* **Authorization**: This plan focuses on authentication. Tenant-specific authorization (what actions a user can perform within a tenant) is out of scope for this feature (covered by `062-tenant-rbac-v1`).