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 Guard Laravel Package

robsontenorio/laravel-keycloak-guard

Laravel Keycloak Guard provides a Keycloak authentication guard for Laravel, validating JWT access tokens and exposing roles and permissions. It integrates with Laravel's native auth system, making it easy to protect routes and APIs with Keycloak-issued tokens.

View on GitHub
Deep Wiki
Context7

Getting Started

  • composer require robsontenorio/laravel-keycloak-guard — v2.x targets modern PHP/Laravel; confirm the constraint matrix in composer.json before dropping it into an older app.
  • Publish the config: php artisan vendor:publish --tag=keycloak → config/keycloak.php. This file is the whole mental model of the package; read it before writing any code.
  • Minimum viable .env:
    • KEYCLOAK_REALM_PUBLIC_KEY — from Keycloak admin → Realm Settings → Keys → RS256 → Public key (full PEM, including the -----BEGIN PUBLIC KEY----- lines).
    • KEYCLOAK_TOKEN_ISSUER — must match the token's iss claim exactly, e.g. https://kc.example.com/realms/myrealm.
    • KEYCLOAK_LOAD_USER_FROM_DATABASE — true if your users table mirrors Keycloak users, false if you want to work straight off token claims.
  • Register the guard in config/auth.php:
    'guards' => [
        'web' => ['driver' => 'session', 'provider' => 'users'],
        'api' => ['driver' => 'keycloak', 'provider' => 'users'],
    ],
    
  • Protect routes with ->middleware('auth:api'). The client (SPA/mobile) obtains a token from Keycloak via authorization code + PKCE and sends Authorization: Bearer <token>.
  • First use case to validate end-to-end: hit a /api/me route, dd(auth('api')->user()), and confirm you see claims (no-DB mode) or an Eloquent model (DB mode) plus auth('api')->id() returning the Keycloak sub.

Implementation Patterns

  • Choose your user strategy early. With load_user_from_database = true the guard resolves an Eloquent user through the provider, so policies, relations, $request->user()->is_admin etc. all work normally. With false you get the decoded token (claims object) — fast, stateless, but no model behaviour. Mixing the two in one app is a common source of confusion.
  • Lookup key matters. In DB mode, your provider's retrieveById receives the identifier derived from the token. If your realm issues UUID subjects (the default for modern Keycloak), store sub in a keycloak_id/uuid column and resolve on that — not on an auto-increment id.
  • Custom resolution. If email-based matching or just-in-time provisioning is needed, use the package's custom-retrieve hook / swap the provider's user retrieval method rather than fighting the guard. Keep that method side-effect-free (no writes on every request) and cache if it hits Keycloak's admin API.
  • Roles and permissions. Don't build a parallel RBAC. Map Keycloak claims to Laravel authorization:
    // AppServiceProvider::boot()
    Gate::before(function ($user, $ability) {
        $roles = data_get($user, 'realm_access.roles', []);
        return in_array($ability, $roles, true) ? true : null;
    });
    
    Remember client roles live under resource_access.{client-id}.roles, not realm_access.
  • Claim-based scoping. A small middleware that reads a claim (tenant id, azp, group) and binds it to the request/container is the cleanest way to do multi-tenant APIs without a DB round trip.
  • Middlewares order. Keep auth:api after CORS/preflight handling; return 401 for missing/expired tokens and let the SPA's interceptor refresh and retry.
  • Blade/Inertia hybrid apps. Keep web (session) and api (keycloak) guards side by side and use auth()->guard('api') explicitly — auth()->user() with no guard picks the default and will silently be null.
  • Testing. In DB mode, $this->actingAs($user, 'api') works. In claim mode, inject a prepared decoded token/claims object via Auth::guard('api')->setUser(...), or ship a long-lived signed JWT fixture (generate once with a throwaway RSA key pair committed to tests/Fixtures).
  • Downstream tokens. When calling other services you often need to forward the raw bearer token — capture it in a request macro or a small TokenHolder singleton rather than re-reading Authorization in every client.

Gotchas and Tips

  • UUID subjects + id() (fixed in 2.0.1). If you are on 2.0.0 or earlier, auth()->id() / getAuthIdentifier() on realms with UUID subjects can be coerced to an integer or blow up with a type error. Upgrade to 2.0.1 and add a test asserting auth('api')->id() equals the exact sub string — then compare with === in policies and route-model-binding guards ((int) casts elsewhere will silently break).
  • Public key format is the #1 setup failure. Paste the whole PEM. In .env, multi-line values must be quoted and newline-escaped: KEYCLOAK_REALM_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIB...\n-----END PUBLIC KEY-----". Docker/CI env injection frequently strips or re-wraps newlines. php artisan config:clear (and re-run config:cache) after every change; a cached config with a stale key is invisible otherwise.
  • Key rotation. Keycloak rotates RS256 keys. If you hardcode the realm public key, tokens start failing with signature errors after rotation. Either rotate the env var as part of ops, or wire the guard to refresh from the JWKS endpoint (/realms/{realm}/protocol/openid-connect/certs).
  • Issuer / audience mismatches. iss must match character-for-character (trailing slashes, http vs https, port). Validate aud/azp against your expected client — otherwise tokens minted for a different client in the same realm are accepted. Use allowed_resources (or an equivalent audience check) if you expose more than one client.
  • Clock skew. Short-lived Keycloak tokens plus a lagging container clock produce intermittent 401s. Set leeway to a small positive value in config/keycloak.php rather than extending token lifespan.
  • Stateless means stateless. With load_user_from_database = false, auth()->user() is not an Eloquent model — ->save(), ->roles(), ->update() all fail. Don't mix modes inside one feature.
  • Preflight/CORS. OPTIONS requests hitting auth:api return 401 and look like a CORS bug in the browser. Exclude preflight in CORS middleware, not by unprotecting the route.
  • Logout/revocation. The guard only verifies a JWT; it cannot revoke it. Deleting a session in Keycloak does not invalidate an unexpired access token. Keep access tokens short and, if you need instant revocation, add a denylist/jti cache or introspect on sensitive routes.
  • Debugging checklist: decode at jwt.io (or base64 -d the payload) → check iss, aud, exp, azp, sub; confirm the signature algorithm is RS256 and the app isn't silently accepting none; log the guard exception class (TokenException variants) rather than a generic 401 message; verify storage/bootstrap/cache writability so config caching isn't silently skipped.
  • Extension points worth knowing: the guard class itself (bind your own via a custom Auth::extend driver if you need extra claim validation), the config's user-retrieval hook for provisioning, and a Gate::before/policy layer for roles. Keep Keycloak-specific logic in one service provider so swapping identity providers later stays a single-file change.
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.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox