JWT Shape, Nothing Else
A JWT (JSON Web Token, RFC 7519) is three segments separated by dots. Each segment is base64url-encoded — the alphabet is A-Z a-z 0-9 _ - and padding is dropped. The signature segment can legally be empty when the alg header is none.
<header>.<payload>.<signature>
# Each segment uses base64url: [A-Za-z0-9_-]+
# Dots are the only separator; three segments total.
# Signature can be empty (alg: none) → "x.y."
# Canonical example from jwt.io:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cThe Strict Validator
Three segments, correct character set, dots in the right positions, anchored to start and end so no sneaky prefixes slip through:
const JWT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
JWT.test("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.sig"); // true
JWT.test("eyJhbGciOiJub25lIn0.eyJzdWIiOiIxIn0."); // true — unsigned
JWT.test("not.a.jwt.has.too.many.dots"); // false
JWT.test("no-dots-at-all"); // false
JWT.test("one.dot"); // false — only 2 segments
JWT.test("has.spaces not.allowed"); // false
// For signed JWTs ONLY (reject alg: none shape):
const JWT_SIGNED = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;Detecting JWTs in Logs
Secret-scanning tools and SIEM pipelines need to find JWTs inside multi-megabyte log files and tell them apart from random hex strings, UUIDs, and base64 blobs. The eyJ prefix is the strongest free signal: every JWT header that is a JSON object starting with {" base64url-encodes to eyJ. False-positive rate is extremely low.
const JWT_LEAK = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\b/g;
const log = `
2026-10-07 10:34 user=alice loaded dashboard
2026-10-07 10:35 ERROR 401 token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abc123
2026-10-07 10:36 trace=12e4ad... refresh=eyJhbGciOiJIUzI1NiJ9.eyJyIjoiMSJ9.xyz
`;
[...log.matchAll(JWT_LEAK)];
// → 2 matches: both JWTs, with no false positives on the trace or user nameExtracting from Authorization Headers
HTTP APIs almost always transport JWTs in Authorization: Bearer <token>. Three variants to parse:
// Strict — exactly one space, Bearer case-sensitive (OAuth2 §2.1)
const BEARER_STRICT = /^Bearer ([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*)$/;
// Lenient — any whitespace, case-insensitive (many SDKs are sloppy)
const BEARER_LENIENT = /^Bearer\s+([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*)$/i;
// Just a JWT, with or without "Bearer" prefix
const JWT_FLEX = /^(?:Bearer\s+)?([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*)$/i;
const header = "Bearer eyJhbGci...j9.eyJzdWIiOiIxIn0.sig";
const [, token] = BEARER_LENIENT.exec(header) || [];
// token = "eyJhbGci...j9.eyJzdWIiOiIxIn0.sig"Parsing the Three Segments
Once the shape passes, splitting into segments is trivial. Named groups make the result self-documenting:
const JWT_PARTS = /^(?<header>[A-Za-z0-9_-]+)\.(?<payload>[A-Za-z0-9_-]+)\.(?<signature>[A-Za-z0-9_-]*)$/;
function parseJwtShape(jwt) {
const m = JWT_PARTS.exec(jwt);
if (!m) throw new Error('Not a well-formed JWT');
return m.groups;
}
const { header, payload, signature } = parseJwtShape(
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.SflKxwRJ..."
);
// header = "eyJhbGciOiJIUzI1NiJ9"
// payload = "eyJzdWIiOiIxIn0"
// signature = "SflKxwRJ..."
// Decode header to peek at the algorithm WITHOUT verifying signature
// (useful when picking a JWKS key by kid):
function peekHeader(jwt) {
const { header } = parseJwtShape(jwt);
const bytes = Buffer.from(header, 'base64url');
return JSON.parse(bytes.toString('utf8'));
}
peekHeader(jwt); // { alg: "HS256", typ: "JWT" }Language-Specific Usage
JavaScript / TypeScript
const JWT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/;
function looksLikeJwt(s) { return typeof s === 'string' && JWT.test(s); }
// Combined with jsonwebtoken for truth:
import jwt from 'jsonwebtoken';
function safeVerify(token, secret) {
if (!looksLikeJwt(token)) throw new Error('Not a JWT shape');
return jwt.verify(token, secret);
}Python
import re
JWT = re.compile(r'^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$')
JWT_LEAK = re.compile(r'\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\b')
def looks_like_jwt(s: str) -> bool:
return bool(JWT.match(s))
def scan_for_jwts(text: str):
return JWT_LEAK.findall(text)
# Combined with PyJWT for verification:
# import jwt
# payload = jwt.decode(token, key, algorithms=["HS256"])Go
package main
import "regexp"
var jwtRe = regexp.MustCompile("^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]*$")
func LooksLikeJWT(s string) bool { return jwtRe.MatchString(s) }
// Pair with github.com/golang-jwt/jwt/v5 for parsing and verification.Common Pitfalls
Thinking the signature segment is required
The JWT spec allows alg: none, which produces a token with an empty signature segment: eyJhbGciOiJub25lIn0.eyJzdWIiOiIxIn0.. Any production code should reject these before verification, but your regex may need to accept them duringparsing. That is why the strict pattern uses * on the final group, not +. Reject them at the policy layer, not the syntax layer.
Padding characters sneaking in
Standard base64 produces padding with =, but JWTs use base64url which drops padding entirely. If you see a = inside a JWT, the token has been corrupted or incorrectly re-encoded. The strict regex rejects any string containing =, which is correct.
Line breaks and quotes
Copy-pasting a long JWT from a terminal often inserts line breaks or wrapping quotes. Strip them before applying the regex: s.replace(/\s|"|'/g, ''). The regex itself must stay strict — a JWT with internal whitespace is not a valid JWT.
Treating regex pass as "token is valid"
Regex says the shape is right. It cannot verify the signature, cannot check expiry (exp), cannot check iss or aud claims, and cannot tell you if the alg matches what you expect (the famous alg: nonedowngrade attack). All of those are the JWT library's job.
JWT Regex Cheatsheet
| Goal | Pattern | Notes |
|---|---|---|
| Strict shape | /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/ | allows alg:none |
| Signed only | /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/ | rejects alg:none |
| Log detector | /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\b/g | eyJ prefix |
| Bearer header | /^Bearer\s+([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*)$/i | captures token |
| Named parts | /^(?<h>[A-Za-z0-9_-]+)\.(?<p>[A-Za-z0-9_-]+)\.(?<s>[A-Za-z0-9_-]*)$/ | for parsing |
Testing Your JWT Regex
Open the live Regex Tester and paste this canonical test block:
# VALID — should ALL match /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0. # alg: none, empty signature
header.payload.signature # minimal shape
a-b_c.d-e_f.g-h_i # all URL-safe chars
# INVALID — should ALL fail
too.few # 2 segments
too.many.dots.here.sorry # 4 segments
has spaces.not.allowed # whitespace
contains+plus.not.allowed # + is std base64, not url
ends=in=equals.not.allowed # no padding in JWTs
.leading.dot # empty header
trailing. # 2 segments (empty 2nd)Pair With the JWT Debugger
A regex that matches is not a token that works. Once your pattern says yes, hand the string to the PromptSpace JWT Debugger to decode header + payload, inspect the alg, iss, aud, exp claims, and (if you provide the key) verify the signature. The debugger supports HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512 and PS256.
When to Reach for a Library
Use regex at the edge: form validation, log scanning, Authorization header parsing. Use a JWT library wherever the token actually controls a decision: jsonwebtoken or jose for Node, PyJWT or python-jose for Python, java-jwt or jose4j for Java, golang-jwt for Go. These libraries handle base64url decoding, JSON parsing, signature verification against JWKS, and clock-skew-tolerant expiry checks — all the things regex deliberately does not.