From laravel-agent-skills
This skill should be used when the user asks to "add authentication", "protect a route", "create a policy", "check permissions", "set up Sanctum", "add authorization", "create middleware", "use gates", or when implementing auth, authorization, or access control in a Laravel 12 API.
How this skill is triggered — by the user, by Claude, or both
Slash command
/laravel-agent-skills:laravel-authThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Always use Sanctum for API authentication. Never roll a custom auth system.
Always use Sanctum for API authentication. Never roll a custom auth system.
Install and configure Sanctum. On Laravel 11+ use the canonical command, which installs Sanctum, publishes the migration, creates routes/api.php, and registers the API routes:
php artisan install:api
The older composer require laravel/sanctum && php artisan vendor:publish ... && php artisan migrate sequence still works but is no longer needed.
Add HasApiTokens to the User model for stateless token-based auth:
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
final class User extends Authenticatable
{
use HasApiTokens;
}
Issue a token on login:
$token = $user->createToken('mobile-app', ['read', 'write']);
return response()->json(['token' => $token->plainTextToken]);
Access ->plainTextToken once, return it to the client, and never store it again server-side. The client stores and sends it as Authorization: Bearer <token>.
For same-domain SPAs, prepend EnsureFrontendRequestsAreStateful to the API middleware group used by routes loaded via withRouting(api: ...) in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->statefulApi();
})
The SPA calls GET /sanctum/csrf-cookie first, then sends all state-changing requests with the X-XSRF-TOKEN header sourced from the XSRF-TOKEN cookie.
Apply auth:sanctum at the route group level, never per individual route:
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('posts', PostController::class);
Route::apiResource('orders', OrderController::class);
});
Create one policy per model. Name policies {Model}Policy and place them in app/Policies/. Laravel 11+ auto-discovers them — no manual registration required. For non-standard locations, register manually with Gate::policy(Post::class, PostPolicy::class) in AppServiceProvider::boot() (Laravel 11+ no longer scaffolds AuthServiceProvider by default).
php artisan make:policy PostPolicy --model=Post
before() Super-Admin BypassUse before() to grant admins unconditional access. Return true to allow, null to fall through to the specific policy method. Never return false from before() unless explicitly blocking admins:
public function before(User $user, string $ability): bool|null
{
if ($user->isAdmin()) {
return true;
}
return null; // continue to specific method
}
Follow this strict rule:
| Operation | Where to authorize |
|---|---|
create, store | Form Request authorize() — no model bound yet |
update, show, destroy | Controller Gate::authorize() — model already bound via route model binding |
Note: Laravel 11+ removed
AuthorizesRequestsfrom the defaultApp\Http\Controllers\Controller. UseGate::authorize(...)instead, or adduse AuthorizesRequests;to your base controller if you prefer the trait form ($this->authorize(...)).
Form Request (no model bound yet — create/store only):
public function authorize(): bool
{
return $this->user()?->can('create', Post::class) ?? false;
}
Controller (model already bound via route model binding — update, show, destroy):
use Illuminate\Support\Facades\Gate;
public function show(Post $post): JsonResponse
{
Gate::authorize('view', $post);
return response()->json($post);
}
public function update(UpdatePostRequest $request, Post $post): JsonResponse
{
Gate::authorize('update', $post);
$post->update($request->validated());
return response()->json($post);
}
public function destroy(Post $post): JsonResponse
{
Gate::authorize('delete', $post);
$post->delete();
return response()->json(null, 204);
}
Never authorize inside a Model. Never authorize inside a Service class.
Use Gates for feature flags, admin checks, and actions that do not belong to a single model. Define all Gates in AppServiceProvider::boot():
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::define('access-dashboard', fn(User $user): bool =>
$user->hasRole('admin') || $user->hasRole('manager')
);
Gate::define('export-reports', fn(User $user): bool =>
$user->subscription_tier === 'enterprise'
);
}
Use Gates in controllers:
Gate::authorize('access-dashboard'); // throws 403 on failure
Or check programmatically:
if (Gate::allows('export-reports')) {
// proceed
}
Apply ->middleware('can:update,post') only for simple single-policy checks directly on a route. For any logic more complex than a single policy method call, use Gate::authorize() inside the controller:
use Illuminate\Support\Facades\Gate;
// simple — acceptable at route level
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('can:update,post');
// complex — always move to controller
public function update(UpdatePostRequest $request, Post $post): JsonResponse
{
Gate::authorize('update', $post);
// ...
}
Frontend can() checks (Inertia, Blade, or API-served permission lists) are UX conveniences only. Every state-changing operation must be authorized server-side. A hidden button does not prevent a direct API call.
Scope tokens to limit what a client can do:
$token = $user->createToken('read-only-client', ['read']);
Check abilities in controllers or middleware:
if ($request->user()->tokenCan('read')) {
// allowed
}
Revoke the current session token on logout:
$request->user()->currentAccessToken()->delete();
return response()->json(null, 204);
Revoke all tokens (force logout everywhere):
$user->tokens()->delete();
Set token lifetime in config/sanctum.php:
'expiration' => 60 * 24 * 30, // 30 days in minutes, null = never
Schedule pruning of expired tokens in routes/console.php:
Schedule::command('sanctum:prune-expired --hours=24')->daily();
Use Sanctum::actingAs() in feature tests. Never make real HTTP calls with real tokens in tests:
use Laravel\Sanctum\Sanctum;
Sanctum::actingAs($user);
$this->getJson('/api/posts')->assertOk();
// Test ability-scoped token
Sanctum::actingAs($user, ['update-post']);
$this->putJson('/api/posts/1', ['title' => 'Updated'])->assertOk();
references/sanctum.md — Full Sanctum reference: token lifecycle, SPA auth flow, expiration, testing, multiple guardsreferences/gates-policies.md — Full Gates and Policies reference: all 7 policy methods, before/after hooks, policy responses, programmatic checks, testingnpx claudepluginhub abdallhmoukdad/laravel-agent-skills --plugin laravel-agent-skillsProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.