Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Security][SecurityBundle] Add IS_AUTHENTICATED_RECENTLY to require a fresh authentication - #66014

Open
nicolas-grekas wants to merge 1 commit into
8.2from
is-authenticated-recently
Open

[Security][SecurityBundle] Add IS_AUTHENTICATED_RECENTLY to require a fresh authentication#66014
nicolas-grekas wants to merge 1 commit into
8.2from
is-authenticated-recently

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Member
Q A
Branch? 8.2
Bug fix? no
New feature? yes
Deprecations? no
Issues Fix #33955
License MIT

Sudo mode, asked for in #33955 seven years ago. The demand was never in question (@fabpot in 2022: "there are no questions about 'do we want it?'. It's more like, 'who wants to work on it?'"), but the issue was parked behind the broader "identity trust level" model in #39308, and both that issue and #30914 have since been closed without it. So this takes the self-contained route.

What it does

IS_AUTHENTICATED_FULLY describes how the session began, never how recently the user proved possession of their credentials. This adds a second axis:

#[IsGranted('IS_AUTHENTICATED_RECENTLY')]
public function deleteAccount(): Response
security:
    recent_authentication_lifetime: 900 # seconds, the default

Three pieces:

  • AuthenticationTimeListener records the moment of an interactive authentication as the auth_time token attribute. auth_time is the name OIDC Core already uses for this, which keeps the door open for the ID token claim to feed the same stamp later.
  • AuthenticatedVoter grants IS_AUTHENTICATED_RECENTLY while that stamp is within the configured lifetime.
  • is_recently_authenticated() in the expression language and IsGrantedContext::isAuthenticatedRecently() for the closure form, matching the existing is_fully_authenticated() pair.

Design notes

  • No BC break and no new storage. AbstractToken::__serialize() already includes $attributes, and ContextListener::refreshUser() mutates the token in place rather than rebuilding it, so the stamp survives the session round trip on its own. A functional test asserts exactly that rather than trusting it.
  • Deliberately outside the granted hierarchy. IS_AUTHENTICATED_RECENTLY is strictly stronger than IS_AUTHENTICATED_FULLY, so being fully authenticated does not grant it. A token with no auth_time is denied, which is the safe default for non-interactive authenticators.
  • Remember-me can never satisfy it. RememberMeAuthenticator::isInteractive() returns true, so a remember-me login does get stamped; the isFullFledged() guard is what keeps it denied. That is covered by a test, since it is the one place the feature could silently go wrong.
  • This answers @stof's objection on the issue, that a shorter session lifetime might be enough. It is not: shortening the session changes when the session dies, not how recently the user authenticated within it.
  • Clock is an optional injected service. Both the voter and the listener take a nullable Psr\Clock\ClockInterface, wired as service('clock')->nullOnInvalid(), which is the idiom FrameworkBundle itself uses at Resources/config/services.php:110 and web.php:66. clock is defined unconditionally by the DI kernel and aliased for both ClockInterface flavours, so a real application always gets it; the time() fallback only covers standalone use of the components, so neither package needs a new dependency. A functional test asserts the clock actually arrives, since nullOnInvalid() would otherwise let a wrong service id fall back to time() unnoticed.

The rest of the stack

This is the base of a three-PR stack; each part is reviewable and mergeable on its own.

  1. this PR, the attribute and the stamp
  2. the OIDC auth_time claim feeding the same stamp, so max_age and sudo mode agree
  3. a re-authentication entry point, so a denial can send the user to a fresh login instead of a bare 403

Docs to follow once the shape is agreed.

Thanks @javiereguiluz for the original RFC, and @stof and @wouterj for the constraints in the thread that shaped this.

Comment thread src/Symfony/Component/Security/Http/EventListener/AuthenticationTimeListener.php Outdated
@nicolas-grekas

nicolas-grekas commented Sep 12, 2026

Copy link
Copy Markdown
Member Author

Edit: the below note is addressed in #66035

With the current approach, it's pretty obvious that the strategy will eventually end up asking users to re-authenticate every N minutes, which is not necessarily the best UX.

There are typically other risk factors that could contribute to a "recent-enough" decision; e.g. whether the IP address changed since the last authentication, a last-activity timer, a combination of those signals, a recent WebAuthn/MFA confirmation, etc.

The current time-based strategy still makes perfect sense as a simple default, but I think AuthenticationTrustResolverInterface could be a good extension point for richer trust decisions in the future.

… fresh authentication

Records the time of the last interactive authentication as the `auth_time` token
attribute, and grants IS_AUTHENTICATED_RECENTLY while it is within the configured
lifetime. This is what lets a sensitive action ask the user to prove possession of
their credentials again instead of trusting a session opened long ago.
@nicolas-grekas
nicolas-grekas force-pushed the is-authenticated-recently branch from c455e1a to 94bf766 Compare September 13, 2026 08:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Add "sudo mode"

3 participants