Creating and Verifying JWT Signatures in PHP using HS256 and RS256

JSON Web Tokens (JWTs) are widely used these days for authentication purposes be it for the traditional session cookie tokens, API tokens or even OAuth 2.0 access tokens. Obviously the major benefit with JWTs is that the server doesn’t need to store the session data into it’s own memory or a separate file or database or a cache system (redis, memcached, etc.). Hence data is not “stored” anywhere nor do we have to read from or write to an external database or caching layer to fetch/store session information which becomes a bigger problem with scale (distributed data with load balancers, etc.). Although practically in most cases you’ll end up taking an ID from the JWT (server-side) and query the DB to get more information or at least validate it. So the last portion (around scale) isn’t really a benefit in the longer run but is still a point to make for a small/medium sized app which still has load balancers and multiple external databases and caching systems.

Overall if you look at JWTs, they just allow you to transfer data from point to another in a compact form which can be sent through URLs, POST params or even HTTP headers.

This post will not explain what JWTs are as this introduction does a very good job at that. We’ll basically look at how to create JWTs (sign tokens) and verify the signatures when the token is sent back from clients without using any PHP library or package, i.e., with just native PHP functions. We’ll cover these two algorithms:

  1. HS256 (HMAC with SHA-256)
  2. RS256 (RSA with SHA-256)

Although as long as you’re using HMACs or RSA key/pairs, using a different algorithm from SHA-256 would be pretty much a matter of passing the right algorithm name as an argument to the functions that we’ll use.

Sample JWT

Let’s first look at a sample JWT and then we’ll get into encoding/decoding business. We’ll pick the sample from jwt.io.

Header

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Encoded JWT

Once the data is encoded following the process defined by the spec (using HS256), the JWT will look something like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

The first portion (before the first period delimiter) is the base64 encoded version of the header. Second portion (before the second period delimiter) is the base64 encoded version of the payload. The last part is the signature generated for which the pseudo syntax looks like this:

HS256(
  base64UrlEncode(header) + ".” + base64UrlEncode(payload),
  secret
)

JWT with HS256

Now let’s look at how we would create and verify JWTs using HMAC with SHA-256 algorithm (HS256) in PHP.

Creation

We’ll create two helper functions first which will be used across to base64 encode and decode data (header, payload and signature) in a URL-safe way. We’ll also define a secret cryptographic key for the HMAC generation.

function base64UrlEncode(string $data): string
{
    $urlSafeData = strtr(base64_encode($data), '+/', '-_');

    return rtrim($urlSafeData, '='); 
} 

function base64UrlDecode(string $data): string
{
    $urlUnsafeData = strtr($data, '-_', '+/');

    $paddedData = str_pad($urlUnsafeData, strlen($data) % 4, '=', STR_PAD_RIGHT);

    return base64_decode($paddedData);
}

// Highly confidential
$secret = "HIGHLY CONFIDENTIAL SECRET KEY";

Although I’ve used a dumb value for $secret you should generate a nice randomised token to be used as secret. You can directly generate one from the command line using:

$ openssl rand -base64 32

# or even better

$ php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'

Now we’ll write a function to generate the JWT which’ll be mostly self-explanatory:

function generateJWT(
    string $algo,
    array $header,
    array $payload,
    string $secret
): string {
    $headerEncoded = base64UrlEncode(json_encode($header));

    $payloadEncoded = base64UrlEncode(json_encode($payload));

    // Delimit with period (.)
    $dataEncoded = "$headerEncoded.$payloadEncoded";

    $rawSignature = hash_hmac($algo, $dataEncoded, $secret, true);

    $signatureEncoded = base64UrlEncode($rawSignature);

    // Delimit with second period (.)
    $jwt = "$dataEncoded.$signatureEncoded";

    return $jwt;
}

// JWT Header
$header = [
    "alg"     => "HS256",
    "typ"     => "JWT"
];

// JWT Payload data
$payload = [
    "sub"        => "1234567890",
    "name"        => "John Doe",
    "admin"        => true
];

// Create the JWT
$jwt = generateJWT('sha256', $header, $payload, $secret);

var_dump($jwt); // string(149) "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.6pteLozCETeYDL9Dgm-k4INQ1oEsUf0nFy8Tn2OIxgo"

What we basically do is:

  1. First base64 encode (in a URL-safe way) the header and and payload and then concatenate (or delimit) them with a period (.).
  2. Create an HMAC out of the new encoded data with a confidential secret using the SHA256 algorithm. This is done using the hash_hmac() PHP function.
  3. Finally encoding the hash and concatenating that to the encoded data. This is the JWT!

Verification

Now if we have to verify the JWT upon reception then that is fairly simple and straightforward. This function will do the trick:

function verifyJWT(string $algo, string $jwt, string $secret): bool
{
    list($headerEncoded, $payloadEncoded, $signatureEncoded) = explode('.', $jwt);

    $dataEncoded = "$headerEncoded.$payloadEncoded";

    $signature = base64UrlDecode($signatureEncoded);

    $rawSignature = hash_hmac($algo, $dataEncoded, $secret, true);

    return hash_equals($rawSignature, $signature);
}

$verify = verifyJWT('sha256', $jwt, $secret);

var_dump($verify); // true or false

The process includes:

  1. Get all the (three) parts of the JWT.
  2. Set aside the “encoded_header.encoded_payload” aside for a new signature generation which will be matched against the JWT’s own signature (third part).
  3. Check whether the generated HMAC is same as the one in the JWT using hash_equals() which prevents timing attacks.

Of course all the code presented above has no error handling which should be in place and everything can be wrapped up into a neat little class that can be used as a library/helper.

JWT with RS256

Now that we’ve learnt the HMAC way, let’s look into the RSA (public/private key pair) way. Let’s create a public/private key pair first using openssl:

# Generate private key first
$ openssl genrsa -out private.key 1024
Generating RSA private key, 1024 bit long modulus
..........++++++
................................................++++++
e is 65537 (0x10001)

# Extract public key out of private next
$ openssl rsa -in private.key -pubout -out public.key
writing RSA key

Create

Now as far as our code is concerned we’ll have the same base64UrlEncode() and base64UrlDecode() helper functions along with another one that we’ll define now. The use of this function will be to generate an error string if openssl_* functions fail or return an erroneous code for some reason.

Also instead of a secret that is used as a crypto key to generate HMACs we’ll use the public/private keys that we just generated with openssl. So along with the helpers (and removing the $secret) this is what we’ll have:

function getOpenSSLErrors()
{
    $messages = [];

    while ($msg = openssl_error_string()) {
        $messages[] = $msg;
    }

    return $messages;
}

// Highly confidential
$privateKeyFile = "private.key";

// Shared with clients for signature verification
$publicKeyFile = "public.key";

The values of $privateKeyFile and $publicKeyFile are basically the path (relative in this case, can also be absolute) to those files that we generated.

Our new generateJWT() method will slightly change to use openssl_sign() with our private key. It’ll look like this:

function generateJWT(
    string $algo,
    array $header,
    array $payload,
    string $privateKeyFile
): string {
    $headerEncoded = base64UrlEncode(json_encode($header));

    $payloadEncoded = base64UrlEncode(json_encode($payload));

    // Delimit with period (.)
    $dataEncoded = "$headerEncoded.$payloadEncoded";

    $privateKey = "file://".$privateKeyFile;

    $privateKeyResource = openssl_pkey_get_private($privateKey);

    $result = openssl_sign($dataEncoded, $signature, $privateKeyResource, $algo);

    if ($result === false)
    {
        throw new RuntimeException("Failed to generate signature: ".implode("\n", getOpenSSLErrors()));
    }

    $signatureEncoded = base64UrlEncode($signature);

    $jwt = "$dataEncoded.$signatureEncoded";

    return $jwt;
}

// JWT Header
$header = [
    "alg"     => "RS256",
    "typ"     => "JWT"
];

// JWT Payload data
$payload = [
    "sub"        => "1234567890",
    "name"        => "John Doe",
    "admin"        => true
];

// Create the JWT
$jwt = generateJWT('sha256', $header, $payload, $privateKeyFile);

var_dump($jwt); // string(277) "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.R-41ycm1V7Kvx_Lnw6nha6OAFQ-vYvdhAdgqa1Ugkj17X4dpSWSO0KRCmnq7yd6ZM-RLEMY3PEXyUAs4F1XtomT6M-CziCpIB5piLfYHLG6V1_FrtieuIOMGLZGs-PpqMZX-JgJf_L19Ly9jnqGjfl9zo6BTTandhgNECE7AVk0"

Everything is similar to our HS256 method except instead of using hash_hmac(), we used openssl_sign() and it’s appropriate arguments to generate the signature.

Verify

As far as the verification goes that’ll also be very similar except we’ll now use openssl_verify() function to verify the signature with our public key.

function verifyJWT(string $algo, string $jwt, string $publicKeyFile): bool
{
    list($headerEncoded, $payloadEncoded, $signatureEncoded) = explode('.', $jwt);

    $dataEncoded = "$headerEncoded.$payloadEncoded";

    $signature = base64UrlDecode($signatureEncoded);

    $publicKey = "file://".$publicKeyFile;

    $publicKeyResource = openssl_pkey_get_public($publicKey);

    $result = openssl_verify($dataEncoded, $signature, $publicKeyResource, $algo);

    if ($result === -1)
    {
        throw new RuntimeException("Failed to verify signature: ".implode("\n", getOpenSSLErrors()));
    }

    return (bool) $result;
}

$verify = verifyJWT('sha256', $jwt, $publicKeyFile);

var_dump($verify); // true or false

There we go!

HS256 vs RS256

You might be wondering when to use which ? It’s simple I think. If your clients are first party trusted clients where you have control over them entirely (end to end), then you can use HS256 where the “secret” can be shared.

If that’s not the case (third party clients) then you can’t share the secret (especially what when you want to revoke or roll it?). In such cases you can use RS256 and provide your clients with public keys. You can additionally give them endpoints to fetch public keys periodically in case the private keys have been revoked/regenerated and there are new public keys extracted out of them.

This SO answer is worth a read as well.

Also as you’d have noticed you can use something other than HS256 and RS256 as well, like HS512. Changing the algorithm is super easy as it’s just a matter of passing the desired algorithm argument to hash_hmac() or openssl_sign().

Payload Verification

We just looked at signature verification but a JWT has the provision to contain some registered claims which are not mandatory but can help do a bunch of verification in terms of token expiration and issuance time checks. A claim is basically a key/value pair inside the payload.

So for instance you can use iat (issued at), nbf (not before) and exp (expiration time) keys inside your payload with timestamp values which can be checked everytime against current time to see if the token should be used or not or whether it has been expired. Here’s a header and payload sample where the payload contains registered and private claims.

json_decode(base64_decode('eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjQ2YTAxZGE0ODgxZDQ2MzYzMTM0ZGMwNTdkNTk1YmM1MDM5YzJlN2EyNTEyN2UzODI2MTRkOTZiYWI4MmZjMzVkNzZkYTJiNWU4MWRmYWQzIn0'), true)
=> [
     "typ" => "JWT",
     "alg" => "RS256",
     "jti" => "46a01da4881d46363134dc057d595bc5039c2e7a25127e382614d96bab82fc35d76da2b5e81dfad3",
   ]

json_decode(base64_decode("eyJhdWQiOiI4SnBybVozZmlvNWdjaSIsImp0aSI6IjQ2YTAxZGE0ODgxZDQ2MzYzMTM0ZGMwNTdkNTk1YmM1MDM5YzJlN2EyNTEyN2UzODI2MTRkOTZiYWI4MmZjMzVkNzZkYTJiNWU4MWRmYWQzIiwiaWF0IjoxNTAxMTY2NjkzLCJuYmYiOjE1MDExNjY2OTMsImV4cCI6MTUwMTE3MDI5MywidXNlcl9pZCI6IjIwMDAwMDAwMDAwMDAwIiwic2NvcGVzIjpbInJlYWRfb25seSJdfQ=="), true)
=> [
     "aud" => "8JprmZ3fio5gci",
     "jti" => "46a01da4881d46363134dc057d595bc5039c2e7a25127e382614d96bab82fc35d76da2b5e81dfad3",
     "iat" => 1501166693,
     "nbf" => 1501166693,
     "exp" => 1501170293,
     "user_id" => "20000000000000",
     "scopes" => [
       "read_only",
     ],
   ]

Libraries

Sometimes we want to quickly implement signing or verification mechanism in which case all the code posted above is super useful. But if you don’t want to roll your own library/helpers then you’re free to use any of the widely available libraries – find a list here. The libraries will take care of all the hassles – JWT generation, signature verification, payload verification, etc.

I’ve put all the code written above on Github as well.

2 thoughts on “Creating and Verifying JWT Signatures in PHP using HS256 and RS256”

    1. Agreed! Although at times if there’s a small use case like someone wants to quickly validate the signature of a one time auth/access JWT given by a third party service it might not be a bad idea to quickly borrow some of the code pieces show above and use them directly.

      The article is pretty much written to explain what goes under the hood (in terms of native PHP methods) when a third party package is used. So it’s more for demonstration purpose than to use it completely as a JWT generation/validation mechanism instead of relying on well tested libraries. Rolling your own code can definitely have security implications and maintenance burden.

Leave a Reply

Your email address will not be published. Required fields are marked *