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-jwtOr using a dedicated Keycloak package:
composer require vizir/laravel-keycloak-web-guard2. 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
-
Invalid State Parameter
- Ensure session driver is properly configured
- Check for cookie/session issues in load-balanced environments
- Verify CSRF token middleware is enabled
-
Token Signature Verification Failed
- Clear cached public keys:
php artisan cache:clear - Verify clock synchronization between servers
- Check
allowed_clock_skewconfiguration
- Clear cached public keys:
-
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);
}
}