To assist in troubleshooting, I wanted to generate JWT (JSON Web Tokens) on-the-fly with bash.

It was the easiest way (I thought) to be able to test various conditions like malformed headers, payloads, mismatching algorithms, and various other edge cases to see how my server would respond.

This nginx blog post and this superuser post were very helpful in getting my script working.

For most people, you might find that the interactive debugger available at jwt.io is actually a much better way to generate JWTs. You can click that link and live edit either the generated token on the left, or the content on the right. It's very nice and simple.

However, since I already spent all this time and energy to do it in bash, I wanted to share my results.

Edit the SECRET, HEADER, and PAYLOAD in this script as needed.

You must have jq and openssl installed for this to work.

This will only generate JWTs with HMAC signing using SHA256. I've seen this signing referred to as both HS256 and HMACSHA256.

#!/usr/bin/env bash

set -euo pipefail
IFS=$'\n\t'

SECRET='SOME SECRET'
HEADER='{
    "typ": "JWT",
    "alg": "HS256",
    "kid": "0001",
    "iss": "Bash JWT Generator",
    "exp": '$(($(date +%s)+1))',
    "iat": '$(date +%s)'
}'
PAYLOAD='{
    "Id": 1,
    "Name": "Hello, world!"
}'

function base64_encode()
{
    declare INPUT=${1:-$(</dev/stdin)}
    echo -n "${INPUT}" | openssl enc -base64 -A
}

# For some reason, probably bash-related, JSON that terminates with an integer
# must be compacted. So it must be something like `{"userId":1}` or else the
# signing gets screwed up. Weird, but using `jq -c` works to fix that.
function json() {
    declare INPUT=${1:-$(</dev/stdin)}
    echo -n "${INPUT}" | jq -c .
}

function hmacsha256_sign()
{
    declare INPUT=${1:-$(</dev/stdin)}
    echo -n "${INPUT}" | openssl dgst -binary -sha256 -hmac "${SECRET}"
}

HEADER_BASE64=$(echo "${HEADER}" | json | base64_encode)
PAYLOAD_BASE64=$(echo "${PAYLOAD}" | json | base64_encode)

HEADER_PAYLOAD=$(echo "${HEADER_BASE64}.${PAYLOAD_BASE64}")
SIGNATURE=$(echo "${HEADER_PAYLOAD}" | hmacsha256_sign | base64_encode)

echo "${HEADER_PAYLOAD}.${SIGNATURE}"

Running the script should generate an encoded JWT that looks like this.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjAwMDEiLCJpc3MiOiJCYXNoIEpXVCBHZW5lcmF0b3IiLCJleHAiOjE0ODE5OTQxMzgsImlhdCI6MTQ4MTk5NDEzN30.eyJJZCI6MSwiTmFtZSI6ImhleSB0aGVyZSJ9.VeREKJ8rj5UuGrKpK85-grqihFhlCkIJjte2XiFIZs8

You can use the JWT Debugger to verify the string is valid and properly signed.

You can also decode and verify the token using this script. Edit SECRET as needed so that you can verify the JWT is properly signed.

#!/usr/bin/env bash

set -euo pipefail
IFS=$'\n\t'

SECRET='SOME SECRET'

function base64_encode()
{
    declare INPUT=${1:-$(</dev/stdin)}
    echo -n "${INPUT}" | openssl enc -base64 -A
}

function base64_decode()
{
    declare INPUT=${1:-$(</dev/stdin)}
    echo -n "${INPUT}" | openssl enc -base64 -d -A
}

function verify_signature()
{
    declare HEADER_AND_PAYLOAD=${1}
    EXPECTED=$(echo "${HEADER_AND_PAYLOAD}" | hmacsha256_encode | base64_encode)
    ACTUAL=${2}

    if [ "${EXPECTED}" == "${ACTUAL}" ]
    then
        echo "Signature is valid"
    else
        echo "Signature is NOT valid"
    fi
}

function hmacsha256_encode()
{
    declare INPUT=${1:-$(</dev/stdin)}
    echo -n "${INPUT}" | openssl dgst -binary -sha256 -hmac "${SECRET}"
}

# Read the token from stdin
declare TOKEN=${1:-$(</dev/stdin)};

IFS='.' read -ra PIECES <<< "$TOKEN"

declare HEADER=${PIECES[0]}
declare PAYLOAD=${PIECES[1]}
declare SIGNATURE=${PIECES[2]}

echo "Header"
echo "${HEADER}" | base64_decode | jq
echo "Payload"
echo "${PAYLOAD}" | base64_decode | jq

verify_signature "${HEADER}.${PAYLOAD}" "${SIGNATURE}"