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.

What 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)

When 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.

When 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:

What 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.

Conclusion

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.

Demo

As usual, you can find a small demo application on our github.

Nos formations sur le sujet

  • Symfony

    Formez-vous à Symfony, l’un des frameworks web PHP les plus connus au monde

  • Symfony avancée

    Décou­vrez les fonc­tion­na­li­tés et concepts avan­cés de Symfo­ny

blog comments powered by Disqus