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

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
Expand Down Expand Up @@ -290,6 +291,10 @@ public function authenticate(Request $request): Passport
new UserBadge($claims[$this->options['user_identifier_claim']], $this->userProvider->loadUserByIdentifier(...), $claims),
);
$passport->setAttribute('oidc_token_data', $tokenData);
// "auth_time" tells when the user actually authenticated at the provider, which a
// silent SSO login can place well in the past; it is only validated when "max_age"
// is requested, so anything non-numeric is discarded rather than trusted
$passport->setAttribute('oidc_auth_time', is_numeric($idTokenClaims['auth_time'] ?? null) ? (int) $idTokenClaims['auth_time'] : null);

return $passport;
}
Expand All @@ -309,6 +314,12 @@ public function createToken(Passport $passport, string $firewallName): TokenInte
$token->setAttribute('oidc_access_token_expires_at', is_numeric($tokenData['expires_in'] ?? null) ? $this->clock->now()->getTimestamp() + (int) $tokenData['expires_in'] : null);
}

// a provider whose clock runs ahead would otherwise extend the window that
// IS_AUTHENTICATED_RECENTLY grants, so the claim never dates from the future
if (null !== $authTime = $passport->getAttribute('oidc_auth_time')) {
$token->setAttribute(AuthenticatedVoter::AUTH_TIME_ATTRIBUTE, min($authTime, $this->clock->now()->getTimestamp()));
}

return $token;
}

Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Security/Http/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
8.2
---

* Stamp the `auth_time` token attribute from the OIDC ID token claim of the same name, so `max_age` and `IS_AUTHENTICATED_RECENTLY` agree
* Add `AuthenticationTimeListener`, which records the time of the last interactive authentication as the `auth_time` token attribute
* Add `allowed_time_drift` option to `OidcTokenHandler` to configure time tolerance for token validation (`iat`, `nbf`, `exp` claims)
* Expose the OAuth2 scopes an access token was granted as the `oauth2_scope` token attribute, read from the `scope` or `scp` claim
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
* a sensitive action can require the user to prove possession of their credentials
* again rather than relying on a session that was opened long ago.
*
* An authenticator that knows when the user actually authenticated, such as an OIDC
* client reading the "auth_time" claim, may stamp the token itself in createToken();
* this listener only fills the gap, it never overwrites such a value.
*
* @see AuthenticatedVoter::IS_AUTHENTICATED_RECENTLY
*/
final class AuthenticationTimeListener implements EventSubscriberInterface
Expand All @@ -35,7 +39,11 @@ public function __construct(

public function onInteractiveLogin(InteractiveLoginEvent $event): void
{
$event->getAuthenticationToken()->setAttribute(AuthenticatedVoter::AUTH_TIME_ATTRIBUTE, $this->clock?->now()->getTimestamp() ?? time());
$token = $event->getAuthenticationToken();

if (!$token->hasAttribute(AuthenticatedVoter::AUTH_TIME_ATTRIBUTE)) {
$token->setAttribute(AuthenticatedVoter::AUTH_TIME_ATTRIBUTE, $this->clock?->now()->getTimestamp() ?? time());
}
}

public static function getSubscribedEvents(): array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\Oidc\OidcDiscovery;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;

#[AllowMockObjectsWithoutExpectations]
class OidcLoginAuthenticatorTest extends TestCase
Expand Down Expand Up @@ -1149,6 +1150,70 @@ public function testCreateTokenStoresTheRefreshTokenAndTheAccessTokenExpiry()
$this->assertSame($clock->now()->getTimestamp() + 300, $token->getAttribute('oidc_access_token_expires_at'));
}

public function testCreateTokenStampsTheAuthenticationTimeFromTheAuthTimeClaim()
{
$nonce = bin2hex(random_bytes(16));
$state = bin2hex(random_bytes(16));
$clock = new MockClock('2026-09-06 12:00:00');
$authTime = $clock->now()->getTimestamp() - 3600;

$this->oidcClient->method('exchangeCode')->willReturn([
'access_token' => 'access-123',
'id_token' => $this->buildIdToken(['nonce' => $nonce, 'auth_time' => $authTime]),
]);
$this->oidcClient->method('fetchUserInfo')->willReturn(['sub' => 'user-42']);

$authenticator = $this->createAuthenticator(clock: $clock);
$passport = $authenticator->authenticate($this->createCallbackRequest($state, $nonce));

$token = $authenticator->createToken($passport, 'main');

// the silent SSO case: the provider says the user authenticated an hour ago,
// so IS_AUTHENTICATED_RECENTLY must not treat this login as fresh
$this->assertSame($authTime, $token->getAttribute(AuthenticatedVoter::AUTH_TIME_ATTRIBUTE));
}

public function testCreateTokenNeverDatesTheAuthenticationTimeInTheFuture()
{
$nonce = bin2hex(random_bytes(16));
$state = bin2hex(random_bytes(16));
$clock = new MockClock('2026-09-06 12:00:00');

$this->oidcClient->method('exchangeCode')->willReturn([
'access_token' => 'access-123',
'id_token' => $this->buildIdToken(['nonce' => $nonce, 'auth_time' => $clock->now()->getTimestamp() + 86400]),
]);
$this->oidcClient->method('fetchUserInfo')->willReturn(['sub' => 'user-42']);

$authenticator = $this->createAuthenticator(clock: $clock);
$passport = $authenticator->authenticate($this->createCallbackRequest($state, $nonce));

$token = $authenticator->createToken($passport, 'main');

$this->assertSame($clock->now()->getTimestamp(), $token->getAttribute(AuthenticatedVoter::AUTH_TIME_ATTRIBUTE));
}

public function testCreateTokenLeavesTheAuthenticationTimeUnsetWithoutTheClaim()
{
// the claim is only mandatory when "max_age" is requested; without it
// AuthenticationTimeListener falls back to stamping the login instant
$nonce = bin2hex(random_bytes(16));
$state = bin2hex(random_bytes(16));

$this->oidcClient->method('exchangeCode')->willReturn([
'access_token' => 'access-123',
'id_token' => $this->buildIdToken(['nonce' => $nonce]),
]);
$this->oidcClient->method('fetchUserInfo')->willReturn(['sub' => 'user-42']);

$authenticator = $this->createAuthenticator();
$passport = $authenticator->authenticate($this->createCallbackRequest($state, $nonce));

$token = $authenticator->createToken($passport, 'main');

$this->assertFalse($token->hasAttribute(AuthenticatedVoter::AUTH_TIME_ATTRIBUTE));
}

public function testCreateTokenReportsAMissingRefreshTokenAndExpiryAsNull()
{
// a provider only issues a refresh token when it was asked for one, e.g. with the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ public function testTheRecordedTimeSurvivesSerialization()
);
}

public function testItDoesNotOverwriteATimeTheAuthenticatorAlreadyRecorded()
{
// AuthenticatorManager calls createToken() before it dispatches INTERACTIVE_LOGIN,
// so an authenticator that knows the real authentication time, such as an OIDC
// client reading the "auth_time" claim, would otherwise be overwritten here
$token = $this->createToken();
$token->setAttribute(AuthenticatedVoter::AUTH_TIME_ATTRIBUTE, 1234567890);

(new AuthenticationTimeListener(new MockClock('2026-09-11 12:00:00')))->onInteractiveLogin(new InteractiveLoginEvent(new Request(), $token));

$this->assertSame(1234567890, $token->getAttribute(AuthenticatedVoter::AUTH_TIME_ATTRIBUTE));
}

public function testItSubscribesToInteractiveLogin()
{
$this->assertSame([SecurityEvents::INTERACTIVE_LOGIN => ['onInteractiveLogin', 256]], AuthenticationTimeListener::getSubscribedEvents());
Expand Down
Loading