A quick guide to JWTs in PHP
The most common use of JWTs is as an authentication token, usually within an OAuth2 workflow. Creating these tokens is part and parcel of the authentication library that you use.
I recently had a requirement to use a JWT independent of authentication and these are some notes on what I learned when researching with Lcobucci\JWT.
Make up of a JWT
To really understand JWTs, read RFC7519. For a more readable introduction, read the one on jwt.io.
Also, JWT is pronounced “jot“.
The most important thing about a JWT is that it contains data and a signature. The signature allows you to verify that the JWT’s data hasn’t been tampered with. This works because the signature is signed with a secret. This can be a shared secret (a symmetrical algorithm) or a public/private key pair (asymmetric algorithm).
In my case, I’m only interested in signing a JWT using a public/private key as my client cannot securely hold a shared secret.
Creating a public/private key
OpenSSL is your friend here. Create a keys directory and then use the command line.
To create a private key:
openssl genpkey -algorithm RSA -out keys/private.key -pkeyopt rsa_keygen_bits:2048
To create the public key from the private key:
openssl rsa -pubout -in keys/private.key -out keys/public.key
You will now have two files in the keys directory: private.key and public.key. Keep private.key safe and make public.key available to your clients.
Creating a token
Using Lcobucci\JWT, we create a Configuration:
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
$configuration = Configuration::forAsymmetricSigner(
new Sha256(),
InMemory::file(__DIR__ . '/keys/private.key'),
InMemory::file(__DIR__ . '/keys/public.key')
);
From our configuration, we can obtain a builder and specify our token:
$keyId = '40597EB1-5E20-49B5-BDDF-B24D1B3B05B5';
$subject = '851828E8-C376-4026-9FD8-D2CCE406CD1C';
$now = new \DateTimeImmutable();
$builder = $configuration->builder()
->identifiedBy($keyId)
->relatedTo($subject)
->issuedBy('https://app.example.com')
->issuedAt($now)
->expiresAt($now->modify('+2 weeks'))
->withClaim('foo', 'bar');
The data elements in a JWT are called claims. There are a number of registered claims that are not mandatory, but you should set if they are relevant as all clients will recognise them and their purpose. In particular JWT ID, issuer, subject, issued at and expiration time are particularly useful. There are also a set of public claims that provide a standardised set of key names for common information which help avoid clashes.
Then there is the data specific to your application which are known as private claims. These can be whatever you like and in the example above, we have created a claim called “foo”.
Finally we create the token itself:
$token = $builder->getToken($configuration->signer(), $configuration->signingKey());
$tokenString = $token->toString();
We can then send the token string in response to an API request, etc.
Validating a token
When we receive a token, we need to validate it. This means that we check that it hasn’t been tampered with and we can also check that the data within it is as expected.
Firstly, we parse the token string back into a token object:
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token\Parser;
$parser = new Parser(new JoseEncoder());
$token = $parser->parse($tokenString);
To validate it, we need a validator and a set of constraints that the validator will test the token for:
$validator = new Validator();
$clock = new SystemClock();
$constraints = [
new SignedWith(new Sha256(), InMemory::file(__DIR__ . '/keys/public.key')),
new LooseValidAt($clock),
new IssuedBy('https://app.example.com'),
];
The three constraints here check that the JWT:
- has been signed with the private key (by using the public key to verify)
- is current and has not expired
- was issued by the expected issuer
There are other constraints too – check the docs.
Note that the time based constraints use the PSR-20:Clock interface, so we use Lcobbucci/clock as an implementation of it.
We can then assert these constraints:
try { $validator->assert($token, ...$constraints); // Token is verified $claims = $token->claims()->all(); } catch (RequiredConstraintsViolated $e) { // Invalid token. echo "**Invalid Token**\n"; foreach ($e->violations() as $violation) { echo "- " . $violation->getMessage() . "\n"; } exit(1); }
The assert() method will throw an exception with all the violations, so we can see everything that failed in one go which is useful.
That’s all there is to issuing and validating arbitrary JWT tokens in PHP with Lcobucci\JWT.