OpenID Connect (OIDC) is the protocol behind most "Log in with ..." buttons. It lets your application delegate user authentication to an identity provider such as Keycloak, Authentik, Auth0, Okta or Microsoft Entra ID.
Symfony 6.3 added OIDC token handlers to the access token authenticator, but
they only validate tokens that clients already have, which is perfect for APIs.
To log users in from a browser, you needed third-party bundles. Symfony 8.2
adds a new oidc_login authenticator that implements the full
OIDC Authorization Code Flow natively.
Logging In with OpenID Connect
First, install the HttpClient component and the library used to validate the ID tokens:
1
$ composer require symfony/http-client web-token/jwt-library
Then, enable oidc_login in your firewall:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# config/packages/security.yaml
security:
providers:
oidc_users:
oidc: ~
firewalls:
main:
provider: oidc_users
oidc_login:
provider_uri: '%env(OIDC_PROVIDER_URI)%'
client_id: '%env(OIDC_CLIENT_ID)%'
client_authentication:
client_secret_basic: '%env(OIDC_CLIENT_SECRET)%'
scope: ['openid', 'profile', 'email']
You don't need to configure the provider's endpoints: Symfony reads them from the
.well-known/openid-configuration document published by the provider. When
anonymous users open a protected page, they are redirected to the provider;
after logging in there, they are sent back to /oidc/callback (the default
value of the check_path option) and the authenticator opens the session.
The routes of the login flow are imported automatically by the Symfony Flex recipe. If you prefer to show a "Log in with ..." button (e.g. in a login page with several login options), link to the route that starts the flow:
1
<a href="{{ path('_oidc_login_start_main') }}">Log in with Acme</a>
Secure by Default
The authenticator handles the following security checks automatically for you:
- the
stateandnonceparameters are checked to prevent login CSRF and to bind the ID token to the original request; - PKCE is applied by default, so intercepted authorization codes are useless;
- the ID token signature is verified against the provider's published keys (JWKS). They are cached, and a key rotation at the provider requires no action on your side;
- the
iss,aud,expandiatclaims of the ID token are validated, as well as theissparameter of the response to prevent mix-up attacks; - all the provider endpoints must use HTTPS (except loopback hosts during local development) and HTTP redirects are never followed.
Loading Users
The built-in oidc user provider shown above creates users on the fly from
the claims returned by the provider. To store users in your database or to map
provider groups to roles, create your own user provider. Its loadUserByIdentifier()
method receives all the user claims as its second argument:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// src/Security/OidcUserProvider.php
namespace App\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\User\AttributesBasedUserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class OidcUserProvider implements AttributesBasedUserProviderInterface
{
public function __construct(
private UserRepository $userRepository,
) {
}
public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface
{
// $identifier is the "sub" claim by default; $attributes contains all claims
$user = $this->userRepository->findOneBy(['oidcSubject' => $identifier]) ?? new User($identifier);
$user->setEmail($attributes['email'] ?? null);
$user->setRoles(\in_array('admins', $attributes['groups'] ?? [], true) ? ['ROLE_ADMIN'] : []);
// ... persist the user
return $user;
}
// ...
}
You can also identify users by another claim (user_identifier_claim: email)
and read the claims from the ID token instead of calling the UserInfo endpoint
(user_data_source: id_token).
Customizing the Authorization Request
Use the authorization_params option to add any static parameter supported
by your provider (e.g. prompt: consent). When a parameter depends on the
current request, listen to the new OidcAuthorizationRequestEvent, which is
dispatched right before redirecting users to the provider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Security/OidcAuthorizationRequestListener.php
namespace App\Security;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Security\Http\Event\OidcAuthorizationRequestEvent;
#[AsEventListener]
final class OidcAuthorizationRequestListener
{
public function __invoke(OidcAuthorizationRequestEvent $event): void
{
$request = $event->getRequest();
// show the provider login page in the same language as your application
$event->setParam('ui_locales', $request->getLocale());
// pre-fill the email field in the provider login page
if ($email = $request->getSession()->get('login_email')) {
$event->setParam('login_hint', $email);
}
}
}
Parameters that the authenticator relies on (state, nonce, redirect_uri,
etc.) can't be changed through configuration or the event.
Client Authentication with Signed JWTs
The client_authentication option defines how your application authenticates
itself when exchanging the authorization code for tokens. You can send a client
secret (client_secret_basic or client_secret_post), or use none for
public clients without a secret.
With private_key_jwt, your application signs a JWT assertion using its own
private key:
1 2 3 4 5 6 7 8 9 10
# config/packages/security.yaml
security:
firewalls:
main:
oidc_login:
# ...
client_authentication:
private_key_jwt:
key: '%env(OIDC_CLIENT_SIGNING_KEY)%'
algorithm: 'ES256'
The client_secret_jwt method is supported too. For any other scheme (e.g. mutual
TLS), pass the ID of a service implementing the new ClientAuthenticationInterface.
Renewing Access Tokens
The tokens returned by the provider are stored in the security token (e.g.
$token->getAttribute('oidc_access_token')), so you can call the provider
APIs on behalf of the user. Access tokens usually expire after a few minutes,
so Symfony can now renew them using the refresh token:
1 2 3 4 5 6 7 8 9
# config/packages/security.yaml
security:
firewalls:
main:
oidc_login:
# ...
scope: ['openid', 'profile', 'offline_access']
refresh_access_token:
enabled: true
When enabled, the access token is renewed automatically when a request arrives
shortly before it expires. If you prefer to do it on demand, inject the
OidcTokenRefresher service and call its refreshIfNeeded() method.
Logging Out of the Provider
By default, logging out only closes the session in your application. Users stay
logged in at the provider, so the next visit to a protected page logs them in
again without any prompt. Enable the enable_end_session option to also log
users out of the provider (this is called RP-Initiated Logout):
1 2 3 4 5 6 7 8
# config/packages/security.yaml
security:
firewalls:
main:
oidc_login:
# ...
enable_end_session: true
post_logout_redirect_path: /
Other Options
You can also use max_age to require a recent authentication at the provider,
or http_client to configure a custom HTTP client with your own timeouts,
retries or User-Agent.
Providers that send their response using an HTTP POST are supported
too (response_mode: form_post).
See the OpenID Connect login docs for the full configuration reference.
Nice addition. Is remember_me supported with oidc_login?
I'm moving a Google login from a custom authenticator to this. The old authenticator added a RememberMeBadge, and the firewall uses always_remember_me: true to keep users logged in after the session expires. The docs don't say whether the OIDC authenticator adds that badge.
Hi @Massimiliano
No oidc_login doesn't add badges and that's on purpose. The authentication belongs to the provider, not to the application. Adding the badge from a CheckPassportEvent listener would technically work, but I wouldn't recommend it as it will defeat the security means decided on the OIDC Provider. If you need to control how recent the authentication is then you should use the OIDC mechanisms such as max_age or prompt=login