"Our login module was custom-built by the previous development company, but honestly, we don't know if it's secure."—When taking over maintenance in custom development, we are frequently consulted about such concerns. Authentication resides deep within a system, and day-to-day it appears to function smoothly. For that very reason, even if holes exist in token verification logic, operations continue unnoticed, and code gets handed over without anyone being able to explain what is happening inside. Just because it works does not mean it works securely.
The impetus hit close to home. In early 2026, a vulnerability in the JWT/JWK verification middleware of the lightweight framework Hono was discovered and patched, where signature verification algorithms were dictated by token headers sent by attackers. Even in widely used libraries, JWT verification harbors such pitfalls if vigilance slips. Inherited authentication code written from scratch by a predecessor is all the more reason for careful inspection. In this article, from the perspective of custom maintenance, we organize how to detect typical vulnerabilities lurking in inherited authentication middleware and fix them safely.
The "Thought We Signed It" Vulnerability in JWT Verification
A JSON Web Token (JWT) comprises three concatenated parts: header, payload, and signature. The server verifies the signature of received tokens, confirming that "this was genuinely issued by me and contents have not been tampered with" before authenticating the user. Conversely, if signature verification is lax, attackers can forge arbitrary contents, and the server accepts the token as legitimate. This dynamic underlies almost all JWT vulnerabilities.
The most classic is the alg=none attack. The JWT header contains an alg field indicating the algorithm used to sign the token. Specifying none here signifies "unsecured/no signature"; if verification logic accepts this naively, tokens with forged payloads and empty signatures will pass through.
# 攻撃者が作る改ざんトークン(イメージ)
ヘッダー : {"alg":"none","typ":"JWT"}
ペイロード: {"sub":"attacker","role":"admin"} ← 自分を管理者に
署名 : (空)
When the verifier is structured to "trust the alg header value and verify using that method," it reads none, deems signature checking unnecessary, and accepts the fraudulent token. Simply modifying role to admin allows impersonating an administrator—a classic authentication bypass (PortSwigger). Regardless of the intended signing algorithm, this deep-seated issue can occur.
"Algorithm Confusion" and the Hono Case Study
Harder to detect than alg=none is algorithm confusion. Signing methods fall broadly into two families: symmetric key schemes like HS256 (using the same secret key for signing and verification) and asymmetric public key schemes like RS256 (signing with a private key, verifying with a public key).
An attack succeeds when the verifier "trusts the alg in the token header to switch verification methods." When a system operates on RS256 (asymmetric) and an attacker rewrites the header to HS256, the server attempts to verify using symmetric key logic. In implementations that mistakenly use the publicly accessible public key as the symmetric secret key (HMAC secret), attackers can forge signatures using that public key, which is available to anyone. This flaw permits forging legitimate tokens without ever knowing the private key (PortSwigger).
The Hono issue mentioned earlier falls into this confusion category. In JWK/JWKS verification prior to version 4.11.4, if selected keys lacked an explicit algorithm parameter, the alg value in the token header could influence signature verification. Furthermore, the server did not check whether the expected algorithm matched the token's alg. In patched versions, explicitly specifying the alg option became mandatory, refactoring the design so that verification algorithms are never derived from untrusted header values (CVE-2026-22817).
The takeaway is clear: pin the verification algorithm on the server using an allowlist, and never switch methods based on what the token header claims. Whether using custom implementations or libraries, failing to follow this leaves the door wide open.
A "Valid Signature" Is Not Enough — Recipient Verification
A valid signature alone does not ensure safety. Even with a legitimate signature, failing to verify who the token was issued by and intended for leads to separate vulnerabilities. This is where verifying iss (issuer) and aud (audience/recipient) comes in.
Similarly, in Hono, the JWT authentication middleware did not have a mechanism for aud (Audience) verification. In environments where multiple services share the same issuer and key, there was a risk that a valid token issued for another service could be accepted (CVE-2025-62610, CVSS 8.1). RFC 7519 specifies: "If an aud claim is present, each principal processing the JWT MUST identify itself with a value in the aud claim. If not, the JWT MUST be rejected." Starting with version 4.10.2, it is now possible to specify verification.aud for verification.
In short, JWT verification requires confirming more than just signature correctness. You must intentionally verify all four items: signature, issuer (iss), audience (aud), and expiration (exp) (Curity: JWT Best Practices). It is well worth inspecting whether inherited code allows any of these to pass unchecked.
Inspection Procedure When Taking Over a System
When inspecting authentication in an inherited system, review items in the following order.
- Is the verification algorithm pinned? Check whether
algorithmspassed to the verification function is pinned via an allowlist, or left to the token'salgclaim. Ensurenoneis not included in the allowlist. - Are
issandaudverified? Check whether issuer and audience match expectations. This is especially critical when multiple services share signing keys. - Handling of expiration and clock skew. Confirm
expis verified and clock skew tolerance is not excessively permissive. - Robustness of key retrieval (JWKS). If public keys are fetched from a JWKS endpoint, ensure caching strategies and key rotation support are appropriate. A safe pattern is refetching once when
kid(key ID) is missing, and rejecting if still not found (MojoAuth). - Verification library versions. Check whether libraries—such as
jsonwebtoken,jose, or framework-bundled middleware—have known CVEs or remain on neglected legacy versions.
Note that errors appearing as "auth failures" in browsers can stem from different layers. Because preflight failures are easily confused with authentication errors—as covered in How to Properly Fix CORS Errors—isolating that before suspecting authentication code prevents wasted effort.
The table below contrasts patterns found in insecure versus secure verification code.
| Verification Item | Insecure Pattern | Secure Pattern |
|---|---|---|
| Algorithm | Left to header alg | Pinned / Allowlisted on server |
| Signing Key | Public key can be misused as HMAC secret | Strictly isolated keys per algorithm |
| Audience (aud) | Unverified | Mandates own service's aud |
| Key Retrieval (JWKS) | Bypasses verification on retrieval failure | Refetches on kid miss; rejects if absent |
Proper verification generally settles into the following structure.
// 検証方式とaudをサーバー側で固定し、ヘッダーには委ねない
import { jwtVerify, createRemoteJWKSet } from 'jose'
const JWKS = createRemoteJWKSet(new URL('https://issuer.example/.well-known/jwks.json'))
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ['RS256'], // alg=none・混同を封じる
issuer: 'https://issuer.example', // iss を検証
audience: 'service-a', // aud を検証
})
Handling externally retrieved keys or responses requires robust HTTP client architecture, as explored in our article on building resilient HTTP clients with the fetch API; otherwise, the system falters before verification even starts. Authentication represents one piece of overall web application security, so reviewing our Web Security Fundamentals alongside this will help minimize blind spots during audits.
How We Inspected Authentication Code We Took Over
In a membership service whose maintenance we took over (company name withheld), post-login API authentication was custom-built by the predecessor, verifying JWTs through proprietary logic. Handover documentation merely stated "authenticated via JWT," with nobody able to explain the underlying verification process.
Upon reading the code, while signature verification was performed, the verification algorithm was extracted from alg in the token header, adopting that method dynamically. Despite operating on RS256, changing the header allowed switching verification algorithms, creating a vulnerable structure where algorithm confusion was feasible. Furthermore, aud was completely unverified, allowing tokens intended for other internal systems sharing the issuance infrastructure to pass through this API. Fortunately, no signs of exploitation were found, but left unaddressed, it could have led to unauthorized account impersonation.
The fix was not adding clever logic, but reclaiming verification assumptions on the server side. We pinned the verification algorithm to ['RS256'] in code so it could not be overridden by claims in the token header. We made validation of iss and aud mandatory against our service's values. Finally, we replaced custom verification logic with calls to an actively maintained verification library. Prior to deployment, we verified through testing that legitimate tokens passed while tampered tokens were rejected. The rationale for pinning this configuration was recorded to prevent successors from reopening the same vulnerability.
What worked well in this project was setting aside the assumption that "it's running, so it must be secure" and re-verifying the signature, issuer, audience, and expiration one by one. Because authentication rarely throws errors during normal operation, vulnerabilities often go unnoticed in production. Taking over a system presents one of the few opportunities to thoroughly inspect its internals.
Where to look first
If you have concerns about authentication in a system you have taken over, the very first thing to check is whether the verification algorithm is strictly pinned on the server side. If this is left up to the token header, alg=none and algorithm confusion attacks can both succeed. Next, checking whether aud and iss are verified, along with inspecting the version of the verification library, will give you a good idea of just how vulnerable the authentication code really is.
If you are unsure whether the custom authentication in a system you took over is secure, want to inspect whether JWT verification is handled properly, or need to safely replace an outdated verification library, please reach out through GleamHub's contact page. We will review your current authentication code, verify that signatures, issuers, audiences, and expiration dates are properly validated, and work with you to design a phased migration to secure verification without disrupting ongoing operations.
Sources
- Improper Authorization in hono(CVE-2025-62610) - GitHub Security Advisory
- CVE-2026-22817: Hono JWT Alg Confusion Bypass - Miggo
- Lab: JWT authentication bypass via flawed signature verification - PortSwigger Web Security Academy
- Algorithm confusion attacks - PortSwigger Web Security Academy
- JWT Security Best Practices: Checklist for APIs - Curity
- JWKS URL and JWT Validation Guide - MojoAuth








