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 :
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
Enregistrer un commentaire