Skip to main content
Most auth packages collapse “who you are” and “who you’re acting as” into a single user model. That works fine for simple apps, but it creates real problems once you add multi-tenancy, per-tenant roles, or M2M tokens that act on behalf of a specific workspace membership. Laravel Authentication solves this by giving each concern its own contract: Identity, Principal, Device, Tenant, and the optional Type label that sits on a tenant.

Why the single-model approach falls short

When one model does everything, you end up with row-level checks scattered across your codebase, fat User 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

ConceptWhat it representsContract
IdentityWho logged in — the person or service account behind the requestIdentity
PrincipalWho the request is acting as — often the same as the identity, but in 3D mode it’s a tenant-scoped membership or rolePrincipal
DeviceWhich client obtained the login — a browser, mobile app, or CLI session that holds the refresh tokenDevice
TenantThe isolation boundary the principal acts within — a company, workspace, or projectTenant
TypeAn optional categorical label on the tenant, e.g. "staff" vs "customer"HasType
Each concept maps to a PHP interface in the 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 the pid claim in every access token
  • getIdentity() — the identity that owns this principal (in 2D mode this returns $this)
  • getTenant() — the tenant the principal acts within, or null if none
  • isActive() — 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 a devices() Eloquent relation builder; required for device tracking and refresh-token rotation
  • HasPrincipals — exposes a principals() relation and resolveDefaultPrincipal(); required for 3D mode where identity and principal are separate models

Runtime accessors

All five values are readable through the standard Auth facade after a successful authentication. Import the package facade to get the contextual methods alongside the standard Laravel ones:
use SineMacula\Laravel\Authentication\Facades\Auth;

Auth::check();       // bool — same as Laravel
Auth::user();        // Identity|null — same as Laravel

Auth::identity();    // Identity|null   — the authenticated subject
Auth::principal();   // Principal|null  — the acting principal
Auth::device();      // Device|null     — the issuing device
Auth::tenant();      // Tenant|null     — the tenant the principal acts within
Auth::type();        // string|null     — the tenant's type string, or null
These are not new globals — they read from the active guard’s contextual state, which the guard populates automatically when it resolves a bearer token or validates Basic credentials.

When Auth::type() returns null

Auth::type() returns null in two situations:
  1. The request has no authenticated principal (the guard has no bound state).
  2. The resolved tenant does not implement HasType.
If your tenant model has different tiers but you have not yet added 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:
1

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).
2

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.
3

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”.
4

Tenant scopes the principal

Auth::tenant() reads principal->getTenant(). The guard itself does not load the tenant; your principal model exposes it, so you control the query shape (eager-loaded, lazy, joined — whatever fits your schema).
If tenant resolution is on your hot path, return the tenant relation already hydrated from resolveDefaultPrincipal() or resolveHintedPrincipal(). That way Auth::tenant() hits no extra queries.