> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sinemacula.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Set Up 3D Multi-Tenant Authentication

> Configure Laravel Authentication in 3D mode with separate Identity, Principal, and Tenant models for multi-tenant SaaS applications.

In 3D mode, three distinct models represent three distinct concepts: the human who authenticated (`Identity`), the tenant-scoped role or membership they are acting as (`Principal`), and the isolation boundary they operate within (`Tenant`). The same guards that power 2D mode support this shape without any driver changes — only your models and resolver wiring differ.

<Note>
  When an access token carries a `pid` claim, the guard resolves exactly that principal and fails closed if it cannot. A token that claims `pid = 42` but resolves to a different principal — or no principal at all — is rejected rather than silently downgraded. This prevents privilege-escalation across tenant boundaries.
</Note>

<Steps>
  <Step title="Create the Identity model">
    The identity represents the human account. It implements `Identity`, `HasDevices` (for refresh-token rotation), and `HasPrincipals` (to expose the principals it may act as). Optionally implement `ResolvesHintedPrincipal` to short-circuit the default `principals()->find($hint)` lookup with a more efficient joined query.

    ```php theme={null}
    use Illuminate\Database\Eloquent\Relations\HasMany;
    use Illuminate\Database\Eloquent\Relations\MorphMany;
    use Illuminate\Foundation\Auth\User;
    use SineMacula\Laravel\Authentication\Contracts\HasDevices;
    use SineMacula\Laravel\Authentication\Contracts\HasPrincipals;
    use SineMacula\Laravel\Authentication\Contracts\Identity;
    use SineMacula\Laravel\Authentication\Contracts\Principal as PrincipalContract;
    use SineMacula\Laravel\Authentication\Contracts\ResolvesHintedPrincipal;
    use SineMacula\Laravel\Authentication\Models\Device;
    use SineMacula\Laravel\Authentication\Traits\Authenticatable;

    class AppIdentity extends User implements Identity, HasDevices, HasPrincipals, ResolvesHintedPrincipal
    {
        use Authenticatable;

        /**
         * Returns the principals this identity is permitted to act as.
         * The package's default resolver calls ->find($hint) when the
         * access token carries a `pid` claim, and resolveDefaultPrincipal()
         * otherwise.
         */
        public function principals(): HasMany
        {
            return $this->hasMany(AppMembership::class, 'identity_id');
        }

        public function devices(): MorphMany
        {
            return $this->morphMany(Device::class, 'authenticatable');
        }

        public function resolveDefaultPrincipal(): ?PrincipalContract
        {
            return $this->principals()->where('is_active', true)->first();
        }

        /**
         * Optional: when a JWT carries a `pid`, resolve the hinted principal
         * directly. Use this hook to perform a joined lookup and pre-hydrate
         * the tenant relation so Auth::tenant() does not trigger a separate query.
         */
        public function resolveHintedPrincipal(mixed $hint): ?PrincipalContract
        {
            return AppMembership::query()
                ->join('app_tenants', 'app_tenants.id', '=', 'app_memberships.tenant_id')
                ->where('app_memberships.identity_id', $this->getKey())
                ->where('app_memberships.id', $hint)
                ->select('app_memberships.*')
                ->first();
        }
    }
    ```

    `resolveDefaultPrincipal()` is called when the token carries no `pid` hint — typically on first login before you embed the principal id into the token. `resolveHintedPrincipal()` is the preferred seam for optimising the hot bearer path: perform the joined lookup you need, attach the tenant relation you want `Auth::tenant()` to read, and set the inverse identity relation when appropriate.
  </Step>

  <Step title="Create the Principal model">
    The principal represents the tenant-scoped actor — typically a membership or role row. It implements `Principal` and belongs to a `Tenant`. The `ActsAsPrincipal` trait provides default `getPrincipalIdentifier()`, `getIdentity()`, `getTenant()`, and `isActive()` implementations sourced from conventional attribute and relation names (`id`, `identity`, `tenant`, `is_active`).

    ```php theme={null}
    use Illuminate\Database\Eloquent\Model;
    use Illuminate\Database\Eloquent\Relations\BelongsTo;
    use SineMacula\Laravel\Authentication\Contracts\Principal as PrincipalContract;
    use SineMacula\Laravel\Authentication\Traits\ActsAsPrincipal;

    class AppMembership extends Model implements PrincipalContract
    {
        use ActsAsPrincipal;

        protected $fillable = ['identity_id', 'tenant_id', 'role', 'is_active'];

        public function tenant(): BelongsTo
        {
            return $this->belongsTo(AppTenant::class);
        }
    }
    ```

    `ActsAsPrincipal` reads `is_active` for the `isActive()` check and follows the `tenant` relation for `getTenant()`. Override `getActiveAttributeName()` or `getTenantRelationName()` if your schema uses different column or relation names.
  </Step>

  <Step title="Create the Tenant model">
    The tenant is the isolation boundary the principal acts within. It implements `Tenant` so `Auth::tenant()` can return it. Optionally implement `HasType` and use `ProvidesTenantType` to expose a categorical label (`Auth::type()`) — useful when a single app serves multiple audience types such as `'staff'` and `'customer'`.

    ```php theme={null}
    use Illuminate\Database\Eloquent\Model;
    use SineMacula\Laravel\Authentication\Contracts\HasType;
    use SineMacula\Laravel\Authentication\Contracts\Tenant as TenantContract;
    use SineMacula\Laravel\Authentication\Traits\ActsAsTenant;
    use SineMacula\Laravel\Authentication\Traits\ProvidesTenantType;

    class AppTenant extends Model implements HasType, TenantContract
    {
        use ActsAsTenant, ProvidesTenantType;
    }
    ```

    `ProvidesTenantType` reads the `type` column by default and resolves `BackedEnum` via `value`, `UnitEnum` via `name`, and any other value via string cast. Override `getTenantTypeName()` if your column is named differently. Drop `HasType` and `ProvidesTenantType` entirely if your app does not need tenant type discrimination.
  </Step>

  <Step title="Register the guard in config/auth.php">
    The guard registration is identical to 2D mode. Point the provider at your identity model.

    ```php theme={null}
    // config/auth.php
    'guards' => [
        'api' => [
            'driver'   => 'jwt',
            'provider' => 'users',
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'model',
            'model'  => App\Models\AppIdentity::class,
        ],
    ],
    ```
  </Step>

  <Step title="Understand what each accessor returns">
    Once a request carries a valid bearer token the guard rehydrates and binds all three layers. Each accessor reflects a distinct model:

    ```php theme={null}
    use SineMacula\Laravel\Authentication\Facades\Auth;

    Auth::identity();    // AppIdentity — the human who authenticated
    Auth::principal();   // AppMembership — the tenant-scoped actor
    Auth::tenant();      // AppTenant — resolved via $membership->tenant()
    Auth::type();        // 'staff' | 'customer' | null — from AppTenant::getType()
    Auth::device();      // Device|null — the issuing device
    Auth::user();        // AppIdentity — same as Auth::identity()
    ```

    `Auth::tenant()` follows the `tenant` relation on the resolved principal. If you pre-hydrate the relation inside `resolveHintedPrincipal()`, no additional query is issued.
  </Step>
</Steps>

## Custom PrincipalResolver

The package ships a `DefaultPrincipalResolver` that calls `resolveDefaultPrincipal()` for hint-free requests and `resolveHintedPrincipal()` (or `principals()->find($hint)`) when a `pid` is present. This covers most 3D apps.

When you need a domain-specific resolution strategy — subdomain-scoped tenancy, a custom request header, a session claim, or any logic that cannot live on the identity model — implement the `PrincipalResolver` contract and bind it in a service provider:

```php theme={null}
use SineMacula\Laravel\Authentication\Contracts\PrincipalResolver;

// AppServiceProvider or a dedicated AuthServiceProvider
$this->app->singleton(PrincipalResolver::class, MyTenantScopedResolver::class);
```

All guards without a local `principal_resolver` override in `config/auth.php` will use this binding for both bearer-token resolution and refresh exchange. Guards that declare their own resolver take precedence:

```php theme={null}
// config/auth.php — per-guard override takes highest precedence
'guards' => [
    'staff' => [
        'driver'             => 'jwt',
        'provider'           => 'users',
        'principal_resolver' => App\Auth\Resolvers\StaffPrincipalResolver::class,
    ],
],
```

The full precedence order is:

1. `auth.guards.<name>.principal_resolver` (per-guard config)
2. The app-wide `PrincipalResolver::class` container binding
3. The package default `DefaultPrincipalResolver`
