Search by

bannerstop / keycloak-bundle

mluex

Symfony integration of bannerstop/keycloak: Keycloak single sign-on, bearer tokens, role mapping and logout for the security component

Package info

github.com/bannerstop/keycloak-bundle

Type:symfony-bundle

pkg:composer/bannerstop/keycloak-bundle

Statistics

Installs: 15

Dependents: 0

Suggesters: 1

Stars: 0

Open Issues: 0

v10.3.0 2026-10-08 15:45 UTC

README

Symfony integration of bannerstop/keycloak: single sign-on with Keycloak for the Symfony security component.

  • Browser login (authorization code flow with PKCE) as an authenticator, including the entry point
  • Bearer tokens for stateless API firewalls
  • Logout that also ends the Keycloak session
  • Role mapping from realm roles, client roles and groups
  • Works without a user table (stateless KeycloakUser) or with your own users (UserProvisioner)

Versions

Version PHP Symfony
1.x ≥ 7.1.3 4.4 (Guard), 5.4 (Guard or authenticator system)
2.x ≥ 7.2 4.4 (Guard), 5.4 (Guard or authenticator system)
3.x ≥ 7.3 4.4 (Guard), 5.4 (Guard or authenticator system)
4.x ≥ 7.4 4.4 (Guard), 5.4 (Guard or authenticator system)
5.x ≥ 8.0 5.4, 6.x (authenticator system)
6.x ≥ 8.1 5.4, 6.4
7.x ≥ 8.2 6.4, 7.x
8.x ≥ 8.3 6.4, 7.x
9.x ≥ 8.4 7.4, 8.x
10.x ≥ 8.5 7.4, 8.x

Installation

composer require bannerstop/keycloak-bundle symfony/http-client nyholm/psr7

Without Symfony Flex, register the bundle in config/bundles.php:

Bannerstop\KeycloakBundle\BannerstopKeycloakBundle::class => ['all' => true],

Import the routes (/login/keycloak and /login/keycloak/callback), e.g. in config/routes/bannerstop_keycloak.yaml:

bannerstop_keycloak:
    resource: '@BannerstopKeycloakBundle/Resources/config/routes.php'

In Keycloak, register https://your-app.example/login/keycloak/callback as redirect URI and your logout target as post logout redirect URI. See the core README for the full client setup.

Configuration

# config/packages/bannerstop_keycloak.yaml
bannerstop_keycloak:
    server_url: 'https://sso.example.com'
    realm: 'example'
    client_id: 'my-app'
    client_secret: '%env(KEYCLOAK_CLIENT_SECRET)%'

    login:
        # a list, or a comma separated string from the environment:
        # allowed_email_domains: '%env(KEYCLOAK_ALLOWED_EMAIL_DOMAINS)%'
        allowed_email_domains: ['example.com'] # empty: everybody in the realm
        default_target_path: '/'
        failure_path: 'app_login'   # route or path; shows the error via AuthenticationUtils
        logout_target: '/'

    roles:
        default_roles: ['ROLE_USER']
        realm_roles:
            admin: ROLE_ADMIN
        client_roles:
            my-app:
                editor: [ROLE_EDITOR]
        groups:
            /staff/it: ROLE_IT

    bearer:
        audience: 'my-api'          # defaults to the client id

    session:                        # see "Ending sessions with Keycloak"
        check_interval: 300         # seconds, 0 = back-channel logouts only; or '%env(int:KEYCLOAK_SESSION_CHECK_INTERVAL)%'

    directory:                      # optional, see below
        client_id: 'my-app-directory'
        client_secret: '%env(KEYCLOAK_DIRECTORY_CLIENT_SECRET)%'

    # user_provisioner: App\Security\KeycloakUserProvisioner

directory gives the user directory (UserDirectory, admin REST API) its own confidential client. We recommend it: create a client with only Service accounts roles enabled and assign realm-management → view-users to its service account, so that the login client has no admin API rights. Without directory, the login client is used for both and needs the service account and view-users itself.

cache (default cache.app) caches the discovery document and the signing keys. http_client, request_factory and stream_factory take service ids if you do not use symfony/http-client.

Security

security:
    providers:
        keycloak:
            id: bannerstop_keycloak.user_provider
    firewalls:
        api:
            pattern: ^/api/
            stateless: true
            provider: keycloak
            custom_authenticators: [bannerstop_keycloak.bearer_authenticator]
        main:
            lazy: true
            provider: keycloak
            custom_authenticators: [bannerstop_keycloak.authenticator]
            logout:
                path: app_logout
    access_control:
        - { path: ^/login, roles: PUBLIC_ACCESS }
        - { path: ^/, roles: ROLE_USER }

Your own users

By default users only live in the session (KeycloakUser, identified by the Keycloak subject) and are served by bannerstop_keycloak.user_provider.

To use your own user entity instead, implement Bannerstop\KeycloakBundle\User\UserProvisioner. After every successful login and every accepted bearer token, the bundle calls provision() with the verified identity and the mapped roles; the user it returns is the authenticated user. Link accounts by the Keycloak subject, not by e-mail address, which can change:

// src/Security/KeycloakUserProvisioner.php
namespace App\Security;

use App\Entity\User;
use App\Repository\UserRepository;
use Bannerstop\Keycloak\Identity;
use Bannerstop\KeycloakBundle\User\UserProvisioner;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;

final class KeycloakUserProvisioner implements UserProvisioner
{
    public function __construct(
        private UserRepository $users,
        private EntityManagerInterface $entityManager,
    ) {
    }

    public function provision(Identity $identity, array $roles): UserInterface
    {
        $user = $this->users->findOneBy(['keycloakId' => $identity->getSubject()])
            ?? (new User())->setKeycloakId($identity->getSubject());
        $user->setEmail($identity->getEmail())
            ->setName($identity->getDisplayName())
            ->setRoles($roles);
        $this->entityManager->persist($user);
        $this->entityManager->flush();

        return $user;
    }
}

user_provisioner is an option of this bundle (not of Symfony itself) and takes the service id of your provisioner. With autowiring, the id is the class name:

# config/packages/bannerstop_keycloak.yaml
bannerstop_keycloak:
    # ...
    user_provisioner: App\Security\KeycloakUserProvisioner

The firewall then uses your usual entity provider instead of bannerstop_keycloak.user_provider:

# config/packages/security.yaml
security:
    providers:
        app_users:
            entity:
                class: App\Entity\User
                property: keycloakId
    firewalls:
        main:
            provider: app_users
            custom_authenticators: [bannerstop_keycloak.authenticator]

The provisioner hands Symfony the user at login; on every following request Symfony reloads it through the firewall's provider. That provider must therefore return the same entity class.

remember_me on the firewall applies to Keycloak logins as well. It needs a user with a password property or other signature_properties, so it works with your own users, not with the session-only KeycloakUser.

Ending sessions with Keycloak

Logging out of Keycloak, or of another application, does not end the Symfony session by itself. The bundle closes that gap in two ways:

  • Back-channel logout: Keycloak posts a signed logout token to /login/keycloak/backchannel-logout when a session ends. The bundle verifies it and records the ended Keycloak session in the cache (cache, default cache.app, which must be shared by all web servers). In the Keycloak client set Backchannel logout URL to https://your-app.example/login/keycloak/backchannel-logout and turn on Backchannel logout session required. The route must be reachable without a login; the ^/login access rule above already covers it.
  • Session check (session.check_interval): every N seconds the refresh token of the session is redeemed. Once Keycloak refuses it (logout elsewhere, user disabled, SSO session expired), the session ends. If Keycloak is unreachable, the session is kept.

On the next request after its Keycloak session ended, a session is logged out (including the remember-me cookie): pages redirect to themselves, so the firewall's entry point asks for a new login, and XHR/JSON requests get a 401. Only sessions that came from a Keycloak login are checked.

Keycloak does not always send a back-channel call for every session, e.g. when an administrator signs a user out of all sessions. Keep check_interval switched on as a safety net; 300 seconds is a good start.

Login errors

A failed login redirects to failure_path. AuthenticationUtils::getLastAuthenticationError() then returns an exception whose message key is one of keycloak.login.state_mismatch, .cancelled, .provider_error, .invalid_token or .not_allowed. Translate them in the security domain.

Services

Service Use
Bannerstop\Keycloak\KeycloakClient refresh tokens, userinfo, verify tokens yourself
Bannerstop\Keycloak\Admin\UserDirectory list users of the realm through the service account of the directory client, or of the login client (view-users)
Bannerstop\Keycloak\Role\RoleMapper the configured role mapping

License

MIT, see LICENSE. Security issues: see the core package's security policy.