Debug a JWT token expiration locally with bash or PowerShell

Whenever you need to check the meta data of a JWT token, you might be tempted to use jwt.io and paste your token in. Of course you should not do this, but how else would you debug your token? Well, a small Google will give you a ton of scripts that can do this. I've taken some of these scripts and added some nice token expiration debugging to it.

JQ + Bash

Usually I have JQ installed to do some JSON debugging. This script will take your JWT as input and process it locally:

#!/bin/bash
set -e

# Decoding a JWT token, based on the discussion here:
# https://gist.github.com/thomasdarimont/46358bc8167fce059d83a1ebdb92b0e7
# More here:
# https://keestalkstech.com/2024/12/debug-a-jwt-token-expiration-locally-with-bash-or-powershell/

function jwt_decode(){
    jq -R 'split(".") | .[1] | @base64d | fromjson' <<< "$1"
}

function check_expiration {
  local payload
  payload=$(jwt_decode "$1")

  if [ -z "$payload" ]; then
    echo "Invalid JWT or unable to decode."
    return 1
  fi

  local exp
  exp=$(echo "$payload" | jq -r '.exp')

  if [ "$exp" == "null" ]; then
    echo "No expiration ('exp') field found in the token."
    return 1
  fi

  local now
  now=$(date +%s)

  if [ "$now" -lt "$exp" ]; then
    local remaining
    remaining=$((exp - now))
    local minutes
    minutes=$((remaining / 60))
    echo "Token expires at: $(date -d @$exp)"
    echo "Time remaining: $minutes minutes."
  else
    echo "Token has expired. Expired at: $(date -d @$exp)"
  fi
}

echo ""

# Decode and display JWT info
jwt_decode "$1"
echo ""

# Check expiration
check_expiration "$1"
echo ""

Let's execute it on Windows using WSL2 with a test token:

bash jwt.sh eddyJhbGciOiJSUzI1NiIsInRasfasff5cCI6IkpXVCJ9.ewogICJpc3MiOiAidGVzdC1zZXJ2aWNlIiwKICAiYXVkIjogIm91ci1zZXJ2aWNlIiwKICAidXNlcm5hbWUiOiAidHN0dXNyIiwKICAiaWF0IjogMTczMzcxNTI4MiwKICAiZXhwIjogMjUyMjExNTI4Mgp9.ew-4BWfh815a_pppqlDqrjvj5CY1ZS2uYdiVOeazCDutlfnxdmrqwgSSo4Ot2riT8nMOozoN-68AioEhFphem6fFTLbqDCjONIi_ncxkSUaedavzFwvJBW0HugTfYpoUTq0uR344ffffftEaPCPg6n3GrPKFTEitzoYuVJdMWjHa_IHVzDygpMmErjBr43qPDU9TGQaYtcWFvtDOgNvGwdUl5KTauW716HBvbvBgO4zLqH_B9c80StOSt8L2aJiX6Ue5ZMgmipPdddK4mK2Vb9PoikUftv1bej_prvHBOYYVAwq-de9Ewn7P-12KVRMpRy9kuymqBgJtXVbDXdddUQY1sVwhG9cR_ryA

{
  "iss": "test-service",
  "aud": "our-service",
  "username": "tstusr",
  "iat": 1733715282,
  "exp": 2522115282
}

Token expires at: Fri Dec  3 04:34:42 CET 2049
Time remaining: 13139971 minutes.

The token I've used is valid for 25 years (it is a debug token). I've displayed the number of minutes to expiration to make the date more readable, so you don't have to do the math in your head.

Powershell

If you're on Windows you could use this PowerShell version to debug your JWT headers to see if your token is still active:

# Decoding a JWT token, based on the discussion here:
# https://gist.github.com/thomasdarimont/46358bc8167fce059d83a1ebdb92b0e7
# More here:
# https://keestalkstech.com/2024/12/debug-a-jwt-token-expiration-locally-with-bash-or-powershell/

function Decode-JWT {
    param (
        [string]$Token
    )

    $parts = $Token -split '\.'
    if ($parts.Count -ne 3) {
        Write-Error "Invalid JWT format."
        return $null
    }

    try {
        $payloadBase64 = $parts[1].PadRight($parts[1].Length + (4 - $parts[1].Length % 4) % 4, '=')
        $payload = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payloadBase64))
        return $payload | ConvertFrom-Json
    } catch {
        Write-Error "Failed to decode or parse JSON payload. Ensure the token is a valid Base64-encoded string."
        return $null
    }
}

function Check-Expiration {
    param (
        [string]$Token
    )

    $payload = Decode-JWT $Token
    if (-not $payload) {
        Write-Error "Invalid JWT or unable to decode."
        return
    }

    if (-not $payload.exp) {
        Write-Error "No expiration ('exp') field found in the token."
        return
    }

    $exp = [long]$payload.exp
    $now = [long][DateTimeOffset]::Now.ToUnixTimeSeconds()

    if ($now -lt $exp) {
        $remaining = $exp - $now
        $minutes = [math]::Floor($remaining / 60)
        Write-Host "Token expires at: $(Get-Date -Date ([datetime]::FromFileTimeUtc($exp * 10000000 + 116444736000000000)))"
        Write-Host "Time remaining: $minutes minutes."
    } else {
        Write-Host "Token has expired. Expired at: $(Get-Date -Date ([datetime]::FromFileTimeUtc($exp * 10000000 + 116444736000000000)))"
    }
}

if ($args.Count -eq 0) {
    Write-Error "No JWT provided."
    return
}

$Token = $args[0]

Decode-JWT $Token
Check-Expiration $Token
Write-Host ""

You can check it like this:

./jwt.ps1 eddyJhbGciOiJSUzI1NiIsInRasfasff5cCI6IkpXVCJ9.ewogICJpc3MiOiAidGVzdC1zZXJ2aWNlIiwKICAiYXVkIjogIm91ci1zZXJ2aWNlIiwKICAidXNlcm5hbWUiOiAidHN0dXNyIiwKICAiaWF0IjogMTczMzcxNTI4MiwKICAiZXhwIjogMjUyMjExNTI4Mgp9.ew-4BWfh815a_pppqlDqrjvj5CY1ZS2uYdiVOeazCDutlfnxdmrqwgSSo4Ot2riT8nMOozoN-68AioEhFphem6fFTLbqDCjONIi_ncxkSUaedavzFwvJBW0HugTfYpoUTq0uR344ffffftEaPCPg6n3GrPKFTEitzoYuVJdMWjHa_IHVzDygpMmErjBr43qPDU9TGQaYtcWFvtDOgNvGwdUl5KTauW716HBvbvBgO4zLqH_B9c80StOSt8L2aJiX6Ue5ZMgmipPdddK4mK2Vb9PoikUftv1bej_prvHBOYYVAwq-de9Ewn7P-12KVRMpRy9kuymqBgJtXVbDXdddUQY1sVwhG9cR_ryA

iss      : test-service
aud      : our-service
username : tstusr
iat      : 1733715282
exp      : 2522115282

Token expires at: 12/03/2049 03:34:42
Time remaining: 13139952 minutes.

Final thoughts

So debugging JWT locally is super easy. Extending these scripts can make it easier to check if your token is expired or not.

Here is a pure bash version of the Bash script that does not use JQ. I did not include it because of licensing.

The code is on GitHub, so check it out: code gallery / 2. simple JWT access policies for API security in .NET.

expand_less brightness_auto