Authentication: Sanctum vs Passport

Laravel offers two first-party authentication packages for APIs. Choosing the right one matters for your security model:

Laravel Sanctum — lightweight, token-based auth ideal for SPAs and mobile apps. Sanctum issues plain API tokens (stored hashed in the database) and supports SPA authentication via cookie-based sessions. For most Nigerian fintech APIs, Sanctum is the right choice. It's simpler, has fewer moving parts, and provides token abilities (scopes) for fine-grained access control.

Laravel Passport — full OAuth2 implementation. Use Passport when you're building an open API for third-party integrations (like an open banking API), where you need authorization code grants, client credentials, and standardized token introspection. For internal APIs consumed only by your own apps, Passport adds complexity without proportional benefit.

// Sanctum token with scoped abilities
$token = $user->createToken('mobile-app', ['transfer:create', 'balance:read']);

// Checking abilities in middleware
Route::middleware(['auth:sanctum', 'ability:transfer:create'])
    ->post('/transfers', [TransferController::class, 'store']);

Whichever you choose, configure token expiration. Sanctum tokens don't expire by default — set expiration in config/sanctum.php. For Passport, configure token lifetimes in the AuthServiceProvider. Short-lived access tokens (15–30 minutes) with refresh rotation is the standard for fintech.

Middleware: defence in layers

Laravel's middleware stack is where you enforce security policies across your application. Apply authentication middleware at the route group level, not per-route, to prevent accidental exposure of new endpoints:

// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
    Route::apiResource('transactions', TransactionController::class);
    Route::post('/transfers', [TransferController::class, 'store']);
});

// Public routes — explicitly outside the auth group
Route::post('/auth/login', [AuthController::class, 'login']);
Route::post('/auth/register', [AuthController::class, 'register']);

Add custom middleware for logging, IP whitelisting on admin routes, and request signing verification for webhook endpoints. Layer them in the kernel so they execute in the correct order.

Form requests: structured input validation

Laravel's Form Request classes are the cleanest way to validate API input. They separate validation logic from controller logic and automatically return 422 responses with structured error messages:

class StoreTransferRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Transfer::class);
    }

    public function rules(): array
    {
        return [
            'amount' => ['required', 'numeric', 'min:100', 'max:10000000'],
            'recipient_account' => ['required', 'string', 'regex:/^\d{10}$/'],
            'bank_code' => ['required', 'string', 'size:3'],
            'narration' => ['nullable', 'string', 'max:100'],
        ];
    }
}

The authorize() method handles object-level permission checks — use it. This is your defence against BOLA vulnerabilities where authenticated users access resources they don't own.

Pentest finding

Mass assignment on user profile endpoint

A Laravel-based lending platform used $request->all() to update user profiles. By adding "is_admin": true to the request body, we escalated a regular user account to admin privileges. The $fillable property on the User model included is_admin because it was used in an admin seeder.

Mass assignment protection

Laravel's Eloquent ORM uses $fillable (whitelist) or $guarded (blacklist) to control which fields can be set via mass assignment. For fintech models, always use $fillable and be explicit:

class User extends Authenticatable
{
    // GOOD — explicit whitelist
    protected $fillable = ['name', 'email', 'phone'];

    // BAD — implicit allowance of everything except these
    // protected $guarded = ['id'];
}

Never use $guarded = [] (allow all) and never pass $request->all() directly to create() or update(). Use $request->validated() from a Form Request, which only returns fields that passed validation.

Rate limiting

Laravel's rate limiter (built on the cache driver) supports per-route and per-user throttling. Configure it in App\Providers\RouteServiceProvider:

RateLimiter::for('login', function (Request $request) {
    return Limit::perMinute(5)->by($request->ip());
});

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

RateLimiter::for('otp', function (Request $request) {
    return Limit::perMinute(3)->by($request->ip());
});

Apply the login limiter to authentication routes and the otp limiter to OTP verification and password reset. Without these, an attacker can brute-force 4-digit OTPs in under 20 minutes. See rate limiting for payment APIs for detailed patterns.

Eloquent parameterization and raw queries

Eloquent and the query builder parameterize queries automatically. SQL injection in Laravel typically happens when developers use DB::raw(), whereRaw(), or selectRaw() with string interpolation:

// VULNERABLE
$users = DB::select("SELECT * FROM users WHERE email = '$email'");

// SAFE — parameterized binding
$users = DB::select("SELECT * FROM users WHERE email = ?", [$email]);

// SAFE — Eloquent
$user = User::where('email', $email)->first();

Audit your codebase for every Raw method call. If you're using raw expressions for complex reporting queries (common in fintech reconciliation), ensure every variable is bound as a parameter.

Running a fintech product on Laravel? We test for the exact vulnerabilities Laravel teams overlook.

Book a Laravel API security audit

CSRF, encryption, and queue security

CSRF — Laravel's CSRF middleware protects session-based routes. For Sanctum SPA authentication (cookie mode), CSRF is enforced automatically. For token-only APIs, CSRF middleware is bypassed via the api middleware group. Don't manually disable CSRF on web routes for convenience.

Encryption — Use Laravel's Crypt facade for encrypting sensitive data at rest (BVN numbers, account details). Laravel uses AES-256-CBC with your APP_KEY. Rotate the APP_KEY carefully — it's the master encryption key. Store it in a secret manager, never in source code.

Queue security — If you process payments or sensitive operations via Laravel queues (Redis, SQS), ensure your queue workers run with least-privilege database credentials, your Redis instance requires authentication, and serialized job payloads don't contain unencrypted sensitive data. A compromised queue gives an attacker access to every pending operation.

Key takeaway

Laravel gives you the tools — use them intentionally

Laravel's security primitives (Sanctum, Form Requests, Eloquent parameterization, encryption) are excellent. The vulnerabilities we find aren't framework weaknesses — they're configuration gaps. Explicit $fillable, scoped tokens, Form Request authorization, rate limiting on auth endpoints, and production environment hardening (APP_DEBUG=false, proper APP_KEY management) close most of the attack surface.

Related reading

Blog: Securing Express APIs · Securing Django APIs · Webhook security

Guides: OWASP for fintech · Web app pentesting · Security checklist

Services: API security · Penetration testing · Authentication security