stevenmaguire/oauth2-keycloak
Laravel-friendly OAuth2 client provider for Keycloak using theleague/oauth2-client. Handles Keycloak authorization, token retrieval/refresh, and user profile fetching so your app can authenticate via Keycloak with minimal setup.
Installation
composer require stevenmaguire/oauth2-keycloak
Ensure league/oauth2-client (v2.x) is also installed (this package extends it).
Basic Setup
Register the provider in your Laravel app’s config/auth.php:
'providers' => [
'keycloak' => [
'driver' => 'oauth',
'model' => App\Models\User::class,
'provider' => Stevenmaguire\OAuth2\Client\Provider\Keycloak::class,
'client_id' => env('KEYCLOAK_CLIENT_ID'),
'client_secret' => env('KEYCLOAK_CLIENT_SECRET'),
'redirect' => env('KEYCLOAK_REDIRECT_URI'),
'scopes' => ['openid', 'profile', 'email'],
'config' => [
'realm' => env('KEYCLOAK_REALM'),
'domain' => env('KEYCLOAK_DOMAIN'),
'auth_server_url' => env('KEYCLOAK_AUTH_SERVER_URL'),
],
],
],
First Use Case: Login Flow
Auth::routes()) or manually trigger:
$provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
'clientId' => env('KEYCLOAK_CLIENT_ID'),
'clientSecret' => env('KEYCLOAK_CLIENT_SECRET'),
'redirectUri' => env('KEYCLOAK_REDIRECT_URI'),
'scopes' => ['openid', 'profile'],
'config' => [
'realm' => env('KEYCLOAK_REALM'),
'domain' => env('KEYCLOAK_DOMAIN'),
'auth_server_url' => env('KEYCLOAK_AUTH_SERVER_URL'),
],
]);
$authUrl = $provider->getAuthorizationUrl();
return redirect()->to($authUrl);
routes/web.php:
Route::get('/auth/keycloak/callback', [AuthController::class, 'handleProviderCallback']);
User Authentication
Socialite or use the provider directly:
public function handleProviderCallback()
{
$provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([...]);
$token = $provider->getAccessToken('authorization_code', [
'code' => request('code')
]);
$user = $provider->getResourceOwner($token);
// Map Keycloak user to your User model.
}
Token Refresh
$newToken = $provider->getAccessToken('refresh_token', [
'refresh_token' => $storedRefreshToken
]);
Role-Based Access
realm_access claim:
$userData = $provider->getResourceOwner($token)->toArray();
$roles = $userData['realm_access']['roles'] ?? [];
Custom Claims
User model to hydrate from Keycloak’s userinfo endpoint:
public function findForPassport($identifier)
{
$user = User::where('email', $identifier)->first();
if (!$user) {
$provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([...]);
$token = $provider->getAccessToken('client_credentials', [...]); // Use client creds for admin tasks
$userData = $provider->getResourceOwner($token)->toArray();
$user = User::create([
'email' => $userData['email'],
'name' => $userData['name'],
// Hydrate custom claims (e.g., 'department', 'employee_id')
]);
}
return $user;
}
Admin Operations (User Management)
client_credentials grant to interact with Keycloak’s admin API:
$adminProvider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
'clientId' => env('KEYCLOAK_ADMIN_CLIENT_ID'),
'clientSecret' => env('KEYCLOAK_ADMIN_CLIENT_SECRET'),
'grant_type' => 'client_credentials',
'scopes' => ['manage-users'],
]);
$token = $adminProvider->getAccessToken('client_credentials', []);
$adminClient = new \GuzzleHttp\Client();
$response = $adminClient->get('https://keycloak.example.com/admin/realms/master/users', [
'headers' => ['Authorization' => 'Bearer ' . $token->getToken()]
]);
Laravel Socialite Wrapper
Create a custom KeycloakProvider to integrate seamlessly with Socialite:
namespace App\Providers;
use Stevenmaguire\OAuth2\Client\Provider\Keycloak as KeycloakProvider;
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
use Laravel\Socialite\Two\AbstractProvider;
class KeycloakProvider extends AbstractProvider {
public function getAuthUrl($scopes = [])
{
return $this->createKeycloakProvider()->getAuthorizationUrl();
}
public function getToken($code)
{
return $this->createKeycloakProvider()->getAccessToken('authorization_code', ['code' => $code]);
}
public function getUser()
{
return $this->createKeycloakProvider()->getResourceOwner($this->token);
}
protected function createKeycloakProvider()
{
return new KeycloakProvider([
'clientId' => $this->clientId,
'clientSecret' => $this->clientSecret,
'redirectUri' => $this->redirectUrl,
'scopes' => $this->scopes,
'config' => [
'realm' => config('services.keycloak.realm'),
'domain' => config('services.keycloak.domain'),
'auth_server_url' => config('services.keycloak.auth_server_url'),
],
]);
}
}
Register it in config/services.php:
'keycloak' => [
'client_id' => env('KEYCLOAK_CLIENT_ID'),
'client_secret' => env('KEYCLOAK_CLIENT_SECRET'),
'redirect' => env('KEYCLOAK_REDIRECT_URI'),
'realm' => env('KEYCLOAK_REALM'),
'domain' => env('KEYCLOAK_DOMAIN'),
'auth_server_url' => env('KEYCLOAK_AUTH_SERVER_URL'),
],
Passport Integration Use Keycloak as an OAuth2 provider for Laravel Passport:
use Stevenmaguire\OAuth2\Client\Provider\Keycloak;
use Laravel\Passport\Bridge\PersonalAccessClient;
$provider = new Keycloak([...]);
$token = $provider->getAccessToken('password', [
'username' => 'user@example.com',
'password' => 'password',
'scope' => 'profile',
]);
$user = $provider->getResourceOwner($token);
Middleware for Role Checks Create middleware to validate Keycloak roles:
public function handle($request, Closure $next)
{
$user = $request->user();
if (!$user->hasRole('admin')) { // Assume role is stored in User model
abort(403);
}
return $next($request);
}
Token Expiry Handling
refresh_token:
$token = $provider->getAccessToken('refresh_token', [
'refresh_token' => $storedRefreshToken
]);
auth:refresh event to silently refresh tokens in the background.Scopes and Permissions
openid, profile, email, etc.) configured in the Keycloak admin console.Realm vs. Domain Configuration
domain in the config is optional but required if your Keycloak instance uses a custom domain (e.g., auth.example.com). Omit it if using a direct URL (e.g., https://keycloak.example.com/auth).CSRF and State Parameters
state parameter in the authorization URL to prevent CSRF attacks:
$auth
How can I help you explore Laravel packages today?