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