Symfony security includes several significant improvements and new features in Symfony 7.3.
Deprecate eraseCredentials()
Method
The eraseCredentials()
method has been part of the UserInterface
since
its introduction. It is used to delete sensitive data on the user object,
typically their password:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
// ...
public function eraseCredentials(): void
{
// clear any temporary, sensitive data on the user, e.g.:
// $this->plainPassword = null;
}
}
In Symfony 7.3, this method has been deprecated as it's no longer considered
a best practice. Instead, you should use a dedicated DTO or manually delete
sensitive data during the AuthenticationTokenCreatedEvent
.
To ease the upgrade path, Symfony 7.3 allows you to add the #[\Deprecated]
attribute to your eraseCredentials()
method. When present, Symfony will
stop calling the method automatically.
Support Hashed Passwords in the Session
Storing any form of a password in the session, plain or hashed, can be risky.
To mitigate this, you can implement the __serialize()
method in your user
class to exclude or transform the password before it's stored in the session.
If you remove the password entirely, getPassword()
returns null
after
unserialization. In that case, Symfony refreshes the user without password
verification, which is only relevant if you're storing plaintext passwords (which
is not recommended).
Symfony 7.3 improves on this by allowing you to store CRC32 hashed passwords in the session. Symfony will hash the password of the refreshed user and compare it to the session value. This avoids storing real hashes and allows you to invalidate sessions when a password changes.
For example, if your password is stored in a private password
property:
1 2 3 4 5 6 7 8 9 10 11
class User
{
// ...
public function __serialize(): array
{
$data = (array) $this;
$data["\0".self::class."\0password"] = hash('crc32c', $this->password);
return $data;
}
OAuth2 Token Introspection Endpoint
Symfony 7.3 introduces built-in support for the OAuth2 Token Introspection Endpoint as defined in RFC 7662. This allows Symfony apps to validate access tokens and fetch related user information by querying the authorization server, removing the need for your application to decode access tokens on its own.
This simplifies token handling because the OAuth2 specification doesn't mandate a specific way for resource servers to verify access tokens. These tokens can have any format and are not always standard JWTs.
This new feature is especially useful when you don't control the format of the access tokens, which is often the case when you don't own the Authorization Server.
Example configuration:
1 2 3 4 5 6 7 8 9
framework:
http_client:
scoped_clients:
oauth2.client:
base_uri: 'https://authorization-server.example.com/introspection'
scope: 'https://authorization-server\.example\.com'
headers:
# introspection endpoints usually require client authentication
Authorization: 'Basic Y2xpZW50OnBhc3N3b3Jk'
And the corresponding firewall configuration:
1 2 3 4 5 6 7 8 9
# config/security.yaml
security:
# ...
firewalls:
main:
pattern: ^/
access_token:
token_handler:
oauth2: ~
OIDC Discovery
Symfony added OpenID Connect (OIDC) support in version 6.3 as an authentication
mechanism. In Symfony 7.3, we're adding support for OIDC discovery, which
enables clients to fetch server metadata from a well-known URL like
.well-known/openid-configuration
.
This endpoint returns public information such as: endpoints URIs (e.g., userinfo, token, etc.), public keys for verifying token signatures and other metadata needed for OIDC client-server interactions. That means less manual configuration and easier integration with compliant identity providers.
To enable it, configure your firewall as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
oidc:
# no need to define 'keyset' manually anymore
claim: 'email'
audience: 'symfony'
issuers: ['https://example.com/']
algorithms: ['RS256']
discovery:
base_uri: 'https://example.com/oidc/realms/master/'
cache:
id: cache.app # must be created in framework.yaml
OIDC discovery reduces boilerplate and improves maintainability by allowing dynamic updates from the identity provider.
Exposing Security Errors
User enumeration is a common security issue where attackers infer valid usernames based on error messages. For example, a message like _"This user does not exist"_ shown by your login form reveals whether a username is valid.
Symfony provides the hide_user_not_found option to address this, returning a
generic BadCredentialsException
when the user is not found. However, this
option also hides all other user account status exceptions (e.g. blocked
or expired accounts).
That's why in Symfony 7.3, this option has been renamed to expose_security_errors and made more flexible by supporting multiple levels:
1 2 3 4 5 6 7 8 9 10 11 12 13
# config/packages/security.yaml
security:
# ...
# (default value) hide all user-related security exceptions
expose_security_errors: 'none'
# show account-related exceptions (e.g. blocked or expired accounts)
# for users who provided the correct password
expose_security_errors: 'account_status'
# don't hide any security-related exceptions
expose_security_errors: 'all'
IsGranted Callables
PHP 8.5 (to be released in November 2025) will allow using static callables inside PHP attributes. Symfony 7.3 anticipates this and updates the #[IsGranted] attribute to support callables when your PHP version allows it.
You can now write complex access checks in a more expressive and inline manner:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Controller/MyController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Security\Http\Attribute\IsGrantedContext;
class MyController extends AbstractController
{
#[IsGranted(static function (IsGrantedContext $context, mixed $subject) {
return $context->user === $subject['post']->getAuthor();
}, subject: static function (array $args) {
return [
'post' => $args['post'],
];
})]
public function index($post): Response
{
// ...
}
}
Special thanks to Robin, Florent, and Christophe for helping review the contents of this post.
That seems rather unintuitive.
that key-value combination indicates the reverse. It explicitly says expose "none" security errors.
that makes sense
that should do the reverse, it should expose all security errors. I hope this is just a mixup in the example between the first and the last setting?
@Clemens you are right! There was an error in the original blog post and we've just fixed it. Thanks!