The JSON Web Tokens (JWT) standard describes a compact method for verifiable data transfers. Each token contains a signature that allows the issuing party to check the message’s integrity.
In this article, you’ll learn what the JWT structure includes and how you can generate your own tokens. JWTs are a popular way to secure APIs and authenticate user sessions because they’re simple and self-contained.
How JWTs Work
One of the most common tasks in any API is validating that users are who they claim to be. Authentication’s usually handled by having the client include an API key with requests it sends to the server. The key contains embedded information that identifies the user. This still leaves one big question: how can the server validate that it issued the key in the first place?
JWTs conveniently solve this problem by using a secret to sign each token. The server can check a token’s valid by trying to recompute the presented signature using its private secret. Any tampering will cause the verification to fail.
The JWT Format
JWTs are formed from three distinct components:
- Header – This includes metadata about the token itself, such as the signing algorithm that was used.
- Payload – The token’s payload can be any arbitrary data relevant to your system. It could include the user’s ID and a list of features they can interact with.
- Signature – The signature allows the token’s integrity to be validated in the future. It’s created by signing the header and payload using a secret value that’s known only to the server.
These three components are joined with periods to produce the JWT:
Each piece is encoded using Base-64. The complete token is a string of text that can be easily consumed in programming environments and sent with HTTP requests.
Creating a JWT
The steps to create a JWT can be implemented in all programming languages. This example uses PHP but the process will be similar in your own system.
Start out by creating the header. This usually includes two fields,
alg– The hashing algorithm that will be used to create the signature. This is normally HMAC SHA256 (
typ– The type of token that’s being generated. This should be
Here’s the JSON that defines the header:
"alg": "HS256", "typ": "JWT"
The header JSON needs to be Base64-encoded next:
$headerData = ["alg" => "HS256", "typ" => "JWT"]; $header = base64_encode(json_encode($headerData));
Next define your token’s payload as another JSON object. This is application-specific. The example offers details of the authenticated user account, as well as information about the token itself.
nbf are fields used by convention to express the token’s expiry time, issued at time, and not valid before (start) time. The payload needs to be Base64-encoded too.
$payloadData = [ "userId" => 1001, "userName" => "demo", "licensedFeatures" => ["todos", "calendar", "invoicing"], "exp" => (time() + 900), "iat" => time(), "nbf" => time() ]; $payload = base64_encode(json_encode($payloadData));
All that remains is to create the signature. To produce this you first combine the header and payload into a single string separated by a
$headerAndPayload = "$header.$payload";
Next you must generate a unique secret to use as your signing key. The secret needs to be stored securely on your server and must never be sent to clients. Exposure of this value would allow anyone to create valid tokens.
// PHP method to generate 32 random characters $secret = bin2hex(openssl_random_pseudo_bytes(16));
You complete the process by using the secret to sign the combined header and payload string using the hashing algorithm you indicated in the header. The output signature must be Base64-encoded like the other components.
$signature = base64_encode(hash_hmac("sha256", $headerAndPayload, $secret, true));
Now you’ve got the header, payload, and signature as individual textual components. Join them all together with
. separators to create the JWT to send to your client:
$jwt = "$header.$payload.$signature";
Verifying Incoming JWTs
const tokenComponents = jwt.split("."); const payload = token; const payloadDecoded = JSON.parse(atob(payload)); // ["todos", "calendar", "invoicing"] console.log(payloadDecoded.licensedFeatures);
An attacker might realize this data is plain text and looks easy to modify. They could try to convince the server they’ve got a bonus feature by changing the token’s payload in their next request:
// Create a new payload component const modifiedPayload = btoa(JSON.stringify( ...payloadDecoded, licensedFeatures: ["todos", "calendar", "invoicing", "extraPremiumFeature"] )); // Stitch the JWT back together with the original header and signature const newJwt = `$token.$modifiedPayload.$token`
The answer to how the server defends against these attacks lies in the method used to generate the signature. The signature value takes into account the token’s header and payload. Modifying the payload, as in this example, means the signature is no longer valid.
Server-side code verifies incoming JWTs by recomputing their signatures. The token has been tampered with if the signature sent by the client doesn’t match the value generated on the server.
$tamperedToken = $_POST["apiKey"]; list($header, $payload, $signature) = $tamperedToken; // Determine the signature this token *should* have // when the server's secret is used as the key $expectedSignature = hash_hmac("sha256", "$header.$payload", $secret, true); // The token has been tampered with because its // signature is incorrect for the data it includes if ($signature !== $expectedSignature) http_response_code(403); // The signatures match - we generated this // token and can safely trust its data else $user = fetchUserById($payload["userId"]);
It’s impossible for an attacker to generate a valid token without access to the server’s secret. This also means that accidental loss – or deliberate rotation – of the secret will immediately invalidate all previously issued tokens.
In a real-world situation, your authentication code should also inspect the expiration and “not before” timestamps in the token’s payload. These are used to determine whether the user’s session is still valid.
When to Use JWTs
JWTs are frequently used for API authentication because they’re straightforward to implement on the server, easy to consume on the client, and simple to transmit across network boundaries. Despite their simplicity they have good security because each token is signed using the server’s secret key.
JWTs are a stateless mechanism so you don’t need to record information about issued tokens on your server. You can get information about the client that presents a JWT from the token’s payload, instead of having to perform a look-up in a database. This information can be safely trusted once you’ve verified the token’s signature.
Using JWTs is a good choice whenever you need to exchange information between two parties without the risk of tampering. There are weak spots to be aware of though: the whole system will be compromised if your server’s secret key is leaked, or your signature verification code contains a bug. For this reason many developers choose to use an open-source library to implement JWT generation and validation. Options are available for all popular programming languages. They eliminate the risk of oversight when you verify tokens yourself.
The JWT standard is a data exchange format that includes built-in integrity verification. JWTs are commonly used to secure interactions between API servers and client applications. The server can trust incoming tokens if it’s able to reproduce their signatures. This allows actions to be safely performed using information obtained from the token’s payload.
JWTs are convenient but they do have some drawbacks. A JWT’s Base64-encoded textual representation can quickly become large if you’ve got more than a handful of payload fields. This could become an unacceptable overhead when your client needs to send the JWT with each request.
The statelessness of JWTs is another potential downside too: once they’re issued, tokens are immutable and must be used as-is until they expire. Clients that use JWT payloads to determine a user’s permissions or licensed features will need to get a new token from the backend whenever their assignations change.