From 4acb86d49d30663294b158b98e333f68e698cfda Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Mon, 26 Jan 2026 23:54:33 +0100 Subject: [PATCH] feat(063-entra-signin): Add technical plan for Entra Sign-in feature This commit adds the initial technical plan for the 063-entra-signin feature. The plan outlines the high-level architecture, key components, database changes, test plan, and deployment considerations based on the clarified feature specification. The plan addresses: - Authentication flow via Laravel Socialite and Entra ID. - User provisioning and upsert logic. - Post-login routing based on tenant memberships, including a dedicated chooser page for multiple memberships. - Handling of disabled user logins. - Database schema details for Entra ID fields. - Comprehensive test coverage using Pest (unit, feature, browser tests). --- specs/063-entra-signin/plan.md | 104 +++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 specs/063-entra-signin/plan.md diff --git a/specs/063-entra-signin/plan.md b/specs/063-entra-signin/plan.md new file mode 100644 index 0000000..b237eaa --- /dev/null +++ b/specs/063-entra-signin/plan.md @@ -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 tenant’s 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`).