-
-
Notifications
You must be signed in to change notification settings - Fork 610
Invalidate token on reset password #1005
Replies: 1 comment · 5 replies
-
Hi, I think you should open this at discussion and not issue (since this more like an Q&A). Back to the topic, you may save the token to database after user successfully login and then every time user access your api the authenticator should look up from the table for the token. You can do anything you want from there since you have fully control of the user token (such as removing the token from database to act as invalidate, etc). |
Beta Was this translation helpful? Give feedback.
All reactions
-
Also I suggest to rely on the |
Beta Was this translation helpful? Give feedback.
All reactions
-
So I guess everytime user succesfully login and obtain the token, the |
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
What is the correct way to store token in db and validate token from db? |
Beta Was this translation helpful? Give feedback.
All reactions
-
You'll have to add in more code for whatever use cases you'll have on top of this, but, I'll share what I just implemented in a client app to handle blocking JWTs from being used in certain conditions.
<?php declare(strict_types=1);
namespace App\EventListener;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
final class AddClaimsToJWTListener
{
#[AsEventListener(event: Events::JWT_CREATED)]
public function __invoke(JWTCreatedEvent $event): void
{
$data = $event->getData();
if (!isset($data['jti'])) {
$data['jti'] = bin2hex(random_bytes(16));
$event->setData($data);
}
}
}
namespace App\EventListener;
use App\JWT\BlockedTokenManager;
use App\JWT\Exception\MissingClaimException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
use Symfony\Component\Security\Http\Event\LogoutEvent;
final class BlockJWTListener
{
public function __construct(
private readonly BlockedTokenManager $tokenManager,
private readonly TokenExtractorInterface $tokenExtractor,
private readonly JWTTokenManagerInterface $jwtManager,
) {
}
#[AsEventListener(dispatcher: 'security.event_dispatcher.internal_api')]
public function onLoginFailure(LoginFailureEvent $event): void
{
if ($event->getException() instanceof DisabledException) {
$this->blockTokenFromRequest($event->getRequest());
}
}
#[AsEventListener(dispatcher: 'security.event_dispatcher.internal_api')]
public function onLogout(LogoutEvent $event): void
{
$this->blockTokenFromRequest($event->getRequest());
}
private function blockTokenFromRequest(Request $request): void
{
$token = $this->tokenExtractor->extract($request);
if ($token === false) {
// There's nothing to block if the token isn't in the request
return;
}
try {
$payload = $this->jwtManager->parse($token);
} catch (JWTDecodeFailureException) {
// Ignore decode failures, this would mean the token is invalid anyway
return;
}
try {
$this->tokenManager->add($payload);
} catch (MissingClaimException) {
// We can't block a token missing the claims our system requires, so silently ignore this one
}
}
}
namespace App\JWT;
use App\JWT\Exception\MissingClaimException;
use Psr\Cache\CacheItemPoolInterface;
class BlockedTokenManager
{
public function __construct(private readonly CacheItemPoolInterface $cacheJwt)
{
}
/**
* @throws MissingClaimException if required claims do not exist in the payload
*/
public function add(array $payload): bool
{
if (!isset($payload['exp'])) {
throw new MissingClaimException('exp');
}
$expiration = new \DateTime('@' . $payload['exp'], new \DateTimeZone('UTC'));
$now = new \DateTime(timezone: new \DateTimeZone('UTC'));
// If the token is already expired, there's no point in adding it to storage
if ($expiration <= $now) {
return false;
}
$cacheExpiration = (clone $expiration)->add(new \DateInterval('PT5M'));
if (!isset($payload['jti'])) {
throw new MissingClaimException('jti');
}
$cacheItem = $this->cacheJwt->getItem($payload['jti']);
$cacheItem->set([]);
$cacheItem->expiresAt($cacheExpiration);
$this->cacheJwt->save($cacheItem);
return true;
}
/**
* @throws MissingClaimException if required claims do not exist in the payload
*/
public function has(array $payload): bool
{
if (!isset($payload['jti'])) {
throw new MissingClaimException('jti');
}
return $this->cacheJwt->hasItem($payload['jti']);
}
/**
* @throws MissingClaimException if required claims do not exist in the payload
*/
public function remove(array $payload): void
{
if (!isset($payload['jti'])) {
throw new MissingClaimException('jti');
}
$this->cacheJwt->deleteItem($payload['jti']);
}
}
namespace App\JWT\Exception;
final class MissingClaimException extends \RuntimeException
{
public function __construct(
public readonly string $claim,
int $code = 0,
\Throwable $previous = null,
) {
parent::__construct(sprintf('Missing required "%s" claim on JWT payload.', $claim), $code, $previous);
}
}
namespace App\EventListener;
use App\JWT\BlockedTokenManager;
use App\JWT\Exception\MissingClaimException;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
final class RejectBlockedTokenListener
{
public function __construct(private readonly BlockedTokenManager $tokenManager)
{
}
/**
* @throws InvalidTokenException if the JWT is blocked
*/
#[AsEventListener(event: Events::JWT_AUTHENTICATED)]
public function __invoke(JWTAuthenticatedEvent $event): void
{
try {
if ($this->tokenManager->has($event->getPayload())) {
throw new InvalidTokenException('JWT blocked');
}
} catch (MissingClaimException) {
// Do nothing if the required claims do not exist on the payload (older JWTs won't have the "jti" claim the manager requires)
}
}
} We haven't done anything in our app to deal with invalidating tokens on key events such as on a password change, nor are we tracking issued/active JWTs anywhere in storage. But with this infrastructure, you'd really just need to add a database table to track a user and all of their active JWTs (an entity with a relation to your |
Beta Was this translation helpful? Give feedback.
All reactions
-
🚀 5
-
Thanks a lot for sharing @mbabker! |
Beta Was this translation helpful? Give feedback.
All reactions
This discussion was converted from issue #1004 on April 18, 2022 19:06.
-
Symfony - 5.2.5
PHP - 7.4
URL - /reset/password
This route has public access and no token need to be passed.
Now, When user password has been reset, I want to invalidate/expire the old token of this user server-side (passing email in body).
What is the correct way to do this?
Beta Was this translation helpful? Give feedback.
All reactions