bannerstop / keycloak-laravel
Laravel integration of bannerstop/keycloak: Keycloak single sign-on, bearer token guard, role middleware and logout
Requires
- php: ^8.5
- bannerstop/keycloak: ^10.0
- guzzlehttp/guzzle: ^7.2
- illuminate/auth: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/routing: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- nyholm/psr7: ^1.4
Requires (Dev)
- ext-curl: *
- orchestra/testbench: ^10.0 || ^11.0
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel integration of bannerstop/keycloak: single sign-on with Keycloak.
- Login and logout routes that use Laravel's own session guard
- A
keycloak-bearerguard for APIs - A
keycloak.rolemiddleware - Role mapping from realm roles, client roles and groups
- Works without a user table (session-only
KeycloakUser) or with your own models (UserProvisioner)
Versions
| Version | PHP | Laravel |
|---|---|---|
| 1.x | ≥ 7.1.3 | 5.8 – 8.x |
| 2.x | ≥ 7.2 | 6.x – 8.x |
| 3.x | ≥ 7.3 | 6.x – 8.x |
| 4.x | ≥ 7.4 | 6.x – 8.x |
| 5.x | ≥ 8.0 | 8.x – 9.x |
| 6.x | ≥ 8.1 | 9.x – 10.x |
| 7.x | ≥ 8.2 | 10.x – 12.x |
| 8.x | ≥ 8.3 | 11.x – 13.x |
| 9.x | ≥ 8.4 | 12.x – 13.x |
| 10.x | ≥ 8.5 | 12.x – 13.x |
Installation
composer require bannerstop/keycloak-laravel
The package talks to Keycloak through Guzzle 7. Set keycloak.http_client
to the class name of another PSR-18 client to replace it.
Publish the configuration and set the environment variables:
php artisan vendor:publish --tag=keycloak-config
KEYCLOAK_SERVER_URL=https://sso.example.com KEYCLOAK_REALM=example KEYCLOAK_CLIENT_ID=my-app KEYCLOAK_CLIENT_SECRET=
In Keycloak, register https://your-app.example/keycloak/callback as redirect
URI and your logout target as post logout redirect URI. See the
core README for the
full client setup.
Authentication
Users without a user table
// config/auth.php 'guards' => [ 'web' => ['driver' => 'session', 'provider' => 'keycloak'], 'api' => ['driver' => 'keycloak-bearer'], ], 'providers' => [ 'keycloak' => ['driver' => 'keycloak'], ],
auth()->user() is then a KeycloakUser with getSubject(), getEmail(),
getName() and getKeycloakRoles().
Your own users
Implement UserProvisioner and set keycloak.user_provisioner to the class.
Keep the guard's normal provider (e.g. eloquent), so that Laravel can load the
user on the following requests:
use Bannerstop\Keycloak\Identity; use Bannerstop\KeycloakLaravel\Auth\UserProvisioner; use Illuminate\Contracts\Auth\Authenticatable; final class KeycloakUserProvisioner implements UserProvisioner { public function provision(Identity $identity, array $roles): Authenticatable { return User::updateOrCreate( ['keycloak_id' => $identity->getSubject()], ['email' => $identity->getEmail(), 'name' => $identity->getDisplayName(), 'roles' => $roles] ); } }
Let your model implement HasKeycloakRoles to use the keycloak.role middleware.
Routes
| Route | Name | |
|---|---|---|
GET /keycloak/login?return_to=/path |
keycloak.login |
starts the login |
GET /keycloak/callback |
keycloak.callback |
the redirect URI |
POST /keycloak/logout |
keycloak.logout |
ends the local and the Keycloak session |
Point Laravel's login route to keycloak.login, e.g.
Route::redirect('/login', '/keycloak/login')->name('login');. After the login
the user goes to return_to (local paths only), the intended URL or
keycloak.login.redirect_to. Failed logins go to keycloak.login.failure_redirect_to
with the reason flashed as keycloak_error (state_mismatch, cancelled,
provider_error, invalid_token, not_allowed).
Logout is POST only, so that other sites cannot log your users out:
<form method="POST" action="{{ route('keycloak.logout') }}"> @csrf <button>Log out</button> </form>
Roles
// config/keycloak.php 'roles' => [ 'default_roles' => ['user'], 'realm_roles' => ['admin' => ['admin']], 'client_roles' => ['my-app' => ['editor' => ['editor']]], 'groups' => ['/staff/it' => ['it']], ],
Route::get('/admin', AdminController::class)->middleware(['auth', 'keycloak.role:admin']);
APIs
Route::middleware('auth:api')->get('/api/orders', ...);
Requests need Authorization: Bearer <access token>; the token must carry the
audience from keycloak.bearer.audience (default: the client id). Add an
audience mapper in Keycloak for that.
User directory
Bannerstop\Keycloak\Admin\UserDirectory lists the users of the realm through
the admin REST API, e.g. to sync a user table. It authenticates with a service
account that needs the client role realm-management → view-users.
Without further configuration it uses the login client, which then needs
Service accounts roles and view-users itself. We recommend a separate
confidential client that only has Service accounts roles enabled and
view-users assigned, so that the login client has no admin API rights:
KEYCLOAK_DIRECTORY_CLIENT_ID=my-app-directory KEYCLOAK_DIRECTORY_CLIENT_SECRET=
use Bannerstop\Keycloak\Admin\UserDirectory; foreach (app(UserDirectory::class)->users() as $user) { // $user->getId() equals the "sub" of the user's tokens }
Services
Bannerstop\Keycloak\KeycloakClient, Bannerstop\Keycloak\Admin\UserDirectory
and Bannerstop\Keycloak\Role\RoleMapper are bound in the container.
License
MIT, see LICENSE. Security issues: see the core package's security policy.