To add REST API authentication with Laravel Passport in Laravel 13, run php artisan install:api --passport, implement OAuthenticatable on the User model, set the api guard driver to passport, and protect routes with auth:api. Users then receive an access token on login and send it as a Bearer header.
This guide targets Laravel 13 (PHP 8.3 to 8.5) and the setup steps documented for Laravel Passport on the 13.x docs. The code was written for this article and not run against a live project, so test it in a fresh app first.
When should you choose Passport instead of Sanctum?
Passport is a full OAuth2 server. Pick it when third-party applications must request access on behalf of users, or when you need OAuth2 grants and scopes. For a first-party mobile app or single-page app that only needs simple tokens, Sanctum is lighter. This article uses personal access tokens, the simplest Passport flow for a login endpoint.
Prerequisites
- PHP 8.3+, Composer and a Laravel 13 project with a configured database.
- The default
userstable migrated. - An API client such as Postman, Insomnia or curl.
Step 1: How do you install Passport?
Run the install command. It publishes the API routes file, installs Passport, runs its migrations and generates the encryption keys.
php artisan install:api --passportIf prompted, accept running the migrations. Afterwards, create a personal access client once:
php artisan passport:client --personalStep 2: How do you update the User model?
Add the HasApiTokens trait and implement the OAuthenticatable contract.
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable implements OAuthenticatable
{
use HasApiTokens, Notifiable;
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
protected function casts(): array
{
return ['password' => 'hashed'];
}
}Step 3: How do you configure the API guard?
Open config/auth.php and add an api guard that uses the passport driver.
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],Step 4: How do you build register and login endpoints?
Create the controller with php artisan make:controller Api/AuthController.
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
public function register(Request $request): JsonResponse
{
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
$user = User::create($data);
return response()->json([
'user' => $user,
'token' => $user->createToken('api')->accessToken,
], 201);
}
public function login(Request $request): JsonResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (! Auth::attempt($credentials)) {
return response()->json(['message' => 'Invalid credentials.'], 401);
}
$user = $request->user();
return response()->json([
'user' => $user,
'token' => $user->createToken('api')->accessToken,
]);
}
public function logout(Request $request): JsonResponse
{
$request->user()->token()->revoke();
return response()->json(['message' => 'Logged out.']);
}
}The model's hashed cast hashes the password on create, so the controller never calls Hash::make(). Auth::attempt() here uses the default web guard to check the credentials; the token itself is created by Passport.
Step 5: How do you protect the routes?
In routes/api.php add the public and protected routes. Protected ones use the auth:api middleware.
use App\Http\Controllers\Api\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:api')->group(function () {
Route::get('/user', fn (Request $request) => $request->user());
Route::post('/logout', [AuthController::class, 'logout']);
});How do you call the API?
Send JSON and ask for JSON responses. Replace the token with the value returned by login.
curl -X POST http://localhost:8000/api/login \
-H "Accept: application/json" -H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"secret-pass"}'
curl http://localhost:8000/api/user \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_TOKEN"How do you limit what a token can do?
Pass scopes to createToken(), for example $user->createToken('api', ['orders:read']). Declare allowed scopes with Passport::tokensCan([...]) in a service provider and check them with Passport's scope middleware on routes that need them. See the Passport documentation for the exact middleware aliases in your version.
Common mistakes
- Skipping the personal client.
createToken()fails untilpassport:client --personalhas been run. - Wrong guard. Routes protected with
authinstead ofauth:apilook for a session, so API calls redirect or return 401. - Missing Accept header. Without
Accept: application/json, authentication failures may redirect to a login page instead of returning JSON 401. - Different keys per server. Behind a load balancer, every server must share the same Passport keys, or tokens issued by one are rejected by another.
- Committing keys. The generated key files are secrets; keep them out of version control.
How do you verify the setup?
- Register a user and confirm you get a 201 response with a token.
- Call
GET /api/userwith the token and confirm you get the user JSON. - Call it without the header and confirm a 401.
- Call
POST /api/logout, then reuse the old token and confirm a 401.
FAQ
Is Passport or Sanctum better for a mobile app?
Sanctum is usually simpler for your own first-party app. Choose Passport when you need a full OAuth2 server.
How do I revoke a token?
Call $request->user()->token()->revoke() for the current token.
Why do I get "Unauthenticated" with a valid token?
Check that the route uses auth:api, the guard driver is passport, and the header is exactly Authorization: Bearer TOKEN.



