Published

How to Decode and Verify a JWT in JavaScript, Python, and Go

This guide provides working code examples for decoding and verifying JWTs in the three most common backend languages. It covers two levels: raw Base64url decoding without any library (useful for understanding the format or for constrained environments) and production-ready verification using the canonical library for each language, with the security options that matter most.

Decoding vs Verifying — A Reminder

Before the code examples, a critical distinction: decoding and verifying are different operations.

  • ·Decoding extracts the JSON from the Base64url-encoded header and payload. No key is needed. Anyone can decode any JWT. Decoded claims must not be trusted for authorization without verification.
  • ·Verifying cryptographically confirms the signature using the correct key and validates the standard claims (exp, iss, aud). This is the operation your server-side code must perform before using any JWT claims.
!
Never use decoded-but-unverified JWT claims for authorization decisions in production. The decoding examples below are for debugging and educational purposes.

JavaScript: Decoding Without a Library

This pure JavaScript function decodes a JWT without any dependencies. It works in both Node.js and browser environments.

function decodeJwt(token) {
  const parts = token.split('.')
  if (parts.length !== 3) {
    throw new Error('Invalid JWT format: expected 3 dot-separated segments')
  }

  function base64urlDecode(str) {
    // Convert Base64url to standard Base64
    const b64 = str.replace(/-/g, '+').replace(/_/g, '/')
    // Add padding if needed
    const padded = b64.padEnd(b64.length + (4 - b64.length % 4) % 4, '=')
    // Decode to bytes, then to UTF-8 string
    return JSON.parse(atob(padded))
  }

  const header  = base64urlDecode(parts[0])
  const payload = base64urlDecode(parts[1])
  // parts[2] is the signature — raw binary, not JSON

  return { header, payload }
}

// Usage:
const { header, payload } = decodeJwt('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...')
console.log(header.alg)     // "RS256"
console.log(payload.sub)    // "user_123"
console.log(payload.exp)    // 1716480961 (Unix timestamp)
console.log(new Date(payload.exp * 1000).toISOString())  // human-readable

In Node.js, atob is available from Node 16. For older versions use Buffer.from(padded, 'base64').toString('utf-8').

JavaScript: Verifying with jsonwebtoken (Node.js)

For production use, use the jsonwebtoken package (the most widely used JWT library for Node.js). Install it with npm install jsonwebtoken.

Verifying an HS256 token

const jwt = require('jsonwebtoken')

function verifyHs256(token, secret) {
  return jwt.verify(token, secret, {
    algorithms: ['HS256'],            // explicit — prevents alg confusion
    issuer: 'https://auth.example.com/',
    audience: 'https://api.example.com',
    clockTolerance: 60,               // allow 60s clock skew
  })
}

try {
  const payload = verifyHs256(token, process.env.JWT_SECRET)
  console.log('Valid token for user:', payload.sub)
} catch (err) {
  if (err.name === 'TokenExpiredError') {
    console.error('Token expired at:', new Date(err.expiredAt))
  } else if (err.name === 'JsonWebTokenError') {
    console.error('Invalid token:', err.message)
  } else if (err.name === 'NotBeforeError') {
    console.error('Token not yet valid')
  }
}

Verifying an RS256 token with JWKS

const jwt = require('jsonwebtoken')
const jwksClient = require('jwks-rsa')   // npm install jwks-rsa

const client = jwksClient({
  jwksUri: 'https://auth.example.com/.well-known/jwks.json',
  cache: true,
  cacheMaxEntries: 5,
  cacheMaxAge: 10 * 60 * 1000,          // 10 minutes
})

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    if (err) return callback(err)
    callback(null, key.getPublicKey())
  })
}

function verifyRs256(token) {
  return new Promise((resolve, reject) => {
    jwt.verify(token, getKey, {
      algorithms: ['RS256'],
      issuer: 'https://auth.example.com/',
      audience: 'https://api.example.com',
      clockTolerance: 60,
    }, (err, payload) => {
      if (err) reject(err)
      else resolve(payload)
    })
  })
}

// Usage in Express middleware:
async function authenticate(req, res, next) {
  const token = req.headers.authorization?.replace(/^Bearer /, '')
  if (!token) return res.status(401).json({ error: 'Missing token' })

  try {
    req.user = await verifyRs256(token)
    next()
  } catch (err) {
    res.status(401).json({ error: err.message })
  }
}

Python: Decoding Without a Library

import json
import base64

def decode_jwt(token: str) -> dict:
    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError(f'Invalid JWT: expected 3 segments, got {len(parts)}')

    def base64url_decode(segment: str) -> dict:
        # Add padding
        padding = 4 - len(segment) % 4
        if padding != 4:
            segment += '=' * padding
        # Decode base64url → bytes → UTF-8 → JSON
        decoded = base64.urlsafe_b64decode(segment)
        return json.loads(decoded.decode('utf-8'))

    header  = base64url_decode(parts[0])
    payload = base64url_decode(parts[1])
    # parts[2] is the raw signature bytes — not JSON

    return {'header': header, 'payload': payload}

# Usage:
result = decode_jwt('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...')
print(result['header']['alg'])      # RS256
print(result['payload']['sub'])     # user_123

import datetime
exp = result['payload'].get('exp')
if exp:
    print(datetime.datetime.utcfromtimestamp(exp).isoformat())

Python: Verifying with PyJWT

Use PyJWT (2.x) for production. Install with pip install PyJWT cryptography. The cryptography package is required for RS256/ES256 algorithm support.

Verifying an RS256 token

import jwt
from jwt import PyJWKClient

ISSUER   = "https://auth.example.com/"
AUDIENCE = "https://api.example.com"
JWKS_URL = "https://auth.example.com/.well-known/jwks.json"

# jwks_client caches keys automatically
jwks_client = PyJWKClient(JWKS_URL)

def verify_rs256(token: str) -> dict:
    # Look up the signing key by kid
    signing_key = jwks_client.get_signing_key_from_jwt(token)

    payload = jwt.decode(
        token,
        signing_key,
        algorithms=["RS256"],      # explicit — never omit
        issuer=ISSUER,
        audience=AUDIENCE,         # MUST be provided — PyJWT skips aud check if omitted
        leeway=60,                 # clock skew tolerance in seconds
    )
    return payload

# Usage:
try:
    payload = verify_rs256(token)
    print("Valid token for user:", payload["sub"])
except jwt.ExpiredSignatureError:
    print("Token has expired")
except jwt.InvalidAudienceError:
    print("Wrong audience")
except jwt.InvalidIssuerError:
    print("Wrong issuer")
except jwt.InvalidSignatureError:
    print("Signature verification failed")

Verifying an HS256 token

import jwt, os

def verify_hs256(token: str) -> dict:
    return jwt.decode(
        token,
        os.environ["JWT_SECRET"],
        algorithms=["HS256"],
        issuer=ISSUER,
        audience=AUDIENCE,
        leeway=60,
    )

Go: Decoding Without a Library

package main

import (
    "encoding/base64"
    "encoding/json"
    "fmt"
    "strings"
)

type JwtParts struct {
    Header  map[string]interface{}
    Payload map[string]interface{}
}

func DecodeJwt(token string) (*JwtParts, error) {
    parts := strings.Split(token, ".")
    if len(parts) != 3 {
        return nil, fmt.Errorf("invalid JWT: expected 3 segments, got %d", len(parts))
    }

    decode := func(segment string) (map[string]interface{}, error) {
        // RawURLEncoding handles Base64url without padding
        b, err := base64.RawURLEncoding.DecodeString(segment)
        if err != nil {
            return nil, err
        }
        var result map[string]interface{}
        return result, json.Unmarshal(b, &result)
    }

    header, err := decode(parts[0])
    if err != nil {
        return nil, fmt.Errorf("decode header: %w", err)
    }

    payload, err := decode(parts[1])
    if err != nil {
        return nil, fmt.Errorf("decode payload: %w", err)
    }

    return &JwtParts{Header: header, Payload: payload}, nil
}

// Usage:
func main() {
    parts, err := DecodeJwt("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...")
    if err != nil {
        panic(err)
    }
    fmt.Println(parts.Header["alg"])   // RS256
    fmt.Println(parts.Payload["sub"])  // user_123
}

Go: Verifying with golang-jwt/jwt

Use github.com/golang-jwt/jwt/v5 for production verification. Install with go get github.com/golang-jwt/jwt/v5.

Verifying an RS256 token

package main

import (
    "crypto/rsa"
    "fmt"
    "github.com/golang-jwt/jwt/v5"
)

func VerifyRS256(tokenString string, publicKey *rsa.PublicKey) (jwt.MapClaims, error) {
    parser := jwt.NewParser(
        jwt.WithValidMethods([]string{"RS256"}),    // explicit — required
        jwt.WithIssuer("https://auth.example.com/"),
        jwt.WithAudience("https://api.example.com"),
        jwt.WithLeeway(60),                          // clock skew tolerance
    )

    token, err := parser.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
        // Verify the algorithm matches what we expect
        if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
        }
        return publicKey, nil
    })
    if err != nil {
        return nil, err
    }

    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok || !token.Valid {
        return nil, fmt.Errorf("invalid token claims")
    }

    return claims, nil
}

Loading a public key from PEM

import (
    "crypto/x509"
    "encoding/pem"
    "errors"
)

func ParseRSAPublicKey(pemBytes []byte) (*rsa.PublicKey, error) {
    block, _ := pem.Decode(pemBytes)
    if block == nil {
        return nil, errors.New("failed to decode PEM block")
    }

    pub, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return nil, err
    }

    rsaKey, ok := pub.(*rsa.PublicKey)
    if !ok {
        return nil, errors.New("not an RSA public key")
    }
    return rsaKey, nil
}

Handling key rotation in Go

// keystore maintains a map of kid → public key, refreshed from JWKS
type KeyStore struct {
    mu   sync.RWMutex
    keys map[string]*rsa.PublicKey
}

func (ks *KeyStore) GetKey(kid string) (*rsa.PublicKey, error) {
    ks.mu.RLock()
    key, ok := ks.keys[kid]
    ks.mu.RUnlock()

    if ok {
        return key, nil
    }

    // Unknown kid — refresh from JWKS endpoint
    if err := ks.Refresh(); err != nil {
        return nil, fmt.Errorf("key %q not found and JWKS refresh failed: %w", kid, err)
    }

    ks.mu.RLock()
    key, ok = ks.keys[kid]
    ks.mu.RUnlock()

    if !ok {
        return nil, fmt.Errorf("key %q not found after JWKS refresh", kid)
    }
    return key, nil
}

Testing with a Sample Token

To verify your decode implementation against a known-good token, use the sample token from this tool (click "Load sample" on the home page). The sample token uses HS256 with the secret your-256-bit-secret and contains the following payload:

{
  "sub":   "1234567890",
  "name":  "Jane Developer",
  "iat":   1516239022,
  "exp":   <current time + 1 hour>,
  "iss":   "https://jwtdecode.app",
  "roles": ["user"]
}

You can also construct test tokens manually using JWT libraries in your language of choice, or use the golang-jwt/jwt, PyJWT, or jsonwebtoken libraries to sign test tokens with a known key, then verify that your decode and verify implementations produce the expected output.

Security Checklist for JWT Code

  • ·Always specify algorithms explicitly — never use the library default or trust the token's alg header alone
  • ·Always validate issuer — use exact string matching
  • ·Always validate audience — PyJWT skips this if omitted; check your library's default behaviour
  • ·Set clock skew tolerance to 30–60 seconds — never higher
  • ·Use kid to select the correct key when multiple keys exist
  • ·Cache JWKS responses — don't fetch the JWKS on every request
  • ·Refresh the JWKS cache when you encounter an unknown kid
  • ·Handle all error types explicitly — do not swallow JWT errors silently
  • ·Never implement JWT signature verification from scratch — use a reviewed library
Ready to decode a token?
Use the free JWT decoder — paste any token for instant results, entirely in your browser.
Open JWT Decoder