Using JWT for a single page application in combination with Symfony in the backend is great when using the bundles lexik/jwt-authentication-bundle and gesdinet/jwt-refresh-token-bundle.

  • lexik/jwt-authentication-bundle provides general JWT based authentication.
  • gesdinet/jwt-refresh-token-bundle provies a way to add a refresh token to refresh the JWT when it's expired.

The only issue I run into is when the current user is changing his/her email address in his/her profile settings. I use the email address also as username in my applications and the JWT is bound on the username of the user. Therefore when changing the email address, the JWT is automatically invalidated. So the next API communication will fail and depending on the frontend logic the user might be logged out.

What we need to do here is to

  1. Create new JWT when the email address is changed
  2. Return this JWT to the frontend
  3. Replace the locally stored JWT and refresh token with the new one

The frontend part is highly dependent on the framework and implementation. The second part is trivial. So I will only show part 1. To make it usable in multiple use cases, we will create service for it which then can be injected wherever you need it.

The two kind of tokens will be created through their respective token manager classes JWTTokenManagerInterface and RefreshTokenManager. The JWT is dependend on the user entity. The lexik/jwt-authentication-bundle has an environment variable for the time to live (JWT_TOKEN_TTL) and we want to use it here. The last important part is that the refresh token has to be unique and we need to use the validator to check that it is.

// Create new JWT
$token = $this->tokenManager->create($user);

// Set the validation date depending on the configuration
$datetime = new \DateTime();
$datetime->modify('+'.$this->ttl.' seconds');

// Create new refresh token
$refreshToken = $this->refreshTokenManager->create();
$refreshToken->setUsername($user->getUsername());
$refreshToken->setRefreshToken();
$refreshToken->setValid($datetime);

// The refresh token has to be unique
$valid = false;
while (false === $valid) {
    $valid = true;
    $errors = $this->validator->validate($refreshToken);
    if ($errors->count() > 0) {
        foreach ($errors as $error) {
            if ('refreshToken' === $error->getPropertyPath()) {
                $valid = false;
                $refreshToken->setRefreshToken();
            }
        }
    }
}

$this->refreshTokenManager->save($refreshToken);

For a full implementation I assume you have a user entity compatible with the fos user bundle in App\Entity\User. The JWT class is simply a value object to hold the two values, pipe it through the business logic and to serialize it to the frontend (optional). This is the full service:

<?php

declare(strict_types=1);

namespace App\Service\JWT;

use App\Entity\User;
use App\Service\JWT\ValueObject\JWT;
use Gesdinet\JWTRefreshTokenBundle\Model\RefreshTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class JWTService
{
    /** @var JWTTokenManagerInterface */
    private $tokenManager;

    /** @var RefreshTokenManagerInterface */
    private $refreshTokenManager;

    /** @var ValidatorInterface */
    private $validator;

    /** @var int */
    private $ttl;

    public function __construct(
        JWTTokenManagerInterface $tokenManager,
        RefreshTokenManagerInterface $refreshTokenManager,
        ValidatorInterface $validator,
        int $ttl
    ) {
        $this->tokenManager = $tokenManager;
        $this->refreshTokenManager = $refreshTokenManager;
        $this->validator = $validator;
        $this->ttl = $ttl;
    }

    public function createNewJWT(User $user): JWT
    {
        $token = $this->tokenManager->create($user);

        $datetime = new \DateTime();
        $datetime->modify('+'.$this->ttl.' seconds');

        $refreshToken = $this->refreshTokenManager->create();

        $refreshToken->setUsername($user->getUsername());
        $refreshToken->setRefreshToken();
        $refreshToken->setValid($datetime);

		// Validate, that the new token is a unique refresh token
        $valid = false;
        while (false === $valid) {
            $valid = true;
            $errors = $this->validator->validate($refreshToken);
            if ($errors->count() > 0) {
                foreach ($errors as $error) {
                    if ('refreshToken' === $error->getPropertyPath()) {
                        $valid = false;
                        $refreshToken->setRefreshToken();
                    }
                }
            }
        }

        $this->refreshTokenManager->save($refreshToken);

        return new JWT($token, $refreshToken->getRefreshToken());
    }
}

Unfortunately the refresh token manager can't be autowired, which is why we have to add a service definition for it. But we would need it any, as we want to inject the time to live from the environment configuration.

App\Service\JWT\JWTService:
    arguments:
        $refreshTokenManager: '@gesdinet.jwtrefreshtoken.refresh_token_manager'
        $ttl: '%env(int:JWT_TOKEN_TTL)%'

With this you can replace the JWT and refresh token in the frontend and won't loose your connection to the API.