Why you don’t need JWT
In this article, we will see why you may not in fact need JWT, despite it being a great technology. We will also find out how to get rid of it within a Symfony application.
Section intitulée what-is-jwtWhat is JWT?
JSON Web Token, aka JWT, is a JSON-based open standard (RFC 7519) for creating access tokens that assert a number of claims. Usually, the token is generated by a server and sent to the client. It is signed by one party’s private key, so that both parties are able to verify that the token is legitimate. And both parties are able to read its content.
A token looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
It’s composed of 3 parts:
- Headers that identifies which algorithm is used to generate the signature:
{ "alg" : "HS256", "typ" : "JWT" }
- Payload, almost whatever you want:
{ "loggedInAs" : "admin", "iat" : 1422779638 }
- Signature to validate the token:
HMAC-SHA256(base64urlEncoding(header) + '.' +base64urlEncoding(payload),secret)
Finally, the token is the concatenation of the following parts, each one are url encoded, then base 64 encoded:
const token = base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(signature)
Section intitulée when-do-we-need-jwtWhen do we need JWT?
Since the token is signed by the server, and since the key is private, no-one is able to modify it. So, if a user has a token with "username"="greg"
in it, you can trust this user. This is where JWT really shines. When you build a distributed system or a system with micro-services, you don’t need to authenticate the user in every part of the system. And since we can store more information in the payload, we can attach the user’s roles.
When a user issues a request with a JWT, we do not need to query the database to verify user credentials.
Section intitulée when-don-t-we-need-jwtWhen don’t we need JWT?
Almost every time! Why? First, because JWT adds lots of complexity:
- You need a private/public key. And this private key should be kept private (obviously). What? You committed this key in your repository?! Like all other secrets, it’s always hard to keep them really secret.
- JWT doesn’t come with a revocation system. Usually, we generate a token with a limited validity period. So to revoke an authorization… we have to wait. Or to build a more sophisticated mechanism.
- As we add a token validity, the user need to reconnect quite often. To mitigate this issue, we can use the concept of refresh token. But, this adds an extra request to get a fresh token from time to time.
- JWT is not native with Symfony. Depending to the solution used, up to 3 bundles/packages are needed (lexik/jwt-authentication-bundle, gesdinet/jwt-refresh-token-bundle, gfreeau/get-jwt-bundle).
Then, people often misuse JWT:
- They keep reloading users from the database. That’s what lexik does by default.
- They are using it to store session data. And workarounds does not work either too.
- They are using it to store too much user information to avoid an extra request
GET https://example.com/users/me
. - Many encryption methods have security issues.
Section intitulée what-can-i-useWhat can I use?
If we all agree we don’t need this complexity, and we don’t need to be authenticated against many different systems, we can dramatically simplify everything.
Symfony has from day 1 builtin support for HTTP Basic. We will need to add an apiToken
to our User
class:
/**
* @ORM\Column(type="string", length=255)
*/
private $apiToken;
public function __construct()
{
// By doing that, the apiToken is not encrypted in the database.
// You should consider using the PasswordEncoder to encode/verify the apiToken
$this->apiToken = bin2hex(random_bytes(20));
}
Then we will need an endpoint to authenticate our user with its username/password in order to get it’s apiToken:
class SecurityController extends AbstractController
{
/**
* @Route("/api/login", name="api_login", methods={"POST"})
*/
public function login()
{
$user = $this->getUser();
return new JsonResponse([
'apiToken' => $user->getApiToken(),
'roles' => $user->getRoles(),
'email' => $user->getEmail(),
// Add whatever you want
]);
}
}
And we protect this endpoint:
security:
encoders:
App\Entity\User:
algorithm: auto
providers:
users:
entity:
class: App\Entity\User
property: email
firewalls:
api_login:
pattern: ^/api/login$
anonymous: false
stateless: true
json_login:
check_path: api_login
The user will have to login as follows:
curl --request POST \
--url http://127.0.0.1:8002/api/login \
--header 'content-type: application/json' \
--data '{
"username": "lyrixx@lyrixx.info",
"password": "password"
}'
And they will get something like:
{
"apiToken": "61bbeaeae8b30999cfd1caf10c3ee7faaf798eb4",
"roles": [
"ROLE_USER"
],
"email": "lyrixx@lyrixx.info"
}
Finally, we will have to add a Guard and its configuration to secure the API:
class ApiAuthenticator extends AbstractGuardAuthenticator
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function supports(Request $request)
{
return true;
}
public function getCredentials(Request $request)
{
return [
'apiToken' => $request->headers->get('PHP_AUTH_USER'),
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $this->userRepository->findOneBy([
'apiToken' => $credentials['apiToken'],
]);
}
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response('', 401);
}
public function supportsRememberMe()
{
return false;
}
public function start(Request $request, AuthenticationException $authException = null)
{
}
}
security:
firewalls:
api:
pattern: ^/api
anonymous: false
stateless: true
guard:
authenticators:
- App\Security\ApiAuthenticator
Dead simple, isn’t it?
Oh! And if you want to revoke an access, it’s as simple as deleting an apiToken
from the database.
Section intitulée conclusionConclusion
JWT is really powerful but has some security issues. Paseto is a better alternative to address these issues.
But most of the time, you don’t need JWT, and your project will be simpler if you can resist to hype by refusing to use it.
Instead, use plain old Authorization (Basic) Header as seen in this article.
Section intitulée demoDemo
As usual, you can find a small demo application on our github.
Commentaires et discussions
Nos formations sur ce sujet
Notre expertise est aussi disponible sous forme de formations professionnelles !
Symfony
Formez-vous à Symfony, l’un des frameworks Web PHP les complet au monde
Symfony avancée
Découvrez les fonctionnalités et concepts avancés de Symfony
Ces clients ont profité de notre expertise
Nous avons entrepris une refonte complète du site, initialement développé sur Drupal, dans le but de le consolider et de jeter les bases d’un avenir solide en adoptant Symfony. La plateforme est hautement sophistiquée et propose une pléthore de fonctionnalités, telles que la gestion des abonnements avec Stripe et Paypal, une API pour l’application…
Refonte complète de la plateforme d’annonces immobilières de Cushman & Wakefield France. Connecté aux outils historiques, cette nouvelle vitrine permet une bien meilleure visibilité SEO et permet la mise en avant d’actifs qui ne pouvaient pas l’être auparavant.
JoliCode a été sollicité pour accompagner le développement de la nouvelle version du site. Conçue avec le framework Symfony2, cette nouvelle version bénéficie de la performance et la fiabilité du framework français. Reposant sur des technologies comme Elasticsearch, cette nouvelle version tend à offrir une expérience optimale à l’internaute. Le développement…