Inspecting and Validating JSON Web Tokens
Access tokens routinely carry tenant identifiers, user roles, permission scopes, and lifecycle timestamps (exp, nbf, iat). When testing token issuance pipelines or debugging authorization middleware, inspecting claims and verifying signatures helps pinpoint expiration mismatches and secret key misconfigurations.
This debugger parses tokens, decodes base64URL segments, formats header and payload JSON, and verifies HMAC signatures (HS256, HS384, HS512) directly in your browser using Go’s standard library.
Anatomy of a JSON Web Token
A JSON Web Token (RFC 7519) consists of three base64URL-encoded strings separated by periods:
1<Header> . <Payload> . <Signature>1. Header
The header defines the metadata for the token, primarily the cryptographic algorithm used to generate the signature (alg) and the token type (typ):
1{
2 "alg": "HS256",
3 "typ": "JWT"
4}2. Payload (Claims)
The payload contains the claims. Claims are statements about an entity (typically the user or service) along with additional metadata. Claims fall into three categories: registered, public, and private.
3. Signature
The signature ensures the token has not been tampered with in transit. For symmetric HMAC algorithms like HS256, it is computed by hashing the header and payload with a shared secret:
1HMACSHA256(
2 base64UrlEncode(header) + "." + base64UrlEncode(payload),
3 secret
4)Registered Claims Reference
The JWT specification (RFC 7519) reserves seven standard claim keys. While none are strictly mandatory according to the specification, production authentication systems rely on them for session lifecycle management:
| Claim | Name | Format | Description |
|---|---|---|---|
| iss | Issuer | String | Identifies the principal that issued the JWT (e.g. auth.lorbic.com) |
| sub | Subject | String | Identifies the principal that is the subject of the JWT (e.g. user_9921) |
| aud | Audience | String or Array | Identifies the recipients that the JWT is intended for |
| exp | Expiration Time | NumericDate (Seconds) | Timestamp on or after which the JWT must not be accepted |
| nbf | Not Before | NumericDate (Seconds) | Timestamp before which the JWT must not be accepted |
| iat | Issued At | NumericDate (Seconds) | Timestamp at which the JWT was issued |
| jti | JWT ID | String | Unique identifier for one-time token replay prevention |
Common Production Vulnerabilities
When rolling out custom JWT validation in backend services, several well-known design flaws frequently appear in security audits:
The alg: none Bypass
Early JWT specifications allowed an algorithm value of none for unsigned tokens. If a backend verification library blindly trusts the header algorithm without validating it against an allowed list, an attacker can modify the payload (e.g. changing "role": "user" to "role": "admin"), set "alg": "none", strip the signature segment, and bypass authentication entirely.
Always enforce an explicit whitelist of acceptable signing algorithms in your backend verifier.
Key Confusion Attack (RS256 vs HS256)
If your authentication service issues tokens signed with an asymmetric private key (RS256) and exposes the corresponding public key for consumers to verify, an attacker might modify the token header to "alg": "HS256".
If the backend uses the same verification function for both algorithms and passes the RSA public key as the secret, HMAC will use the public key bytes as the symmetric secret. Because the RSA public key is known to everyone, the attacker can forge arbitrary valid tokens signed with that public key.
Clock Skew
Network time protocol (NTP) drift between authentication servers and API gateways can cause tokens to be rejected immediately after issuance or accepted shortly after expiration. Production token validators should always permit a configurable clock skew window (typically 30 to 60 seconds) when comparing exp and nbf timestamps.
Validating HMAC Signatures in Go
Here is how to verify an HS256 token using only the Go standard library, taking care to use constant-time byte comparisons to prevent timing attacks:
1package main
2
3import (
4 "crypto/hmac"
5 "crypto/sha256"
6 "crypto/subtle"
7 "encoding/base64"
8 "errors"
9 "strings"
10)
11
12func VerifyHS256(token, secret string) error {
13 parts := strings.Split(token, ".")
14 if len(parts) != 3 {
15 return errors.New("invalid token format")
16 }
17
18 signingInput := parts[0] + "." + parts[1]
19
20 mac := hmac.New(sha256.New, []byte(secret))
21 mac.Write([]byte(signingInput))
22 expectedSig := mac.Sum(nil)
23
24 // Decode incoming signature using unpadded base64URL
25 actualSig, err := base64.RawURLEncoding.DecodeString(parts[2])
26 if err != nil {
27 return errors.New("failed to decode signature")
28 }
29
30 // ConstantTimeCompare prevents side-channel timing attacks
31 if subtle.ConstantTimeCompare(expectedSig, actualSig) != 1 {
32 return errors.New("signature mismatch")
33 }
34
35 return nil
36}