sandstorm / filament-keycloak-admin
Filament v4 admin UI for managing Keycloak users through the Keycloak Admin REST API
Package info
github.com/sandstorm/filament-keycloak-admin
pkg:composer/sandstorm/filament-keycloak-admin
Requires
- php: ^8.3
- filament/filament: ^4.0
- guzzlehttp/guzzle: ^7.0
- sandstorm/keycloak-admin-api: ^1.0.0
- spatie/laravel-package-tools: ^1.15.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
- heloufir/filament-keycloak-sso: OIDC login + session store for auth_mode=sso; the bundled HeloufirAdminKeycloakSession adapter reads the admin's token from it (or bind your own AdminKeycloakSession).
README
A Filament v4 admin panel to manage Keycloak users through the Keycloak Admin REST API. Keycloak is the source of
truth — there is no local user mirror. This package is UI only; every HTTP call goes through the standalone client
library sandstorm/keycloak-admin-api.
- Target: Filament v4, PHP ^8.3, Keycloak 26.5.3+.
- Features: searchable user list; per-user detail with Identity (edit + enable toggle + User-Profile attributes), Groups (add/remove), Security/2FA (remove second factors), Active sessions (log out all), User events, and Admin history; and a "Send password-reset email" action.
The whole package is work in progress, and is extended as needed.
Thanks to BroodfondsMakers for sponsoring the development of this package, and for agreeing to Open Source it!
Installation
composer require sandstorm/filament-keycloak-admin
Register the plugin on a panel:
use Sandstorm\FilamentKeycloakAdmin\FilamentKeycloakAdminPlugin; $panel->plugin(FilamentKeycloakAdminPlugin::make());
Panel integration
Where the module shows up, and who sees it, is configured on the plugin instance:
use Filament\Support\Icons\Heroicon; use Sandstorm\FilamentKeycloakAdmin\FilamentKeycloakAdminPlugin; $panel->plugin( FilamentKeycloakAdminPlugin::make() ->authorize(fn (): bool => auth()->user()?->can('manage-keycloak-users') ?? false) ->navigationLabel('Login accounts') ->navigationGroup('User management') ->navigationIcon(Heroicon::OutlinedKey) ->navigationSort(30), );
| Method | Default | Effect |
|---|---|---|
authorize(Closure|bool) |
true (everyone) |
Who may use the module. Gates canAccess() on both the list and the detail page, so an unauthorized admin gets a 403 on the URL as well as no menu entry. |
navigationLabel(Closure|string|null) |
'Keycloak Users' |
Name in the main menu; also the list page's own title. |
navigationGroup(Closure|string|UnitEnum|null) |
null (top level) |
Parent menu category to list the module under. |
navigationParentItem(Closure|string|null) |
null |
Nests the module under another navigation item (by its label) instead of under a group. |
navigationIcon(Closure|string|BackedEnum|null) |
Heroicon::OutlinedUsers |
Menu icon. Pass null for no icon. Ignored while a navigation group is set — see below. |
navigationSort(Closure|int|null) |
null |
Position within its menu group. |
registerNavigation(Closure|bool) |
true |
Whether the module appears in the menu at all. false keeps it reachable by URL (still subject to authorize()) — for panels that link to it themselves. |
Every setter also accepts a closure, evaluated on each access, so the value may depend on the authenticated admin or on runtime configuration:
Configuration
The plugin reads resolved config('filament-keycloak-admin.*'), the consuming app owns
the authoritative config. Provide config/filament-keycloak-admin.php in your app:
return [ 'connection' => [ 'backchannel_url' => 'http://keycloak.internal:8080/', // required 'frontend_url' => 'https://login.example/', // optional, defaults to backchannel_url 'administration_url' => null, // optional, defaults to frontend_url 'realm' => 'YourRealm', 'client_id' => 'admin-panel-serviceaccount', 'client_secret' => '…', ], 'auth_mode' => 'service_account', // 'service_account' | 'sso' (act-as-user; see Auth modes below) 'http' => [ 'connect_timeout' => 5, 'timeout' => 15, ], 'pw_reset' => [ 'lifespan' => 43200, 'client_id' => null, 'redirect_uri' => null, ], ];
The package publishes only a structure-only stub (keys + docs, no values, no env()).
Connection URLs
The three URLs follow Keycloak's hostname nomenclature, because a Keycloak instance can be reachable under a different address per channel:
| Key | Keycloak option | Used for |
|---|---|---|
backchannel_url |
--hostname-backchannel-dynamic |
This application's own calls: the Admin REST API and the service-account token endpoint. Frequently an internal/cluster address. |
frontend_url |
--hostname |
Anything the admin's browser is sent to. |
administration_url |
--hostname-admin |
The administration console's own base URL. |
Only backchannel_url is required; administration_url falls back to frontend_url, which falls back to
backchannel_url.
Only the backchannel URL is used today; the other two exist so a browser is never handed an internal address.
Auth modes
service_account— aclient_credentialsgrant on a confidential client with the requiredrealm-managementroles:view-users,manage-users,query-users,query-groups(plusview-eventsfor the event tabs). This is one shared identity for all actions.sso(act-as-user) — the Admin-API bearer is the logged-in admin's own Keycloak token, so Keycloak evaluates that person's fine-grained permissions and attributes admin events to them.FilamentSsoTokenProviderreads the token throughAdminKeycloakSession. We recommend you to installheloufir/filament-keycloak-ssofor the bundledHeloufirAdminKeycloakSessionadapter.
Testing
Use mise (see mise.toml):
composer install mise run test # unit/feature suite (hermetic, no Keycloak) mise run analyse # PHPStan mise run lint # Pint mise run e2e # full E2E cycle: boot Keycloak → integration suite → tear down
End-to-end against a real Keycloak
mise run e2e boots a throwaway Keycloak 26.5.3 (tests/Integration/docker-compose.yml) importing two realms and runs
the opt-in integration suite against it. To iterate:
mise run e2e:up # boot Keycloak (both realms) on http://localhost:9911 mise run test:integration # run the integration suite (sets KEYCLOAK_E2E_BASE_URL for you) mise run e2e:down # tear down
Log into the Keycloak admin console at http://localhost:9911 with admin / admin (the
KC_BOOTSTRAP_ADMIN_* creds in the compose file). Seeded users all have password changeit.
Two realms are imported so the sso act-as-user path is proven in both authorization modes:
| Realm | Admin Permissions (FGAP) | Notable seeded users |
|---|---|---|
test-realm |
off (classic roles) | admin-user (realm-management roles), login-user (none), jane in /staff |
test-realm-fgap |
on | admin-user, login-user, sarah + jane in /staff, emma in /endusers |
FGAP staff policy (baked into realm-import-fgap.json)
Staff read everyone, edit endusers, can't touch other staff:
match-staff = Group policy → members of /staff
│
┌────────────┴─────────────────────────────┐
▼ ▼
"staff can view all" "staff can manage endusers"
Users · view · All users Groups · manage-members · group /endusers
│ │
▼ ▼
sarah ──view──▶ everyone sarah ──manage──▶ emma (∈/endusers) ✔
sarah ──manage──▶ jane (∈/staff) ✘ 403
License
MIT. See LICENSE.md.