Why the single-model approach falls short
When one model does everything, you end up with row-level checks scattered across your codebase, fatUser models that know about every feature, and authorization logic that has to re-derive “which workspace is this request for?” from request context instead of reading it off the authenticated state. Worse, a token that grants access to any of a user’s tenants becomes a wider blast radius than necessary — a compromised credential can act across every workspace the user belongs to.
Separating identity from principal gives you a natural enforcement point: the access token is minted for a specific principal (e.g. a tenant membership), the guard rehydrates that exact principal on every request, and a token for one membership simply cannot be used to act as another.
The five concepts
| Concept | What it represents | Contract |
|---|---|---|
| Identity | Who logged in — the person or service account behind the request | Identity |
| Principal | Who the request is acting as — often the same as the identity, but in 3D mode it’s a tenant-scoped membership or role | Principal |
| Device | Which client obtained the login — a browser, mobile app, or CLI session that holds the refresh token | Device |
| Tenant | The isolation boundary the principal acts within — a company, workspace, or project | Tenant |
| Type | An optional categorical label on the tenant, e.g. "staff" vs "customer" | HasType |
SineMacula\Laravel\Authentication\Contracts namespace. Your Eloquent models implement whichever contracts match your domain.
Contracts at a glance
Identity extends Laravel’s Authenticatable. Any model that implements it can be loaded by the guard’s identity provider and rehydrated from a bearer token’s sub claim.
Principal is the acting entity. It exposes three core methods:
getPrincipalIdentifier()— the stable ID embedded as thepidclaim in every access tokengetIdentity()— the identity that owns this principal (in 2D mode this returns$this)getTenant()— the tenant the principal acts within, ornullif noneisActive()— lets the guard reject inactive memberships without a separate active-state check
Device is the client record that anchors refresh-token rotation. It carries the hashed refresh key, a revoked_at timestamp, last-login metadata, and the operating system captured at registration. The guard reads Device::getDeviceIdentifier() to confirm a did claim matches a real row.
Tenant is the isolation boundary. It exposes getTenantIdentifier() so the package can identify it without knowing anything about your domain model.
HasType is an optional capability contract that can be added to any Tenant implementation. When present, Auth::type() reads its getType() return value. When absent, Auth::type() returns null.
Two additional capability contracts extend what an Identity model can do:
HasDevices— exposes adevices()Eloquent relation builder; required for device tracking and refresh-token rotationHasPrincipals— exposes aprincipals()relation andresolveDefaultPrincipal(); required for 3D mode where identity and principal are separate models
Runtime accessors
All five values are readable through the standardAuth facade after a successful authentication. Import the package facade to get the contextual methods alongside the standard Laravel ones:
When Auth::type() returns null
Auth::type() returns null in two situations:
- The request has no authenticated principal (the guard has no bound state).
- The resolved tenant does not implement
HasType.
HasType, the method safely returns null rather than throwing. You can add HasType incrementally once you need it.
Auth::tenant() and Auth::type() are only non-null in 3D mode where a separate Tenant model exists. In 2D mode, where one model implements both Identity and Principal, getTenant() on the principal returns null and both accessors return null accordingly.How the pieces fit together
The authentication lifecycle follows a single directed path on every request:Identity logs in
The guard reads the bearer token or Basic credentials, looks up the identity via the configured provider, and confirms it is active (if the model implements
CanBeActive).Principal is resolved
The principal resolver maps the identity to its acting principal. In 3D mode this uses the
pid claim embedded in the token; in 2D mode the identity is the principal.Device is rehydrated
When the token carries a
did claim and the identity implements HasDevices, the guard loads the device row and binds it. If the did is present but resolves to nothing, the guard rejects the request — it never silently drops to “no device”.