Authenticate any Eloquent model in your Laravel API
The User model is not special. Any Eloquent model can extend Authenticatable, issue Sanctum tokens, and resolve as auth()->user() behind auth:sanctum. A pattern that fits surprisingly well whenever the API client is a Project, an Organization, or a device rather than a human.
When most Laravel developers hear "authenticated", they think App\Models\User. That is the model make:auth scaffolds, the one config/auth.php points to, the one every tutorial reaches for. It is easy to walk away believing the framework treats User as a special class.
It does not. Authentication in Laravel is just a contract. Any Eloquent model that implements Authenticatable can log in, issue tokens, and resolve through auth()->user(). That sounds academic until you build an API and realize the thing making the request is not always a human.
#The use case: a project IS the API client
SimpleStats is our analytics product for Laravel apps. Customers create a Project in our dashboard, copy a bearer token, and drop it into their app:
1SIMPLESTATS_PROJECT_TOKEN=aQfX7r...
From there, the SDK fires events to our API. The token represents the project, not the user who created it. Two engineers on the same team push events for the same project; we do not care which human is at the keyboard, we care which project is reporting.
Modelling that with a User-centric auth setup gets awkward fast: you would issue tokens to a user and then carry a project_id around in every request, hoping nobody mixes them up. The cleaner solution is to let the Project model itself be the authenticatable entity.
#The implementation
A Project becomes authenticatable by implementing the Authenticatable contract (via Laravel's built-in trait of the same name) and pulling in Sanctum's HasApiTokens trait:
1namespace App\Models;
2
3use Illuminate\Auth\Authenticatable;
4use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
5use Illuminate\Database\Eloquent\Model;
6use Laravel\Sanctum\HasApiTokens;
7
8class Project extends Model implements AuthenticatableContract
9{
10 use Authenticatable;
11 use HasApiTokens;
12
13 // ...the rest of your project logic
14}
That is the whole change to the model. No custom guard provider, no interface gymnastics. The Authenticatable trait satisfies every method Laravel's auth system calls on the subject (getAuthIdentifier(), getAuthPassword(), etc.), and HasApiTokens gives the model a tokens() relation plus the createToken() method Sanctum uses.
Why not
extends Illuminate\Foundation\Auth\User? That class bundlesAuthenticatable+Authorizable+CanResetPassword+MustVerifyEmail, handy for a real user, irrelevant for aProject.
Now issuing a token in the dashboard (after a user creates a project) is a one-liner:
1$token = $project->createToken('API Token')->plainTextToken;
Show that string to the user once, store the hash, done. Sanctum's personal_access_tokens table is a polymorphic store (tokenable_type, tokenable_id), so it does not care whether you point it at a User, a Project, an Organization, or all three at once.
#Protecting the routes
The Sanctum middleware does not hardcode User either. It loads whichever model the incoming token belongs to. So our API routes look exactly like a normal Sanctum-protected group:
1// routes/api.php
2Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
3 Route::post('stats-visitor', [StatsVisitorController::class, 'store']);
4 Route::post('stats-payment', [StatsPaymentController::class, 'store']);
5 // ...
6});
#$request->user() IS the project
This is where the pattern earns its keep. Inside the controller, $request->user() does not return a User. It returns the Project that owns the incoming token:
1public function store(StatsVisitorRequest $request): JsonResponse
2{
3 $project = $request->user(); // Project, not User
4 TrackStatsVisitor::dispatch($project, $request->validated());
5
6 return response()->json(['message' => 'Accepted.']);
7}
Look at everything that just didn't have to be written:
- No
project_idroute parameter. The token already encodes which project is calling. No/projects/{project}/stats-visitorstyle URL, no manualProject::findOrFail(...)in the controller. - No "current project" middleware. Multi-tenant apps usually grow a
SetCurrentProjectmiddleware that reads a header, looks it up, stuffs it into a container binding, and passes it down. Here, Laravel's auth system already carries the actor through the request lifecycle. - No authorization shim. You do not need a policy that maps "is this user allowed to write to this project?". The authenticated subject is the project, so the question collapses.
- No mismatch between layers. Controller, job, policy, broadcast channel, and event listener all ask
auth()->user()/$request->user()/Auth::user()and get back the same naturally-typedProject. No adapter classes, no hand-rolled resolvers, noAuth::user()->currentProject()chains.
That alignment is the real architectural payoff. Laravel already gives you a slot for "the actor of this request", and it threads that slot through every layer that cares (routing, middleware, controllers, policies, jobs, broadcasts). When the actor of your API genuinely is a Project, fitting it into that built-in slot keeps the codebase from sprouting parallel concepts ("the authenticated user" vs. "the current project") that would otherwise drift apart over time and end up enforced by three different middlewares written six months apart.
Type safety comes along for the ride: with proper @method annotations or a typed user() override, your IDE knows $request->user() is a Project and autocompletes ->team, ->createToken(), ->stats() instead of User methods that do not exist on it.
#Making a request
From the consumer side it is the standard Sanctum bearer flow:
1curl -X POST https://simplestats.io/api/v1/stats-visitor \
2 -H "Authorization: Bearer aQfX7r..." \
3 -H "Accept: application/json" \
4 -H "Content-Type: application/json" \
5 -d '{"url": "https://example.com/pricing", "user_agent": "..."}'
Sanctum looks up the token, finds the tokenable_type = App\Models\Project, hydrates the project, and hands it to the controller. The SDK that ships with SimpleStats does exactly this under the hood; the customer just sets one env variable.
#Why this pattern is worth knowing
The mental shift is small but it changes how you design APIs:
- Tokens belong to the entity that acts, not the human who created it. A CI pipeline, an IoT device, a tenant, a billing customer, each can be its own authenticatable model with its own scoped abilities.
- Authorization stays clean. Policies receive the actual subject, so you write
can('viewStats', $project)against aProject, not a fake "service user". - You can mix and match. Sanctum happily issues tokens to multiple models in the same app. Your dashboard signs in
User, your public API authenticatesProject, your partner integrations authenticateOrganization. Onepersonal_access_tokenstable covers all of them.
User is not special. It is just the model Laravel happens to scaffold. Once that clicks, "who is the authenticated subject" becomes a domain decision rather than a framework constraint, and a lot of API designs get noticeably simpler.