Skip to content

Instantly share code, notes, and snippets.

@angelo-v
Last active June 15, 2026 06:42
Show Gist options
  • Select an option

  • Save angelo-v/e0208a18d455e2e6ea3c40ad637aac53 to your computer and use it in GitHub Desktop.

Select an option

Save angelo-v/e0208a18d455e2e6ea3c40ad637aac53 to your computer and use it in GitHub Desktop.
Decode a JWT via command line
# will not work in all cases, see https://gist.github.com/angelo-v/e0208a18d455e2e6ea3c40ad637aac53#gistcomment-3439904
function jwt-decode() {
sed 's/\./\n/g' <<< $(cut -d. -f1,2 <<< $1) | base64 --decode | jq
}
JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
jwt-decode $JWT
@oldschoolsysadmin

Copy link
Copy Markdown

why not just cut -d"." -f1,2 <<< $1 | sed 's/\./\n/g' | base64 --decode | jq?

@lukaslihotzki

Copy link
Copy Markdown

With just jq: jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "$JWT"

@micklove

micklove commented Mar 3, 2020

Copy link
Copy Markdown

@lukaslihotzki, nice work :)

With just jq: jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "$JWT"

@philpennock

Copy link
Copy Markdown

JWTs use base64url encoding, which neither jq nor base64 (from GNU coreutils) can handle. So the above examples all work some of the time, not reliably.

@angelo-v

angelo-v commented Sep 2, 2020

Copy link
Copy Markdown
Author

Valid point @philpennock. Do you have a reliable solution?

@lukaslihotzki

Copy link
Copy Markdown

Just replace the characters? jq -R 'gsub("-";"+") | gsub("_";"/") | split(".") | .[1] | @base64d | fromjson'

@seva-ramin

seva-ramin commented Sep 26, 2020

Copy link
Copy Markdown

JWTs use base64url encoding, which neither jq nor base64 (from GNU coreutils) can handle. So the above examples all work some of the time, not reliably.

@philpennock is correct. Here is my solution in a shell script:

#!/bin/bash

# pad base64URL encoded to base64
paddit() {
  input=$1
  l=`echo -n $input | wc -c`
  while [ `expr $l % 4` -ne 0 ]
  do
    input="${input}="
    l=`echo -n $input | wc -c`
  done
  echo $input
}

# read and split the token and do some base64URL translation
read jwt
read h p s <<< $(echo $jwt | tr [-_] [+/] | sed 's/\./ /g')

h=`paddit $h`
p=`paddit $p`

# assuming we have jq installed
echo $h | base64 -d | jq
echo $p | base64 -d | jq

@philpennock

Copy link
Copy Markdown

Oh, I never spoke up, sorry: I use shell around Perl because Perl had the easiest access to base64url when I last went looking, but I was impressed by the solution from @lukaslihotzki using gsub inside jq as being the solution with the fewest additional dependencies.

@eum602

eum602 commented Oct 13, 2020

Copy link
Copy Markdown

I was also trying to decode a JWT token. I could decode a JWT with:

for line in `echo $JWT | tr "." "\n"`; do echo $line | base64 --decode | jq  && echo;done

@danmactough

Copy link
Copy Markdown

@seva-ramin's script is fantastic, but just a small bug: tr [+_] [-/] should be tr [-_] [+/]

@seva-ramin

Copy link
Copy Markdown

@seva-ramin's script is fantastic, but just a small bug: tr [+_] [-/] should be tr [-_] [+/]

Ugh! what a silly mistake! @danmactough, Thank you for catching that. I have updated my post.

@genghisjahn

genghisjahn commented May 7, 2021

Copy link
Copy Markdown

@seva-ramin for me, on OSX Mojave I get base64: invalid option -- d
Changing to -D works for last two lines:

# assuming we have jq installed
echo $h | base64 -D | jq
echo $p | base64 -D | jq

@seva-ramin

seva-ramin commented May 7, 2021

Copy link
Copy Markdown

@seva-ramin for me, on OSX Mojave I get base64: invalid option -- d
Changing to -D works for last two lines:

# assuming we have jq installed
echo $h | base64 -D | jq
echo $p | base64 -D | jq

Darn it. They broke base64 in Mojave? I am on Catalina and both options work. Here is the man page for base64 on Catalina.

image

@dbubenheim

Copy link
Copy Markdown

I additionally had to remove empty parts but then it worked perfectly fine

jq -R 'split(".") | select(length > 0) | .[0],.[1] | @base64d | fromjson' <<< $1

@subratamazumder

Copy link
Copy Markdown

With just jq: jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "$JWT"

@lukaslihotzki thanks, very useful πŸ‘

@suhlig

suhlig commented Jul 5, 2022

Copy link
Copy Markdown

With just jq: jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "$JWT"

Thanks @lukaslihotzki, very useful!

@FlorinTP

FlorinTP commented Jul 7, 2022

Copy link
Copy Markdown

Or an universal GO approach using RawStdEncoding (with temporary file):

cat << EOFT > ./temp.go &&  go run ./temp.go $JWT |jq  '.|select(.type=="wrapping")' ; rm ./temp.go
package main
import (
        "encoding/base64"
        "strings"
        "fmt"
        "os"
)
var  encoded = os.Args[1]
func main() {
split := strings.Split(encoded, ".")
for i := 0; i < len(split); i++ {
        tokenBytes, err := base64.RawStdEncoding.DecodeString(split[i])
        if err != nil {
          return
        }
        var sToken=string(tokenBytes)
        fmt.Printf("%s",sToken)
    }
}
EOFT

@gustavoromerobenitez

Copy link
Copy Markdown

With just jq: jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "$JWT"

Excellent solution, thanks @lukaslihotzki

@indian0ch

Copy link
Copy Markdown

What about echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" | cut -d '.' -f 2 | base64 -d

@kloverde25

Copy link
Copy Markdown

what about basenc it's part of coreutils ?

basenc -d --base64url -i <your_file> | jq

@philpennock

Copy link
Copy Markdown

Sure; coreutils 8.31 and newer, so was not in stable OS releases at the time of the gist. Today, I'd recommend basenc.

@philpennock

Copy link
Copy Markdown

Oh, beware though that basenc complains about missing = signs, even in --base64url mode, so you'll also need to suppress stderr.

@mvillafuertem

Copy link
Copy Markdown

What about echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" | cut -d '.' -f 2 | base64 -d

@indian0ch thanks πŸ‘πŸ»

@jpbochi

jpbochi commented Jul 12, 2024

Copy link
Copy Markdown

To get around the broken/unreliable @base64d from jq, I got this solution:

jwtd () {
  local input="${1:-}"
  if [ -z "$input" ]; then
    if [ ! -t 0 ]; then
      input=$(cat /dev/stdin)
    else
      echo >&2 'βœ— Need an argument or have a piped input!'
      return 1
    fi
  fi
  echo "$input" \
    | jq -Rrce 'split(".")[1] | . + "=" * (. | 4 - length % 4)' \
    | openssl base64 -d -A \
    | jq .
}

It will append the = padding as needed, then pipe into openssl base64 -d -A, which I found to be more reliable and cross-platform than base64. I tested this both on Ubuntu and MacOS.

The bash function accepts either a direct param or piped input (e.g., echo 'base64…==' | jwtd).

@rickgm

rickgm commented Oct 2, 2024

Copy link
Copy Markdown

@jpbochi Thanks for your script! Why don't you include the tr -- '-_' '+/' step? openssl needs it right? (e.g. openssl/openssl#17559)

@jpbochi

jpbochi commented Jan 10, 2025

Copy link
Copy Markdown

@rickgm I ended up settling for python3 -m base64 -d as a replacement for openssl base64 -d -A. It's more robust, supporting both base64 and base64url modes.

@ronnyworm

Copy link
Copy Markdown

Just use jc - e.g. echo "ey..." | jc --jwt | jq
That also shows the signature very cleanly if you're interested in that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment