Skip to content
AldeaCode Logo
JWT Decoder / PHP Developer 100% local

Decode a JWT in PHP: firebase/php-jwt, lcobucci/jwt, and the manual route

Decoding a JWT in PHP is three string operations and two json_decode calls. The library route is one line. The trap that breaks half of all hand-rolled decoders is the URL-safe base64 alphabet, which PHP's base64_decode does not understand by default.

Decoding is not verifying

A JWT has three parts joined by dots: header.payload.signature. The header and payload are base64url-encoded JSON. The signature is a MAC or asymmetric signature over header.payload. Decoding only reads the first two parts. It tells you what the token claims, not whether the claim is genuine.

If you accept a decoded JWT as proof of identity without verifying the signature, an attacker swaps the payload, re-encodes, and walks in as anyone. The classic mistake is logging the decoded sub claim and treating that user as authenticated. Decode for inspection, debugging, and CLI tooling. Verify in production every single time.

The libraries below all have a verify path. Use it. The manual snippet at the bottom of this page is for inspection only and includes a comment that says so.

firebase/php-jwt is the default choice

firebase/php-jwt is the most widely deployed library and the one the Composer registry recommends. Install with composer require firebase/php-jwt. The decode call takes the token, the key, and the algorithm:

```php use Firebase\JWT\JWT; use Firebase\JWT\Key;

$payload = JWT::decode($token, new Key($secret, 'HS256')); ```

The result is a stdClass of the payload claims. The library validates the signature, the exp, nbf, and iat claims, and throws on any failure. If you only want to inspect the token without a key, the library does not expose a public decode-without-verify call; you fall back to splitting by hand.

lcobucci/jwt for richer parsing

lcobucci/jwt is the second mainstream option, with a more object-oriented API and stronger typing on the claim set. It is the choice when you need to inspect the token structure, iterate over claims, or build a token with a fluent builder.

The parse step does not verify; it returns a Token object you can introspect. Verification is a separate Validator step. The split lets you log token contents for debugging without conflating the two operations. The library is heavier than firebase/php-jwt but worth it on larger codebases.

The URL-safe base64 quirk in PHP

JWTs use base64url, the URL-safe variant defined in RFC 4648 section 5. It replaces + with - and / with _, and drops the trailing = padding. PHP's base64_decode expects the standard alphabet. If you pass a JWT segment directly, it returns garbage or false on strict mode.

The fix is a two-step str_replace plus padding restoration before base64_decode:

function base64UrlDecode(string $data): string {
    $remainder = strlen($data) % 4;
    if ($remainder) {
        $data .= str_repeat('=', 4 - $remainder);
    }
    return base64_decode(strtr($data, '-_', '+/'));
}

Every library wraps this internally. Hand-rolled decoders forget the padding and produce an empty string for tokens whose payload length is not a multiple of four. The padding step is not optional.

Working example

php
<?php
// Manual decode for inspection only. NEVER trust the result without verifying.
function base64UrlDecode(string $data): string {
    $remainder = strlen($data) % 4;
    if ($remainder) {
        $data .= str_repeat('=', 4 - $remainder);
    }
    return base64_decode(strtr($data, '-_', '+/'));
}

function jwtInspect(string $token): array {
    $parts = explode('.', $token);
    if (count($parts) !== 3) {
        throw new InvalidArgumentException('Not a JWT');
    }
    return [
        'header'  => json_decode(base64UrlDecode($parts[0]), true),
        'payload' => json_decode(base64UrlDecode($parts[1]), true),
        'sig_b64' => $parts[2],
    ];
}

// Production verify path with firebase/php-jwt
// composer require firebase/php-jwt
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$verified = JWT::decode($token, new Key($secret, 'HS256'));
echo $verified->sub;

Just need the result?

When you have a token in a Postman tab or a server log and just want to read the claims without writing PHP, paste it into the JWT decoder at aldeacode.com. The page parses header and payload in your browser, never sends the token anywhere, and shows the exp claim in your local timezone.

Open JWT Decoder →

Frequently asked questions

Can I decode a JWT in PHP without a library?

Yes for inspection. Split on the dots, base64url-decode the first two parts, json_decode them. Do not use the result for authorization. The signature is unverified and trivially forgeable.

Why does base64_decode return false on JWT segments?

JWTs use the URL-safe base64 alphabet with no padding. PHP's base64_decode expects standard base64. Translate dash to plus, underscore to slash, and pad to a multiple of four with equals signs before decoding.

Should I use firebase/php-jwt or lcobucci/jwt?

firebase/php-jwt is smaller, simpler, and what most tutorials assume. lcobucci/jwt has a richer object model and better validation primitives. For new projects pick firebase/php-jwt unless you need the extra structure.