Decoding JWTs in the terminal

These past few days, I have been working on integrating IBM App ID into our Java backend and Android frontend codebases. Because of this, I would find myself going back and forth between the terminal and JWT.io whenever I need to inspect a JWT's payload. I don't want to call it a "JWT token" because that would be a bad case of RAS and I'm pedantic like that, but I digress.

Instead of relying on a website to do that for me, I figured why not just do it from the terminal. I did some reading and it turns out that JWTs are relatively easy to parse :

  • Split the token using the dot character as a delimiter
  • Base 64 decode the first portion to get the header
  • Base 64 decode the second portion to get the payload

The third portion serves as a signing mechanism for the token. I chose to ignore the signing logic for the script I intended to write because it was irrelevant for my use case.

I ended up writing a command line tool in D to help me inspect JWTs. The main requirement was for it to let me feed it a token and have it not only decode it, but also inject two extra fields called "issued_at" and "expires_at". Since the "iat" and "exp" claims are Unix timestamps, I deemed it reasonable to present them in a human readable format. D's standard library already supports base 64 decoding and JSON parsing out of the box and as a consequence, I did not need to reach out to external dependencies :

import std;
alias decode = Base64URLNoPadding.decode;

void main(string[] args)
{
    string jwt = args.length > 1 ? args[1] : readln();
    string[] parts = jwt.split(".");
    writeln("Header :");
    parts[0]
        .decode
        .map!(to!dchar)
        .parseJSON()
        .toPrettyString()
        .writeln();

    auto payload = parts[1]
        .decode
        .map!(to!dchar)
        .parseJSON();

    if("iat" in payload)
    {
        payload.object["issued_at"] = JSONValue(SysTime.fromUnixTime(payload["iat"].integer).toSimpleString());
    }
    if("exp" in payload)
    {
        payload.object["expires_at"] = JSONValue(SysTime.fromUnixTime(payload["exp"].integer).toSimpleString());
    }

    writeln("Payload :");
    payload
        .toPrettyString()
        .writeln();
}

Notice how the code neither has error checking nor input validation. Since this is just a glorified bash script for productivity, I didn't really mind if it blew up in my face. I proceeded by compiling the code above to a binary called "jwt" that I aliased in my .zshrc file for easier access :

alias jwt='~/code/jwt'

And here's what it looks like with a random token :

jwt decoding output

Having used this tool for a while, I realized that nine times out of ten, the tokens I feed it come straight from the clipboard. In light of these new circumstances, I decided to further simplify the process by using xclip instead of manually pasting tokens on the terminal :

xclip -sel clipboard -o | jwt

Since I'm a naturally lazy person, I included clipboard retrieval support in the code itself, making sure to add a -c (optionally --clipboard) flag to enable this behavior :

import std;
alias decode = Base64URLNoPadding.decode;

void main(string[] args)
{
    string jwt;
    if(args.canFind("--clipboard") || args.canFind("-c"))
    {
        auto xclip = executeShell("xclip -sel clipboard -o");
        if(xclip.status != 0)
        {
            writeln("Unable to retrieve a JWT from the clipboard");
            return;
        }
        jwt = xclip.output;
    }
    else
    {
        jwt = args.length > 1 ? args[1] : readln();
    }

    immutable parts = jwt.split(".");
    writeln("Header :");
    parts[0]
        .decode
        .map!(to!dchar)
        .parseJSON()
        .toPrettyString()
        .writeln();


    auto payload = parts[1]
        .decode
        .map!(to!char)
        .parseJSON();

    if("iat" in payload)
    {
        payload.object["issued_at"] = JSONValue(SysTime.fromUnixTime(payload["iat"].integer).toSimpleString());
    }
    if("exp" in payload)
    {
        payload.object["expires_at"] = JSONValue(SysTime.fromUnixTime(payload["exp"].integer).toSimpleString());
    }

    writeln("Payload :");
    payload
        .toPrettyString()
        .writeln();
}

While this violates the "do one thing and do it well" Unix principle, doing things this way allowed me to acquaint myself with the std.process package of Phobos. A good compromise if you ask me.

Commentaires

Posts les plus consultés de ce blog

Writing a fast(er) youtube downloader

My experience with Win by Inwi

Porting a Golang and Rust CLI tool to D