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

Keycloak Security Bundle Laravel Package

idci/keycloak-security-bundle

IDCI KeycloakSecurityBundle integrates Keycloak with Symfony security. It provides an authenticator and user provider for OpenID Connect/OAuth2 SSO, token validation, role mapping, and user provisioning, helping secure Symfony applications with Keycloak authentication and authorization.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: idci/keycloak-security-bundle is a Symfony bundle, not a Laravel package. It has no ServiceProvider, no config/*.php publish tag, and it wires itself into Symfony's Security component, DependencyInjection compiler passes, and firewall config. Dropping it into a Laravel app will not boot. As a Laravel developer you have exactly two realistic entry points:

  1. Port the ideas, not the code. The bundle is essentially four concepts built on league/oauth2-client — already a Composer dependency you can use directly in Laravel:
    • an OAuth2 provider pointed at {server_url}/realms/{realm}/protocol/openid-connect/*
    • a resource owner (sub, email, preferred_username, realm_access.roles, resource_access.{client}.roles)
    • a guard/authenticator that redirects to Keycloak and handles the callback
    • a user provider that maps Keycloak identities onto local users
  2. Run it as a sidecar. Stand up a tiny Symfony app whose only job is the Keycloak dance, then hand a signed session or JWT to your Laravel app. Usually overkill.

If you are in a Symfony codebase (or evaluating a port):

composer require idci/keycloak-security-bundle:^2.0
// config/bundles.php
IDCI\Bundle\KeycloakSecurityBundle\IDCIKeycloakSecurityBundle::class => ['all' => true],
# config/packages/idci_keycloak_security.yaml
idci_keycloak_security:
    server_url: '%env(KEYCLOAK_SERVER_URL)%'   # e.g. https://sso.example.com/auth
    realm: '%env(KEYCLOAK_REALM)%'
    client_id: '%env(KEYCLOAK_CLIENT_ID)%'
    client_secret: '%env(KEYCLOAK_CLIENT_SECRET)%'
    redirect_route: 'keycloak_callback'         # see Gotchas: must match Keycloak exactly
    login_route: 'keycloak_login'
    logout_route: 'keycloak_logout'

Then add the guard to a firewall and register the callback route. The bundle ships its own controller/route for the callback in most versions — check the bundle's Resources/config/routing.xml before writing your own. As of v2.0.2 the docs got a refresh; read the README against the code before trusting it, but it's a better starting point than it was.

Where to look first:

  • DependencyInjection/Configuration.php — the config tree is the documentation. Even post-v2.0.2 docs, the config tree is the ground truth.
  • Security/ — provider, token, and listener/authenticator classes. This is the whole bundle.
  • Security/Logout/ (or the logout listener / event subscriber) — new in v2.0.2, this is where server-side session logout is handled. Read it before assuming it does what you expect.
  • Resources/config/ — service definitions and routes; tells you what is auto-wired.

First use case: protect /admin behind Keycloak, map a client role (admin) to ROLE_ADMIN, and read email/preferred_username off the token for display. That single flow exercises every part of the bundle.

First use case in Laravel terms: composer require league/oauth2-client, write a KeycloakProvider extends AbstractProvider, register a Socialite driver or a RequestGuard, add keycloak_id to users, and JIT-provision on first login. Budget a day; it is not a large surface area.

Implementation Patterns

Authorization-code flow (the only flow the bundle really targets):

  1. Request hits a protected firewall → listener sees no KeycloakToken → redirects to {server_url}/realms/{realm}/protocol/openid-connect/auth with client_id, redirect_uri, response_type=code, scope=openid, and a state.
  2. User authenticates at Keycloak, gets bounced back to redirect_route with ?code=...&state=....
  3. The bundle exchanges the code at .../protocol/openid-connect/token, gets access_token, refresh_token, id_token.
  4. It calls the userinfo endpoint (.../protocol/openid-connect/userinfo) or decodes the ID token, builds a resource owner, and wraps it in a KeycloakToken stored in the session.
  5. The user provider resolves/creates the local user; the firewall grants roles from the token.

Role mapping is the part that eats your afternoon. Keycloak returns roles without the ROLE_ prefix, and it splits them across realm_access.roles (global) and resource_access.{client_id}.roles (per-client). In the Symfony bundle this is config-driven; in a Laravel port you write it yourself:

protected function mapRoles(array $resourceOwner): array
{
    $realm  = data_get($resourceOwner, 'realm_access.roles', []);
    $client = data_get($resourceOwner, "resource_access.{$this->clientId}.roles", []);

    return collect([...$realm, ...$client])
        ->map(fn ($r) => str_starts_with($r, 'ROLE_') ? $r : 'ROLE_'.strtoupper($r))
        ->unique()
        ->values()
        ->all();
}

Local user linking. Never key users on email — Keycloak lets users change email, and email may be unverified or absent depending on the mapper. Store keycloak_id (the sub claim) as a unique column and match on that; use email only for initial provisioning.

User::firstOrCreate(
    ['keycloak_id' => $claims['sub']],
    ['name' => $claims['preferred_username'] ?? $claims['email'], 'email' => $claims['email']]
);

Guard/stateless split. The bundle's authenticator is stateful (session token). For Laravel APIs, don't reuse that assumption — validate the bearer access token against Keycloak's introspection endpoint (.../token/introspect) or verify the JWT signature against the realm JWKS (.../certs). Cache the JWKS; don't fetch per request.

Logout — three distinct things, don't conflate them:

  1. Local logout. Clear the Symfony session. The bundle always did this.
  2. RP-initiated (front-channel) logout. Redirect to .../protocol/openid-connect/logout with id_token_hint and post_logout_redirect_uri. Mirror in Laravel with a controller redirect or LogoutResponse.
  3. Server-side session logout (new in v2.0.2). This is the back-channel case: an admin or another app kills the user's Keycloak SSO session, and Keycloak tells relying parties to invalidate. v2.0.2 adds handling for this — the bundle now reacts to server-side logout so a still-valid local session doesn't outlive the Keycloak session.

What "server-side session logout" means for you operationally (verify against the code, but this is the shape):

  • You must configure a Backchannel logout URL on the Keycloak client (Settings → Capability config / Fine-grained, depending on version) pointing at the bundle's logout endpoint. If you don't register it, nothing happens — the feature is inert.
  • Keycloak sends a logout_token (a signed JWT with events: {"http://schemas.openid.net/event/backchannel-logout": {}}, sid, sub). The handler must validate it and invalidate matching local sessions. If your sessions live in files on a single host, fine; if you run multiple app servers, you need shared session storage (Redis/DB) or the invalidation only hits one node.
  • It requires knowing which local sessions map to a sid/sub. If the bundle only stores the token and not an index, revocation may be best-effort. Read the implementation before promising single-logout to your security team.

In a Laravel port, the equivalent: implement POST /auth/keycloak/backchannel-logout, verify the logout_token against the realm JWKS, check the events claim, and delete sessions/tokens for the sub/sid. Persist a keycloak_sid on your session or a sessions table row; laravel/session's file/redis driver can be purged by user ID if you index it.

Config mirroring. Keep realm/client/redirect in one place (config/keycloak.php + .env), and use the same values across local/staging/prod Keycloak clients so you don't discover a redirect URI mismatch in production. Give each environment its own Keycloak client rather than one shared client with wildcard redirects.

Client credentials. If you need machine-to-machine, the bundle won't help — that's a client_credentials grant, no user, no guard. Use league/oauth2-client's ClientCredentials provider or Guzzle directly.

Gotchas and Tips

Symfony-only. No Laravel integration exists. Anything you read in the README about firewalls, security.yaml, and access_control has no Laravel analogue — translate it mentally to middleware + guards.

The v2.0.2 logout feature is opt-in on the Keycloak side. You have to set the Backchannel logout URL on the client. It also does nothing useful on multi-node deployments without shared session storage. Treat "handles server-side session logout" as "has a handler", not "single logout works out of the box" — test it explicitly by killing the session from the Keycloak admin console while logged in, then hitting a protected route.

Docs improved in v2.0.2, but the config tree still wins. The README is now a better starting point, and the new session-logout section is worth reading before you go hunting in Security/. Still sanity-check config keys against DependencyInjection/Configuration.php; docs drift, config trees don't.

License is NOASSERTION. That is not a green light. Before commercial use, confirm the actual license in LICENSE/composer.json and get legal sign-off. This alone can disqualify the package for many teams.

Maintenance risk is real — but improved. v2.0.2 (current) followed a long gap; still a small package (sub-100 stars, negligible dependents). Symfony's Security component has had BC breaks across 6.x → 7.x (authenticator-based security replaced guards). Pin the bundle and your Symfony minor version, and be prepared to fork. Treat it as vendored reference code you keep an eye on, not a dependency you upgrade casually. The v2.0.2 logout feature means upstream is alive again — worth watching, not worth depending on blindly.

Redirect URI mismatch (Invalid parameter: redirect_uri) is the single most common failure. Keycloak requires an exact match — scheme, host, port, path, trailing slash. http://localhost:8000 and http://127.0.0.1:8000 are different clients as far as Keycloak is concerned.

state and PKCE. The bundle relies on league/oauth2-client for state. PKCE (code_challenge) support depends on the version of that library and whether the bundle sets it. For public clients (SPA/mobile), verify it — and if it's missing, don't put the client secret in the browser.

Token expiry vs session lifetime. The session may outlive the access token (default 5 minutes in Keycloak). If the bundle stores the token and never refreshes it, users get logged out or hit 401s mid-session. Either shorten the session, implement refresh via the refresh_token, or set a sensible access-token lifespan in the realm.

Realm roles vs client roles. Check both realm_access.roles and resource_access.{client}.roles. Reading only one is the classic "roles work for me but not for the other team" bug.

Groups and attributes are not returned by default. You must add a Keycloak protocol mapper (Group Membership, User Attribute) to the client — the bundle cannot conjure data Keycloak isn't sending.

SSL in local dev. Self-signed Keycloak certs will fail Guzzle verification. Fix the trust store; don't disable verification in code that reaches production. If the bundle exposes a verify option, gate it behind app()->environment('local').

Debugging checklist:

  • Symfony: bin/console debug:config idci_keycloak_security and debug:firewall <name> show the effective config — faster than reading YAML.
  • bin/console debug:router and debug:event-dispatcher — confirm the callback route and the logout listener are actually registered before debugging logic that never runs.
  • Check the Keycloak Events log for LOGIN_ERROR, CODE_TO_TOKEN_ERROR, LOGOUT_ERROR, and backchannel logout delivery failures; it tells you whether the problem is client config or your code.
  • Dump the resource owner once (dd($claims)) to see exactly which claims your mappers produce. Do this before writing role logic.
  • Browser devtools: the Location header on the initial redirect shows the exact redirect_uri and state being sent.
  • For session logout: tail the app logs while triggering a logout from the Keycloak admin console. Silence means the backchannel URL isn't registered or isn't reachable from Keycloak.

Extension points worth knowing (Symfony): swap the user provider for your own to control provisioning, override the redirect/login/logout routes via config, and decorate the provider service if you need to add scopes or custom headers. For logout behavior, the logout listener/handler is the seam — decorate it if you need to add your own session invalidation (e.g. a Redis purge or a sessions table delete in a hybrid app). In a Laravel port, the equivalent seams are the guard (Auth::extend), the user provider (Auth::provider), and a KeycloakClient service you can bind and fake in tests.

Testing. Fake the token endpoint and userinfo with Http::fake() (Laravel) or an HTTP mock (Symfony) and assert on: user created once on repeat logins, roles mapped correctly, unverified email rejected, and expired token handled. Add a fifth now: a valid logout_token invalidates the session, and an invalid/unsigned one does not. That fifth test is the one v2.0.2 introduces, and it's the one most likely to be silently broken by misconfiguration.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
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