1. The alg:none attack
The JWT specification includes an alg header that tells the server which algorithm to use for signature verification. If your JWT library accepts "alg": "none", an attacker can craft a token with any payload, set the algorithm to none, remove the signature entirely, and your server will treat it as valid.
This is not theoretical. Older versions of popular libraries like jsonwebtoken (Node.js), PyJWT, and php-jwt were vulnerable. The fix: always specify the allowed algorithms explicitly when verifying tokens. Never let the token itself dictate which algorithm to use.
// VULNERABLE — trusts the token's alg claim
jwt.verify(token, secret);
// SAFE — explicitly restricts allowed algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] }); 2. Weak signing secrets
If you use HMAC-based algorithms (HS256, HS384, HS512), the security of every token depends entirely on the strength of your secret. We've cracked JWT secrets during pentests using hashcat with a wordlist in under 10 minutes. Common weak secrets: secret, password, company names, your-256-bit-secret (the default from jwt.io).
Generate secrets with at least 256 bits of entropy: openssl rand -base64 64. Store them in a secret manager, not in source code or .env files committed to git. Rotate the secret on a defined schedule and whenever team members with access leave.
JWT secret cracked from wordlist
During a security assessment of a Nigerian mobile money platform, we extracted a JWT from the mobile app's traffic, cracked the HMAC secret using hashcat in 8 minutes (the secret was the company name in lowercase), and forged admin-level tokens. Full account takeover of any user was possible.
3. No token expiration
JWTs without an exp (expiration) claim are valid forever. If a token leaks through logs, a compromised device, or a man-in-the-middle attack, the attacker has permanent access. This is especially dangerous for fintech where tokens grant access to financial operations.
Set short expiration times for access tokens — 15 minutes is a good baseline for fintech. Use refresh tokens (stored securely, with rotation) to issue new access tokens without forcing re-authentication. Always validate the exp claim server-side. Libraries handle this by default, but verify your configuration doesn't disable the check.
4. Storing tokens in localStorage
For web applications, storing JWTs in localStorage or sessionStorage exposes them to cross-site scripting (XSS) attacks. A single XSS vulnerability anywhere on your domain allows an attacker to execute localStorage.getItem('token') and exfiltrate every user's JWT.
For SPAs, the more secure approach is to store tokens in httpOnly, secure, sameSite cookies. The browser sends them automatically, and JavaScript cannot access them — eliminating the XSS exfiltration vector. Yes, this requires CSRF protection, but CSRF is a solved problem with sameSite=Lax or sameSite=Strict cookies and anti-CSRF tokens.
For mobile apps, use the platform's secure storage: Android Keystore or iOS Keychain. Never store tokens in SharedPreferences (Android) or UserDefaults (iOS) without encryption.
5. Not validating issuer and audience
The iss (issuer) and aud (audience) claims tell the server who created the token and who it was created for. If you don't validate these, a token from one service can be used to authenticate against a completely different service — especially dangerous in microservices architectures where multiple services share infrastructure.
// Set claims when creating the token
const token = jwt.sign(
{ userId: user.id },
secret,
{
algorithm: 'HS256',
expiresIn: '15m',
issuer: 'api.yourfintech.com',
audience: 'app.yourfintech.com'
}
);
// Validate claims when verifying
jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'api.yourfintech.com',
audience: 'app.yourfintech.com'
}); 6. Missing token revocation
JWTs are stateless by design — the server doesn't store them. This means you can't "log out" a user by deleting a session. If a user's account is compromised, if they change their password, or if you need to force a logout, the existing JWT remains valid until it expires.
For fintech, you need a revocation mechanism. Common approaches:
- Token blacklist — store revoked token IDs (the
jticlaim) in Redis with a TTL matching the token's remaining lifetime. Check the blacklist on every request. - Token version — store a version number on the user record. Include it in the JWT payload. Increment it on password change, forced logout, or account compromise. Reject tokens with a stale version.
- Short expiration + refresh rotation — keep access tokens at 15 minutes. When a refresh token is used, invalidate it immediately and issue a new pair. If a refresh token is used twice (replay detection), revoke all tokens for that user.
Not sure if your JWT implementation has these vulnerabilities? We test authentication flows in every API security engagement.
Get an authentication security audit7. Symmetric vs asymmetric keys
HMAC algorithms (HS256) use a single shared secret for both signing and verification. This means every service that needs to verify tokens must have the secret — and any service with the secret can forge tokens. In a monolithic API, this is fine. In a microservices architecture, it's a single-point-of-compromise risk.
RSA or ECDSA algorithms (RS256, ES256) use a private key for signing and a public key for verification. Only the auth service holds the private key. All other services verify with the public key — they can confirm a token is legitimate but cannot create new ones. For any fintech with multiple services, asymmetric signing is the correct choice.
There's also the algorithm confusion attack: if your server is configured for RS256 but accepts HS256, an attacker can use the RSA public key (which is public) as the HMAC secret and forge valid tokens. Prevent this by always specifying allowed algorithms explicitly during verification.
Implementation checklist
- Specify allowed algorithms explicitly in every
verify()call - Use 256+ bit random secrets for HMAC, or RSA/ECDSA for multi-service architectures
- Set
expto 15 minutes for access tokens, implement refresh token rotation - Store tokens in
httpOnlycookies (web) or secure storage (mobile), neverlocalStorage - Validate
iss,aud, andexpon every request - Implement token revocation via blacklist, versioning, or refresh rotation
- Use asymmetric keys (RS256/ES256) when multiple services verify tokens
JWTs are not secure by default
JWTs are a token format, not a security solution. Their security depends entirely on how you configure signing, expiration, storage, validation, and revocation. Get any one of these wrong in a fintech context, and you've handed attackers the keys to your users' financial data.
Related reading
Blog: Securing Express APIs · Securing Django APIs · Microservices security
Guides: OWASP for fintech · Security checklist · Pentest report explained
Services: Authentication security · API security · Penetration testing