DEV Community

Cover image for Laravel API Authentication: A Practical Guide by Riad Hasan
Riad Hasan
Riad Hasan

Posted on

Laravel API Authentication: A Practical Guide by Riad Hasan

Riad Hasan has built secure APIs for dozens of applications. In this guide, he tackles one of the most common problems developers face: implementing robust API authentication in Laravel.

Many developers struggle with choosing the right authentication method. Should they use Sanctum? Passport? JWT? Riad Hasan breaks down when to use each approach and provides production-ready implementations.
The Problem: Authentication Confusion

When building a Laravel API, developers often ask:

"Which authentication package should I use?"
"How do I secure my API endpoints?"
"What about token management and expiration?"
"How do I handle multiple device logins?"
Enter fullscreen mode Exit fullscreen mode

Riad Hasan has seen projects delayed by weeks because developers chose the wrong authentication strategy. Here's his systematic approach to solving this.
Riad Hasan's Authentication Decision Matrix
Use Case Recommended Solution
SPA (Vue/React) Laravel Sanctum
Mobile App Laravel Sanctum
Third-party Apps Laravel Passport
Machine-to-Machine API Keys
Microservices JWT (custom)

"I've seen teams use Passport for simple SPAs — that's overkill," Riad Hasan explains. "Sanctum handles 90% of use cases with less complexity."
Enter fullscreen mode Exit fullscreen mode

Solution 1: Laravel Sanctum for SPAs

Riad Hasan's preferred approach for single-page applications.
Setup

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Configuration

// config/sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),
'guard' => ['web'],
'expiration' => 60 * 24 * 7, // 7 days
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
],
];

Login Endpoint

Riad Hasan's production-ready login implementation:

// app/Http/Controllers/Api/AuthController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required|string',
]);

    $credentials = $request->only('email', 'password');

    if (!Auth::attempt($credentials)) {
        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

    $user = $request->user();

    // Revoke old tokens for this device
    $user->tokens()->where('name', $request->device_name)->delete();

    // Create new token
    $token = $user->createToken($request->device_name)->plainTextToken;

    return response()->json([
        'user' => $user,
        'token' => $token,
        'message' => 'Login successful',
    ]);
}

public function logout(Request $request)
{
    $request->user()->currentAccessToken()->delete();

    return response()->json([
        'message' => 'Logged out successfully',
    ]);
}

public function user(Request $request)
{
    return response()->json($request->user());
}
Enter fullscreen mode Exit fullscreen mode

}

Routes

// routes/api.php
use App\Http\Controllers\Api\AuthController;

Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);

Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', [AuthController::class, 'user']);
Route::post('/logout', [AuthController::class, 'logout']);

// Protected routes
Route::apiResource('projects', ProjectController::class);
Enter fullscreen mode Exit fullscreen mode

});

Frontend Integration (React)

Riad Hasan's React authentication hook:

// hooks/useAuth.js
import { useState, useEffect, useContext, createContext } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
checkAuth();
}, []);

const checkAuth = async () => {
const token = localStorage.getItem('token');
if (token) {
try {
const response = await fetch('/api/user', {
headers: {
Authorization: Bearer ${token},
},
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
localStorage.removeItem('token');
}
} catch (error) {
localStorage.removeItem('token');
}
}
setLoading(false);
};

const login = async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
device_name: 'web-browser',
}),
});

if (!response.ok) {
  throw new Error('Invalid credentials');
}

const data = await response.json();
localStorage.setItem('token', data.token);
setUser(data.user);
return data;
Enter fullscreen mode Exit fullscreen mode

};

const logout = async () => {
const token = localStorage.getItem('token');
await fetch('/api/logout', {
method: 'POST',
headers: {
Authorization: Bearer ${token},
},
});
localStorage.removeItem('token');
setUser(null);
};

return (

{children}

);
}

export const useAuth = () => useContext(AuthContext);

Solution 2: Laravel Passport for OAuth2

When Riad Hasan needs third-party app access, he uses Passport.
Setup

composer require laravel/passport
php artisan passport:install

Configuration

// app/Models/User.php
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable
{
use HasApiTokens, Notifiable;

// ...
Enter fullscreen mode Exit fullscreen mode

}

Creating OAuth Clients

Password grant client (first-party apps)

php artisan passport:client --password

Client credentials grant (machine-to-machine)

php artisan passport:client --client

Riad Hasan's OAuth Controller

// app/Http/Controllers/Api/OAuthController.php
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Laravel\Passport\Client;
use Laravel\Passport\Http\Controllers\AccessTokenController;
use Psr\Http\Message\ServerRequestInterface;

class OAuthController extends Controller
{
public function issueToken(ServerRequestInterface $request)
{
$controller = app(AccessTokenController::class);
return $controller->issueToken($request);
}

public function refreshToken(Request $request)
{
    $request->validate([
        'refresh_token' => 'required',
    ]);

    $client = Client::where('password_client', 1)->first();

    $response = \Http::asForm()->post(url('/oauth/token'), [
        'grant_type' => 'refresh_token',
        'refresh_token' => $request->refresh_token,
        'client_id' => $client->id,
        'client_secret' => $client->secret,
        'scope' => '',
    ]);

    return $response->json();
}
Enter fullscreen mode Exit fullscreen mode

}

Solution 3: API Keys for Machine-to-Machine

For services and webhooks, Riad Hasan uses simple API keys.
Migration

// database/migrations/create_api_keys_table.php
Schema::create('api_keys', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->string('key', 64)->unique();
$table->text('permissions')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});

Middleware

// app/Http/Middleware/ApiKeyAuth.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use App\Models\ApiKey;

class ApiKeyAuth
{
public function handle(Request $request, Closure $next)
{
$key = $request->header('X-API-KEY') ?? $request->query('api_key');

    if (!$key) {
        return response()->json([
            'error' => 'API key required',
        ], 401);
    }

    $apiKey = ApiKey::where('key', $key)->first();

    if (!$apiKey) {
        return response()->json([
            'error' => 'Invalid API key',
        ], 401);
    }

    // Update last used
    $apiKey->update(['last_used_at' => now()]);

    $request->setUserResolver(function () use ($apiKey) {
        return $apiKey->user;
    });

    return $next($request);
}
Enter fullscreen mode Exit fullscreen mode

}

Riad Hasan's Security Best Practices

  1. Token Expiration

// config/sanctum.php
'expiration' => 60 * 24, // 24 hours for sensitive apps

// Or dynamically
$token = $user->createToken('device', ['*'], now()->addHours(4));

  1. Rate Limiting

// app/Http/Kernel.php
protected $middlewareAliases = [
'throttle.auth' => \App\Http\Middleware\ThrottleAuth::class,
];

// routes/api.php
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:5,1'); // 5 attempts per minute

  1. Token Abilities

// Create token with limited abilities
$token = $user->createToken('read-only', ['read']);

// Check ability in controller
if (!$request->user()->tokenCan('write')) {
return response()->json(['error' => 'Insufficient permissions'], 403);
}

  1. Secure Password Reset

Riad Hasan's password reset flow:

// app/Http/Controllers/Api/PasswordResetController.php
public function reset(Request $request)
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed|min:8',
]);

$status = Password::reset(
    $request->only('email', 'password', 'password_confirmation', 'token'),
    function ($user, $password) {
        $user->forceFill([
            'password' => Hash::make($password),
        ])->setRememberToken(Str::random(60));
        $user->save();

        // Revoke all existing tokens
        $user->tokens()->delete();

        event(new PasswordReset($user));
    }
);

return $status === Password::PASSWORD_RESET
    ? response()->json(['message' => 'Password reset successfully'])
    : response()->json(['error' => 'Unable to reset password'], 400);
Enter fullscreen mode Exit fullscreen mode

}

Common Mistakes Riad Hasan Avoids
Mistake Solution
Storing tokens in localStorage Use httpOnly cookies for sensitive apps
No token expiration Always set expiration times
Not revoking tokens on logout Delete tokens server-side
Using Passport for simple SPAs Use Sanctum instead
No rate limiting on auth endpoints Implement throttle middleware
Storing plain API keys Hash keys like passwords
Work with Riad Hasan

Riad Hasan specializes in building secure, scalable APIs with Laravel. He offers:

Laravel API development
Authentication system implementation
Security audits
Performance optimization
Team training and consultation
Enter fullscreen mode Exit fullscreen mode

Connect with Riad Hasan:

Portfolio: riadhasan.io
Projects: riadhasan.io/projects
LinkedIn: linkedin.com/in/riad-hasan-100a231a6
GitHub: github.com/RiadHasan15
Email: hire.riadhasan@gmail.com
Enter fullscreen mode Exit fullscreen mode

Which authentication method do you prefer for your Laravel APIs? Share your experience in the comments.

laravel #php #api #authentication #webdev #security #sanctum #passport #webdevelopment

Top comments (0)