API REST in Laravel: autenticazione con Sanctum e autorizzazione
La nostra API è funzionale e robusta, ma completamente aperta: chiunque può creare, modificare ed eliminare libri. In questa puntata la mettiamo in sicurezza. Introduciamo l'autenticazione con Laravel Sanctum, il pacchetto a token già installato quando abbiamo lanciato install:api nella prima puntata, e l'autorizzazione tramite policy, che stabilisce non solo chi è l'utente ma anche che cosa gli è permesso fare. Al termine avremo endpoint pubblici in lettura e protetti in scrittura, con token che il client presenta a ogni richiesta.
Autenticazione e autorizzazione
Conviene distinguere subito due concetti che spesso si confondono. L'autenticazione risponde alla domanda: chi sei? Verifica l'identità di chi effettua la richiesta. L'autorizzazione risponde a una domanda diversa: che cosa ti è permesso fare? Presuppone l'identità già nota e decide se quell'utente può compiere una determinata azione su una determinata risorsa. Un utente autenticato non è necessariamente autorizzato a tutto: potrà magari leggere qualsiasi libro ma modificare soltanto quelli che ha inserito.
Laravel Sanctum in breve
Sanctum offre un sistema leggero di autenticazione a token. Ogni utente può possedere uno o più personal access token: stringhe segrete che il client conserva e allega a ogni richiesta nell'intestazione Authorization. Il server, ricevuto il token, risale all'utente e considera la richiesta autenticata. È il modello ideale per API consumate da app mobili, servizi esterni o frontend a pagina singola.
Poiché abbiamo già eseguito install:api, Sanctum è installato e la tabella dei token è pronta. Ci resta da predisporre il modello utente, creare gli endpoint di registrazione e login e proteggere le rotte.
Preparare il modello User
Il modello App\Models\User deve poter emettere token. Verifichiamo che usi il trait HasApiTokens, aggiungendolo se assente.
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
// HasApiTokens abilita il metodo createToken() sul modello
use HasApiTokens, Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
// Applica automaticamente l'hashing quando si assegna la password
'password' => 'hashed',
];
}
}
Il cast hashed sulla password è comodo: ogni volta che assegniamo una password in chiaro al modello, Laravel la sottopone ad hashing prima di salvarla, così non dobbiamo ricordarci di farlo a mano.
Gli endpoint di registrazione e login
Creiamo un controller dedicato all'autenticazione.
php artisan make:controller AuthController
Aggiungiamo i metodi di registrazione, login e logout. La registrazione crea l'utente ed emette subito un token; il login verifica le credenziali ed emette un token; il logout revoca il token in uso.
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
// Registra un nuovo utente e restituisce un token di accesso
public function register(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'string', 'min:8'],
]);
$user = User::create($validated);
$token = $user->createToken('api-token')->plainTextToken;
return response()->json(['token' => $token], 201);
}
// Verifica le credenziali e restituisce un token
public function login(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
$user = User::where('email', $credentials['email'])->first();
// Confrontiamo la password in chiaro con l'hash memorizzato
if ($user === null || ! Hash::check($credentials['password'], $user->password)) {
throw ValidationException::withMessages([
'email' => ['Le credenziali fornite non sono corrette.'],
]);
}
$token = $user->createToken('api-token')->plainTextToken;
return response()->json(['token' => $token]);
}
// Revoca il token usato per la richiesta corrente
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json(status: 204);
}
}
Il valore restituito da createToken espone la proprietà plainTextToken: è l'unico momento in cui il token in chiaro è disponibile. Nel database ne viene salvato soltanto l'hash, quindi se il client lo perde non c'è modo di recuperarlo, va emesso un nuovo token.
Registrare le rotte di autenticazione
Aggiungiamo le rotte in routes/api.php. Registrazione e login sono pubbliche; il logout richiede un token valido, quindi lo proteggiamo con il middleware di Sanctum.
use App\Http\Controllers\AuthController;
use App\Http\Controllers\BookController;
use Illuminate\Support\Facades\Route;
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
});
Proteggere le rotte dei libri
Vogliamo che la lettura del catalogo resti pubblica, ma che la scrittura sia riservata agli utenti autenticati. Separiamo quindi la risorsa in due blocchi: le azioni di lettura fuori dal middleware, quelle di modifica dentro un gruppo protetto.
// Lettura pubblica
Route::apiResource('books', BookController::class)->only(['index', 'show']);
// Scrittura riservata agli utenti autenticati
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('books', BookController::class)->only(['store', 'update', 'destroy']);
});
Ora un tentativo di creare un libro senza token riceve una risposta 401 Unauthorized. Per superarla, il client deve allegare il token ottenuto al login.
curl -i -X POST http://localhost:8000/api/books \
-H "Authorization: Bearer IL_TUO_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"title":"Il barone rampante","author":"Italo Calvino","year":1957}'
All'interno di una rotta protetta, l'utente autenticato è sempre raggiungibile con $request->user() oppure con l'helper auth()->user(). Da qui in poi possiamo legare i dati all'utente: per esempio, registrare chi ha inserito ciascun libro.
Autorizzazione con le policy
L'autenticazione ci dice chi è l'utente; l'autorizzazione decide cosa può fare. Supponiamo di aver aggiunto una colonna user_id alla tabella dei libri, così da sapere chi ha creato ciascun record, e di voler consentire la modifica e l'eliminazione soltanto al proprietario. Questa logica trova casa naturale in una policy, una classe che raccoglie le regole di autorizzazione per un modello. Generiamola.
php artisan make:policy BookPolicy --model=Book
Definiamo i metodi che ci interessano in app/Policies/BookPolicy.php. Ogni metodo riceve l'utente e la risorsa e restituisce un valore booleano.
<?php
namespace App\Policies;
use App\Models\Book;
use App\Models\User;
class BookPolicy
{
// Solo il proprietario può aggiornare il libro
public function update(User $user, Book $book): bool
{
return $user->id === $book->user_id;
}
// Solo il proprietario può eliminare il libro
public function delete(User $user, Book $book): bool
{
return $user->id === $book->user_id;
}
}
Laravel associa automaticamente la policy al modello grazie alla convenzione sui nomi. Nel controller invochiamo il controllo con il metodo authorize: se la policy nega il permesso, l'esecuzione si interrompe con una risposta 403 Forbidden.
public function update(UpdateBookRequest $request, Book $book)
{
// Verifica che l'utente corrente possa aggiornare questo libro
$this->authorize('update', $book);
$book->update($request->validated());
return new BookResource($book);
}
public function destroy(Book $book)
{
$this->authorize('delete', $book);
$book->delete();
return response()->json(status: 204);
}
Autorizzazione nelle Form Request
Ricordi il metodo authorize delle Form Request, che nella puntata precedente restituiva sempre true? Ora possiamo collegarlo alla policy, spostando il controllo di autorizzazione ancora più a monte, prima che il metodo del controller venga eseguito.
// Dentro UpdateBookRequest: autorizza tramite la policy del libro
public function authorize(): bool
{
return $this->user()->can('update', $this->route('book'));
}
Con questo approccio, un utente non autorizzato riceve la risposta 403 prima ancora che i dati vengano validati. Sta a noi scegliere dove collocare il controllo: nella Form Request per bloccare in anticipo, o nel controller per maggiore visibilità. L'importante è non dimenticarlo.
Abilità dei token
Sanctum consente di assegnare abilities a un token, ovvero permessi granulari. Possiamo emettere un token che può solo leggere e un altro che può anche scrivere, indicando le abilità al momento della creazione.
// Token con permessi limitati alla sola lettura
$token = $user->createToken('read-only', ['books:read'])->plainTextToken;
Nelle rotte possiamo poi richiedere una specifica abilità con il middleware dedicato, ottenendo un controllo degli accessi ancora più fine.
// Richiede che il token possieda l'abilità books:write
Route::middleware(['auth:sanctum', 'abilities:books:write'])->group(function () {
Route::apiResource('books', BookController::class)->only(['store', 'update', 'destroy']);
});
Conclusione
La nostra API è ora sotto controllo. Con Sanctum abbiamo aggiunto l'autenticazione a token, creato gli endpoint di registrazione, login e logout e protetto le operazioni di scrittura, lasciando pubblica la lettura. Con le policy abbiamo introdotto l'autorizzazione, distinguendo ciò che un utente autenticato può effettivamente fare, e abbiamo visto come collegarla sia al controller sia alle Form Request. Con le abilità dei token abbiamo infine accennato a un controllo degli accessi granulare.
Manca un ultimo tassello per portare l'API a un livello davvero professionale: la gestione uniforme degli errori, la strategia di versioning, la protezione contro l'abuso tramite rate limiting e, soprattutto, i test automatici. Sarà l'argomento della settima e ultima puntata, che chiuderà la serie.