Published
How to Decode and Verify a JWT in JavaScript, Python, and Go
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.
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-readableIn 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
algorithmsexplicitly — never use the library default or trust the token'salgheader 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
kidto 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