bannerstop / keycloak-bundle
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
Requires
- php: ^8.5
- bannerstop/keycloak: ^10.0
- symfony/config: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/routing: ^7.4 || ^8.0
- symfony/security-bundle: ^7.4 || ^8.0
Requires (Dev)
- ext-curl: *
- nyholm/psr7: ^1.8.2
- phpunit/phpunit: ^12.0
- symfony/browser-kit: ^7.4 || ^8.0
- symfony/cache: ^7.4 || ^8.0
- symfony/http-client: ^7.4 || ^8.0
Suggests
- symfony/cache: Caches the discovery document and the signing keys
- symfony/http-client: PSR-18 client the bundle uses by default (with nyholm/psr7)
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 10.x-dev
- v10.2.0
- v10.1.1
- v10.1.0
- v10.0.0
- 9.x-dev
- v9.2.0
- v9.1.1
- v9.1.0
- v9.0.0
- 8.x-dev
- v8.2.0
- v8.1.1
- v8.1.0
- v8.0.0
- 7.x-dev
- v7.2.0
- v7.1.1
- v7.1.0
- v7.0.0
- 6.x-dev
- v6.2.0
- v6.1.1
- v6.1.0
- v6.0.0
- 5.x-dev
- v5.2.0
- v5.1.1
- v5.1.0
- v5.0.0
- 4.x-dev
- v4.2.0
- v4.1.1
- v4.1.0
- v4.0.0
- 3.x-dev
- v3.2.0
- v3.1.1
- v3.1.0
- v3.0.0
- 2.x-dev
- v2.2.0
- v2.1.1
- v2.1.0
- v2.0.0
- 1.x-dev
- v1.2.0
- v1.1.1
- v1.1.0
- v1.0.0
This package is auto-updated.
Last update: 2026-10-08 11:12:36 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 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.
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.