Roll your own jwt.io in the terminal

 · 2 min · torgeir

Pasting a jwt token into someone else's website feels wrong. Let's roll a small diy version of it in the terminal.

Terminal Jwt

Pasting a jwt token into someone else’s website, like jwt.io , feels kinda wrong. Actually it feels worse than pasting your details into someone’s website to check if your accounts have been pwned. And for the record, I trust both jwt.io and Troy Hunt .

You could easily roll your own small version of what the jwt.io website does (except validate the signature) to expose the details of those pesky, base64 url encoded JWT tokens.

The post Cut is useful showed how you can extract parts of a token, and combining it with tee you can show all base64 decoded parts at once. By piping output into tee, you can steer its output into three separate program paths, and still have its output displayed to stdout.

echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c \
  | tee >(cut -d. -f1 | base64 -d) \
       >(cut -d. -f2 | base64 -d) \
       >(cut -d. -f3) \
       >/dev/null
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c {"alg":"HS256","typ":"JWT"}{"sub":"1234567890","name":"John Doe","iat":1516239022}

However, tee runs the process substitution in parallell, so the order of the result is non-deterministic, and based on the content.

Something like this ought to preserve the order

function extract_jwt {
  local input=$(cat -)
  echo $input | cut -d. -f1 | base64 -d
  echo $input | cut -d. -f2 | base64 -d
  echo $input | cut -d. -f3
}

The signature part is also base64 url encoded, but I choose to keep it encoded, as decoding it often results in non-printable characters, so you might as well keep it encoded until you really need it.

Experiencing base64: invalid input in the printed result? Chatgpt suggests its due to JWTs using base64 url encoding, which checks out , and is not the same as what base64 -d expects. The former replaces + with - and / with _, and does not use padding =.

Swap base64 -d for this and its gone.

function base64url_decode {
  local input=$(cat -)
  input="${input//-/+}"         # replace - with +
  input="${input//_/\/}"        # replace _ with /
  case $((${#input} % 4)) in    # add padding
    2) input+="==" ;;
    3) input+="=" ;;
  esac
  echo "$input" | base64 -d
}
function extract_jwt {
  local input=$(cat -)
  echo $input | cut -d. -f1 | base64url_decode
  echo $input | cut -d. -f2 | base64url_decode
  echo $input | cut -d. -f3
}

I did not extensively test this, but it does make sense, and seems to work fine for the couple of JWT tokens I tested.

echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c \
  | extract_jwt
{"alg":"HS256","typ":"JWT"}{"sub":"1234567890","name":"John Doe","iat":1516239022}SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Edits:

  • [2024-10-10 tor] wording