There was a time when the only way to authenticate yourself with an application was by providing your credentials (usually a username or email address and a password) and a session was then used to maintain user state until the user logged out. A little while later, we started using authentication APIs. And in yet more recent times, JWTs, or JSON Web Tokens, have been increasingly used as another way to authenticate requests to a server.
In this article, you’ll learn what JWTs are and how to use them with PHP to make authenticated user requests.
JWTs versus Sessions
But first, why are sessions not such a good thing? Well, there are three key reasons:
- Data is stored in plain text on the server.
Even though the data is usually not stored in a public folder, anyone with sufficient access to the server can read the contents of session files. - They involve filesystem read/write requests.
Every time a session starts or its data is modified, the server needs to update the session file. The same goes for every time the application sends a session cookie. If you have a large number of users, you can end up with a slow server unless you use alternative session storage options, such as Memcached and Redis. - Distributed/Clustered applications.
Since session files are, by default, stored on the file system, it’s hard to have a distributed or clustered infrastructure for high availability applications — ones that require the use of technologies such as load balancers and clustered servers. Other storage media and special configurations have to be implemented — and be done so in full awareness of their implications.
JWT
Now, let’s start learning about JWTs. The JSON Web Token specification (RFC 7519) was first published on December 28, 2010, and was most recently updated in May 2015.
JWTs have many advantages over API keys, including:
- API keys are random strings, whereas JWTs contain information and metadata. This information and metadata can describe a wide range of things, such as a user’s identity, authorization data, and the validity of the token within a time frame or in relation to a domain.
- JWTs don’t require a centralized issuing or revoking authority.
- JWTs are OAUTH2 compatible.
- JWT data can be inspected.
- JWTs have expiration controls.
- JWTs are intended for space-constrained environments, such as HTTP Authorization headers.
- Data is transmitted in JavaScript Object Notation format (JSON).
- JWTs are represented using Base64url encoding
What Does a JWT Look Like?
Here is a sample JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MTY5MjkxMDksImp0aSI6ImFhN2Y4ZDBhOTVjIiwic2NvcGVzIjpbInJlcG8iLCJwdWJsaWNfcmVwbyJdfQ.XCEwpBGvOLma4TCoh36FU7XhUbcskygS81HE1uHLf0E
At first glance, it appears that the string is just random groups of characters concatenated with a period or dot character. As such, it may not seem very different from an API key. However, if you look more closely, there are three separate strings.
The JWT Header
The first string is the JWT header. It’s a Base64, URL-encoded JSON string. It specifies which cryptographic algorithm was used to generate the signature, and the token’s type, which is always set to JWT
. The algorithm can be either symmetric or asymmetric.
A symmetric algorithm uses a single key to both create and verify the token. The key is shared between the creator of the JWT and the consumer of it. It’s essential that you make sure only the creator and consumer knows the secret. Otherwise, anyone can create a valid token.
An asymmetric algorithm uses a private key to sign the token and a public key to verify it. These algorithms should be used when a shared secret is impractical or other parties only need to verify the integrity of the token.
The JWT’s Payload
The second string is the JWT’s payload. It’s also a Base64, URL-encoded JSON string. It contains some standard fields, which are referred to as “claims”. There are three types of claims: registered, public, and private.
Registered claims are predefined. You can find a list of them in the JWT’s RFC. Here are some commonly used ones:
iat
: the timestamp of token issuing.key
: a unique string, which could be used to validate a token, but goes against not having a centralized issuer authority.iss
: a string containing the name or identifier of the issuer. Can be a domain name and can be used to discard tokens from other applications.nbf
: a timestamp of when the token should start being considered valid. Should be equal to or greater thaniat
.exp
: a timestamp of when the token should cease to be valid. Should be greater thaniat
andnbf
.
Public claims can be defined as you see fit. However, they can’t be the same as registered claims, or claims of already existing public claims. You can create private claims at will. They’re only for use between two parties: a producer and a consumer.
The JWT’s Signature
The JWT’s signature is a cryptographic mechanism designed to secure the JWT’s data with a digital signature unique to the contents of the token. The signature ensures the JWT’s integrity so that consumers can verify it hasn’t been tampered with by a malicious actor.
The JWT’s signature is a combination of three things:
- the JWT’s header
- the JWT’s payload
- a secret value
These three are digitally signed (not encrypted) using the algorithm specified in the JWT’s header. If we decode the example above, we’ll have the following JSON strings:
The JWT’s Header
{
"alg": "HS256",
"typ": "JWT"
}
The JWT’s Data
{
"iat": 1416929109,
"jti": "aa7f8d0a95c",
"scopes": [
"repo",
"public_repo"
]
}
Try out jwt.io for yourself, where you can play around with encoding and decoding your own JWTs.
Let’s Use JWTs in a PHP-based Application
Now that you’ve learned what JWTs are, it’s now time to learn how to use them in a PHP app. Before we dive in, feel free to clone the code for this article, or follow along and create it as we go.
There are many ways that you can approach integrating JWTs, but here’s how we’re going to do it.
All requests to the application, except for the login and logout page, need to be authenticated via a JWT. If a user makes a request without a JWT, they’ll be redirected to the login page.
After a user fills out and submits the login form, the form will be submitted via JavaScript to the login endpoint, authenticate.php
, in our application. The endpoint will then extract the credentials (a username and password) from the request and check if they’re valid.
If they are, it will generate a JWT and send it back to the client. When the client receives a JWT, it will store it and use it with every future request to the application.
For a simplistic scenario, there’ll only be one resource the user can request — a PHP file aptly named resource.php
. It won’t do much, just returning a string, containing the current timestamp at the time of the request.
There’s couple of ways to use JWTs when making requests. In our application, the JWT will be sent in the Bearer authorization header.
If you’re not familiar with Bearer Authorization, it’s a form of HTTP authentication, where a token (such as a JWT) is sent in a request header. The server can inspect the token and determine if access should be given to the “bearer” of the token.
Here’s an example of the header:
Authorization: Bearer ab0dde18155a43ee83edba4a4542b973
For each request received by our application, PHP will attempt to extract the token from the Bearer header. If it’s present, it’s then validated. If it’s valid, the user will see the normal response for that request. If the JWT is invalid, however, the user won’t be allowed to access the resource.
Please note that JWT was not designed to substitute session cookies.
Prerequisites
To begin with, we need to have PHP and Composer installed on our systems.
In the project’s root, run composer install
. This will pull in Firebase PHP-JWT, a third-party library that simplifies working with JWTs, as well as laminas-config, designed to simplify access to configuration data within applications
The Login Form
With the library installed, let’s step through the login code in authenticate.php
. We first do the usual setup, ensuring that the Composer-generated autoloader is available.
<?php
declare(strict_types=1);
use Firebase\JWT\JWT;
require_once('../vendor/autoload.php');
After receiving the form submission, the credentials are validated against a database, or some other data store. For the purposes of this example, we’ll assume that they’re valid, and set $hasValidCredentials
to true.
<?php
// extract credentials from the request
if ($hasValidCredentials) {
Next, we initialize a set of variables to be used for generating the JWT. Please bear in mind that since a JWT can be inspected client-side, do not include any sensitive information in it.
Another thing worth pointing out, again, is that $secretKey
wouldn’t be initialized like this. You’d likely set it in the environment and extract it, using library such as phpdotenv, or in a config file. I’ve avoided doing that in this example, as I want to focus on the JWT code.
Never disclose it or store it under version control!
$secretKey = 'bGS6lzFqvvSQ8ALbOxatm7/Vk7mLQyzqaS34Q4oR1ew=';
$issuedAt = new DateTimeImmutable();
$expire = $issuedAt->modify('+6 minutes')->getTimestamp(); // Add 60 seconds
$serverName = "your.domain.name";
$username = "username"; // Retrieved from filtered POST data
$data = [
'iat' => $issuedAt->getTimestamp(), // Issued at: time when the token was generated
'iss' => $serverName, // Issuer
'nbf' => $issuedAt->getTimestamp(), // Not before
'exp' => $expire, // Expire
'userName' => $username, // User name
];
With the payload data ready to go, we next use php-jwt’s static encode
method to create the JWT.
The method:
- transforms the array to JSON
- produce the headers
- signs the payload
- encodes the final string
It takes three parameters:
- the payload information
- the secret key
- the algorithm to use to sign the token
By calling echo
on the result of the function, the generated token is returned:
<?php
// Encode the array to a JWT string.
echo JWT::encode(
$data,
$secretKey,
'HS512'
);
}
Consuming the JWT
Now that the client has the token, you can store it using JavaScript or whichever mechanism you prefer. Here’s an example of how to do so using vanilla JavaScript. In index.html
, after a successful form submission, the returned JWT is stored in memory, the login form is hidden, and the button to request the timestamp is displayed:
const store = {};
const loginButton = document.querySelector('#frmLogin');
const btnGetResource = document.querySelector('#btnGetResource');
const form = document.forms[0];
// Inserts the jwt to the store object
store.setJWT = function (data) {
this.JWT = data;
};
loginButton.addEventListener('submit', async (e) => {
e.preventDefault();
const res = await fetch('/authenticate.php', {
method: 'POST',
headers: {
'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: JSON.stringify({
username: form.inputEmail.value,
password: form.inputPassword.value
})
});
if (res.status >= 200 && res.status <= 299) {
const jwt = await res.text();
store.setJWT(jwt);
frmLogin.style.display = 'none';
btnGetResource.style.display = 'block';
} else {
// Handle errors
console.log(res.status, res.statusText);
}
});
Using the JWT
When clicking on the “Get current timestamp” button, a GET request is made to resource.php
, which sets the JWT received after authentication in the Authorization header.
btnGetResource.addEventListener('click', async (e) => {
const res = await fetch('/resource.php', {
headers: {
'Authorization': `Bearer ${store.JWT}`
}
});
const timeStamp = await res.text();
console.log(timeStamp);
});
When we click the button, a request similar to the following is made:
GET /resource.php HTTP/1.1
Host: yourhost.com
Connection: keep-alive
Accept: */*
X-Requested-With: XMLHttpRequest
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0
Assuming that the JWT is valid, we’d see the resource, after which the response is written to the console.
Validating the JWT
Finally, let’s look at how we can validate the token in PHP. As always, we’d include Composer’s autoloader. We could then, optionally, check if the correct request method’s been used. I’ve skipped over the code to do that, to continue focusing on the JWT-specific code:
<?php
chdir(dirname(__DIR__));
require_once('../vendor/autoload.php');
// Do some checking for the request method here, if desired.
Then, the code would attempt to extract the token from the Bearer header. I’ve done so using preg_match. If you’re not familiar with the function, it performs a regular expression match on a string
The regular expression that I’ve used here will attempt to extract the token from the Bearer header, and dump everything else. If it’s not found, an HTTP 400 Bad Request is returned:
if (! preg_match('/Bearer\s(\S+)/', $_SERVER['HTTP_AUTHORIZATION'], $matches)) {
header('HTTP/1.0 400 Bad Request');
echo 'Token not found in request';
exit;
}
Note that, by default, Apache will not pass the HTTP_AUTHORIZATION
header to PHP. The reason behind this is:
The basic authorization header is only secure if your connection is done over HTTPS, since otherwise the credentials are sent in encoded plain text (not encrypted) over the network which is a huge security issue.
I fully appreciate the logic of this decision. However, to avoid a lot of confusion, add the following to your Apache configuration. Then the code will function as expected. If you’re using NGINX, the code should function as expected:
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
Next, we attempt to extract the matched JWT, which would be in the second element of the $matches
variable. If it’s not available, then no JWT was extracted, and an HTTP 400 Bad Request is returned:
$jwt = $matches[1];
if (! $jwt) {
// No token was able to be extracted from the authorization header
header('HTTP/1.0 400 Bad Request');
exit;
}
If we get to this point, a JWT was extracted, so we move to the decoding and validation stage. To do that, we need our secret key again, which would be pulled from the environment or the application’s configuration. We then use php-jwt’s static decode
method, passing to it the JWT, the secret key, and an array of algorithms to use to decode the JWT.
If it’s able to be successfully decoded, we then attempt to validate it. The example I have here is quite simplistic, as it only uses the issuer, not before and expiry timestamps. In a real application, you’d likely use a number of other claims as well.
$secretKey = 'bGS6lzFqvvSQ8ALbOxatm7/Vk7mLQyzqaS34Q4oR1ew=';
$token = JWT::decode($jwt, $secretKey, ['HS512']);
$now = new DateTimeImmutable();
$serverName = "your.domain.name";
if ($token->iss !== $serverName ||
$token->nbf > $now->getTimestamp() ||
$token->exp < $now->getTimestamp())
{
header('HTTP/1.1 401 Unauthorized');
exit;
}
If the token isn’t valid because, for example, the token has expired, the user will be sent an HTTP 401 Unauthorized header, and the script will exit.
If the process to decode the JWT fails, it could be that:
- The number of segments provided did not match the standard three as described earlier.
- The header or the payload is not a valid JSON string
- The signature is invalid, which means the data was tampered with!
- The
nbf
claim is set in the JWT with a timestamp when the current timestamp is less than that. - The
iat
claim is set in the JWT with a timestamp when the current timestamp is less than that. - The
exp
claim is set in the JWT with a timestamp when the current timestamp is more than that.
As you can see, JWT has a nice set of controls that will mark it as invalid, without the need to manually revoke it or check it against a list of valid tokens.
If the decode and validation process succeeds, the user will be allowed to make the request, and will be sent the appropriate response.
In Conclusion
That’s a quick introduction to JSON Web Tokens, or JWTs, and how to use them in PHP-based applications. From here on, you can try to implement JWTs in your next API, maybe trying some other signing algorithms that use asymmetric keys like RS256, or integrating it in an existing OAUTH2 authentication server to be the API key.
If you have any comments or questions, feel free to get in touch on Twitter.