Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Keycloak Admin Laravel Package

overtrue/laravel-keycloak-admin

Laravel wrapper for the Keycloak Admin REST API. Manage Keycloak users, roles, groups, and clients from your Laravel app with a fluent, configurable client—ideal for syncing accounts, assigning permissions, and automating identity workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require overtrue/laravel-keycloak-admin
php artisan vendor:publish --provider="Overtrue\LaravelKeycloakAdmin\KeycloakAdminServiceProvider"

Minimum config (config/keycloak.php, all env-driven):

KEYCLOAK_AUTH_SERVER_URL=https://sso.example.com
KEYCLOAK_REALM=my-realm
KEYCLOAK_CLIENT_ID=laravel-admin
KEYCLOAK_CLIENT_SECRET=xxxxxxxx

The package authenticates as a confidential client via the client_credentials grant, so the service account behind laravel-admin must be granted the relevant realm-management roles (view-users, manage-users, view-realm, manage-clients, …). This is the #1 source of "it doesn't work" — the package will happily return 401/403 and developers waste hours looking at Laravel config when the problem is Keycloak role mapping.

Where to look first

  1. config/keycloak.php after publishing — realm, auth server URL, and the cache block.
  2. The facade Overtrue\LaravelKeycloakAdmin\Facades\KeycloakAdmin — this is the entry point, not the whole API.
  3. The underlying SDK (overtrue/keycloak) — it exposes the fluent resource API (users(), groups(), roles(), clients(), realms()). The Laravel package is a thin container binding + token/cache layer; almost all real documentation lives in the SDK README.
  4. Keycloak's own Admin REST API docs — method names map 1:1, so when in doubt read the REST docs.

First use case — provision a user from an admin panel and assign a realm role:

use Overtrue\LaravelKeycloakAdmin\Facades\KeycloakAdmin;

$user = KeycloakAdmin::users()->create([
    'username' => $request->email,
    'email'    => $request->email,
    'firstName' => $request->first_name,
    'lastName'  => $request->last_name,
    'enabled'   => true,
    'emailVerified' => false,
    'credentials' => [[
        'type'      => 'password',
        'value'     => Str::password(24),
        'temporary' => true,
    ]],
]);

KeycloakAdmin::users()->roles()->attach($user->getId(), [
    ['id' => $roleId, 'name' => 'member'],
]);

If that round-trips, the service account is configured correctly and you can build on it.

Version note (1.0.3): this release is a maintenance drop — Laravel 13 support plus Dependabot dev-dependency bumps. No API or config changes from 1.0.2, so anything written against 1.0.x keeps working. If you're on Laravel 13 this is the version you need; on Laravel 10–12 there's no functional reason to upgrade urgently.

Implementation Patterns

Wrap the facade, don't sprinkle it. The facade is a singleton bound in the container and its API is broad and untyped at the call site. Keep it behind one or two small application services (KeycloakUserService, KeycloakRoleService) so that:

  • IDEs and static analysis only see your narrow, documented surface;
  • you can mock your own class in tests instead of the vendor client;
  • a future SDK swap is a one-file change.
final class KeycloakUserService
{
    public function findById(string $id): ?UserRepresentation
    {
        try {
            return KeycloakAdmin::users()->get($id);
        } catch (NotFoundException) {
            return null;
        }
    }
}

Sync, don't dual-write. The durable pattern is: local users table is the source of truth for app data; keycloak is the identity provider. Store keycloak_id on your user model, write locally first inside a transaction, then push to Keycloak in a queued job. If the Keycloak call fails, the job retries and the local record stays consistent. Doing both writes inline gives you split-brain on any network blip.

Queue the write operations. users()->create() and role attachments are 200–800ms round-trips; a bulk import of 500 users will time out a web request. Use ShouldBeQueued jobs with $tries = 3 and backoff() — Keycloak occasionally returns 502 behind a load balancer and a retry fixes it.

Login-side integration. For authentication itself, prefer a dedicated OIDC/Socialite driver. Use this package for the admin surface: user provisioning, role/group management, session termination (users()->logout($id)), required-action resets (users()->executeActionsEmail(...)), and back-office tooling. Mixing admin-API calls into the hot login path adds a service-account dependency to every login.

Group-based authorization. Model entitlements as Keycloak groups with realm roles mapped to them, then resolve a user's effective roles server-side and cache them in your own cache keyed by user ID. Reading users()->groups()->all($id) on every request is a guaranteed latency problem.

Cache-aware token handling. The package caches the access token in the configured Laravel cache store. In multi-node deployments that means the cache store must be shared (redis, database) — array or file on a per-node basis causes every node to re-authenticate, and Keycloak's token endpoint will start rate-limiting or logging failed logins under load.

Testing. Http::fake() will not intercept these calls — the SDK uses its own PSR-18 client, bypassing Laravel's HTTP layer. Two workable approaches:

// 1. Bind a fake client into the container
$this->mock(Keycloak::class, function ($mock) {
    $mock->shouldReceive('users->get')->with('abc')->andReturn($fakeUser);
});

// 2. Point the config at a local stub server in tests
config(['keycloak.auth_server_url' => 'http://localhost:8081']);

Option 1 is faster and is what most teams end up doing.

Gotchas and Tips

409 Conflict is the normal "user already exists" path. users()->create() throws rather than returning null. Catch the conflict, then look the user up by username (users()->search(['username' => $x])->first()) and reconcile. Don't pre-check with a search then create — that's a race.

Updates are full replacements. Keycloak's PUT /users/{id} expects a complete UserRepresentation. Sending ['firstName' => 'X'] alone will null out email, enabled, etc. Always get() → mutate → update(), and be aware that this is last-write-wins across concurrent editors.

Pagination defaults are deceptive. users()->all() returns the first page only (Keycloak defaults to a small max). For anything user-facing iterate explicitly:

$first = 0;
do {
    $page = KeycloakAdmin::users()->search(['first' => $first, 'max' => 100]);
    // ... process
    $first += 100;
} while ($page->count() === 100);

Search syntax is Keycloak's, not SQL's. users()->search(['username' => 'jo']) is a prefix/infix match, not exact, and email requires exact=true to avoid surprises. Read the GET /users query params section before trusting a filter.

Attributes are array<string, array<string>>. 'attributes' => ['tenant' => 'acme'] silently misbehaves; it must be ['tenant' => ['acme']]. This is the most common silent data-loss bug with the package.

No transactions, no rollback. Creating a user then attaching roles is two calls. If the second fails you have a half-provisioned account. Make the provisioning job idempotent (check before attach, or use roles()->all() then diff) and safe to re-run.

Realms are per-config, not per-call. Everything goes to config('keycloak.realm'). Multi-tenant setups need either a per-tenant config swap (config()->set() + forget the resolved singleton) or a second container binding per realm. The singleton means a naive config()->set() mid-request will reuse a client built with the old realm — resolve/bind explicitly per tenant.

Timeouts and retries belong to you. There's no built-in retry policy. Wrap calls that touch the admin API in retry(3, fn () => ..., 200) with jitter, and set a sane HTTP timeout. A hung Keycloak should degrade your admin UI, not hang a queue worker for 60s.

Debugging. When you get an opaque failure, drop down to the raw response: catch the exception, log getCode(), getMessage(), and the response body (Keycloak returns a useful errorMessage field). Enabling Guzzle/SDK request logging at debug level in a local environment is the fastest way to see the exact URL and payload — most "403" issues are visible immediately as a missing role on the service account.

Long-running processes. Under Octane or long queue workers, the facade resolves once per process. Tokens are refreshed via the cache layer, but if you use per-tenant config swapping, remember to app()->forgetInstance(...) between jobs so a stale realm/client doesn't leak into the next tenant's work.

Extension points are thin but sufficient. You can publish and edit the config, rebind the underlying client in a service provider to inject a custom HTTP client (useful for proxies, mTLS, or correlation-ID middleware), and swap the cache store via the cache block. Beyond that the package is deliberately a passthrough — plan on your own service layer rather than expecting hooks.

Keep an eye on the SDK, not the wrapper. Because this package is a thin adapter, meaningful behaviour changes (new resource methods, signature changes) arrive via overtrue/keycloak minor bumps. Pin the SDK in composer.json if you depend on specific method signatures, and read its changelog on upgrade — the Laravel package's own release notes (like this 1.0.3) often won't mention them.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core