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 Client Bundle Laravel Package

mainick/keycloak-client-bundle

Symfony bundle for Keycloak authentication that adds OpenID Connect single sign-on to your app, handling login, logout, token validation, and role-based access with minimal configuration. Supports Symfony security integration and route/API protection.

View on GitHub
Deep Wiki
Context7

Getting Started

You're coming from Laravel, so translate as you go: a Symfony bundle is a Composer package plus a DI extension — there's no vendor:publish. Config lives in config/packages/, services are autowired by interface, and the "middleware" is an event listener/authenticator.

1. Install and register

composer require mainick/keycloak-client-bundle

There is no Flex recipe, so register it by hand in config/bundles.php:

return [
    // ...
    Mainick\KeycloakClientBundle\KeycloakClientBundle::class => ['all' => true],
];

2. Configure the client

# config/packages/mainick_keycloak_client.yaml
mainick_keycloak_client:
    auth_server_url: '%env(KEYCLOAK_AUTH_SERVER_URL)%'
    realm: '%env(KEYCLOAK_REALM)%'
    client_id: '%env(KEYCLOAK_CLIENT_ID)%'
    client_secret: '%env(KEYCLOAK_CLIENT_SECRET)%'
    redirect_route_name: 'keycloak_check'
    default_scopes: ['openid', 'profile', 'email']
KEYCLOAK_AUTH_SERVER_URL=https://sso.example.com
KEYCLOAK_REALM=my-realm
KEYCLOAK_CLIENT_ID=my-symfony-app
KEYCLOAK_CLIENT_SECRET=change-me

3. Where to look first, in this order

  • README.md — the quick-start controller is the fastest end-to-end path.
  • vendor/mainick/keycloak-client-bundle/DependencyInjection/Configuration.php — the source of truth for option names. This package moves fast and the README occasionally lags; treat the config tree, not the docs, as canonical.
  • vendor/mainick/keycloak-client-bundle/src/Interface/ — tells you exactly which operations are wrapped (token exchange, userinfo, and the Admin API surface: users, groups, roles) before you design around assumptions.

4. First use case: browser login

use Mainick\KeycloakClientBundle\Interface\KeycloakClientInterface;

#[Route('/login/keycloak', name: 'keycloak_login')]
public function login(KeycloakClientInterface $keycloakClient): Response
{
    return $this->redirect($keycloakClient->getAuthorizationUrl());
}

#[Route('/login/keycloak/check', name: 'keycloak_check')]
public function check(Request $request, KeycloakClientInterface $keycloakClient): Response
{
    $token    = $keycloakClient->getAccessToken($request->query->get('code'));
    $userInfo = $keycloakClient->getUserInfo($token);

    // provision/lookup your local user here, then start a session

    return $this->redirectToRoute('dashboard');
}

keycloak_check must match redirect_route_name, and it must be anonymous-accessible — it's the route Keycloak bounces the user back to with ?code=, so it cannot sit behind your auth firewall.

Implementation Patterns

The bundle gives you tokens, not a session. This is the biggest mental shift. There is no Auth::login(). You decide where state lives: Symfony session (fast, matches Laravel's session guard), or a table with access_token / refresh_token / expires_at (survives PHP-FPM restarts and lets you call the Admin API from workers). Wrap it once:

final class KeycloakTokenManager
{
    public function __construct(
        private KeycloakClientInterface $client,
        private CacheInterface $cache,
    ) {}

    public function validAccessToken(string $refreshToken): array
    {
        $token = $this->cache->get('kc.token', fn () => null);
        if ($token === null) {
            $token = $this->client->refreshToken($refreshToken);
            $this->cache->set('kc.token', $token, max(1, ($token['expires_in'] ?? 60) - 30));
        }
        return $token;
    }
}

Two integration styles — pick one deliberately.

  • Listener/authenticator mode: enable the bundle's per-request token verification (enable_listener in the config tree) and wire the firewall in security.yaml. This is your auth:api middleware equivalent — every request carries a verified token. Good for APIs and stateless front ends.
  • Manual mode: you call getUserInfo() in one controller, mint a normal Symfony session token, and never install the listener. Simpler, fewer moving parts, and much easier to debug on day one.

Do not mix them; you'll end up verifying twice and wondering which component rejected the request.

Map Keycloak roles into Symfony roles in exactly one class. Realm roles arrive under realm_access.roles, client roles under resource_access.<client>.roles. Normalise to ROLE_* in a single mapper so #[IsGranted('ROLE_ADMIN')] never depends on Keycloak naming conventions leaking across your codebase.

Use two Keycloak clients. The browser-facing confidential client for login, and a separate service-account client (client_credentials) for server-side user management — listing users, assigning groups, provisioning on signup:

$users = $keycloakClient->getUsers();
$keycloakClient->assignRealmRole($userId, 'editor');

Keep the service-account secret out of anything the front end can reach.

Decorate, don't fork. Need retries, structured logging, or a token cache? Decorate the interface rather than editing vendor code:

App\Security\LoggingKeycloakClient:
    decorates: Mainick\KeycloakClientBundle\Interface\KeycloakClientInterface

Testing. Bind a stub for the interface in config/services_test.yaml and assert on redirect URLs and your user-provisioning logic — that's where the real bugs live. Don't try to unit-test the bundle itself; its behaviour is the IdP's behaviour.

Console/queue context. There is no request and no session in a worker. Acquire a token via client_credentials and cache it in Redis with a TTL below expires_in, keyed per realm.

Gotchas and Tips

You are the QA. 45 stars and zero dependents means you're near the front of the queue for whatever is broken. Pin with composer.lock, read the released source at install time, and put a thin App\Security\KeycloakAuthenticator adapter in front of it so swapping to league/oauth2-client or Symfony's own OIDC support is a one-class change rather than a refactor.

redirect_route_name typos fail loudly at the worst time. Keycloak authenticates the user, redirects back, and Symfony 404s. It reads like an IdP misconfiguration but it's a route name.

Keycloak 17+ dropped the /auth path segment. If auth_server_url is https://sso.example.com/auth, every endpoint 404s. Set the base URL and pin the version explicitly in config so the bundle doesn't guess.

Docker networking is a classic. auth_server_url must be reachable from the PHP container for back-channel calls (token exchange) but from the browser for the redirect. localhost inside a container means the container. Use the compose service name internally and the public hostname for the redirect — the same trap as APP_URL vs. internal host in Laravel.

"Valid token rejected" is almost always a missing audience mapper. Keycloak doesn't add your client to aud by default. Add an Audience mapper to the client scope, otherwise audience validation fails on a token that decodes perfectly at jwt.io. Checklist when a token is rejected: iss equals <auth_server_url>/realms/<realm>, aud/azp contains your client_id, exp is in the future, and the container clock isn't skewed (date inside the container).

Empty roles is a mapper problem, not a bundle problem. Realm roles are in realm_access.roles by default; client roles need a mapper to appear. Check the decoded token before filing a bug.

Refresh token rotation causes race conditions. Keycloak invalidates the old refresh token by default, so two concurrent requests refreshing simultaneously means one loses and the user is logged out mid-session. Serialise refreshes with a lock, or keep access tokens short and the session long.

Per-request verification costs a network round trip unless realm public keys are cached. If you use listener mode, confirm JWKS caching is on and exclude public/webhook/health routes from the verification pattern.

Logout is only half-implemented by default. Clearing the local token leaves the Keycloak SSO session alive, so the user is silently logged straight back in. Implement RP-initiated logout against end_session_endpoint with id_token_hint and post_logout_redirect_uri; keep the id_token around for exactly this.

Set explicit Guzzle timeouts. An unreachable IdP with a default timeout will hang login requests until PHP's max_execution_time kills them, producing an empty 500 with nothing in the logs.

%env()% in bundle config is resolved at container compile time. Change a .env value and you need bin/console cache:clear before anything moves — the opposite of Laravel's per-request env() feel.

Dev-only SSL bypass. A self-signed Keycloak locally needs certificate verification disabled via the client's HTTP options. Never let that flag reach production config; gate it on %kernel.environment%.

Extension points worth knowing: the config tree (multi-realm by defining separate client instances via a factory), service decoration for cross-cutting concerns, and the token listener's route exclusions for anything that must stay anonymous.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib