In Symfony, IS_AUTHENTICATED_FULLY is the strongest check about how users logged in.
However, it tells you how the session started, not when. A user who logged
in with their password eight hours ago and left the laptop unlocked is still
fully authenticated, so anyone can make a payment or delete their account.
Services like GitHub solve this with a sudo mode which asks for your
credentials again before sensitive actions. Symfony 8.2 adds this feature natively.
Requiring Recent Authentication
Symfony 8.2 adds two security attributes that go beyond IS_AUTHENTICATED_FULLY:
IS_AUTHENTICATED_RECENTLY: the user entered their credentials within the last two hours;IS_AUTHENTICATED_VERY_RECENTLY: the user entered their credentials within the last five minutes.
For example, you could require a recent login to change an email address, and a very recent one to delete an account:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/Controller/AccountController.php
use Symfony\Component\Security\Http\Attribute\IsGranted;
// ...
#[IsGranted('IS_AUTHENTICATED_RECENTLY')]
public function changeEmail(): Response
{
// ...
}
#[IsGranted('IS_AUTHENTICATED_VERY_RECENTLY')]
public function deleteAccount(): Response
{
// ...
}
You can use these attributes wherever you use the other IS_AUTHENTICATED_*
attributes, including access_control and Twig templates. Security
expressions also get two new functions: is_recently_authenticated()
and is_very_recently_authenticated().
If users are logged in only because of a remember-me cookie, they never pass these checks. They must log in again first, because the cookie could be used by anyone who has access to their browser.
You can change both time limits in your security configuration. The values are in seconds:
1 2 3 4
# config/packages/security.yaml
security:
recent_authentication_lifetime: 3_600 # default: 7_200
very_recent_authentication_lifetime: 60 # default: 300
Asking Users to Authenticate Again
When these attributes are denied, users get a 403 error by default. A better
option is to ask users for their credentials again and then take them back to
the page they were visiting. To do that, create a service that implements the
new ReAuthenticationEntryPointInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// src/Security/ConfirmPasswordEntryPoint.php
namespace App\Security;
use Symfony\Component\Security\Http\EntryPoint\ReAuthenticationEntryPointInterface;
// ...
final class ConfirmPasswordEntryPoint implements ReAuthenticationEntryPointInterface
{
public function __construct(
private UrlGeneratorInterface $urlGenerator,
) {
}
public function startReAuthentication(Request $request, TokenInterface $token): Response
{
// this page displays a password form that submits to the form_login check_path
return new RedirectResponse($this->urlGenerator->generate('app_confirm_password'));
}
}
Then configure your firewall to use it with the new re_authentication_entry_point option:
1 2 3 4 5 6 7
# config/packages/security.yaml
security:
firewalls:
main:
form_login:
# ...
re_authentication_entry_point: App\Security\ConfirmPasswordEntryPoint
If your firewall's entry point already implements this interface, you can
skip that option. The new OpenID Connect login does this for you: it sends
users back to their provider with prompt=login to ask for their
credentials again.
Symfony also uses the ID token's auth_time claim to check when the user
actually authenticated. That way, silently signing in through an old session
at the provider doesn't count as a fresh login.
Customizing the Sudo Mode Check
Sometimes, checking how recently someone logged in isn't enough. A banking application, for example, might also require them to have used a hardware key during the session.
The trust resolver decides whether these checks pass. To add your own rules,
extend it and override isAuthenticatedRecently() or
isAuthenticatedVeryRecently(). This example requires a hardware key to
have been used during the session, plus an authentication within the last
five minutes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Security/BankTrustResolver.php
namespace App\Security;
use Symfony\Component\Security\Core\Authentication\AuthenticationMethod;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
final class BankTrustResolver extends AuthenticationTrustResolver
{
public function isAuthenticatedVeryRecently(?TokenInterface $token = null): bool
{
if (!$this->isFullFledged($token)) {
return false;
}
// e.g. ['pwd' => 1789140200, 'hwk' => 1789133290]
$proofs = $token->getAuthenticationProofs();
return isset($proofs[AuthenticationMethod::HARDWARE_KEY])
&& time() - max($proofs) <= 300;
}
}
The token now tracks which authentication methods were used during the session and when each was last used. The method names come from RFC 8176.
The OIDC authenticator gets these methods from the ID token's amr claim.
If you're writing your own authenticator, you can declare them with the new
AuthenticationMethodBadge.
See the recent authentication docs for how to register your own trust resolver.
Amazing !