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.

Zacharias Creutznacher Zacharias Creutznacher

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 bundles Authenticatable + Authorizable + CanResetPassword + MustVerifyEmail, handy for a real user, irrelevant for a Project.

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_id route parameter. The token already encodes which project is calling. No /projects/{project}/stats-visitor style URL, no manual Project::findOrFail(...) in the controller.
  • No "current project" middleware. Multi-tenant apps usually grow a SetCurrentProject middleware 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-typed Project. No adapter classes, no hand-rolled resolvers, no Auth::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 a Project, 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 authenticates Project, your partner integrations authenticate Organization. One personal_access_tokens table 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.