π API Authentication with Passport
A public API is a door anyone can walk through. Laravel Passport turns it into a guarded entrance: a full OAuth2 server that hands out access tokens, checks them on every request, and lets you scope exactly what each token may do β all with a handful of Artisan commands.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain token-based auth and the core OAuth2 vocabulary
- Install and configure Passport in Laravel 11 with
install:api --passport - Choose the right grant type for first-party vs. third-party clients
- Protect routes with the
auth:apiguard and restrict them by scope - Send Bearer tokens from a client and revoke them for logout
- Test authenticated endpoints with
Passport::actingAs()
Estimated Time: 45β60 minutes β’ Difficulty: IntermediateβAdvanced
Hands-on: Protect an endpoint, issue a token via login, and call it with a Bearer header.
In This Lesson
Why Token Auth?
Traditional web apps authenticate with sessions and cookies: you log in, the server remembers you in a session, and the browser sends a cookie on every request. That model assumes a browser and a single server. APIs are different β the caller might be a mobile app, another server, or a script, and REST says each request must be stateless.
So APIs use tokens. The client authenticates once, receives a token string, and then attaches that token to every subsequent request. The server validates the token β no session required.
π‘ Analogy: A session cookie is like a coat-check ticket that only works at one cloakroom. A token is like a hotel key card: you prove who you are at check-in, get a card, and it opens exactly the doors you're allowed through, for a limited time β at any reader in the building.
OAuth2 in Plain English
Passport implements OAuth2, the industry-standard authorization framework. You don't need to memorize the spec, but these five terms carry the whole model:
| Term | What it means |
|---|---|
| OAuth2 | The standard protocol for granting access without sharing passwords everywhere. |
| Grant type | The flow a client uses to obtain a token (password, authorization code, client credentialsβ¦). |
| Access token | The short-lived credential sent with each request to reach protected resources. |
| Refresh token | A longer-lived token used to get a fresh access token without re-login. |
| Scope | A named permission that limits what a token is allowed to do. |
The key idea: authenticating (proving who you are) and authorizing (deciding what you may do) are separated. A token proves identity; its scopes decide its powers.
Passport vs. Sanctum
Laravel ships two official auth packages, and picking the right one matters. Both issue API tokens, but they solve different problems.
| Passport | Sanctum | |
|---|---|---|
| Model | Full OAuth2 server | Simple API tokens + SPA cookie auth |
| Best for | Third-party clients, scopes, refresh tokens, "log in withβ¦" | Your own SPA, mobile app, or simple token needs |
| Complexity | Heavier β encryption keys, OAuth clients | Lightweight β a tokens table |
π‘ Which should you reach for?
Start with Sanctum for a first-party SPA or mobile app β it's the Laravel default and covers most projects. Choose Passport when you need genuine OAuth2: issuing tokens to other people's applications, standardized scopes, refresh-token flows, or an "authorize this app" consent screen. This lesson focuses on Passport because that OAuth2 case is what it uniquely handles.
Installing Passport
On Laravel 11 the whole setup is one Artisan command. The --passport flag makes install:api pull in Passport instead of Sanctum, run its migrations, and generate the encryption keys:
php artisan install:api --passport
That command creates routes/api.php, publishes and runs the Passport migrations (which add the OAuth tables), and generates the encryption keys used to sign tokens. Behind the scenes it's the equivalent of composer require laravel/passport followed by php artisan migrate and php artisan passport:keys.
1. Add the trait to your User model
<?php
// app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
// ... the rest of your model
}
2. Point the api guard at Passport
<?php
// config/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
3. (Optional) Configure token lifetimes
In modern Passport you no longer call Passport::routes() β the OAuth routes register themselves. Configuration like token lifetimes goes in a service provider's boot() method (for example App\Providers\AppServiceProvider):
<?php
// app/Providers/AppServiceProvider.php
use Laravel\Passport\Passport;
public function boot(): void
{
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}
β οΈ Legacy code you'll see online
Older tutorials call Passport::routes() inside AuthServiceProvider and run php artisan passport:install. Those steps are outdated: routes are automatic now, and Laravel 11 has no AuthServiceProvider by default. If you copy old snippets you'll hit "method not found" errors β stick with the install:api --passport flow above.
Grant Types
A grant type is simply the flow a client uses to get a token. Passport supports several; you pick based on who the client is.
Personal access tokens (first-party)
The simplest flow: an authenticated user asks your app for a token directly, no OAuth redirect. Ideal for your own mobile app or a "generate API key" screen.
<?php
// In a controller β issue a token for the logged-in user
$token = $request->user()->createToken('mobile-app')->accessToken;
return response()->json(['token' => $token]);
Authorization Code (third-party)
The classic "Log in withβ¦" / "App X wants access to your account" flow. The user is redirected to your app, approves the requested scopes on a consent screen, and the third-party app receives a token. Use this when you don't control the client.
Client Credentials (machine-to-machine)
No user at all β one server authenticates as itself to call another. Protect these routes with the client middleware.
Protecting Routes
Once the api guard uses Passport, guarding routes is just the auth:api middleware. Requests without a valid Bearer token get a 401 Unauthorized automatically.
<?php
// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// A single protected route
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
// A whole group of protected routes
Route::middleware('auth:api')->group(function () {
Route::get('/products', [ProductController::class, 'index']);
Route::post('/products', [ProductController::class, 'store']);
Route::apiResource('orders', OrderController::class);
});
Inside any protected action, $request->user() returns the token's owner, so you can scope data to them:
<?php
public function index(Request $request)
{
// Only this user's products
return ProductResource::collection(
$request->user()->products()->paginate(15)
);
}
Token Scopes
Scopes are named permissions attached to a token. A token can be authenticated (valid) yet still forbidden from an action because it lacks the required scope β that's the principle of least privilege in action.
Define the available scopes
<?php
// AppServiceProvider::boot()
use Laravel\Passport\Passport;
Passport::tokensCan([
'view-products' => 'View products',
'create-products' => 'Create new products',
'edit-products' => 'Edit existing products',
'delete-products' => 'Delete products',
]);
Request scopes when creating a token
<?php
// Grant only the scopes this client needs
$token = $user->createToken('mobile-app', ['view-products', 'create-products'])
->accessToken;
Require a scope on a route
The scope middleware requires all listed scopes; scopes requires any of them:
<?php
Route::middleware(['auth:api', 'scope:view-products'])
->get('/products', [ProductController::class, 'index']);
Route::middleware(['auth:api', 'scope:create-products'])
->post('/products', [ProductController::class, 'store']);
Check a scope inside a controller
<?php
public function update(Request $request, Product $product)
{
if (! $request->user()->tokenCan('edit-products')) {
return response()->json(['message' => 'Missing scope.'], 403);
}
$product->update($request->validated());
return new ProductResource($product);
}
Using & Revoking Tokens
Once issued, a token travels in the Authorization header as a Bearer token on every request:
// Client side (fetch)
const token = localStorage.getItem('access_token');
const res = await fetch('/api/user', {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
},
});
const user = await res.json();
A reusable client keeps you from repeating the header everywhere:
// A preconfigured axios instance
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
headers: {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
Accept: 'application/json',
},
});
await api.get('/user');
await api.post('/products', { name: 'New Product', price: 99.99 });
Login: issue a token
<?php
// app/Http/Controllers/Api/AuthController.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
public function login(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (! Auth::attempt($credentials)) {
return response()->json([
'message' => 'The provided credentials are incorrect.',
], 401);
}
$user = $request->user();
$token = $user->createToken('auth-token', ['view-profile', 'place-orders']);
return response()->json([
'token' => $token->accessToken,
'user' => new UserResource($user),
]);
}
Logout & token management
Revoking tokens is how you log out and how you power "sign out of all devices":
<?php
// Log out the current token only
public function logout(Request $request)
{
$request->user()->token()->revoke();
return response()->json(['message' => 'Logged out.']);
}
// List a user's tokens
$request->user()->tokens;
// Revoke one specific token
$request->user()->tokens()->where('id', $tokenId)->update(['revoked' => true]);
// Revoke every token (log out everywhere)
$request->user()->tokens()->update(['revoked' => true]);
Testing Auth
Passport gives you actingAs() to authenticate a fake user in tests β optionally with specific scopes β so you never juggle real tokens:
<?php
// tests/Feature/Api/ProductAuthTest.php
namespace Tests\Feature\Api;
use App\Models\Product;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Passport\Passport;
use Tests\TestCase;
class ProductAuthTest extends TestCase
{
use RefreshDatabase;
public function test_guests_are_rejected(): void
{
$this->getJson('/api/products')->assertUnauthorized(); // 401
}
public function test_a_user_can_view_their_products(): void
{
$user = User::factory()->create();
$product = Product::factory()->create(['user_id' => $user->id]);
// Authenticate with just the scope under test
Passport::actingAs($user, ['view-products']);
$this->getJson('/api/products')
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $product->id);
}
public function test_missing_scope_is_forbidden(): void
{
Passport::actingAs(User::factory()->create(), ['view-products']);
// No create-products scope granted β 403
$this->postJson('/api/products', ['name' => 'X', 'price' => 1])
->assertForbidden();
}
}
Hands-on Exercise
ποΈ Lock Down and Log In
Objective: Protect an endpoint with Passport, issue a token through a login route, and call the endpoint with a Bearer token.
Steps
- Run
php artisan install:api --passportin a Laravel 11 app. - Add
use HasApiTokens;toUserand set theapiguard driver topassport. - Create an
AuthControllerwith aloginaction that validates credentials and returnscreateToken('auth-token')->accessToken. - Register a public
POST /api/loginroute and a protectedGET /api/meroute behindauth:apithat returns$request->user(). - With Postman or curl: POST valid credentials to
/api/login, copy the token, then GET/api/mewith anAuthorization: Bearer <token>header. - Confirm that calling
/api/mewithout the header returns401.
π‘ Hint
Always send Accept: application/json from the client so Laravel returns JSON (including a JSON 401) instead of trying to redirect. In curl: curl -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" http://localhost:8000/api/me.
β Sample solution (routes + protected action)
<?php
// routes/api.php
use App\Http\Controllers\Api\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:api')->get('/me', function (Request $request) {
return response()->json(['data' => $request->user()]);
});
π― Quick Quiz
Question 1: Why do APIs favor token-based auth over sessions and cookies?
Question 2: You're building a first-party mobile app for your own service. Which is usually the better fit?
Question 3: A token is valid but a route returns 403. What's the most likely cause?
Summary & Quiz
π Key Takeaways
- APIs use stateless tokens instead of sessions; the client sends a Bearer token on every request.
- Passport is a full OAuth2 server; install it on Laravel 11 with
install:api --passport, addHasApiTokens, and set theapiguard topassport. - Choose a grant type by client: personal access (yours), authorization code (third-party), client credentials (machine).
- Guard routes with
auth:apiand narrow them with thescopemiddleware β least privilege per token. - Revoke tokens to log out; test protected routes with
Passport::actingAs().
π Security reminders
- Always serve your API over HTTPS so tokens can't be sniffed in transit.
- Store tokens carefully on the client; prefer secure storage over plain
localStoragewhere possible. - Set sensible expiration times and combine auth with rate limiting to blunt brute-force attempts.
π Further Reading
π What's Next?
You've built a REST API, shaped its responses, and secured it. In the Weekend Project, you'll pull all of Module 19 together β routing, Eloquent, validation, resources, and Passport β into one complete Laravel API you can be proud of.
π Door locked!
Only clients with a valid, correctly-scoped token get through. Let's ship a full project.