Skip to content

Instantly share code, notes, and snippets.

@ctf0
Last active May 4, 2024 17:37
Show Gist options
  • Save ctf0/0b3bc4d5289982a071976ac834cb5498 to your computer and use it in GitHub Desktop.
Save ctf0/0b3bc4d5289982a071976ac834cb5498 to your computer and use it in GitHub Desktop.

this is how we can use jwt token & jwt token refresh to auth api calls.

  • first we have to use user username which is the default for both packages or it wont work, i couldnt find away to get it to work with email, specialy lexik/jwt-authentication-bundle

  • second we intercept requests to check if the jwt token has expired & if so we recreate new tokens and update request & response headers.

<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Event\LogoutEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler;
#[Route('/api', name: 'api_')]
#[AsController]
final class AuthController extends AbstractController
{
public function __construct(
private EntityManagerInterface $entityManager,
private AuthenticationSuccessHandler $authenticationSuccessHandler,
) {
}
public function login(Request $request): Response
{
try {
if ($email = $request->getPayload()->get('email')) {
$user = $this->entityManager->getRepository(User::class)->findOneBy([
'email' => $email,
]);
}
return $this->authenticationSuccessHandler->handleAuthenticationSuccess($user);
} catch (\Throwable $throwable) {
return $throwable->getMessage();
}
}
public function logout(Request $request): void
{
$dispatcher = new EventDispatcher();
$dispatcher->dispatch(new LogoutEvent($request));
}
}
"require": {
"gesdinet/jwt-refresh-token-bundle": "*",
"lexik/jwt-authentication-bundle": "^3",
}
gesdinet_jwt_refresh_token:
refresh_token_class: App\Entity\RefreshToken
ttl: 604800 # in seconds default is 1 week
ttl_update: true
single_use: false
<?php
declare(strict_types = 1);
namespace App\EventListener;
use App\Entity\User;
use App\Entity\RefreshToken;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\JWTAuthenticator;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler;
class InvalidJWTEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => [
['onRequest', 32],
],
KernelEvents::RESPONSE => [
['onResponse', 32],
],
];
}
public function __construct(
private EntityManagerInterface $entityManager,
private JWTAuthenticator $jWTAuthenticator,
private AuthenticationSuccessHandler $authenticationSuccessHandler,
private string $refreshTokenHeaderName = 'X-Refresh-Token',
private string $authTokenHeaderName = 'X-Authorization-Token'
) {
}
public function __invoke(RequestEvent $event): void
{
$request = $event->getRequest();
$refreshTokenHeaderName = $this->refreshTokenHeaderName;
try {
if ($request->headers->has($refreshTokenHeaderName)) {
$this->jWTAuthenticator->authenticate($request);
}
} catch (ExpiredTokenException) {
$oldRefreshToken = $request->headers->get($refreshTokenHeaderName);
$em = $this->entityManager;
$refreshToken = $em->getRepository(RefreshToken::class)->findOneBy([
'refreshToken' => $oldRefreshToken,
]);
if ($refreshTokenEntity && $refreshTokenEntity->isValid()) {
$user = $em->getRepository(User::class)->findOneBy([
'userName' => $refreshTokenEntity->getUsername(),
]);
if ($user) {
$newTokens = json_decode(
$this->authenticationSuccessHandler->handleAuthenticationSuccess($user)->getContent(),
true
);
$token = $newTokens['token'];
// remove newly created token
$refreshTokenEntity = $em->getRepository(RefreshToken::class)->findOneBy([
'refreshToken' => $newTokens['refresh_token'],
]);
$em->remove($refreshTokenEntity);
$em->flush();
// set headers
$request->headers->set('Authorization', 'Bearer ' . $token);
$request->headers->set($this->authTokenHeaderName, $token);
$request->headers->set($this->refreshTokenHeaderName, $oldRefreshToken);
}
}
}
}
public function onResponse(ResponseEvent $event): void
{
$request = $event->getRequest();
$response = $event->getResponse();
$authTokenHeaderName = $this->authTokenHeaderName;
$refreshTokenHeaderName = $this->refreshTokenHeaderName;
if ($request->headers->has($authTokenHeaderName)) {
$response->headers->set($authTokenHeaderName, $request->headers->get($authTokenHeaderName));
$response->headers->set($refreshTokenHeaderName, $request->headers->get($refreshTokenHeaderName));
}
}
}
<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken as BaseRefreshToken;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity]
#[ORM\Table(name: 'refresh_tokens')]
class RefreshToken extends BaseRefreshToken
{
public static function createForUserWithTtl(string $refreshToken, UserInterface $user, int $ttl): RefreshTokenInterface
{
$model = parent::createForUserWithTtl($refreshToken, $user, $ttl);
$model->setUsername($user->getUsername());
return $model;
}
}
// ...
api_login:
path: /api/login
methods: POST
controller: App\Controller\AuthController::login
_logout_api:
path: /api/token/invalidate
controller: App\Controller\AuthController::logout
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
# password_hashers:
# Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
# providers:
# users_in_memory: { memory: null }
# # used to reload user from session & other features (e.g. switch_user)
providers:
users:
entity:
class: App\Entity\User
property: userName
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/api/login
stateless: true
api:
pattern: ^/api
stateless: true
entry_point: jwt
jwt: ~
json_login:
check_path: api_login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
refresh_jwt:
check_path: gesdinet_jwt_refresh_token
provider: users
logout:
path: _logout_api
api_token_refresh:
pattern: ^/api/token/refresh
stateless: true
refresh_jwt: ~
main:
lazy: true
provider: users
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/api/(login|doc), roles: PUBLIC_ACCESS }
- { path: ^/api/token/(refresh|invalidate), roles: PUBLIC_ACCESS }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
<?php
declare(strict_types = 1);
use App\EventListener\InvalidJWTEventSubscriber;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\JWTAuthenticator;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
// ...
$services->alias(JWTAuthenticator::class, 'security.authenticator.jwt.api');
$services->set(InvalidJWTEventSubscriber::class)->tag('kernel.event_listener');
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment