keepcalculator

06 Numbers and Powers

JWT Signature Verification Without a Library: Safe Manual Checklist?

Card No. 06·31101 · Updated · Standard formula, shown below

Total token length 200 chars
Overhead (header+signature)
65 chars
Result
Payload share
25.0 %
Result
Token length
200 chars
You entered
You enter
Token length, Header length, Payload length, Signature length
You get
Total token length, Overhead (header+signature), Payload share
Rounding
0 dp · 1 dp

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 alg field 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.

  1. Strict split: Reject any token that does not have exactly two dots. Do not use a regex that allows extra dots.
  2. Algorithm whitelist: Do not trust the alg header blindly. Only accept a fixed set (e.g., HS256, RS256). Reject none, HS384 if you only use HS256, and any algorithm you do not explicitly support.
  3. 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.
  4. Base64url decoding: Use a decoder that handles the URL-safe alphabet (- and _) and adds padding correctly. Many standard base64 decoders fail or misinterpret.
  5. 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.
  6. Constant-time comparison: Use a function like crypto.timingSafeEqual (Node.js) or hmac.compare_digest (Python). A simple string equality check leaks timing information.
  7. Expiration and claims: After signature verification, check exp, nbf, iss, and aud as required. Signature verification alone does not mean the token is valid.
  8. 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., crypto in 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

The whole Numbers and Powers drawer →