Remember-me like behaviour with JWT

Dev Diary

Do you want to have a remember-me like behaviour while using JWTs for authentication? Then read on.
 


 

Author
Michael Zangerle
Date
August 12, 2021
Reading time
7 Minutes

In a new project we are using Sylius headless and also their new api which uses JWT tokens to authenticate. Our customer wanted a remember-me like behaviour which people are used to. So the question is, how do we get such a behaviour with JWT tokens in Sylius?

Refresh tokens

As the tokens are self-contained and cannot be revoked it’s recommended that they are short-lived. To prevent a user from having to reauthenticate again and again there exists this markitosgv/JWTRefreshTokenBundle bundle which provides a refresh token in addition to the JWT token. This refresh token can be used to request a new valid JWT token. The refresh token itself can only be used for that process and can also be revoked if needed. For this to work the user has to be regularly online and the client has to implement a logic to get a new JWT token regularly. If that’s the case for your project then you should go with this approach.

What we were trying to achieve was the possibility to have two slightly different TTLs (Time-to-Live) for the JWT tokens (e.g. 1 day and 30 days). Some users might be online every day, some only every few weeks and some even less. The tokens shouldn’t be valid forever, but we wanted to provide a certain level of comfort by not having to re-authenticate every time the majority of users visits the shop. Revoking a token or disabling a user would also result in the same amount of work and we didn’t want to introduce all this complexity with refresh tokens just for that. So what to do?

Cookies

Sylius uses the lexik/LexikJWTAuthenticationBundle bundle for the whole JWT logic and this is pretty well documented. So we decided to switch to JWT via cookies as a first step. This removes the need to keep the token on the client side with easy access through JS and instead store it in a cookie. That’s still on the client obviously but now it’s possible to make it more secure by setting httpOnly and secure to true and samesite to lax. This done by a few lines of configuration:

lexik_jwt_authentication: // ... token_extractors: cookie: enabled: true name: BEARER set_cookies: BEARER: samesite: lax secure: true httpOnly: true // ...

Remember-me

To get a remember-me like behaviour we would need to have two different TTLs for those tokens (and cookies) depending on the decision of the user. Did the user check the remember-me checkbox then the longer TTL should be used, otherwise the default.

So let’s create some class that provides the right TTL based on the request first and let’s call it TtlProvider. Nothing fancy there. The getTtl function returns the right TTL depending on the request body.

<?php declare(strict_types=1); namespace App\Security; use Symfony\Component\HttpFoundation\RequestStack; final class TtlProvider implements TtlProviderInterface { public const REMEMBER_ME = 'rememberMe'; public const DEFAULT_TTL = 86400; // one day in seconds public const REMEMBER_ME_TTL = 2592000; // one month in seconds private RequestStack $requestStack; public function __construct(RequestStack $requestStack) { $this->requestStack = $requestStack; } public function getTtl(): int { if ($this->isLoginRequestWithRememberMeEnabled()) { return self::REMEMBER_ME_TTL; } return self::DEFAULT_TTL; } private function isLoginRequestWithRememberMeEnabled(): bool { $request = $this->requestStack->getMasterRequest(); if (!$request || !$request->getContent()) { return false; } $content = json_decode((string) $request->getContent(), true, 512, JSON_THROW_ON_ERROR); return is_array($content) && array_key_exists(self::REMEMBER_ME, $content) && true === $content[self::REMEMBER_ME]; } }

Now let’s make use of the new TtlProvider and integrate it with the LexikBundle. The easiest way to integrate it seems to be to add an event subscriber and override it there (thanks @chalasr). This could look similar to this:

<?php declare(strict_types=1); namespace App\Security; use Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Cookie\JWTCookieProvider as BaseJWTCookieProvider; use Symfony\Component\HttpFoundation\Cookie; final class JWTCookieProvider { private BaseJWTCookieProvider $cookieProvider; private TtlProviderInterface $ttlProvider; public function __construct(BaseJWTCookieProvider $cookieProvider, TtlProviderInterface $ttlProvider) { $this->cookieProvider = $cookieProvider; $this->ttlProvider = $ttlProvider; } /** * The expiresAt parameter will be ignored here and is just kept to be compatible with the BaseJWTCookieProvider. * The real ttl will be determined based on the request by the ttl provider. */ public function createCookie( string $jwt, ?string $name = null, ?int $expiresAt = null, ?string $sameSite = null, ?string $path = null, ?string $domain = null, ?bool $secure = null, ?bool $httpOnly = null, array $split = [] ): Cookie { return $this->cookieProvider->createCookie( $jwt, $name, (time() + $this->ttlProvider->getTtl()), $sameSite, $path, $domain, $secure, $httpOnly, $split ); } }

In a last step we have to configure everything in our services.yaml:

services: // ... App\Security\EventSubscriber\JWTOnCreateEventSubscriber: arguments: - '@App\Security\TtlProviderInterface' tags: - { name: kernel.event_subscriber } App\Security\JWTCookieProvider: decorates: lexik_jwt_authentication.cookie_provider.BEARER arguments: - '@.inner' - '@App\Security\TtlProvider'

That’s it. Now if you send a login request with rememberMe:true in the payload the longer TTL will be used and in all other cases the shorter one.

More of that?

Pixel-perfect frontend
Dev Diary
The pixel perfect front-end
September 20, 2021 | 4 Min.
From request to typed objects
Dev Diary
From requests to typed objects
August 5, 2021 | 4 Min.

Contact form

*Required field
*Required field
*Required field
*Required field
We protect your privacy

We keep your personal data safe and do not share it with third parties. You can find out more about this in our privacy policy.