Yes, you can safely verify a JWT signature without a library, but only if you follow a strict, complete checklist that covers algorithm confusion, key handling, and strict parsing. This article gives you that checklist, explains each step, and highlights the common pitfalls that make manual verification risky.
Formula
Total token length = header length + payload length + signature length + 2 (for the two dots). Payload share = payload length / total length × 100. This helps you estimate how much of your token is actual data versus fixed overhead.
Worked Example
Using the defaults: total = 20 + 50 + 43 + 2 = 115 chars. Overhead = 20 + 43 + 2 = 65 chars. Payload share = 50 / 115 × 100 = 43.5%. This shows a typical JWT is more than half overhead.
Background: What a JWT Actually Is
A JSON Web Token (JWT) is a compact, URL-safe string with three parts separated by dots: header.payload.signature. Each part is base64url-encoded JSON (except the signature, which is raw bytes encoded as base64url). The header typically contains alg (algorithm) and typ (type). The payload contains claims like sub (subject), exp (expiration), and iat (issued at). The signature is computed over the ASCII string header.payload using the algorithm specified in the header.
Libraries abstract away the details, but they also enforce security checks. When you verify manually, you take on every responsibility the library would have handled. The risk is not in the math—HMAC-SHA256 or RSA-SHA256 are straightforward—but in the edge cases and attack vectors that libraries are specifically designed to block.
How Signature Verification Works
For HMAC (symmetric), the verification is: compute HMAC-SHA256(secret, base64url(header) + “.” + base64url(payload)) and compare it to the provided signature using a constant-time comparison. For RSA or ECDSA (asymmetric), you use the public key to verify the signature against the same string. The key point is that the input to the signature function is exactly the first two parts of the token, with the dot, not the decoded JSON.
Here is the minimal manual process:
- Split the token on “.”—you must get exactly three parts.
- Base64url-decode the header and payload (with padding added correctly).
- Parse the JSON and check the
algfield against a whitelist. - Re-encode the header and payload exactly as they appeared (do not re-serialize JSON, as whitespace changes break the signature).
- Compute the signature using the correct algorithm and key.
- Compare signatures in constant time.
The Manual Verification Checklist
If you must verify manually, this checklist is non-negotiable. Missing any step can lead to accepting a forged token.
- Strict split: Reject any token that does not have exactly two dots. Do not use a regex that allows extra dots.
- Algorithm whitelist: Do not trust the
algheader blindly. Only accept a fixed set (e.g.,HS256,RS256). Rejectnone,HS384if you only use HS256, and any algorithm you do not explicitly support. - Key selection: Choose the key based on the expected algorithm, not the header. If you expect RS256 but the header says HS256, an attacker can sign with the public key as if it were a symmetric secret. This is the classic algorithm confusion attack.
- Base64url decoding: Use a decoder that handles the URL-safe alphabet (
-and_) and adds padding correctly. Many standard base64 decoders fail or misinterpret. - Signature input: The signature is computed over the exact ASCII bytes of
header.payload(with the dot). Do not decode the header and re-encode it—any whitespace or key order change breaks the signature. - Constant-time comparison: Use a function like
crypto.timingSafeEqual(Node.js) orhmac.compare_digest(Python). A simple string equality check leaks timing information. - Expiration and claims: After signature verification, check
exp,nbf,iss, andaudas required. Signature verification alone does not mean the token is valid. - Error handling: On any failure—malformed JSON, bad base64, unexpected alg—reject the token. Do not partially accept.
Common Problems and Real-World Attacks
The most famous attack is the algorithm confusion (or “none� attack). If your server expects RS256 but does not check the header, an attacker sends a token with alg: HS256 and signs it with the public key (which is often public). The server, thinking it is HMAC, uses the public key as the secret and accepts the forged token. This is why the algorithm whitelist is critical.
Another issue is key confusion when you have multiple keys (e.g., different audiences). If you pick the key based on a kid header without validating it against a trusted list, an attacker can point to any key they control. Always map kid to a pre-registered key ID, and never use a key that is not in your configuration.
Also, beware of base64url variants. Some decoders accept standard base64 with + and /, which can cause parsing differences. The JWT spec requires the URL-safe alphabet. If your decoder is too permissive, you might accept a token that another system would reject—or worse, one where the signature verification passes on a malformed string.
How to Choose: Library vs. Manual
If you are building a production system, use a well-maintained library. Libraries like jsonwebtoken (Node.js), PyJWT (Python), or java-jwt (Java) have been audited and handle edge cases. They also enforce best practices by default, such as rejecting alg: none and providing constant-time comparison.
Manual verification is only reasonable when:
- You are on a platform with no library (e.g., a constrained embedded system).
- You are learning or auditing a library’s behavior.
- You have a very narrow use case with a single algorithm and a single key.
If you choose manual, you must write and maintain the security-critical code yourself. A single mistake can compromise your entire authentication system. The checklist above is the minimum; even then, you are re-implementing what a library already does.
Safety and Security Considerations
The biggest safety concern is that manual verification often leads to incomplete checks. Developers verify the signature but forget to check the algorithm, or they check the algorithm but not the key ID. Another common mistake is using a generic JSON parser that silently accepts duplicate keys or invalid UTF-8, which can lead to claim confusion.
Also, do not log the token or its parts. JWTs often contain sensitive claims. If you must debug, log only the header and payload after verification, and redact the signature.
Finally, always use a constant-time comparison. Even if your application is not high-value, timing attacks are practical and well-documented. Do not try to implement your own constant-time function; use the language’s built-in secure comparison.
Alternatives to Manual Verification
If you cannot use a full JWT library, consider these lighter alternatives:
- Use a crypto primitive library only (e.g.,
cryptoin Node.js) and implement the JWT parsing yourself. This is still manual but reduces the risk of using a broken base64 function. - Use a JWT validation library that is minimal—some libraries only verify signatures without claim validation, which is safer than doing everything yourself.
- Use a different token format—if you control both issuer and verifier, you can use a simple HMAC over a JSON string with a timestamp. But this is not standardized and may be harder to interoperate.
- Use a managed service—if you are on a cloud provider, their identity services often handle JWT verification for you (e.g., AWS Cognito, Firebase Auth). This offloads the security burden entirely.
Remember that “no library� does not mean “no security.� The goal is to reduce the attack surface, not to prove you can do it from scratch.
Frequently Asked Questions
Is it ever safe to verify a JWT manually in production?
It can be safe if you strictly follow the checklist above, use a well-audited crypto library for the signature primitive, and have a single, fixed algorithm and key. However, the risk of subtle mistakes is high, and a library would handle these cases for you. If you are not absolutely certain about every step, use a library.
What is the algorithm confusion attack?
An attacker changes the alg header to HS256 while the server expects RS256. The server, if it blindly trusts the header, uses the RSA public key as the HMAC secret. The attacker signs the token with that public key, and the server accepts it. The fix is to never trust the header; always use a fixed algorithm.
Why must I use constant-time comparison for signatures?
If you compare signatures with a normal string equality, the comparison stops at the first mismatched byte. An attacker can measure the time it takes and deduce the correct bytes one by one. Constant-time comparison always takes the same time, regardless of where the mismatch occurs, preventing this timing side-channel.
Can I decode the header and re-encode it before verifying?
No. The signature is computed over the exact bytes of the header and payload as they appear in the token, including any whitespace or key order. If you decode and re-encode, the bytes may change, and the signature will fail. Always use the original strings.
What if my language doesn’t have a JWT library?
You can still use the standard crypto library to compute HMAC or RSA signatures, and then implement the base64url encoding and JSON parsing yourself. Follow the checklist carefully, and consider using a minimal library for the parsing part if one exists. If not, add extensive tests for edge cases.
Is the kid header safe to trust?
No. The kid header is just a string. You must map it to a key that you have pre-registered in your configuration. If you accept any kid and look it up dynamically, an attacker can point to a key they control. Always validate kid against a fixed list.
Related
Formula used
in t | Token length | chars | 200 | 0 | 10000 in h | Header length | chars | 20 | 0 | 1000 in p | Payload length | chars | 50 | 0 | 10000 in s | Signature length | chars | 43 | 0 | 1000 out total | Total token length | chars | t | 0 out overhead | Overhead (header+signature) | chars | h + s + 2 | 0 out payload_pct | Payload share | % | p / t * 100 | 1
Each line is one input (in, sel, list) or one result (out: label, unit, formula, decimals). How the calculators work
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- Secure Boot Battlefield 6 ASUS: Fix & Enable Guide
- Best Tech Websites to Follow for Pros & Beginners
- Second Wind Quest Walkthrough: Where Winds Meet6.00 min/km
- Enshrouded Weapon Upgrade Guide: How to Improve Gear5.00 min/km
- Connect Phone to Controller for Cloud Gaming
- Python Search Highlighting: Snippets & Matches