Laravel Integration

Laravel Integration

This guide covers how to integrate Skycloak authentication into Laravel applications using OAuth2/OIDC libraries and Laravel best practices.

Prerequisites

  • Laravel 8+ application
  • PHP 7.4+ or PHP 8+
  • Skycloak cluster with configured realm and client
  • Composer package manager
  • Basic understanding of Laravel authentication

Quick Start

1. Install Dependencies

composer require laravel/socialite socialiteproviders/keycloak firebase/php-jwt

Or using a dedicated Keycloak package:

composer require vizir/laravel-keycloak-web-guard

2. Configure Service Provider

Add to config/app.php:

'providers' => [
    // ...
    \SocialiteProviders\Manager\ServiceProvider::class,
    Vizir\KeycloakWebGuard\KeycloakWebGuardServiceProvider::class,
],

'aliases' => [
    // ...
    'Socialite' => Laravel\Socialite\Facades\Socialite::class,
],

3. Configure Authentication

Update config/auth.php:

'guards' => [
    'web' => [
        'driver' => 'keycloak-web',
        'provider' => 'users',
    ],
    
    'api' => [
        'driver' => 'keycloak-api',
        'provider' => 'users',
    ],
],

'providers' => [
    'users' => [
        'driver' => 'keycloak-users',
        'model' => App\Models\User::class,
    ],
],

4. Environment Configuration

Add to .env:

KEYCLOAK_BASE_URL=https://your-cluster-id.app.skycloak.io
KEYCLOAK_REALM=your-realm
KEYCLOAK_CLIENT_ID=your-laravel-app
KEYCLOAK_CLIENT_SECRET=your-secret
KEYCLOAK_REDIRECT_URI=${APP_URL}/auth/callback
KEYCLOAK_SCOPE="openid profile email"

5. Publish Configuration

php artisan vendor:publish --provider="Vizir\KeycloakWebGuard\KeycloakWebGuardServiceProvider"

Authentication Implementation

Routes Configuration

// routes/web.php
use App\Http\Controllers\Auth\KeycloakController;

Route::get('/', function () {
    return view('welcome');
});

// Authentication routes
Route::prefix('auth')->group(function () {
    Route::get('login', [KeycloakController::class, 'login'])->name('login');
    Route::get('logout', [KeycloakController::class, 'logout'])->name('logout');
    Route::get('callback', [KeycloakController::class, 'callback'])->name('auth.callback');
});

// Protected routes
Route::middleware(['keycloak-web'])->group(function () {
    Route::get('/dashboard', function () {
        return view('dashboard');
    })->name('dashboard');
    
    Route::get('/profile', [ProfileController::class, 'show'])->name('profile');
});

Authentication Controller

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class KeycloakController extends Controller
{
    /**
     * Redirect to Keycloak for authentication.
     */
    public function login()
    {
        $url = config('keycloak-web.base_url') . '/realms/' . config('keycloak-web.realm') . '/protocol/openid-connect/auth?' . http_build_query([
            'client_id' => config('keycloak-web.client_id'),
            'redirect_uri' => config('keycloak-web.redirect_uri'),
            'response_type' => 'code',
            'scope' => config('keycloak-web.scope'),
            'state' => csrf_token(),
        ]);
        
        return redirect($url);
    }
    
    /**
     * Handle callback from Keycloak.
     */
    public function callback(Request $request)
    {
        if ($request->has('error')) {
            return redirect('/')->withErrors(['error' => $request->get('error_description')]);
        }
        
        // Verify state to prevent CSRF
        if ($request->get('state') !== csrf_token()) {
            return redirect('/')->withErrors(['error' => 'Invalid state parameter']);
        }
        
        try {
            // Exchange code for tokens
            $tokens = $this->exchangeCodeForTokens($request->get('code'));
            
            // Decode ID token to get user info
            $userInfo = $this->decodeIdToken($tokens['id_token']);
            
            // Create or update user
            $user = $this->findOrCreateUser($userInfo, $tokens);
            
            // Log in the user
            Auth::login($user, true);
            
            // Store tokens in session
            session([
                'access_token' => $tokens['access_token'],
                'refresh_token' => $tokens['refresh_token'] ?? null,
                'id_token' => $tokens['id_token'],
                'token_expires_at' => now()->addSeconds($tokens['expires_in']),
            ]);
            
            return redirect()->intended('/dashboard');
            
        } catch (\Exception $e) {
            \Log::error('Keycloak authentication failed: ' . $e->getMessage());
            return redirect('/')->withErrors(['error' => 'Authentication failed']);
        }
    }
    
    /**
     * Log out from application and Keycloak.
     */
    public function logout(Request $request)
    {
        $idToken = session('id_token');
        
        Auth::logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();
        
        // Redirect to Keycloak logout
        $logoutUrl = config('keycloak-web.base_url') . '/realms/' . config('keycloak-web.realm') . '/protocol/openid-connect/logout?' . http_build_query([
            'id_token_hint' => $idToken,
            'post_logout_redirect_uri' => url('/'),
        ]);
        
        return redirect($logoutUrl);
    }
    
    /**
     * Exchange authorization code for tokens.
     */
    private function exchangeCodeForTokens($code)
    {
        $response = \Http::asForm()->post(
            config('keycloak-web.base_url') . '/realms/' . config('keycloak-web.realm') . '/protocol/openid-connect/token',
            [
                'grant_type' => 'authorization_code',
                'client_id' => config('keycloak-web.client_id'),
                'client_secret' => config('keycloak-web.client_secret'),
                'code' => $code,
                'redirect_uri' => config('keycloak-web.redirect_uri'),
            ]
        );
        
        if (!$response->successful()) {
            throw new \Exception('Failed to exchange code for tokens');
        }
        
        return $response->json();
    }
    
    /**
     * Decode and validate ID token.
     */
    private function decodeIdToken($idToken)
    {
        // Get Keycloak public keys
        $jwks = \Http::get(
            config('keycloak-web.base_url') . '/realms/' . config('keycloak-web.realm') . '/protocol/openid-connect/certs'
        )->json();
        
        // Decode token header to get key ID
        $tokenParts = explode('.', $idToken);
        $header = json_decode(base64_decode($tokenParts[0]), true);
        $keyId = $header['kid'];
        
        // Find the correct key
        $publicKey = null;
        foreach ($jwks['keys'] as $key) {
            if ($key['kid'] === $keyId) {
                $publicKey = $this->createPublicKey($key);
                break;
            }
        }
        
        if (!$publicKey) {
            throw new \Exception('Public key not found');
        }
        
        // Decode and verify token
        return JWT::decode($idToken, new Key($publicKey, 'RS256'));
    }
    
    /**
     * Create public key from JWK.
     */
    private function createPublicKey($jwk)
    {
        $modulus = $this->base64UrlDecode($jwk['n']);
        $exponent = $this->base64UrlDecode($jwk['e']);
        
        $components = [
            'modulus' => $modulus,
            'exponent' => $exponent,
        ];
        
        $publicKey = \openssl_pkey_new([
            'n' => $components['modulus'],
            'e' => $components['exponent'],
        ]);
        
        return openssl_pkey_get_details($publicKey)['key'];
    }
    
    /**
     * Base64 URL decode.
     */
    private function base64UrlDecode($input)
    {
        $remainder = strlen($input) % 4;
        if ($remainder) {
            $padlen = 4 - $remainder;
            $input .= str_repeat('=', $padlen);
        }
        return base64_decode(strtr($input, '-_', '+/'));
    }
    
    /**
     * Find or create user from Keycloak data.
     */
    private function findOrCreateUser($userInfo, $tokens)
    {
        return User::updateOrCreate(
            ['keycloak_id' => $userInfo->sub],
            [
                'name' => $userInfo->name ?? $userInfo->preferred_username,
                'email' => $userInfo->email,
                'email_verified_at' => $userInfo->email_verified ? now() : null,
                'username' => $userInfo->preferred_username,
                'first_name' => $userInfo->given_name ?? '',
                'last_name' => $userInfo->family_name ?? '',
                'realm_roles' => $userInfo->realm_access->roles ?? [],
                'client_roles' => $userInfo->resource_access ?? [],
                'groups' => $userInfo->groups ?? [],
                'attributes' => $userInfo->attributes ?? [],
            ]
        );
    }
}

User Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'username',
        'keycloak_id',
        'first_name',
        'last_name',
        'realm_roles',
        'client_roles',
        'groups',
        'attributes',
    ];

    protected $hidden = [
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
        'realm_roles' => 'array',
        'client_roles' => 'array',
        'groups' => 'array',
        'attributes' => 'array',
    ];

    /**
     * Check if user has a specific realm role.
     */
    public function hasRealmRole($role)
    {
        return in_array($role, $this->realm_roles ?? []);
    }

    /**
     * Check if user has any of the specified realm roles.
     */
    public function hasAnyRealmRole(...$roles)
    {
        return !empty(array_intersect($roles, $this->realm_roles ?? []));
    }

    /**
     * Check if user has a specific client role.
     */
    public function hasClientRole($clientId, $role)
    {
        $clientRoles = $this->client_roles[$clientId]['roles'] ?? [];
        return in_array($role, $clientRoles);
    }

    /**
     * Check if user is in a specific group.
     */
    public function inGroup($group)
    {
        return in_array($group, $this->groups ?? []);
    }
}

Advanced Features

Role-Based Middleware

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class RequireRealmRole
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next, ...$roles)
    {
        if (!auth()->check()) {
            return redirect()->route('login');
        }

        if (!auth()->user()->hasAnyRealmRole(...$roles)) {
            abort(403, 'Unauthorized - Missing required role(s): ' . implode(', ', $roles));
        }

        return $next($request);
    }
}

class RequireClientRole
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next, $clientId, ...$roles)
    {
        if (!auth()->check()) {
            return redirect()->route('login');
        }

        $hasRole = false;
        foreach ($roles as $role) {
            if (auth()->user()->hasClientRole($clientId, $role)) {
                $hasRole = true;
                break;
            }
        }

        if (!$hasRole) {
            abort(403, 'Unauthorized - Missing required client role(s): ' . implode(', ', $roles));
        }

        return $next($request);
    }
}

Register middleware in app/Http/Kernel.php:

protected $routeMiddleware = [
    // ...
    'realm-role' => \App\Http\Middleware\RequireRealmRole::class,
    'client-role' => \App\Http\Middleware\RequireClientRole::class,
];

Usage:

Route::middleware(['keycloak-web', 'realm-role:admin,manager'])->group(function () {
    Route::resource('users', UserController::class);
});

Route::middleware(['keycloak-web', 'client-role:my-app,editor'])->group(function () {
    Route::resource('posts', PostController::class);
});

Token Refresh Middleware

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class RefreshKeycloakToken
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next)
    {
        if (!auth()->check()) {
            return $next($request);
        }

        $expiresAt = session('token_expires_at');
        
        // Check if token is about to expire (within 5 minutes)
        if ($expiresAt && now()->addMinutes(5)->gte($expiresAt)) {
            $refreshToken = session('refresh_token');
            
            if ($refreshToken) {
                try {
                    $response = Http::asForm()->post(
                        config('keycloak-web.base_url') . '/realms/' . config('keycloak-web.realm') . '/protocol/openid-connect/token',
                        [
                            'grant_type' => 'refresh_token',
                            'client_id' => config('keycloak-web.client_id'),
                            'client_secret' => config('keycloak-web.client_secret'),
                            'refresh_token' => $refreshToken,
                        ]
                    );
                    
                    if ($response->successful()) {
                        $tokens = $response->json();
                        
                        session([
                            'access_token' => $tokens['access_token'],
                            'refresh_token' => $tokens['refresh_token'] ?? $refreshToken,
                            'id_token' => $tokens['id_token'],
                            'token_expires_at' => now()->addSeconds($tokens['expires_in']),
                        ]);
                    }
                } catch (\Exception $e) {
                    \Log::error('Token refresh failed: ' . $e->getMessage());
                }
            }
        }

        return $next($request);
    }
}

API Authentication

<?php

namespace App\Http\Middleware;

use Closure;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Illuminate\Http\Request;

class VerifyKeycloakToken
{
    private $publicKeys = null;

    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next)
    {
        $token = $request->bearerToken();
        
        if (!$token) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        try {
            $decoded = $this->verifyToken($token);
            
            // Attach user info to request
            $request->merge(['token_claims' => $decoded]);
            
            // Optionally authenticate the user
            if ($user = \App\Models\User::where('keycloak_id', $decoded->sub)->first()) {
                auth()->login($user);
            }
            
        } catch (\Exception $e) {
            return response()->json(['error' => 'Invalid token: ' . $e->getMessage()], 401);
        }

        return $next($request);
    }

    /**
     * Verify JWT token.
     */
    private function verifyToken($token)
    {
        $publicKeys = $this->getPublicKeys();
        
        // Decode token header to get key ID
        $tokenParts = explode('.', $token);
        $header = json_decode(base64_decode($tokenParts[0]), true);
        $keyId = $header['kid'];
        
        if (!isset($publicKeys[$keyId])) {
            throw new \Exception('Public key not found');
        }
        
        return JWT::decode($token, new Key($publicKeys[$keyId], 'RS256'));
    }

    /**
     * Get Keycloak public keys.
     */
    private function getPublicKeys()
    {
        if ($this->publicKeys === null) {
            $response = Http::get(
                config('keycloak-web.base_url') . '/realms/' . config('keycloak-web.realm') . '/protocol/openid-connect/certs'
            );
            
            $jwks = $response->json();
            $this->publicKeys = [];
            
            foreach ($jwks['keys'] as $key) {
                $this->publicKeys[$key['kid']] = $this->createPublicKey($key);
            }
        }
        
        return $this->publicKeys;
    }

    /**
     * Create public key from JWK.
     */
    private function createPublicKey($jwk)
    {
        // Implementation same as in controller
    }
}

Blade Directives

// app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Blade;

public function boot()
{
    // Check realm role
    Blade::if('realmRole', function ($role) {
        return auth()->check() && auth()->user()->hasRealmRole($role);
    });
    
    // Check any realm role
    Blade::if('anyRealmRole', function (...$roles) {
        return auth()->check() && auth()->user()->hasAnyRealmRole(...$roles);
    });
    
    // Check client role
    Blade::if('clientRole', function ($clientId, $role) {
        return auth()->check() && auth()->user()->hasClientRole($clientId, $role);
    });
    
    // Check group membership
    Blade::if('inGroup', function ($group) {
        return auth()->check() && auth()->user()->inGroup($group);
    });
}

Usage in Blade templates:

@realmRole('admin')
    <a href="{{ route('admin.dashboard') }}">Admin Dashboard</a>
@endrealmRole

@anyRealmRole('editor', 'moderator')
    <a href="{{ route('content.manage') }}">Manage Content</a>
@endanyRealmRole

@clientRole('my-app', 'premium-user')
    <div class="premium-features">
        <!-- Premium content -->
    </div>
@endclientRole

@inGroup('/premium-users')
    <div class="premium-badge">Premium Member</div>
@endinGroup

Service Provider Pattern

<?php

namespace App\Providers;

use App\Services\KeycloakService;
use Illuminate\Support\ServiceProvider;

class KeycloakServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     */
    public function register()
    {
        $this->app->singleton(KeycloakService::class, function ($app) {
            return new KeycloakService(
                config('keycloak-web.base_url'),
                config('keycloak-web.realm'),
                config('keycloak-web.client_id'),
                config('keycloak-web.client_secret')
            );
        });
    }

    /**
     * Bootstrap services.
     */
    public function boot()
    {
        // Register custom guards
        Auth::extend('keycloak-web', function ($app, $name, array $config) {
            return new KeycloakWebGuard(
                Auth::createUserProvider($config['provider']),
                $app->make('request')
            );
        });
    }
}

Keycloak Service

<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;

class KeycloakService
{
    private $baseUrl;
    private $realm;
    private $clientId;
    private $clientSecret;

    public function __construct($baseUrl, $realm, $clientId, $clientSecret)
    {
        $this->baseUrl = $baseUrl;
        $this->realm = $realm;
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
    }

    /**
     * Get user info from access token.
     */
    public function getUserInfo($accessToken)
    {
        $response = Http::withToken($accessToken)->get(
            "{$this->baseUrl}/realms/{$this->realm}/protocol/openid-connect/userinfo"
        );

        if (!$response->successful()) {
            throw new \Exception('Failed to get user info');
        }

        return $response->json();
    }

    /**
     * Introspect token to check validity.
     */
    public function introspectToken($token)
    {
        $response = Http::asForm()->post(
            "{$this->baseUrl}/realms/{$this->realm}/protocol/openid-connect/token/introspect",
            [
                'token' => $token,
                'client_id' => $this->clientId,
                'client_secret' => $this->clientSecret,
            ]
        );

        return $response->json();
    }

    /**
     * Get realm public key (cached).
     */
    public function getRealmPublicKey()
    {
        return Cache::remember('keycloak_realm_public_key', 3600, function () {
            $response = Http::get("{$this->baseUrl}/realms/{$this->realm}");
            
            if (!$response->successful()) {
                throw new \Exception('Failed to get realm info');
            }

            return $response->json()['public_key'];
        });
    }

    /**
     * Create user in Keycloak.
     */
    public function createUser($userData, $adminToken)
    {
        $response = Http::withToken($adminToken)->post(
            "{$this->baseUrl}/admin/realms/{$this->realm}/users",
            $userData
        );

        if (!$response->successful()) {
            throw new \Exception('Failed to create user: ' . $response->body());
        }

        // Get user ID from location header
        $location = $response->header('Location');
        return basename($location);
    }

    /**
     * Update user attributes.
     */
    public function updateUserAttributes($userId, $attributes, $adminToken)
    {
        $response = Http::withToken($adminToken)->put(
            "{$this->baseUrl}/admin/realms/{$this->realm}/users/{$userId}",
            ['attributes' => $attributes]
        );

        return $response->successful();
    }

    /**
     * Add user to group.
     */
    public function addUserToGroup($userId, $groupId, $adminToken)
    {
        $response = Http::withToken($adminToken)->put(
            "{$this->baseUrl}/admin/realms/{$this->realm}/users/{$userId}/groups/{$groupId}"
        );

        return $response->successful();
    }
}

Testing

Feature Tests

<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use Mockery;

class KeycloakAuthenticationTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        
        // Mock Keycloak responses
        Http::fake([
            '*/protocol/openid-connect/token' => Http::response([
                'access_token' => 'mock-access-token',
                'refresh_token' => 'mock-refresh-token',
                'id_token' => $this->generateMockIdToken(),
                'expires_in' => 3600,
            ]),
            '*/protocol/openid-connect/certs' => Http::response([
                'keys' => [$this->generateMockJwk()],
            ]),
        ]);
    }

    public function test_user_can_login_via_keycloak()
    {
        $response = $this->get(route('login'));
        
        $response->assertRedirect();
        $response->assertRedirectContains('protocol/openid-connect/auth');
    }

    public function test_callback_creates_user_and_logs_in()
    {
        $response = $this->withSession(['_token' => 'test-token'])
            ->get(route('auth.callback', [
                'code' => 'test-code',
                'state' => 'test-token',
            ]));
        
        $response->assertRedirect(route('dashboard'));
        $this->assertAuthenticated();
        
        $user = User::where('email', '[email protected]')->first();
        $this->assertNotNull($user);
        $this->assertEquals('test-keycloak-id', $user->keycloak_id);
    }

    public function test_protected_route_requires_authentication()
    {
        $response = $this->get(route('dashboard'));
        
        $response->assertRedirect(route('login'));
    }

    public function test_role_middleware_blocks_unauthorized_users()
    {
        $user = User::factory()->create([
            'realm_roles' => ['user'],
        ]);
        
        $response = $this->actingAs($user)
            ->get('/admin/users');
        
        $response->assertStatus(403);
    }

    public function test_role_middleware_allows_authorized_users()
    {
        $user = User::factory()->create([
            'realm_roles' => ['admin', 'user'],
        ]);
        
        $response = $this->actingAs($user)
            ->get('/admin/users');
        
        $response->assertStatus(200);
    }

    private function generateMockIdToken()
    {
        // Generate a mock JWT token for testing
        $header = base64_encode(json_encode(['alg' => 'RS256', 'kid' => 'test-key']));
        $payload = base64_encode(json_encode([
            'sub' => 'test-keycloak-id',
            'email' => '[email protected]',
            'preferred_username' => 'testuser',
            'given_name' => 'Test',
            'family_name' => 'User',
            'email_verified' => true,
            'realm_access' => ['roles' => ['user']],
        ]));
        $signature = base64_encode('mock-signature');
        
        return "{$header}.{$payload}.{$signature}";
    }

    private function generateMockJwk()
    {
        return [
            'kid' => 'test-key',
            'kty' => 'RSA',
            'alg' => 'RS256',
            'use' => 'sig',
            'n' => 'mock-modulus',
            'e' => 'AQAB',
        ];
    }
}

Unit Tests

<?php

namespace Tests\Unit;

use App\Models\User;
use Tests\TestCase;

class UserModelTest extends TestCase
{
    public function test_has_realm_role()
    {
        $user = new User(['realm_roles' => ['admin', 'user']]);
        
        $this->assertTrue($user->hasRealmRole('admin'));
        $this->assertTrue($user->hasRealmRole('user'));
        $this->assertFalse($user->hasRealmRole('guest'));
    }

    public function test_has_any_realm_role()
    {
        $user = new User(['realm_roles' => ['editor']]);
        
        $this->assertTrue($user->hasAnyRealmRole('admin', 'editor'));
        $this->assertFalse($user->hasAnyRealmRole('admin', 'manager'));
    }

    public function test_has_client_role()
    {
        $user = new User([
            'client_roles' => [
                'my-app' => ['roles' => ['premium', 'beta']],
            ],
        ]);
        
        $this->assertTrue($user->hasClientRole('my-app', 'premium'));
        $this->assertFalse($user->hasClientRole('my-app', 'standard'));
        $this->assertFalse($user->hasClientRole('other-app', 'premium'));
    }

    public function test_in_group()
    {
        $user = new User(['groups' => ['/admins', '/editors']]);
        
        $this->assertTrue($user->inGroup('/admins'));
        $this->assertFalse($user->inGroup('/viewers'));
    }
}

Production Considerations

Environment Configuration

// config/keycloak-web.php
return [
    'base_url' => env('KEYCLOAK_BASE_URL'),
    'realm' => env('KEYCLOAK_REALM'),
    'client_id' => env('KEYCLOAK_CLIENT_ID'),
    'client_secret' => env('KEYCLOAK_CLIENT_SECRET'),
    'redirect_uri' => env('KEYCLOAK_REDIRECT_URI', url('/auth/callback')),
    'scope' => env('KEYCLOAK_SCOPE', 'openid profile email'),
    
    // Cache settings
    'cache_public_key' => env('KEYCLOAK_CACHE_PUBLIC_KEY', true),
    'public_key_cache_ttl' => env('KEYCLOAK_PUBLIC_KEY_CACHE_TTL', 86400), // 24 hours
    
    // Token settings
    'token_refresh_threshold' => env('KEYCLOAK_TOKEN_REFRESH_THRESHOLD', 300), // 5 minutes
    'allowed_clock_skew' => env('KEYCLOAK_ALLOWED_CLOCK_SKEW', 60), // 1 minute
    
    // SSL settings
    'verify_ssl' => env('KEYCLOAK_VERIFY_SSL', true),
    
    // Logging
    'log_enabled' => env('KEYCLOAK_LOG_ENABLED', true),
    'log_level' => env('KEYCLOAK_LOG_LEVEL', 'info'),
];

Performance Optimization

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Cache;

class CacheKeycloakPublicKeys
{
    public function handle($request, Closure $next)
    {
        // Warm up public keys cache if needed
        if (!Cache::has('keycloak_public_keys')) {
            $this->cachePublicKeys();
        }

        return $next($request);
    }

    private function cachePublicKeys()
    {
        try {
            $response = Http::get(
                config('keycloak-web.base_url') . '/realms/' . 
                config('keycloak-web.realm') . '/protocol/openid-connect/certs'
            );

            if ($response->successful()) {
                Cache::put(
                    'keycloak_public_keys',
                    $response->json(),
                    now()->addHours(24)
                );
            }
        } catch (\Exception $e) {
            \Log::error('Failed to cache Keycloak public keys: ' . $e->getMessage());
        }
    }
}

Health Check

// routes/api.php
Route::get('/health/keycloak', function () {
    $status = [
        'status' => 'healthy',
        'checks' => [],
    ];

    // Check Keycloak connectivity
    try {
        $response = Http::timeout(5)->get(
            config('keycloak-web.base_url') . '/realms/' . config('keycloak-web.realm')
        );
        
        $status['checks']['keycloak_connection'] = $response->successful() ? 'ok' : 'failed';
    } catch (\Exception $e) {
        $status['checks']['keycloak_connection'] = 'error: ' . $e->getMessage();
        $status['status'] = 'unhealthy';
    }

    // Check token validation
    try {
        $testToken = Cache::get('health_check_token');
        if ($testToken) {
            $introspection = app(KeycloakService::class)->introspectToken($testToken);
            $status['checks']['token_validation'] = $introspection['active'] ? 'ok' : 'token_invalid';
        } else {
            $status['checks']['token_validation'] = 'no_test_token';
        }
    } catch (\Exception $e) {
        $status['checks']['token_validation'] = 'error: ' . $e->getMessage();
    }

    return response()->json($status, $status['status'] === 'healthy' ? 200 : 503);
});

Troubleshooting

Common Issues

  1. Invalid State Parameter

    • Ensure session driver is properly configured
    • Check for cookie/session issues in load-balanced environments
    • Verify CSRF token middleware is enabled
  2. Token Signature Verification Failed

    • Clear cached public keys: php artisan cache:clear
    • Verify clock synchronization between servers
    • Check allowed_clock_skew configuration
  3. Redirect URI Mismatch

    • Ensure APP_URL is correctly set in .env
    • Check for trailing slashes in Keycloak client configuration
    • Verify HTTPS is used in production

Debug Logging

// Add to AppServiceProvider boot method
if (config('keycloak-web.log_enabled')) {
    Event::listen('Illuminate\Auth\Events\*', function ($event) {
        \Log::channel('keycloak')->info(class_basename($event), [
            'user' => $event->user->id ?? null,
            'guard' => $event->guard ?? null,
        ]);
    });
}

// config/logging.php
'channels' => [
    // ...
    'keycloak' => [
        'driver' => 'daily',
        'path' => storage_path('logs/keycloak.log'),
        'level' => env('KEYCLOAK_LOG_LEVEL', 'info'),
        'days' => 14,
    ],
],

Performance Monitoring

// app/Console/Commands/MonitorKeycloakIntegration.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class MonitorKeycloakIntegration extends Command
{
    protected $signature = 'keycloak:monitor';
    protected $description = 'Monitor Keycloak integration performance';

    public function handle()
    {
        $stats = [
            'active_sessions' => DB::table('sessions')
                ->where('last_activity', '>', now()->subMinutes(30))
                ->count(),
            'users_with_tokens' => DB::table('sessions')
                ->whereNotNull('payload')
                ->where('payload', 'like', '%access_token%')
                ->count(),
            'recent_logins' => User::where('last_login_at', '>', now()->subHour())
                ->count(),
        ];

        $this->info('Keycloak Integration Stats:');
        foreach ($stats as $key => $value) {
            $this->line("{$key}: {$value}");
        }

        // Log to monitoring system
        \Log::channel('keycloak')->info('Integration stats', $stats);
    }
}

Next Steps