## 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`).