In this deep-dive technical article, I’ll share how we refactored authentication architecture in a Laravel API — evolving from a controller-based SSO approach to a unified middleware pattern. We’ll explore the security considerations and architectural decisions that drove these changes.

Overview of Authentication Layers

The API implements a multi-layered authentication system to support different client types:

  1. UserToken Authentication — For App API consumers
  2. SSO Token Authentication — For embedded iframe/widget functionality
  3. API-Token Authentication — For third-party integrations

Each layer serves a specific purpose while maintaining security best practices.

The Problem: Scattered Authentication Logic

Initially, SSO token verification lived in a dedicated SSOController. While functional, this created several issues:

  • Code duplication — Similar JWT validation logic existed in multiple places
  • Inconsistent security checks — Route protection varied across controllers
  • Maintenance burden — Changes required updates in multiple files
  • Testing complexity — Each controller needed separate auth test suites

The Solution: Unified Middleware Pattern

We refactored authentication into a single Authenticated middleware that handles all token types. Here’s the architecture:

<?php

namespace App\Http\Middleware;

use App\Models\SsoToken;
use App\Models\User;
use App\Models\UserAPI;
use Carbon\Carbon;
use Closure;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\SignatureInvalidException;
use Illuminate\Support\Facades\Auth;

class Authenticated
{
    /**
     * Routes requiring enterprise plan access.
     */
    private $enterpriseRoutes = [
        '/monitor/billing/',
        '/monitor/settings/',
        '/monitor/logout',
        '/monitor/session'
    ];

    /**
     * Whitelisted routes for SSO token access.
     */
    private array $ssoTokenRoutes = [
        'monitor/site/view/{site}',
        'monitor/dashboard/data/{siteId?}',
        'monitor/site/state/{site}',
        'monitor/site/logs/{site}/{days}',
        // ... more routes
    ];

    public function handle($request, Closure $next, $guard = null)
    {
        // Layer 1: UserToken (App API)
        if ($request->hasHeader('UserToken')) {
            return $this->handleUserToken($request, $next);
        }

        // Layer 2: Site-Token (SSO/iframe)
        if ($request->hasHeader('Site-Token')) {
            return $this->handleSSOToken($request, $next);
        }

        // No valid authentication
        if (!Auth::check()) {
            return response('Authentication required.', 401);
        }

        return $next($request);
    }
}

SSO Token System Deep Dive

Token Generation

SSO tokens are JWT-based with database persistence for additional validation:

public function generateSSOToken(GenerateSSOToken $request, Site $site): JsonResponse
{
    // Validate site is properly configured
    $counters = $site->counters;
    if (is_null($counters) || $counters['components_count'] == 0) {
        return response()->json([
            'error' => 'Site not properly configured.'
        ], 409);
    }

    // Get authenticated user
    $userAPI = UserAPI::with('user')
        ->where('token', $request->header('UserToken'))
        ->first();
    $user = $userAPI->user;

    // Generate JWT with 1-hour expiry
    $expiresAt = Carbon::now()->addHour();
    $payload = [
        'user_id' => $user->id,
        'site_id' => $site->id,
        'exp' => $expiresAt->timestamp,
    ];
    $token = JWT::encode($payload, config('app.key'), 'HS256');

    // Persist to database for validation
    SsoToken::updateOrCreate(
        ['user_id' => $user->id],
        [
            'site_id' => $site->id,
            'token' => $token,
            'ip_address' => $request->input('ip_address'),
            'can_write' => $request->input('can_write', 0),
            'can_write_settings' => $request->input('can_write_settings', 0),
            'expires_at' => $expiresAt,
        ]
    );

    return response()->json([
        'access_token' => $token,
        'expires_in' => 3600,
    ]);
}

Token Validation in Middleware

The middleware performs multi-layer validation:

if ($request->hasHeader('Site-Token')) {
    $ssoToken = $request->header('Site-Token');
    
    // 1. Check route whitelist
    if (!in_array($request->route()->uri(), $this->ssoTokenRoutes)) {
        return response()->json(['error' => 'Unauthorized route'], 401);
    }

    try {
        // 2. Decode and verify JWT signature
        $decoded = JWT::decode($ssoToken, new Key(config('app.key'), 'HS256'));
        $userId = $decoded->user_id;
        $siteId = $decoded->site_id;

        // 3. Validate site parameter matches token
        if (stripos($request->route()->uri(), '{site}') !== false) {
            if ($request->route()->originalParameter('site') != $siteId) {
                return response()->json(['error' => 'Unauthorized'], 401);
            }
        }

        // 4. Verify token exists in database and not expired
        $ssoRecord = SsoToken::where('user_id', $userId)
            ->where('token', $ssoToken)
            ->where('expires_at', '>', now())
            ->first();
            
        if (!$ssoRecord) {
            return response()->json(['error' => 'Invalid or expired SSO Token'], 401);
        }

        // 5. Validate IP address binding
        if ($ssoRecord->ip_address && $ssoRecord->ip_address !== $request->ip()) {
            return response()->json(['error' => 'Unauthorized IP address'], 401);
        }

        // 6. Extend token validity (sliding expiration)
        $ssoRecord->expires_at = Carbon::now()->addHour();
        $ssoRecord->accessed = 1;
        $ssoRecord->save();

    } catch (SignatureInvalidException $e) {
        return response()->json(['error' => 'Invalid token signature'], 401);
    }

    Auth::setUser(User::find($decoded->user_id));
}

The SsoToken Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class SsoToken extends Model
{
    protected $fillable = [
        'user_id', 
        'site_id', 
        'token', 
        'ip_address', 
        'accessed', 
        'can_write', 
        'can_write_settings', 
        'expires_at'
    ];

    protected $casts = [
        'expires_at' => 'datetime',
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function site(): BelongsTo
    {
        return $this->belongsTo(Site::class);
    }
}

Authorization with FormRequests

We use Laravel FormRequests for granular authorization:

<?php

namespace App\Http\Requests\Site;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class GenerateSSOToken extends FormRequest
{
    public function authorize(): bool
    {
        $user = auth()->user();

        // Must be authenticated
        if (!$user) {
            return false;
        }

        // Must be enterprise user or admin
        if ($user->class != 7 && !$user->is_admin) {
            return false;
        }

        // Must own the resource
        if (!$user->canAlter($this->route('site')->id)) {
            return false;
        }

        return true;
    }

    protected function failedAuthorization()
    {
        throw new HttpResponseException(
            response()->json([
                'success' => false,
                'message' => 'You are not authorized to access this resource.',
                'code' => 403,
            ], 403)
        );
    }
}

Token Cleanup with Artisan Commands

Expired tokens are cleaned up via scheduled command:

<?php

namespace App\Console\Commands;

use App\Models\SsoToken;
use Carbon\Carbon;
use Illuminate\Console\Command;

class DeleteExpiredSSOTokens extends Command
{
    protected $signature = 'sso:cleanup';
    protected $description = 'Delete expired SSO tokens older than 24 hours.';

    public function handle()
    {
        $deleted = SsoToken::where('expires_at', '<', Carbon::now()->subDay())->delete();
        $this->info("Deleted {$deleted} expired tokens.");
        return Command::SUCCESS;
    }
}

Schedule in Kernel.php:

$schedule->command('sso:cleanup')->hourly();

API Token Authentication for Third-Party Integrations

For external integrations, we use a separate middleware:

<?php

namespace App\Http\Middleware;

use App\Models\UserAPI;
use Closure;

class VerifyUserAPIToken
{
    public function handle(Request $request, Closure $next)
    {
        if (!$request->hasHeader('API-Token')) {
            return response()->json([
                'error' => 'The API-Token header is missing.'
            ], 401);
        }

        $token = UserAPI::with('user')
            ->where('token', $request->header('API-Token'))
            ->first();
            
        if (is_null($token) || !$token->user || $token->user->class == 0) {
            return response()->json([
                'error' => 'Unauthorized API-Token.'
            ], 401);
        }

        // Merge token into request for downstream use
        $request->offsetSet('token', $token);

        return $next($request);
    }
}

Security Best Practices Implemented

1. Route Whitelisting

SSO tokens can only access explicitly whitelisted routes:

private array $ssoTokenRoutes = [
    'monitor/site/view/{site}',
    'monitor/dashboard/data/{siteId?}',
    // Strictly defined - no wildcards
];

2. Resource Parameter Validation

Tokens are bound to specific resources:

if ($request->route()->originalParameter('site') != $siteId) {
    return response()->json(['error' => 'Unauthorized'], 401);
}

3. IP Address Binding

Optional IP restriction prevents token theft:

if ($ssoRecord->ip_address && $ssoRecord->ip_address !== $request->ip()) {
    return response()->json(['error' => 'Unauthorized IP address'], 401);
}

4. Sliding Expiration

Active sessions are automatically extended:

$ssoRecord->expires_at = Carbon::now()->addHour();
$ssoRecord->save();

5. Dual Validation (JWT + Database)

Tokens must be valid JWTs AND exist in database:

// JWT validation
$decoded = JWT::decode($ssoToken, new Key(config('app.key'), 'HS256'));

// Database validation
$ssoRecord = SsoToken::where('token', $ssoToken)
    ->where('expires_at', '>', now())
    ->first();

Benefits of the Middleware Pattern

Before (Controller) After (Middleware)
Auth logic in multiple controllers Single source of truth
Easy to bypass checks Enforced on all routes
Harder to test Isolated, testable unit
Inconsistent error responses Standardized responses
Route-specific implementations Declarative route protection

Key Takeaways

Moving SSO token verification from controllers to middleware provided:

  • Centralized security — All auth logic in one place
  • Consistent enforcement — No route can bypass checks
  • Cleaner controllers — Focus on business logic only
  • Easier auditing — Single file to review for auth issues
  • Scalable pattern — Easy to add new token types

This pattern has proven robust in production, handling thousands of SSO-authenticated requests daily while maintaining strict security boundaries.

The key insight: authentication belongs in middleware, not controllers. Controllers should trust that the request is already authenticated and focus solely on their domain logic.