TenantAtlas/specs/063-entra-signin/plan.md
Ahmed Darrazi 4acb86d49d 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).
2026-01-26 23:54:33 +01:00

105 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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