kaveraa / laravel-keycloak-sanctum
Keycloak login (SSO) for a front-end with a Laravel API: Sanctum token, roles, back-channel logout and idle timeout
Package info
github.com/kaveraa/laravel-keycloak-sanctum
pkg:composer/kaveraa/laravel-keycloak-sanctum
Requires
- php: ^8.2
- firebase/php-jwt: ^6.10 || ^7.0
- illuminate/cache: ^12.0 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/routing: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- laravel/sanctum: ^4.0
- laravel/socialite: ^5.15
- socialiteproviders/keycloak: ^5.3
Requires (Dev)
- orchestra/testbench: ^10.0 || ^11.0
- phpunit/phpunit: ^11.0 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-30 13:34:58 UTC
README
English - Français
Keycloak login (SSO) for a front-end application (Vue, React, Angular...) with a Laravel API.
After the Keycloak login, your front-end gets a normal Sanctum token to call your API. The package does the rest:
- Secure login: the token is never in a URL. The front-end gets a one-time code (valid for 60 seconds) and exchanges it for the token.
- Users: found or created at login, with their Keycloak information (name, email...).
- Roles: read from the Keycloak token, changed into the roles of your application, with a
keycloak.role:adminmiddleware. - Logout from Keycloak (back-channel logout): when the session ends in Keycloak, the Sanctum tokens are deleted.
- Logout after inactivity (optional).
- Token checks: signature, dates, issuer, audience. The Keycloak keys are cached and reloaded automatically when Keycloak changes them.
Contents
- How it works
- Installation
- Configure Keycloak
- Front-end
- Routes
- Users
- Roles
- Logout
- Customize
- Events
- Security
- Development
How it works
Front-end Laravel API Keycloak
| | |
| 1. opens /sso/login --------> | 2. redirects --------------------> | the user logs in
| | <---------- 3. back to /sso/callback |
| | 4. checks the token, finds the user, computes the roles
| <---- 5. redirects to the front-end with ?code=... |
| 6. POST /sso/token {code} --> | |
| <---- 7. { token, user } ---- | |
| 8. API calls with "Authorization: Bearer <token>" |
Installation
Requirements: PHP 8.2+, Laravel 12 or 13, and Sanctum installed (php artisan install:api).
composer require kaveraa/laravel-keycloak-sanctum
php artisan keycloak-sanctum:install
php artisan migrate
The install command publishes the config/keycloak-sanctum.php file and two migrations:
keycloak_sessions: links each Sanctum token to its Keycloak session;keycloak_idon theuserstable: the Keycloak id of the user. Delete this migration if you find your users by email (see Users).
Add the Sanctum trait to the User model:
use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens; }
Then fill in .env:
KEYCLOAK_BASE_URL=https://sso.example.org KEYCLOAK_REALM=my-realm KEYCLOAK_CLIENT_ID=my-application KEYCLOAK_CLIENT_SECRET=client-secret # Front-end page that receives ?code=... (full address if the front-end is on another domain) KEYCLOAK_SANCTUM_FRONTEND_CALLBACK_URL=https://app.example.org/login/callback # Page shown after logout KEYCLOAK_SANCTUM_POST_LOGOUT_REDIRECT_URI=https://app.example.org/
All the options, with explanations, are in config/keycloak-sanctum.php (comments in French).
| Variable | Default | Use |
|---|---|---|
KEYCLOAK_BASE_URL |
- | Server address, without /realms (add /auth for Keycloak < 17) |
KEYCLOAK_REALM |
- | Realm name |
KEYCLOAK_CLIENT_ID / KEYCLOAK_CLIENT_SECRET |
- | Keycloak client of the application |
KEYCLOAK_REDIRECT_URI |
{APP_URL}/sso/callback |
Return address after login |
KEYCLOAK_SANCTUM_FRONTEND_CALLBACK_URL |
/login/callback |
Front-end page that receives the code |
KEYCLOAK_SANCTUM_POST_LOGOUT_REDIRECT_URI |
/ |
Page shown after logout |
KEYCLOAK_SANCTUM_AUTO_CREATE_USERS |
false |
Create the user at the first login |
KEYCLOAK_SANCTUM_DEFAULT_ROLE |
- | Role given to a user with no role |
KEYCLOAK_SANCTUM_SYNC_ROLES |
true |
Save the roles on the user (see Roles) |
KEYCLOAK_SANCTUM_TOKEN_EXPIRATION |
- | Maximum token lifetime, in minutes |
KEYCLOAK_SANCTUM_IDLE_TIMEOUT |
- | Logout after X minutes with no request |
Configure Keycloak
In the Keycloak admin console, create (or open) the client of your application:
| Setting | Value |
|---|---|
| Client authentication | On (confidential client, with a secret) |
| Standard flow | On |
| Valid redirect URIs | https://api.example.org/sso/callback |
| Valid post logout redirect URIs | https://app.example.org/* |
| Backchannel logout URL | https://api.example.org/sso/backchannel-logout |
| Backchannel logout session required | On |
The php artisan keycloak-sanctum:install command shows the exact addresses of your application.
Front-end
Example in JavaScript, with no dependency. It works with Vue, React or Angular.
1. Login button: open the login route of the API.
window.location.href = 'https://api.example.org/sso/login'
2. /login/callback page: exchange the code for the token.
const params = new URLSearchParams(window.location.search) if (params.has('error')) { // access_denied, authentication_failed, invalid_token, user_not_found, no_role showError(params.get('error')) } else { const response = await fetch('https://api.example.org/sso/token', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ code: params.get('code') }), }) const { token, user } = await response.json() localStorage.setItem('token', token) // user contains the user information and the roles: user.roles }
3. API calls: send the token.
fetch('https://api.example.org/api/projects', { headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, Accept: 'application/json' }, })
4. Logout: delete the token, then close the Keycloak session.
const response = await fetch('https://api.example.org/sso/logout', { method: 'POST', headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, Accept: 'application/json' }, }) const { logout_url } = await response.json() localStorage.removeItem('token') window.location.href = logout_url ?? '/'
If the API answers 401, the token is not valid any more (logout from Keycloak, inactivity, expiration): send the user back to the login.
Routes
All routes use the /sso prefix (routes.prefix option).
| Method | Address | Access | Use |
|---|---|---|---|
| GET | /sso/login |
public | Redirects to the Keycloak login page |
| GET | /sso/callback |
Keycloak | Return from Keycloak, redirects to the front-end with ?code= or ?error= |
| POST | /sso/token |
public | Exchanges the code for { token, token_type, expires_at, user } |
| GET | /sso/user |
token | Logged-in user and roles |
| POST | /sso/logout |
token | Deletes the token, returns { logout_url } |
| POST | /sso/backchannel-logout |
Keycloak | End of session sent by Keycloak |
| GET | /sso/settings |
public | { login_url, idle_timeout } for the front-end |
To declare your own routes, turn off the routes of the package (routes.enabled => false) and copy routes/keycloak-sanctum.php.
Users
At each login, the user is found with a column of the users table and a Keycloak value (claim):
// config/keycloak-sanctum.php 'users' => [ 'identifier' => ['column' => 'keycloak_id', 'claim' => 'sub'], // default: Keycloak id // 'identifier' => ['column' => 'email', 'claim' => 'email'], // or by email 'auto_create' => env('KEYCLOAK_SANCTUM_AUTO_CREATE_USERS', false), 'attributes' => [ // columns updated at each login => Keycloak claim 'name' => 'name', 'email' => 'email', ], ],
- If the user does not exist and
auto_createisfalse, the login is refused (?error=user_not_found). - The available claims come from the Keycloak token and profile:
sub,email,name,given_name,family_name,preferred_username, and your custom attributes.
Roles
The roles are read from the Keycloak token, then changed into roles of the application:
'roles' => [ 'source' => 'client', // 'client' (client roles), 'realm' (realm roles) or 'both' 'map' => [ // Keycloak role => application role 'app-admin' => 'admin', 'app-editor' => 'editor', ], 'default' => null, // role given to a user with no role 'required' => false, // refuse the login with no role (?error=no_role) ],
If map is empty, the Keycloak roles are kept as they are. If not, only the roles in the list are kept.
Protect routes:
Route::middleware(['auth:sanctum', 'keycloak.role:admin,editor'])->group(function () { // allowed with the admin OR editor role });
Read the roles in the code:
use Kaveraa\KeycloakSanctum\KeycloakSanctum; KeycloakSanctum::roles(); // ['admin', 'editor'] KeycloakSanctum::hasAnyRole('admin'); // true
Save the roles on the user (roles table, permissions package...): implement SyncsKeycloakRoles on the User model. The method is called at each login.
use Kaveraa\KeycloakSanctum\Contracts\SyncsKeycloakRoles; class User extends Authenticatable implements SyncsKeycloakRoles { public function syncKeycloakRoles(array $roles): void { $this->syncRoles($roles); // for example with spatie/laravel-permission } }
Logout
- From the front-end:
POST /sso/logoutdeletes the token and returns the Keycloak logout address (see Front-end). - From Keycloak: when the session ends in Keycloak (logout from another application, admin action, end of session), Keycloak calls
/sso/backchannel-logoutand the tokens of the session are deleted. - After inactivity: with
KEYCLOAK_SANCTUM_IDLE_TIMEOUT=30, a token not used for 30 minutes is refused. Each request resets the delay. Only the tokens created by this package are affected. - Maximum lifetime: with
KEYCLOAK_SANCTUM_TOKEN_EXPIRATION=480, the token expires 8 hours after login, even with activity.
Warning: the inactivity option uses
Sanctum::authenticateAccessTokensUsing(). If your application already uses this function, leave the option empty and call your own logic.
Customize
Put this in the boot() method of a service provider of the application:
use Kaveraa\KeycloakSanctum\KeycloakSanctum; // Find the user your way (returning null refuses the login) KeycloakSanctum::resolveUsersUsing(function (array $claims) { return User::firstWhere('email', $claims['email']); }); // Compute the roles your way KeycloakSanctum::mapRolesUsing(function (array $keycloakRoles, array $claims) { return in_array('super-user', $keycloakRoles) ? ['admin'] : ['reader']; }); // Choose the user data sent to the front-end (/sso/token and /sso/user routes) KeycloakSanctum::userPayloadUsing(function ($user, array $roles) { return ['id' => $user->id, 'name' => $user->name, 'roles' => $roles]; });
By default, the user is sent with $user->toArray() (the $hidden fields are not sent) and the roles.
Events
| Event | When | Data |
|---|---|---|
Kaveraa\KeycloakSanctum\Events\KeycloakLogin |
After a successful login | $user, $roles, $claims |
Kaveraa\KeycloakSanctum\Events\KeycloakLogout |
After an end of session sent by Keycloak | $sid, $sub, $revokedTokens |
Security
- The Keycloak tokens are checked at each step: signature (public keys of the realm), dates (with 30 seconds of tolerance), issuer and target client.
- The ends of session sent by Keycloak follow the OpenID Connect Back-Channel Logout 1.0 standard: audience, event, no
nonce, and replay protection. - The exchange code can be used only once, is valid for 60 seconds, and only its hash is stored.
- The
id_token(used for the Keycloak logout) is encrypted in the database with the application key. - The Keycloak keys are reloaded at most once per minute: fake tokens cannot make the package call Keycloak again and again.
To report a security problem, open a private security advisory, not a public issue.
Development
git clone https://github.com/kaveraa/laravel-keycloak-sanctum.git cd laravel-keycloak-sanctum composer install composer test
The tests use a fake Keycloak server: real RSA keys and real signed tokens, with no server to install.
To propose a change, read the CONTRIBUTING.md guide. See the CHANGELOG for the list of versions.
License
MIT. See LICENSE.