acsystems/keycloak-authenticator-bundle
Laravel/PHP bundle that integrates Keycloak authentication into your app, providing Keycloak-based login and user session handling for protected routes and APIs with minimal configuration, suitable for SSO-enabled deployments.
Installation
composer require acsystems/keycloak-authenticator-bundle
Add to config/bundles.php:
AcSystems\KeycloakAuthenticatorBundle\KeycloakAuthenticatorBundle::class => ['all' => true],
Configuration Publish the default config:
php artisan vendor:publish --provider="AcSystems\KeycloakAuthenticatorBundle\KeycloakAuthenticatorBundle" --tag="config"
Update config/keycloak.php with your Keycloak realm, client ID, and public key.
First Use Case: JWT Validation
In your middleware or controller, inject the KeycloakAuthenticator service:
use AcSystems\KeycloakAuthenticatorBundle\Service\KeycloakAuthenticator;
public function __construct(private KeycloakAuthenticator $authenticator) {}
public function someProtectedRoute(Request $request) {
$token = $request->bearerToken();
$user = $this->authenticator->authenticate($token);
// $user now contains decoded JWT payload (e.g., username, roles, etc.)
}
Stateless Authentication Middleware Create a middleware to validate JWTs on protected routes:
namespace App\Http\Middleware;
use AcSystems\KeycloakAuthenticatorBundle\Service\KeycloakAuthenticator;
use Closure;
class KeycloakAuthMiddleware {
public function __construct(private KeycloakAuthenticator $authenticator) {}
public function handle($request, Closure $next) {
$token = $request->bearerToken();
if (!$token) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$this->authenticator->authenticate($token);
return $next($request);
}
}
Register in app/Http/Kernel.php:
protected $routeMiddleware = [
'keycloak.auth' => \App\Http\Middleware\KeycloakAuthMiddleware::class,
];
Role-Based Access Control (RBAC) Extract roles from the JWT payload and enforce them:
$user = $this->authenticator->authenticate($token);
$roles = $user->getRoles(); // e.g., ['admin', 'user']
if (!in_array('admin', $roles)) {
abort(403, 'Forbidden');
}
Token Refresh Logic Handle short-lived access tokens by validating refresh tokens:
$refreshToken = $request->input('refresh_token');
$newAccessToken = $this->authenticator->refreshToken($refreshToken);
return response()->json(['access_token' => $newAccessToken]);
Integration with Laravel Auth
Create a custom User model provider or guard:
// config/auth.php
'guards' => [
'keycloak' => [
'driver' => 'keycloak',
'provider' => 'keycloak',
],
],
CORS Configuration
Ensure your Keycloak client is configured to accept requests from your Laravel domain. Update config/cors.php to allow Authorization headers:
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_headers' => ['*'],
Laravel Sanctum/Passport Hybrid Combine with Sanctum for session-based auth while using Keycloak for JWT:
// Middleware to fallback to Sanctum if no JWT is present
public function handle($request, Closure $next) {
if ($request->bearerToken()) {
$this->authenticator->authenticate($request->bearerToken());
} else {
$this->auth->viaRequest($request, 'sanctum');
}
return $next($request);
}
Custom Claims Handling
Extend the KeycloakAuthenticator to handle custom claims (e.g., sub, email_verified):
$user = $this->authenticator->authenticate($token);
$customClaim = $user->getClaim('custom_claim_name');
Logging and Monitoring Log authentication events for auditing:
try {
$user = $this->authenticator->authenticate($token);
} catch (\Exception $e) {
\Log::warning("Keycloak auth failed: {$e->getMessage()}", ['token' => $token]);
throw $e;
}
Public Key Rotation
KeycloakKeyFetcher service to refresh keys:
$publicKey = $this->keyFetcher->fetchPublicKey();
$this->authenticator->setPublicKey($publicKey);
Token Expiration Handling
exp claim manually:
$user = $this->authenticator->authenticate($token);
if ($user->getClaim('exp') < time()) {
abort(401, 'Token expired');
}
Clock Skew Issues
exp validation to fail.authenticate() method:
$leeway = 300; // 5 minutes
$this->authenticator->setLeeway($leeway);
Missing kid Header
kid (key ID) header. The bundle defaults to the first key if kid is missing.kid header or explicitly set it in the bundle:
$this->authenticator->setKeyId('your_key_id_here');
Role Mapping
realm_access.roles) may not map directly to Laravel’s role system. Normalize roles in a custom User model:
public function getRoles() {
return array_map(function($role) {
return strtolower(str_replace('.', '_', $role));
}, $this->getClaim('realm_access.roles', []));
}
Enable Verbose Logging
Add this to config/keycloak.php:
'debug' => env('KEYCLOAK_DEBUG', false),
Logs will include JWT payloads and validation steps.
Validate JWT Manually Use an online tool like jwt.io to decode and verify tokens. Compare the output with Laravel’s decoded payload.
Check Keycloak Events Enable Keycloak admin events to audit failed logins:
# In Keycloak Admin Console:
Configure "Events" -> "Providers" -> "Admin Events" to log authentication failures.
Token Introspection Use Keycloak’s introspection endpoint for debugging:
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $refreshToken,
])->post('https://your-keycloak/auth/realms/your-realm/protocol/openid-connect/token/introspect');
Custom User Provider
Override the default KeycloakUser to integrate with Laravel’s User model:
namespace App\Providers;
use AcSystems\KeycloakAuthenticatorBundle\Model\KeycloakUser;
use Illuminate\Contracts\Auth\Authenticatable;
class KeycloakUserProvider extends KeycloakUser implements Authenticatable {
public function getAuthIdentifier() {
return $this->getClaim('sub'); // Use Keycloak's subject as Laravel's ID
}
public function getAuthPassword() {
return ''; // JWT auth doesn’t use passwords
}
}
Dynamic Realm/Client Configuration Fetch Keycloak settings from environment variables or a database:
$this->authenticator->setRealm(env('KEYCLOAK_REALM'));
$this->authenticator->setClientId(env('KEYCLOAK_CLIENT_ID'));
**Web
How can I help you explore Laravel packages today?