JSON Web Tokens (JWT)
Overview
JSON Web Tokens (JWTs) are a popular mechanism for representing claims securely between two parties, often used for authentication and authorization in web and mobile applications. A JWT is a compact string, composed of three base64-url encoded parts (header, payload, signature) separated by periods (cheatsheetseries.owasp.org). The header specifies metadata including the token type (JWT) and the signing algorithm (e.g., HS256 or RS256). The payload contains a set of claims – statements about the user or context – such as a user identifier, roles, or expiration time. The signature is computed by the issuer using a secret or private key to ensure the token’s integrity (cheatsheetseries.owasp.org) (cheatsheetseries.owasp.org). JWTs are an open standard (IETF RFC 7519) that became a cornerstone of modern stateless authentication due to their portability and statelessness: once a token is issued, it can be passed with each request and validated by servers without storing session state on the backend (cheatsheetseries.owasp.org). This stateless property makes JWTs attractive for distributed systems and microservices, where traditional server-side sessions may be impractical.
However, JWT security is critical because these tokens act as bearer credentials – possession of a valid token confers the rights of the user it represents. If an attacker forges or steals a JWT, they can impersonate users and gain unauthorized access to protected resources. The problem space for JWT security includes ensuring robust issuance (tokens are signed correctly and only given to authenticated users), safe validation (tokens are always cryptographically verified and checked for expiration/audience), proper rotation of signing keys, and the difficult challenge of revocation in a stateless environment. JWTs should be designed and implemented with a clear understanding of the underlying cryptography and the threats that can arise if they are mishandled. As with any security mechanism, incorrect usage can introduce severe vulnerabilities. For instance, early JWT library implementations had flaws that allowed attackers to bypass verification by manipulating the token header, an issue that underscored the importance of using JWTs carefully (cheatsheetseries.owasp.org) (owasp.org). This article provides a rigorous examination of the JWT security landscape, covering common threats, best practices from standards like OWASP and IETF, and actionable guidance with code examples in multiple languages.
Threat Landscape and Models
When using JWT for session management or identity propagation, it is vital to consider the threat model: who might attack the token and how. JWTs are typically issued by a trusted authority (such as an authentication server or Identity Provider) and consumed by one or more services (Resource Servers) that rely on the claims. An attacker’s goal might be to tamper with a token’s claims (to elevate privileges or impersonate another user) or to steal a valid token and reuse it. Unlike opaque server-side session IDs, JWTs carry their state with them and are self-verifying, which shifts more responsibility to the client and the token itself. This makes JWTs susceptible to client-side threats: for example, if a JWT is stored insecurely in a browser (such as in local storage), a cross-site scripting (XSS) attack could exfiltrate it. Attackers may also intercept tokens in transit if transport-layer security is not strictly enforced. Because JWTs are not encrypted by default (only encoded), any sensitive information in the payload is readable to anyone who obtains the token, making eavesdropping or logging a concern.
The threat model must account for token forgery and manipulation. JWTs use a signature (or message authentication code) to prevent tampering, so an attacker will target the verification process. They might try to supply a token with no signature or with a modified header to bypass verification – essentially tricking the application into accepting an invalid token. If the application or library fails to properly verify the signature against the expected algorithm and key, the attacker can gain illegitimate access (owasp.org) (www.rfc-editor.org). Another scenario is replay attacks: since JWTs are bearer tokens, any party that gains hold of a token can reuse it until it expires. An attacker with a stolen token (via XSS, network sniffing on an unencrypted channel, or other client-side breach) can impersonate the user associated with that token. Because JWTs are often long-lived (to avoid frequent reauthentication), the window for abuse might be significant if additional precautions are not taken.
In distributed architectures, JWTs often travel between multiple microservices. Each service that accepts a JWT must be part of the trust domain and configured correctly. The threat landscape includes inconsistent or weak validation in one service that could be exploited even if other services are secure (pentesterlab.com). Attackers may target the “weakest link” in the token validation chain. For example, if one microservice unknowingly accepts tokens signed with an old or none algorithm, the entire system’s security is undermined. Additionally, consider malicious or compromised clients: unlike traditional session cookies that the browser automatically manages, JWTs might be handled by custom client code (e.g., a single-page application or a mobile app). A compromised client could leak the token, or a malicious API consumer could attempt to craft tokens if they somehow got hold of the signing key. Thus, secure JWT design assumes that the token will be exposed to adversarial conditions and robust validation on the server side is the last line of defense.
Common Attack Vectors
JWT implementations have been subject to a variety of attacks, especially when libraries or developers make incorrect assumptions. Below we detail the most common attack vectors against JWTs:
1. Signature Bypass (alg=none) – Perhaps the most notorious JWT vulnerability was the acceptance of tokens with the algorithm field “none.” In the JWT standard, “alg”: “none” indicates an unsigned token (no integrity protection) (pentesterlab.com). This algorithm was intended for specific scenarios but should never be allowed in authentication flows. In early versions of some JWT libraries, if an attacker modified a token’s header to "alg": "none" and stripped the signature, the library would accept the token as valid, essentially bypassing authentication (cheatsheetseries.owasp.org) (www.rfc-editor.org). Exploiting this is trivial: an attacker takes a valid token, changes its header to use “none”, removes the signature part, and alters the payload claims (for example, elevating their role). If the server does not explicitly disallow “none”, the tampered token might be accepted without any cryptographic check (cheatsheetseries.owasp.org). Modern libraries have mostly patched this issue, but it remains a cautionary tale and a potential risk if developers use unvetted JWT code or implement verification manually.
2. Algorithm Confusion – JWT’s header specifies the signing algorithm, which can lead to “algorithm confusion” attacks if not handled carefully (www.rfc-editor.org). A classic example is when an application expects an RSA-signed token (asymmetric key), but an attacker alters the header to indicate an HMAC algorithm (symmetric key). If the server uses the RSA public key as an HMAC secret (due to a naive implementation), the attacker can forge a token because they know the public key. This attack was demonstrated in several JWT libraries around 2015 (www.rfc-editor.org). The attacker would take a token originally signed with RSA (which the server verifies using a public key) and create a new token signed with HS256 using the public key as the secret. By changing the header’s alg to "HS256", some vulnerable implementations would then attempt HMAC verification with the provided public key and consider the token valid (www.rfc-editor.org). Similarly, confusion can occur between ECDSA and HMAC algorithms. The root cause is the application trusting the alg header from the token instead of enforcing a known algorithm. This vector underscores that algorithm agility in tokens can be dangerous: the verifier should not blindly use whatever algorithm is in the token without checking it against expected values.
3. Key Injection (kid parameter abuse) – The JWT header can include a kid (Key ID) field to indicate which key was used to sign the token, useful in systems with multiple keys or key rotation. Attackers can abuse this field in various ways. One attack is key identifier injection: if the server uses kid to retrieve a key (for example, from a file or database) and fails to sanitize it, an attacker might supply a path or identifier that points to a key they control. For instance, an attacker could set kid to a path like ../../../public.key to trick the server into loading a file it shouldn’t, or to the ID of a weak key in a store. In more advanced scenarios, if the JWT library or application supports JWKS (JSON Web Key Set) endpoints via a header like jku (JSON Key URL) or x5u (X.509 URL), an attacker could host a malicious key and set the token’s header to refer to it (www.rfc-editor.org). A naive implementation might fetch this URL and use the attacker’s public key to verify the token, essentially allowing the attacker to sign tokens with their own key (owasp.org). This is both a SSRF (Server-Side Request Forgery) risk and a verification bypass. The attacker provided key scenario is well-documented: the OWASP Web Security Testing Guide notes that if a library blindly accepts a key from a token header without validation, an attacker can impersonate any user by signing tokens with a key they control (owasp.org).
4. Weak Secrets and Brute Force – When HMAC-SHA (symmetric) signing is used (alg HS256/384/512), the security of the token is only as strong as the secret key. A common pitfall is developers using a weak secret (e.g., “secret”, “password”, or something easily guessable). Attackers can mount offline brute force attacks on JWTs: since the header and payload are exposed, an attacker can take a token and attempt to brute force the HMAC key by trying various secrets and checking if the signature matches. Tools exist to automate this (e.g., using John the Ripper with a JWT plug-in (owasp.org)). If the secret is short or a common word, an attacker might crack it, allowing them to forge valid tokens at will. Another risk is reuse of secrets across environments or among many services – if one service’s secret is compromised, all tokens using that secret are at risk. For RSA or EC algorithms, key management becomes the focus: a leaked or stolen private key from the server side would enable an attacker to sign tokens (this is not an attack on JWT itself, but a consequence of poor key protection). The use of public/private keys does mitigate the brute force risk (since public keys are not secrets) but introduces the need to protect the private key strongly.
5. Token Theft (Sidejacking/XSS) – Instead of cryptographic manipulation, an attacker may simply steal a valid token. Because JWTs are often used in JavaScript-heavy web apps, tokens are sometimes stored in browser storage (like localStorage or sessionStorage) for easy client-side use. This exposes them to theft via XSS: if an attacker can run script on the page, they can read the token and exfiltrate it. The OWASP JWT cheat sheet and best practices strongly warn against storing JWTs in places accessible by untrusted scripts (cheatsheetseries.owasp.org). A related threat is token sidejacking (analogous to session sidejacking): an attacker on an open network capturing a token from an HTTP header or cookie if TLS is not enforced. While cookies confer some protection (HttpOnly cookies can’t be accessed by JavaScript, mitigating XSS theft), if not configured with Secure and proper SameSite attributes, they could be intercepted or used in CSRF attacks. Because JWTs are self-contained, a stolen token remains valid until expiration (or until some revocation list or prevention mechanism intervenes). Attackers with a stolen token can replay it to gain access as the victim user. In one known defensive approach, a token binding technique is used – for example, including a user-specific secret (like a hash of a fingerprint cookie or device identifier) in the token claims that the server will validate (cheatsheetseries.owasp.org) (cheatsheetseries.owasp.org). If an attacker steals only the JWT but not the associated cookie or context, the token becomes useless. This however is an application-level design and not built into the JWT standard.
6. Unsanitized Payloads and Injection – Although JWT payload is just data and typically consumed as a dictionary of claims, there could be subtle injection issues. If an application directly uses data from JWT claims in security-critical decisions or in queries, an attacker who can tamper a token (through one of the above methods) might exploit logic flaws. For instance, a token might have a claim that gets reflected in logs or UI; if not handled carefully, this might introduce XSS or log injection. There’s also a category of attacks where JWT libraries themselves had vulnerabilities in parsing – e.g., certain libraries might have been susceptible to JSON parsing quirks or even buffer overflow with extremely large tokens, though these are less common than logic flaws.
In summary, JWT attack vectors range from cryptographic (forgery by exploiting algorithm confusion or weak keys) to implementation mistakes (skipping verification, trusting attacker-controlled header parameters) to operational (token leakage). The impact of these attacks is typically severe: they often lead directly to authentication or authorization bypass, allowing attackers to impersonate users or escalate privileges. In the next sections, we discuss the impact and how to mitigate these issues.
Impact and Risk Assessment
A compromised JWT undermines the core of an application’s security by subverting its authentication or authorization scheme. The impact of JWT vulnerabilities can be as dire as complete account takeover. For example, if an application fails to verify the token signature, an attacker can modify the token’s payload (such as changing their user role to admin) and gain administrative privileges (pentesterlab.com) (pentesterlab.com). This is effectively an authentication bypass – the system is tricked into accepting a bogus identity. In multi-tenant systems or APIs, this might allow data theft across tenant boundaries or unauthorized operations (like financial transactions or data deletion) performed under a forged identity. The business impact ranges from data breaches and fraud to undermining user trust and violating compliance requirements (since unauthorized access often correlates with privacy law violations).
Even when signatures are verified, other lapses can yield high risk. Not enforcing token expiration (exp) or not checking issuance conditions (nbf – not before, iss – issuer, aud – audience) can allow attackers to abuse stale tokens or tokens issued for a different context. For instance, imagine an application that doesn’t check the audience claim and accepts a token issued for a different service; an attacker could use an access token from one service to access another, if cryptographic keys are shared or if the second service naively trusts the issuer without audience restriction. Similarly, if an application does not validate the iss (issuer) claim, it might accept tokens from an unknown source, assuming they are legitimate. The risk is that an attacker could set up their own token service (with a known private key) and feed those tokens to the application unless the application explicitly pins to a trusted issuer and key.
Replay attacks with JWTs reflect the classic risk of bearer tokens: if a token is valid and within its time window, any use of it is indistinguishable from the legitimate user. So the impact of token theft is essentially session hijacking. Unlike server-side sessions, there is no server memory or state to clear to invalidate a stolen JWT (until it expires). This means an attacker who steals a high-privilege token (e.g., an administrator’s JWT) potentially has free rein in the system until the token naturally expires or the signing key is changed. If tokens have long expiration (some JWTs are issued to last hours, days or even weeks), that gives a long opportunity for exploitation. In scenarios such as mobile apps, where JWTs might be used offline and have very long expiry, the risk of a leaked token is even higher because it might not quickly be noticed or rotated.
The algorithm confusion and kid injection attacks discussed can effectively negate the cryptographic protection of JWTs, which is their main defense. The impact is that an attacker can forge tokens at will, acting as any user. In a worst-case scenario, if the signing key or algorithm validation is compromised, the attacker could generate a token claiming to be a highly privileged account (like a system administrator or a specific user) and the application would accept it. This can lead to full system compromise, especially if the JWT is used to protect administrative API endpoints or to bootstrap further trust (for example, a JWT that grants access to a provisioning system could lead to infrastructure takeover).
There’s also an insider or internal risk: since JWTs often pass through various layers (load balancers, gateways, microservices), any component that logs or stores tokens improperly could inadvertently leak them. If logs are not secured, an attacker (or malicious insider) could harvest tokens from log files. Similarly, long-term secrets used for signing, if not stored safely (e.g., checked into source code or in an environment variable exposed to many systems), increase the risk that someone could steal the signing key and then issue tokens. A stolen signing key essentially yields the same impact as the worst-case above: unauthorized token issuance.
In risk assessment terms, JWT vulnerabilities usually score high on impact (since they break authentication, which is a top-tier security control) and, depending on the scenario, can have high likelihood if best practices aren’t followed. The prevalence of JWT means attackers often look for these flaws during penetration testing or bug bounty hunting. For example, manipulating alg or kid is now a standard part of testing any JWT-using endpoint. The presence of known tools and exploits (some of which require minimal skill to use) increases the likelihood of opportunistic attacks. On the defensive side, the good news is that most of these vulnerabilities can be prevented by configuration and correct library usage. Understanding the risks helps prioritize mitigations – for instance, an application might decide that all high-value operations require re-validation of user credentials or a second factor, partly to mitigate the impact of a stolen JWT. In the next section, we move from identifying these serious risks to strategies and controls to prevent them.
Defensive Controls and Mitigations
To defend against JWT-related attacks, a multi-pronged approach is required: use robust libraries, configure them securely, enforce validation checks, and manage the tokens’ lifecycle prudently. Here we outline key controls and best practices to mitigate the threats:
Use Well-Tested JWT Libraries: Avoid writing custom JWT signing or verification code. Established libraries in each language handle the low-level details and receive security updates. Choose libraries that are known to be secure and have features like algorithm whitelisting. For instance, modern JWT libraries will not accept "alg": "none" by default, or will throw an error if a token’s algorithm doesn’t match the expected one. Always keep these libraries up to date so that patches for any discovered vulnerabilities are incorporated.
Enforce Algorithm Validation: The server should never blindly trust the token’s alg header. One mitigation recommended by IETF RFC 8725 (JWT Best Current Practices) is for the application to explicitly specify the allowed algorithm when validating a JWT, rather than using whatever algorithm the token says (owasp.org) (owasp.org). Practically, this means configuring your JWT library to only accept a specific algorithm (e.g., RS256) or a small set of algorithms that your system uses. If a token comes in with a different algorithm, the validation should fail. Many libraries allow setting an expected algorithm in the verify function or through validation parameters. This mitigates both the “none” attack and the RSA/HS256 confusion attack – the token will be rejected if it isn’t signed with the correct algorithm. As an extra layer, some implementations hard-code the check: e.g., if you only use RS256, your code can ignore the alg in the header and always attempt RSA verification with the known key, avoiding any chance of algorithm switching.
Validate Signatures and All Claims: This sounds obvious, but it must be enforced everywhere a JWT is accepted. Every API endpoint or service that consumes JWTs should verify the signature every time before using the token’s data (pentesterlab.com). No exceptions or “decode-only” shortcuts should exist in production code. Additionally, important standard claims should be checked:
- The token’s expiration (
exp) should be in the future; reject tokens that are expired. - If using
nbf(not-before), ensure the current time is past thenbftimestamp (token is valid). - Validate the issuer (
iss) claim matches the expected issuer (e.g., your auth server’s identifier) – this prevents tokens from other sources or environments being accepted. - Validate the audience (
aud) if your tokens are meant for a specific service or set of services; the token should list an audience that includes your service’s identifier, otherwise it could be a token intended for a different service. Many JWT libraries will do some of these checks if you configure the validation parameters (for example, in Node’sjsonwebtoken.verifyyou can pass anoptionsobject withissuerandaudiencefields to enforce those, and in .NET’s token validation parameters you have properties likeValidateIssuer = true, etc.). By treating these claims as mandatory, you significantly reduce misuse of tokens.
Strong Key Management: Protect the signing keys or secrets as you would any sensitive credential. If using HMAC (symmetric keys), the secret should be a high-entropy value (at least 256 bits for HS256) generated securely – not a password or simple phrase. It should not be hard-coded in source code repositories; instead load it from secure configuration (environment variables, vault services, or hardware modules). Rotate secrets periodically and certainly if a breach is suspected. With RSA or ECDSA keys (asymmetric), keep private keys secure (e.g., in a keystore or managed by a cryptographic module). Public keys can be distributed as needed (some systems publish a JWKS endpoint for their public keys). If multiple services need to validate tokens from a common issuer, use a trusted mechanism to share the public keys (for instance, configure the known public key or CA certificate if using x.509). Avoid scenarios where an attacker could dictate which key is used – for example, if you support key IDs, maintain a server-side map of allowed keys and ignore any unexpected kid values or at least validate that the kid corresponds to a known trusted key. As per OWASP recommendations, do not automatically fetch keys from URLs provided in JWT headers (disable jku unless you implement strict allow-lists) (www.rfc-editor.org).
Shortest Practical Token Lifetime: Limit the window of exposure by making JWTs expire relatively quickly. The appropriate lifetime depends on context (for a web session, maybe 15 minutes to 1 hour; for an access token in a mobile app, perhaps an hour or so). The OWASP testing guide emphasizes that tokens should be short-lived to reduce impact if stolen (owasp.org). If longer sessions are needed, implement a refresh mechanism rather than one long-lived token. A refresh token (often JWT or opaque) can be kept more secure (e.g., stored HttpOnly cookie or secure storage on mobile) and used to fetch new short-lived JWTs. This way, if an access JWT is compromised, it expires soon; if a refresh token is compromised, the damage can be mitigated by other means (refresh tokens can be one-time use or revoked server-side since they might be validated by an authorization server with state). Also consider incorporating Sliding Expiration – do not allow tokens to live forever; require re-authentication after a certain period even if refresh tokens are used.
Preventing Replay and Token Binding: Because server-side revocation of stateless tokens is non-trivial, consider design strategies to make a stolen token less useful. One approach is token binding to a context – as mentioned earlier, a token can include a hash or identifier that ties it to a client-specific state (like a cookie or device ID) (cheatsheetseries.owasp.org) (cheatsheetseries.owasp.org). The server, upon validating the token, also checks that the client-provided context (cookie or other identifier) matches the claim in the token. If an attacker steals only the JWT and not the bound context, the token is rejected. Another simpler approach is to keep a server-side denylist (blacklist) of tokens that should no longer be accepted. JWT itself doesn’t have revocation, but you can simulate logout or emergency revocations by tracking token IDs (jti claim, JWT ID) or unique identifiers (like a hash of the token) in a datastore. For example, upon user logout or known compromise, you could record the token’s jti in a denylist with an expiration equal to the token’s expiration (cheatsheetseries.owasp.org) (cheatsheetseries.owasp.org). All token validations would check against this list. This introduces statefulness (and thus some performance cost), but for high-security applications it may be worth it. Yet another mitigation is rotating signing keys frequently. If you have an array of acceptable keys (with kid to allow new keys), you could retire old keys so that any tokens signed with them become invalid. This is a blunt instrument (it will force all clients to reauthenticate or get new tokens) but effective in an incident response scenario.
Secure Token Storage and Transmission: Ensure tokens are always transported over secure channels (HTTPS). It is recommended to transmit JWTs in the HTTP Authorization header as a Bearer token (e.g., Authorization: Bearer <token>), which keeps it out of URLs (preventing it from being logged or leaked via referer). If you store JWTs in cookies for web applications (to leverage automatic sending with requests), mark them HttpOnly and Secure, and consider SameSite=strict or lax to mitigate CSRF (cheatsheetseries.owasp.org) (cheatsheetseries.owasp.org). This prevents JavaScript from reading the token (reducing XSS risk) and ensures it’s only sent over TLS and not in cross-site contexts. If you store JWTs in browser storage, be aware this is accessible to JavaScript; mitigate by using a content security policy to reduce XSS or by not storing long-lived tokens on the client at all. For mobile apps, use the secure key store or Keychain provided by the OS rather than plain storage. Also, never store the token in a location that other apps or third parties can easily read (for instance, avoid storing JWTs in logs or in inappropriate database fields). On the server side, treat JWTs as sensitive data: if you need to log token details, avoid logging the full token or at least sanitize the signature part, and never log secrets or private keys.
Auditing and Dependency Management: Include JWT usage in your security review and testing plans. Make sure to review third-party libraries’ configurations. A secure configuration might involve enabling strict verification modes (e.g., some libraries have a “require expiration” option to mandate exp claim, or a setting to reject tokens without certain claims). Where possible, use compile-time or initialization-time controls – for example, some frameworks allow registering a validation middleware once, so that every endpoint automatically rejects invalid tokens. This reduces developer error where one endpoint might skip verification. Additionally, monitor for updates on JWT-related vulnerabilities. Keep an eye on library announcements or CVE feeds; if a vulnerability is found in the JWT library you use, update it promptly. For instance, the vulnerabilities discovered in 2015 (none algo, alg confusion) were fixed in library updates (auth0.com), and using an outdated version would leave you exposed. Using tools like OWASP Dependency Check or npm audit/pip audit can flag known vulnerable JWT packages.
In summary, mitigating JWT threats means not trusting the token by default. Always verify it cryptographically, check the token’s claims against what is expected for the context, and minimize the token’s powers (scope and time). By following standards-based guidance – such as the OWASP cheat sheets and IETF best practices – and layering defensive measures, developers can significantly reduce the risk associated with JWTs while still benefiting from their flexibility.
Secure-by-Design Guidelines
Secure use of JWTs should begin at the design phase. Architects and developers should decide early if JWT is the right solution for their use case, and if so, how to design the system to avoid common pitfalls. A key principle is simplicity: JWTs should carry only the necessary information and be used only where truly needed. If an application does not require true statelessness or third-party tokens, a traditional server-side session might be simpler and less error-prone (cheatsheetseries.owasp.org). For those applications that do benefit from JWTs (e.g., distributed microservices or cross-domain federation), consider the following design guidelines:
Least-Privilege Claims: Only include claims in the JWT that are absolutely required for the operation of your system. Avoid embedding sensitive personal data or large amounts of information. Remember that the JWT payload is merely base64 encoded, not encrypted – anyone in possession of the token can read its claims. Do not include secrets (passwords, API keys) or information that could assist an attacker if leaked. Often an opaque identifier (like a user ID) and a few relevant flags (roles or scopes) are sufficient, rather than full profiles or extensive metadata. Keeping tokens small also improves performance and reduces exposure in logs or errors.
Use Standard Claims and Profiles: JWTs have registered claims like iss, exp, sub (subject), aud which have specific meanings. Leverage these for clarity and interoperability. For example, always set an expiration (exp) claim. Use iss to identify your token issuer (e.g., "iss": "https://your-domain.com/auth"). If your tokens are used for OAuth2 or OpenID Connect, align with those specifications (e.g., include iat (issued-at), and perhaps use scope or custom claims for permissions). Using well-defined profiles (such as OpenID Connect ID tokens or OAuth2 Access Tokens format) can indirectly enforce good practices, because these profiles come with expectations (like presence of aud, exp, etc.). Furthermore, standard libraries and frameworks are often better tested with standard claims in place.
Plan for Key Management and Rotation: At design time, determine how you will manage signing keys. If using a single key or secret, you should still have a strategy to change it periodically or in case of compromise. One approach is to version your keys and include a kid in tokens – for example, key with ID "2024-01-01" could be the active key, and you include "kid": "2024-01-01" in the JWT header. This allows you to support multiple keys during a rotation period: the validator can pick the correct key based on the kid. Design your system so that introducing a new key (and retiring an old one) is a smooth process (e.g., maintain a keyset and update it atomically, possibly use JWKS for distributing public keys). Also, decide whether you will use symmetric keys (simpler, but shared secret) or asymmetric keys (more complex, but better isolation as the verify side only needs public key). In many designs, an Identity Provider (IdP) service issues JWTs (signing with its private key), and various microservices just hold the IdP’s public key for verification. This separation of concerns (one service handles authentication) is a good design that limits where the secret lives and centralizes token issuance.
Stateless vs Stateful Trade-offs: Recognize the limitations of a stateless token. If your system demands immediate revocation (e.g., an administrator should be able to force-log-out a user or invalidate a token before expiration), pure JWT might not suffice. Design considerations can include a token blacklist as mentioned, or a very short token life combined with frequent re-issuance (which approximates revocation by expiring tokens quickly). Another design variant is to use reference tokens: instead of putting all info in the JWT, the token could be a pointer (like a GUID) that the server can check in a store or cache. This is closer to a traditional session ID and allows server-side invalidation, at the cost of losing the pure statelessness. Some OAuth2 implementations do this (the access token presented to the client is just a reference that the API gateway looks up). Your design should weigh the security requirements against performance and complexity: for most public-facing APIs, a hybrid approach (short-lived JWTs plus a backend check for long-term logout or important events) works well.
Client Context and Defense-in-Depth: Design the system to assume tokens will be stolen or forged, and add additional layers of defense. For example, multi-factor authentication can limit the damage of a stolen token if certain high-risk actions still require re-auth or a second factor. Another design guideline is to incorporate context into the token or session – e.g., include the user’s IP or a device identifier as a claim (hardened with a HMAC so it’s not easily guessable by attackers) and then on the server compare it with the actual request’s IP or device. Be cautious: IP can change and isn’t reliable for mobile clients, but it might be useful for certain threat models. A more reliable context is a secure cookie as discussed in the cheat sheet: the token contains a hash of a random cookie value (cheatsheetseries.owasp.org) (cheatsheetseries.owasp.org). This ties the token to that browser; an attacker who steals only the token (not the cookie) cannot use it. These measures are “secure by design” in that they anticipate failures of the primary mechanism.
Avoid Anti-Patterns Early: Prevent known bad practices from creeping into the design. For instance, do not plan to share one JWT secret across dozens of services arbitrarily – if microservices need to trust each other’s tokens, formalize it through an issuer service. Do not design the system such that clients have to construct or modify JWTs themselves beyond storing and forwarding them; all signing should happen on trusted servers. Ensure that error handling in the design does not leak token secrets – e.g., if a token is invalid or expired, the API should respond with a generic “unauthorized” message, not the reason or the token content. From a UI/UX perspective, design the authentication flow to gracefully handle token expiration (perhaps via silent refresh behind the scenes or redirecting to login), rather than encouraging developers to ignore expiration for convenience.
Compliance and Data Handling: If your application is subject to regulations (GDPR, HIPAA, etc.), consider how JWT usage intersects with those. For example, under GDPR, a JWT could be considered personal data if it contains identifiable info. Storing personal data in JWT (especially if not encrypted) might raise compliance obligations since any system that processes the token now is processing personal data. A design that minimizes personal data in the token and encrypts any sensitive fields (or avoids them entirely) is more compliance-friendly. Also, in some regulations, users have rights like revoking access – not directly a JWT issue, but you need to be able to invalidate tokens if a user withdraws consent or logs out. Design with those scenarios in mind (perhaps by keeping a minimal server record of active tokens per user if required).
By adhering to these secure design principles, you lay a solid foundation. Many JWT vulnerabilities in the wild stemmed not just from coding mistakes, but from design decisions (like overly long token lifetime, no provision for logout, or trusting third-party tokens without careful validation). A secure design will mandate checks and balances such that even if one layer falters (say a missed validation check), other layers (short expiry, context binding, monitoring) provide safety nets.
Code Examples
To illustrate the dos and don’ts of JWT implementation, this section provides code examples in several languages. Each sub-section shows an insecure approach followed by a secure approach in that language, with explanations. These examples highlight common mistakes like skipping signature verification or using JWT libraries incorrectly, and demonstrate proper usage of those libraries.
Python
Insecure Example (Python): In Python, the popular PyJWT library can decode tokens. A common mistake is decoding a JWT without verifying its signature or claims. In the code below, the developer turns off signature verification. This would allow any token (even tampered or completely fake) to be accepted and its payload trusted, defeating the purpose of JWT integrity.
import jwt
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." \
"eyJ1c2VyIjoiam9obiIsInJvbGUiOiJ1c2VyIn0." \
"invalidsignature"
# Insecure: decoding JWT without verifying signature
try:
payload = jwt.decode(token, options={"verify_signature": False})
user = payload.get("user")
role = payload.get("role")
print(f"Authenticated {user} with role {role}")
except Exception as e:
print(f"Token error: {e}")
In this insecure snippet, jwt.decode is called with verify_signature=False, which tells PyJWT to skip signature verification entirely. As a result, the code will print “Authenticated john with role user” even though the token’s signature part is bogus ("invalidsignature"). An attacker could modify the "role" to "admin" in the token payload and the code would still accept it. This is a critical vulnerability: the application is trusting data that has not been verified. The try/except here only catches errors like malformatted tokens; it would not catch a forgery because no verification is attempted.
Secure Example (Python): A secure implementation uses jwt.decode (or jwt.decode_verify in some frameworks) with a known secret/key and explicitly defines which algorithms are allowed. It also handles exceptions, such as expired tokens or invalid signatures, and rejects those tokens.
import jwt
secret_key = b'supersecretkey1234567890' # 256-bit secret for HMAC
token = jwt.encode({"user": "john", "role": "user", "exp": 1749012337},
secret_key, algorithm="HS256")
try:
payload = jwt.decode(token, secret_key, algorithms=["HS256"])
user = payload.get("user")
role = payload.get("role")
print(f"Authenticated {user} with role {role}")
except jwt.ExpiredSignatureError:
print("Token has expired – please log in again.")
except jwt.InvalidSignatureError:
print("Invalid token signature – authentication failed.")
except Exception as e:
print(f"Token validation error: {e}")
In the secure example, the token is encoded with HS256 using a strong secret. When decoding, we provide the same secret_key and specify algorithms=["HS256"], ensuring that only HMAC-SHA256 signed tokens will be accepted. The library will automatically verify the token’s signature and the exp claim (expiration) if present. We include error handling: if the token is expired or the signature doesn’t match, an exception is raised and caught, and the token is rejected (the user would not be authenticated). By not disabling verification and by explicitly whitelisting the algorithm, this code is safe against the earlier attacks. An attacker modifying the token would either produce a signature mismatch (triggering InvalidSignatureError) or an expired token error if they altered the exp. The use of a sufficiently random secret_key makes brute forcing infeasible. This example also shows the importance of setting an expiration (exp) and handling it – the code gracefully prompts re-login when tokens are expired, rather than allowing indefinite use.
JavaScript (Node.js)
Insecure Example (Node.js): In a Node.js environment, developers often use the jsonwebtoken package. A common error is to use the decode function or to verify tokens without restricting algorithms. The following code mistakenly uses jwt.decode which does not verify the token’s signature at all – it only base64-decodes the payload.
const jwt = require('jsonwebtoken');
const token = jwt.sign({ user: 'alice', admin: false }, 'shhsecret', { algorithm: 'HS256' });
// Insecure: using decode instead of verify
const decoded = jwt.decode(token, { complete: true });
if (decoded) {
console.log(`User ${decoded.payload.user}, admin=${decoded.payload.admin}`);
// Code wrongly assumes token is valid since it was decoded
if (decoded.payload.admin) {
console.log("Granting admin access to Alice!");
}
}
Here, jwt.sign creates a token for demonstration. The insecure part is jwt.decode(token, { complete: true }). This returns the decoded header and payload without validating the signature. The code then uses decoded.payload as if it were trustworthy. An attacker could modify the token (for example, change admin to true and re-sign it with any random signature or even remove the signature part if the library allowed it) and pass it to this code. jwt.decode would still output the tampered payload. The code would then log Alice as admin and even execute the branch to grant admin access – all without verifying that the token was genuinely issued by the server. This is essentially the signature-not-checked flaw, which in a real scenario means an attacker can impersonate or escalate privileges by crafting tokens.
Secure Example (Node.js): A secure Node.js usage leverages jwt.verify with an explicit algorithm list and proper error handling. We also ensure that we are using the correct type of key (secret for HMAC or public key for RSA as appropriate) and not mixing them up.
const jwt = require('jsonwebtoken');
const fs = require('fs');
// Suppose we use RSA keys for signing (RS256)
const privateKey = fs.readFileSync('rsa_private.pem');
const publicKey = fs.readFileSync('rsa_public.pem');
// Issue a token (typically done in auth server):
const token = jwt.sign(
{ sub: 'user123', role: 'user', iss: 'https://myapp.test', exp: Math.floor(Date.now()/1000)+300 },
privateKey,
{ algorithm: 'RS256', keyid: 'main-key-v1' }
);
// Secure verification of the token:
try {
const verifiedPayload = jwt.verify(token, publicKey, { algorithms: ['RS256'], issuer: 'https://myapp.test' });
console.log(`Verified user ${verifiedPayload.sub} with role ${verifiedPayload.role}`);
} catch (err) {
if (err.name === 'TokenExpiredError') {
console.error('Token expired, need a new token.');
} else if (err.name === 'JsonWebTokenError') {
console.error('Token verification failed:', err.message);
} else {
console.error('Unexpected token error:', err);
}
}
In this secure example, we assume RS256 (asymmetric signing). The token is signed with a private key; the payload includes standard claims like iss (issuer) and exp (expiry in 5 minutes). On the verification side, we use jwt.verify and provide the corresponding publicKey. Crucially, the options specify algorithms: ['RS256'] – this means if the token’s header is anything other than RS256, verification will fail. This prevents an attacker from substituting a different algorithm. We also specify the expected issuer claim; if the iss in the token doesn’t match, verification fails. The code handles errors: if the token is expired or invalid (wrong signature, wrong issuer, tampered data, etc.), jwt.verify will throw. The catch block then logs an appropriate error. Because we used the correct public key and explicitly limited algorithms, the earlier “algorithm confusion” attack is mitigated – for instance, if an attacker changed the token to HS256 and tried to sign with the public key as a secret, jwt.verify would reject it because we told it to only accept RS256 (and also the key type mismatch would cause failure). The use of keyid in jwt.sign is optional here (it adds "kid": "main-key-v1" in header) – in a system with multiple keys, the verify side would use that to pick the right public key. In any case, this code will only log a user as verified if the token was properly signed by our trusted key and not expired or malformed. No sensitive data is exposed, and we keep tokens short-lived (300 seconds) to minimize risk.
Java
Insecure Example (Java): Java developers often use libraries like JJWT (io.jsonwebtoken) or Auth0’s Java JWT. A common pitfall is neglecting to verify the token or using the library in an insecure way. For instance, some might base64-decode and parse JWT payloads manually, or use the library’s decode method without verification. Here’s an example of bad practice where the developer parses the JWT by hand and trusts its content:
import java.util.Base64;
import com.fasterxml.jackson.databind.ObjectMapper;
// Insecure: manual parsing of JWT without signature check
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
+ "eyJ1c2VyIjoiZGVtb3VzZXIiLCJhZG1pbiI6ZmFsc2V9."
+ "signaturesignature";
String[] parts = token.split("\\.");
if (parts.length == 3) {
try {
String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]));
String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]));
ObjectMapper mapper = new ObjectMapper();
// Parse JSON (assuming no encryption)
var headerMap = mapper.readValue(headerJson, Map.class);
var payloadMap = mapper.readValue(payloadJson, Map.class);
System.out.println("Header alg: " + headerMap.get("alg"));
System.out.println("User: " + payloadMap.get("user") + ", admin: " + payloadMap.get("admin"));
// No verification of the signature part (parts[2]) at all
if (Boolean.TRUE.equals(payloadMap.get("admin"))) {
System.out.println("Granting admin access to " + payloadMap.get("user"));
}
} catch (Exception e) {
e.printStackTrace();
}
}
In this insecure Java snippet, the code splits the JWT and decodes the JSON payload directly. It uses Jackson to parse the JSON into maps, and then proceeds to use the data (printing user info, and crucially, deciding to grant admin access if the claim says admin: true). Nowhere is the parts[2] (the signature) actually used. This means the token’s integrity is not verified at all. An attacker could supply any token string of the format header.payload.signature – even a fake signature or none – and as long as the parts[1] decodes to {"user": "someone", "admin": true}, this code would grant admin access. This is essentially replicating what a vulnerable custom JWT parser does. It’s a severe security flaw because it completely ignores the cryptographic protection JWT is supposed to provide. The developer may have been trying to avoid using a JWT library, or simply didn’t realize decode doesn’t equal verify. This code also doesn’t check any standard claim like expiration, issuer, etc., compounding the insecurity.
Secure Example (Java): A secure Java implementation uses a JWT library’s verification facilities. Here we show using the Auth0 Java JWT library (which the OWASP cheat sheet references) to verify a token. We enforce the algorithm and validate critical claims.
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.exceptions.JWTVerificationException;
// Setup expected algorithm and secret
Algorithm hmacAlgorithm = Algorithm.HMAC256("changeThisSecretToSomethingSecure123!");
JWTVerifier verifier = JWT.require(hmacAlgorithm)
.withIssuer("https://myapi.example") // expected issuer
.withAudience("myapi_clients") // expected audience
.build();
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+ " (a valid token string signed with above secret) ...";
try {
DecodedJWT decoded = verifier.verify(token);
String user = decoded.getSubject(); // assuming 'sub' claim is user ID
boolean isAdmin = decoded.getClaim("admin").asBoolean();
System.out.println("Authenticated user: " + user + ", admin=" + isAdmin);
if (isAdmin) {
// perform admin actions
}
} catch (JWTVerificationException e) {
// Token is invalid or expired
System.err.println("Token verification failed: " + e.getMessage());
}
In this secure code, we use the JWT verifier from Auth0’s library. We define the expected Algorithm as HMAC SHA-256 with a given secret. By using JWT.require(Algorithm.HMAC256(secret)), we ensure the verifier is locked to that algorithm and key (cheatsheetseries.owasp.org). The code further sets expected issuer and audience; these are optional checks but good practice (they add .withIssuer(...) and .withAudience(...) conditions). The verifier.verify(token) call does multiple things under the hood: it checks the token’s signature using the provided HMAC secret and algorithm, it checks the alg in the header matches the Algorithm we specified (so an attacker’s token with a different alg would be rejected), it checks the token hasn’t expired (exp claim) and isn’t before its time (nbf), and it checks the iss and aud claims match the expected values if we set them. If any of those validations fail, it throws a JWTVerificationException. In the try-catch, a failure leads to the catch block logging an error and (in a real app) rejecting the request. If success, we safely use the decoded JWT. The example retrieves the sub (subject) claim via getSubject() and a custom claim “admin” via getClaim("admin"). These values are now trustworthy – the verification guarantees they haven’t been tampered with and the token is from our issuer. Even if an attacker tries the trick from before (changing admin to true), they would not have the secret to re-sign the token properly, so verifier.verify would throw an exception and isAdmin code wouldn’t run. Also, by centralizing this verification logic (possibly as a filter in a web app), we ensure every request goes through this validation. The use of a strong secret (the example string should be replaced with a securely generated random string in practice) defends against brute force. In a real system, you might use RSA keys here (with Algorithm.RSA256(publicKey, privateKey) on the issuing side and only public key on verify side), which the library also supports – the pattern of using JWTVerifier remains similar, just with a different Algorithm instantiation.
.NET (C#)
Insecure Example (.NET): In the .NET ecosystem, the System.IdentityModel.Tokens.Jwt library is commonly used. A frequent mistake is to parse a token without validating it. For example, a developer might use the JwtSecurityTokenHandler to read the token and extract claims, but neglect to set up token validation parameters. Here’s an insecure example:
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using Microsoft.IdentityModel.Tokens;
// Insecure: reading JWT without validating signature or claims
var tokenHandler = new JwtSecurityTokenHandler();
string tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+ ".eyJzdWIiOiJ1c2VySWQiLCJhZG1pbiI6dHJ1ZX0."
+ "invalidSignature";
if (tokenHandler.CanReadToken(tokenString))
{
var jwt = tokenHandler.ReadJwtToken(tokenString);
var subject = jwt.Claims.First(c => c.Type == "sub").Value;
var isAdmin = jwt.Claims.FirstOrDefault(c => c.Type == "admin")?.Value;
Console.WriteLine($"Token for user {subject}, admin={isAdmin}");
if (isAdmin == "true")
{
Console.WriteLine("Granting admin privileges to " + subject);
}
}
This insecure code uses ReadJwtToken to parse the JWT. ReadJwtToken does no signature verification – it merely decodes the token and creates a JwtSecurityToken object from it. The code then directly accesses claims for sub and admin. If admin is "true", it grants admin privileges. The token provided in tokenString has an invalid signature on purpose. But since we never call the validation logic, the code doesn’t know that. CanReadToken only checks that the format is valid JWT, not the cryptographic integrity. Therefore, any token in proper format (header.payload.signature) will be “readable.” An attacker could remove the signature or put garbage, and as long as the header and payload are valid JSON and the token has three parts, ReadJwtToken will happily parse it. The attacker could set admin=true in the payload, and this code would grant admin privileges without hesitation. Moreover, this code doesn’t check expiry or issuer either – so even an expired token or token from elsewhere would be accepted. It’s a textbook example of why just parsing is not enough; one must validate.
Secure Example (.NET): In .NET, the correct approach is to use ValidateToken with TokenValidationParameters. We explicitly specify the signing key, algorithm, issuer, audience, etc., that we expect, so that the framework will do the heavy lifting of verification. For instance:
using System;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.Text;
// Secure: validate JWT using TokenValidationParameters
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("super_long_random_secret_for_HMAC_here!");
var validationParams = new TokenValidationParameters {
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidIssuer = "https://auth.myapp.com",
ValidAudience = "myapp_api",
ValidateIssuer = true,
ValidateAudience = true,
ClockSkew = TimeSpan.Zero // no tolerance on exp
};
string token = /* assume this is a JWT string obtained from client */;
try {
var principal = tokenHandler.ValidateToken(token, validationParams, out var validatedToken);
// If we reach here, the token is valid
var identity = principal.Identity;
Console.WriteLine($"Token valid for {identity.Name}, roles: {string.Join(", ", principal.Claims.Where(c=>c.Type == "role").Select(c=>c.Value))}");
} catch (SecurityTokenExpiredException) {
Console.WriteLine("Token is expired.");
} catch (SecurityTokenValidationException ex) {
Console.WriteLine("Token validation failed: " + ex.Message);
}
In this secure code, we configure TokenValidationParameters thoroughly. We set ValidateIssuerSigningKey = true and provide the IssuerSigningKey (in this case a symmetric key for HMAC, wrapped in a SymmetricSecurityKey object). We also specify the valid issuer and audience, and enable their validation. By setting these, ValidateToken will reject tokens that aren’t signed with the given key, or that come from a different issuer, or intended for a different audience. The ClockSkew = TimeSpan.Zero is set to eliminate the default 5 minute clock skew tolerance on expiration (optionally, depending on whether you want to allow a few seconds of leeway or not). Now, when ValidateToken is called, it will throw an exception if the token is invalid in any way: bad signature, wrong alg, expired, not yet valid, wrong issuer, wrong audience, etc. If it returns a principal (ClaimsPrincipal), that means the token is valid and we can trust the claims contained. In this example, we print out the user’s name (often mapped from the sub or a specific claim depending on how the token was created) and any roles present. The try-catch differentiates an expired token versus other validation failures. This way, we can handle renewal or rejection accordingly. The difference from the insecure example is stark: an attacker’s tampered token will simply not pass ValidateToken. If someone changed the token’s signature or claims, the HMAC check (IssuerSigningKey) would fail. If they changed the alg to none or something unsupported, the JWT handler would notice a mismatch (the JwtSecurityTokenHandler in recent .NET versions by default doesn’t allow “none” if a key is provided, and ValidateToken would error out). Also important, by specifying the exact key and algorithm (the key type implies the algorithm here – a SymmetricSecurityKey will be used for HS256 by default if the token’s alg is HS256), we avoid confusion attacks. If this were RSA, we’d provide an RsaSecurityKey and the library would only accept matching RSA-signed tokens. In essence, this code uses the high-level API correctly to ensure no shortcuts. A developer integrating JWT in an ASP.NET Core app often uses the middleware which internally does exactly this configuration – here we show it explicitly for illustration.
Pseudocode
Insecure Example (Pseudocode): The following pseudocode demonstrates, at an algorithmic level, an insecure process of handling a JWT. It's not a specific language but outlines the steps:
function processApiRequest(request):
token = request.getHeader("Authorization")
if token is not null:
// Insecure: blindly trust the token payload
header, payload, signature = split(token, ".")
claims = Base64UrlDecode(payload)
user = claims["user"]
role = claims["role"]
print("Authenticated as " + user)
if role == "admin":
grantAdminAccess(user)
else:
rejectRequest("No token provided")
This pseudocode process reads an Authorization header for a token. If present, it splits the token into header, payload, signature. It decodes the payload and extracts the claims like user and role. Then it assumes the user is authenticated as given in the token, and if the role claim is “admin”, it grants admin access. Nowhere does this function verify the signature with a secret or public key. This is effectively what happens if a developer were to implement JWT verification "from scratch" incorrectly. The result: any token, even one crafted entirely by an attacker, would be accepted. For instance, an attacker could supply "Authorization: Bearer xyz.header.zyx" where the payload decodes to {"user": "attacker", "role": "admin"} and the signature is random – this code would still set user = attacker and role = admin, and proceed to grantAdminAccess. It’s clear that such pseudocode lacks any cryptographic check (HMACVerify(...) or RSAVerify(...) is missing), which is the critical step in JWT handling.
Secure Example (Pseudocode): Now, a pseudocode representation of a correct approach:
function processApiRequest(request):
token = request.getHeader("Authorization")
if token is null:
rejectRequest("No token")
return
header, payload, signature = split(token, ".")
expectedAlg = "HS256"
expectedKey = secretStore.get("jwt_hmac_secret")
if header.alg != expectedAlg:
rejectRequest("Invalid token algorithm")
return
if !verifyHMAC(signature, header + "." + payload, expectedKey):
rejectRequest("Invalid token signature")
return
claims = Base64UrlDecode(payload)
if claims["exp"] < currentTime():
rejectRequest("Token expired")
return
if claims["iss"] != "https://auth.myapp.com":
rejectRequest("Invalid token issuer")
return
user = claims["user"]
role = claims["role"]
setAuthenticatedUser(user)
print("Authenticated " + user + " with role " + role)
// continue processing request with this user’s privileges
This pseudocode explicitly verifies the token before using it. It checks that the algorithm in the header matches what we expect (HS256 in this scenario). It retrieves the secret key from secure storage (not hard-coded). It then computes verifyHMAC(signature, header + "." + payload, key) – a function that does HMAC-SHA256 with the secret key on the concatenation of header and payload, and compares it to the provided signature. If this fails, the token is rejected as invalid. Only if the signature is valid do we parse the payload. We then check the exp claim to ensure the token is not expired, and the iss claim to ensure the token was issued by our trusted authority. If any of these checks fail, the request is rejected or unauthenticated. Finally, we extract the user and role from the claims and mark the request as authenticated as that user. By doing these steps, the token’s integrity and validity are confirmed before any sensitive action. In a real implementation, there would likely be additional checks (audience, not-before, etc.) and this logic would be encapsulated in a middleware or utility, but the pseudocode captures the essence. An attacker now cannot bypass the checks: if they supply a fake token, the verifyHMAC step will fail and the request gets rejected. If they somehow had a token from a different system, the issuer check might fail. If the token is old, the exp check will catch it. This secure flow aligns with recommendations from both OWASP and IETF on how to handle JWTs safely (owasp.org) (www.rfc-editor.org). Note that if this were using RSA, the pseudocode’s verify step would use a public key to verify a signature (and we’d ensure the header’s alg is “RS256” etc. accordingly). The key point is the inclusion of cryptographic verification and claim validation.
Detection, Testing, and Tooling
Detecting JWT implementation flaws requires a combination of code review, automated scanning, and manual penetration testing. From a defender’s perspective (e.g., AppSec engineer reviewing an app), one should verify that every point where JWTs are consumed, there is proper validation logic in place. This can be done by inspecting the code for usage of JWT library methods: for instance, in Python, flag any occurrence of decode(..., verify=False) or similar; in Node, search for jwt.decode( or jwt.verify( calls without the algorithms option. Static analysis tools can be configured with rules to catch these patterns. Some security linters know about common JWT pitfalls (for example, a linter might warn if JwtSecurityTokenHandler.ReadToken is used without ValidateToken in .NET, or if Algorithm.None appears anywhere).
During testing, one of the first things a penetration tester might do is attempt to bypass signature verification. They can do this by modifying an existing valid token slightly. For example, capture a JWT from a valid session and then change one character in the payload (or flip a bit in the signature) to invalidate it. If the server still accepts it, that’s a red flag that signature checking might be off. Another classic test: replace the token’s signature with a bogus value or remove it and see if the server responds differently (for instance, if it treats an invalid token the same as no token vs. if it errors or succeeds – any success on an invalid signature indicates a vulnerability). Additionally, testers will try the “alg”: “none” trick: manually craft a JWT header with "alg": "none", keep the payload identical to a known-good token, and use an empty signature. Many JWT libraries today will reject this, but if the application built a custom parser or uses an outdated library, it might let it through.
For algorithm confusion testing, if the application is known to use an asymmetric algorithm (like RS256), a tester can attempt to create a token signed with a symmetric algorithm using the public key as the secret. There are tools to automate this: for example, jwt_tool.py (a Python toolkit for JWTs) can attempt various known bypass techniques, including none alg and public-key-as-secret attempts (owasp.org) (owasp.org). The tester would supply the target JWT and the tool will report if any of those techniques yield a valid token that the server accepts.
Another vector to test is kid injection and JWKS handling. If the server’s JWTs have a kid in the header, a tester might try to modify the kid to unusual values (SQL injection strings, path traversal strings, or URL pointing to a server they control) to see if the server is doing something insecure like constructing a filesystem path or performing an HTTP fetch. For example, if kid is originally “main-key”, try kid": "../../../../etc/passwd or kid": "http://attacker.com/evil.jwks". If the server’s response changes (or if it makes an outgoing request that the tester’s server can observe), it indicates a potential flaw. Tools like Burp Suite have extensions (the JSON Web Tokens Burp Extension (owasp.org)) to help generate and modify JWTs on the fly, making it easier to do these tests systematically.
Brute force attacks on JWT secrets can also be attempted if the situation allows. If the algorithm is HMAC and the secret is unknown, testers might use a tool like jwt-cracker or John the Ripper with its JWT mode (owasp.org) to try a dictionary of common secrets against a captured token. If they succeed (which would mean the secret was weak), they can then forge tokens arbitrarily. Detection of such attempts in production might come from monitoring (e.g., lots of token validation failures could indicate someone trying many guesses).
On the defensive tooling side, organizations can integrate JWT hardening checks into CI. For instance, unit tests should cover that an invalid signature is indeed detected. Security scanning tools (SAST/DAST) may include JWT-specific tests by default now. OWASP ZAP has a JWT add-on (owasp.org) that can automate some checks during a scan (checking for none acceptance, etc.). Similarly, modern API security testing tools or fuzzers often include a JWT module.
From a monitoring perspective, detection can include logging token anomalies: e.g., if the system sees a token with an unexpected algorithm, log it (someone might be probing). If the application has a denylist for tokens, log any attempt to use a revoked token (which could indicate a stolen token being reused). For high-security environments, you might even issue tokens with an embedded identifier and track their usage server-side (each time a token is seen, mark it, and if you see the same token used from two distant geolocations within a short period, that could flag a possible theft).
In addition, review of configuration is part of detection: ensure that frameworks (like Spring Security, .NET JWT Bearer middleware, etc.) are configured correctly. A misconfiguration could disable some checks — for example, an Auth0 Node express-jwt middleware might be set with credentialsRequired: false which would not enforce presence of token, or maybe a flag that ignores expiration for testing left enabled. It’s important to inspect those settings or have automated configuration checks.
Tooling Summary: A few notable tools and references for JWT security testing include:
- jwt_tool.py – an open-source Python toolkit for testing JWTs (can test signature bypass, brute force, etc.). (owasp.org) (owasp.org)
- Burp Suite JWT Plugin – to identify and modify JWTs in intercepted traffic.
- OWASP ZAP JWT support – similar to Burp, helps fuzz JWTs in automated scans.
- jwt2john & John the Ripper – for converting JWTs to a hash that JtR can attempt to crack (useful for HS256 weak secret auditing) (owasp.org).
- Postman or Insomnia – while not security tools per se, these can manually craft and send JWTs for testing responses.
- Custom Scripts or PentesterLab exercises – PentesterLab offers exercises on JWT vulnerabilities (pentesterlab.com) (pentesterlab.com), and their blog (as referenced) details how to manually exploit and detect these issues, which can serve as a guide for testers.
Finally, it’s valuable for detection to have robust unit and integration tests in the development cycle. For example, a unit test in the authentication service could attempt to validate a token with an altered signature and assert that it fails. Another test could ensure that tokens without exp are rejected if your policy is to require expiration. Security tests can also simulate a stolen token scenario to ensure that any secondary mechanisms (like the fingerprint cookie) work – for instance, test that if you omit the cookie or provide a wrong cookie, the token is rejected as per design (cheatsheetseries.owasp.org) (cheatsheetseries.owasp.org). Such tests not only prevent regressions but also act as live documentation that the team has considered these edge cases.
Operational Considerations (Monitoring and Incident Response)
Operationalizing JWT security involves ongoing vigilance in production, as well as having plans for responding when something goes wrong.
Monitoring and Logging: It’s important to log authentication events that involve JWTs. For instance, when a token is rejected, log the reason (without logging the entire token in plaintext). If there are many signature failures or “attempted use of expired token” events from a single source, this might indicate an attack or misconfigured client. Monitor these patterns – they can be surfaced on a dashboard or trigger alerts. Successful logins issuing JWTs should also be logged (user X issued token at time Y from IP Z). This helps in traceability; if later a token is suspected to be stolen, you can see where it was originally issued. Be cautious in logs not to record sensitive claim data unnecessarily (e.g., don’t log full JWTs or user personal info). Instead, maybe log the token’s ID (jti) or a hash of the token to correlate events without exposing the token’s contents.
Metrics and Anomaly Detection: You could instrument metrics such as the rate of JWT validation failures. An unusual spike might indicate attempts to brute force or misuse tokens. If your system uses a denylist for revoked tokens, track metrics like “revoked token usage attempts.” A sudden increase might signify an attacker trying to reuse tokens that they shouldn’t have. Another factor is token usage patterns: if tokens are normally used, say, within one geographic region and suddenly the same token shows usage from another continent, that is suspicious. Some advanced setups integrate with anomaly detection systems that flag impossible travel or concurrent usage anomalies (two different IPs using the same token nearly simultaneously). Given JWTs are stateless, the server doesn’t inherently know if a token is being used concurrently, but logging can reveal that after the fact.
Key Management Operations: From an ops standpoint, managing the signing keys is crucial. Ensure that rotation of keys is done carefully and that old keys are retired when no longer needed. It’s wise to monitor for any attempts to use tokens signed with retired keys (this could be as simple as logging an error if a token’s kid is unknown – which might be an attacker’s token or an old token introduced somehow). Store backups of keys securely and restrict access – only the minimal services or accounts should have the ability to read the private keys or secrets. Vaulting solutions (like HashiCorp Vault or cloud key management services) are often used to good effect; monitoring access to the vault or keystore is itself a security measure (if someone accesses the key unexpectedly, it might be an insider threat or compromised account).
Incident Response – Token Compromise: One of the hardest questions with JWT is: what do we do if we suspect tokens have been compromised? Since JWTs by design don’t have an immediate server-side off switch, your incident handling might involve changing the environment. A common response is to rotate the signing key. By issuing a new signing key and updating the services to only accept tokens signed by the new key (and ideally with a new kid if using JWKS), you effectively invalidate all existing tokens at once. This is a heavy-handed approach because it will force all users to re-authenticate (which, in the middle of an incident, might be acceptable). The operation has to be coordinated: all services accepting JWTs need the update of the new key nearly simultaneously to avoid mismatches. If you have used a key identifier and publish keys via JWKS, you might instead mark the old key as no longer valid and require that tokens use the new key’s kid. In an OAuth2 context, if you’re dealing with access tokens and refresh tokens, you could identify tokens of compromised users or clients and add them to a denylist so that even if presented, they won’t be honored.
If only a subset of tokens are considered compromised (say a particular user was victim of XSS), an option is to use the jti claim if present. If you included unique JWT IDs, you could add the specific token’s ID to a blacklist. If not, and you just want to logout a user entirely, you might keep track of a “token issued at” timestamp in the user’s record and reject any token issued before that (thus requiring a fresh login for the user). Some systems, after an incident, might push an invalidation event to clients (for instance, a mobile app might receive a notification that forces it to drop the current token).
Continuous Improvement and Patching: Operations should also include keeping JWT libraries updated. Subscribe to security advisories for the libraries or frameworks you use (many have mailing lists or publish CVEs). For example, if a vulnerability is discovered in the JWT library (like the critical ones from 2015 or any new ones), operations should treat it akin to any critical dependency patch – schedule an immediate update, test compatibility, and deploy. Because JWT is at the heart of auth, an issue there is a high priority. Using dependency management tools in CI can help alert operations to new versions with security fixes.
Session Management and Logout: Provide users or administrators with options to invalidate tokens if possible. For example, you might implement a “Logout All Sessions” feature that behind the scenes rotates that user’s auth state, perhaps by changing a flag that all tokens include (some JWT designs include a user’s password hash version or an session version number as a claim, and by changing it on the server, existing tokens become invalid because the server now expects a different value). If such a mechanism exists, ensure it's monitored when used (so you can see if, say, an attacker who stole an account is trying to log out other sessions to lock the real user out – which might indicate an ongoing attack).
Failure Mode Considerations: Think about how your system behaves if JWT validation fails or some service is misconfigured. A robust design will fail closed – meaning if anything is off about a token, the request is denied. However, a misconfigured system might accidentally fail open (for instance, if the auth service is down and an API decides to let requests in rather than deny all, that’s dangerous). Monitoring should extend to such error conditions: if you ever catch a log saying “JWT validation service unreachable, proceeding without validation”, that’s a glaring issue (unfortunately seen in some systems where logging in fails open). It’s prudent to test these scenarios (simulate the JWKS endpoint being unreachable in a staging environment and see that everything properly fails closed).
Communication and Education: Operational security also includes educating the support staff or developers about JWT. If users report that they remained logged in longer than expected, it might hint that token expiration policies aren’t working – operations can investigate whether clocks are in sync across servers (clock skew issues can cause tokens to appear valid longer). If a user reports seeing someone else’s data, consider the possibility of a flawed multi-tenant token handling. Keeping an eye on such anomalies reported by users or QA can lead to early discovery of JWT misuses.
In summary, operational considerations revolve around maintaining a vigilant stance: logging and monitoring for abuse, being ready with procedures to revoke or re-issue tokens (or keys) in emergencies, and continuously polishing the system’s JWT handling as libraries and standards evolve. JWT doesn’t have built-in revocation, so the onus is on the system operators to design compensating controls and be prepared to act when needed.
Checklists (Build-Time, Runtime, Security Review)
Build-Time Checklist: During development and build, ensure that JWT security controls are baked into the codebase. Developers should confirm that they are using high-quality JWT libraries and that these libraries are configured correctly in code. For example, check that whenever a token is validated, the code specifies the expected algorithm and provides the correct key. The build process can include automated tests for token validation logic, such as a unit test that tries to validate an invalid token and asserts that the result is a failure. Incorporate static code analysis in the build pipeline with rules targeting JWT usage: flag any instance of disabling verification or using dangerous configurations. If your project has a threat modeling or design review phase, use that time to consider JWT-related threats and document how the design mitigates them (e.g., “We use library X which by default rejects ‘none’ alg, we require exp on all tokens, etc.”). Additionally, ensure secrets or keys are not hardcoded in the source – the build should pull them from secure configs. If secrets are needed for tests, use a dummy or ensure they are not production secrets.
Runtime Checklist: When deploying and running the application, a number of security checks apply to JWT handling:
- All tokens must be over HTTPS: Verify that no endpoint is accepting tokens over plaintext. Use HSTS headers or other configuration to enforce TLS. This includes internal service calls in microservice setups – those should also ideally be over TLS or within a secure network.
- Cookie settings (if applicable): If JWTs are in cookies, ensure that the runtime config sets HttpOnly and Secure flags, and consider SameSite. Confirm that cookies aren’t excessively large (which JWTs can cause if they hold too much data).
- Clock synchronization: Ensure all servers that issue or validate tokens have their clocks synced (NTP) to avoid rejecting legitimately issued tokens or allowing expired ones due to clock drift.
- Resource access control: Double-check that every protected resource actually checks for JWT validity. In frameworks, this is often configuration – e.g., in Express, ensure the JWT middleware is applied on all routes that need auth; in .NET, ensure
[Authorize]attribute or middleware is globally applied where needed. No debug or backdoor endpoints should circumvent JWT checks in production. - Key management in environment: Validate that the signing keys and secrets are correctly loaded in the environment (e.g., no dev/test keys accidentally used in prod). Use distinct keys per environment (dev, QA, prod) to avoid cross-environment token acceptance.
- Monitoring setup: At runtime, logging of JWT verification failures or unusual events should be active. The operations team should receive alerts for anomalies like repeated JWT failures (could signal an ongoing attack).
- Performance: Although not purely security, monitor that token validation is performant and not causing timeouts or high CPU due to, say, an expensive key operation or extremely large tokens. Attackers might try denial-of-service by using huge JWTs (with a large payload or header). There should be reasonable size limits configured (some libraries allow setting maximum token length).
- Fallback and error behavior: Check that the system’s behavior on an invalid or missing token is to deny access (HTTP 401/403). Sometimes logic bugs can accidentally let a request through even if auth fails (for instance, if an exception in token parsing isn’t handled, and execution continues). This should be tested and confirmed in runtime (e.g., using an integration test or a manual test with an invalid token against the deployed system).
Security Review Checklist: When performing a security assessment or code review of the application’s JWT usage, reviewers should systematically verify the following:
- Signature Verification Everywhere: For each place a JWT is consumed, is the signature verified using a strong key? This can be done by tracing the code path or testing endpoints. If using a framework, ensure that the framework’s validation is not bypassed.
- No “alg none” or insecure alg allowed: Check configuration or code for algorithm allowances. The application should not accept tokens signed with none or with weak algorithms. Ideally only one algorithm is used system-wide (or a clear set of algorithms if needed for compatibility).
- Claims Validation: Verify that important claims (
exp,iss,aud, etc.) are being checked. For example, in code you might see calls or settings that enforce expiration check (in many libraries it’s automatic, but ensure developers haven’t disabled it). If applicable, ensureiatandnbfare handled correctly (usually libraries automatically enforcenbfnot being in the future). - Token Replay/Reuse Consideration: Determine if the application has any anti-replay measures. While stateless, if
jtiis used, is there any check on it (like not allowing the same jti to be used twice within its lifespan)? If not, note that as a limitation (which might be accepted, but should be conscious). - Error Messaging: Ensure that error responses do not leak whether a token is valid vs. which part failed. A generic 401 Unauthorized is ideal, instead of a verbose error like “Signature validation failed” or “Token expired” returning to the client in detail – such differences can be used by attackers to fine-tune their attacks (though in some contexts, telling the client it’s expired is user-friendly, one must balance info leakage).
- Storage and Token Exposure: Review how tokens are stored on client side in the application. If a web app, see if JWTs are stored in localStorage – if so, is there a specific reason and are XSS mitigations in place? If tokens are in cookies, check that the cookie flags are correctly set in the Set-Cookie headers. Check that tokens are not being inadvertently exposed anywhere (e.g., some apps might include JWT in a URL during OAuth flows – ensure redirects don’t put JWT in query params).
- Third-Party Libraries: Verify that no custom cryptography is used. The presence of something like a custom JWT parser or “DIY” crypto is a red flag. Ensure the libraries in use are up to date (the review might include looking at dependency versions).
- Key/Secret Management: During review, ascertain how the application handles secrets. If in code or config, are they encrypted at rest, who has access, etc. This might be more infrastructure review, but it ties into JWT since secret exposure = total compromise.
- Compliance with Standards: If the system claims compliance with something like OWASP ASVS, cross-check relevant controls. OWASP ASVS 4.0, for example, has sections on token-based session management that require verifying tokens properly and ensuring they expire (owasp.org) (owasp.org). Ensure those are met. If the system uses OAuth/OIDC, ensure it meets those protocols’ security recommendations (e.g., if it’s an OpenID Connect token, ensure nonce is used in implicit flows, etc., albeit that’s beyond just JWT).
- Previous Incidents or Known Issues: Check if there were any past security issues or bug reports related to JWT in this application, and ensure fixes are robust. Sometimes a “patch” might superficially fix an issue but leave edge cases.
The checklists above should ultimately produce a high confidence that JWTs are handled correctly. They serve as defense-in-depth: build time prevents introducing errors, runtime catches misconfigurations or attacks, and review ensures no stone is left unturned. By following such checklists, teams can avoid the vast majority of JWT-related vulnerabilities that have historically plagued applications.
Common Pitfalls and Anti-Patterns
Despite the growing body of knowledge on JWT security, certain mistakes repeat themselves. Recognizing these common pitfalls can help developers avoid them:
Using JWT Incorrectly or Unnecessarily: One pitfall is adopting JWT because it’s trendy, not because it’s needed. JWTs are often used to implement stateless sessions in SPAs and microservices, but sometimes a simple cookie-based session or another mechanism would suffice with less complexity. Using JWT without a clear need can introduce risk without benefit. For example, an internal monolithic app might use JWT to store session info client-side, which actually increases the attack surface (token leakage, no easy logout) compared to a server session.
Failure to Verify Signatures (Assuming JWT is Secure Out-of-the-Box): This anti-pattern cannot be stressed enough – some developers assume that because a JWT is “signed and looks complex,” they don’t need to do anything. They might decode it and trust it. This is a catastrophic error we saw exemplified in code earlier. JWT provides the capability for integrity, but only if you actually check the signature with the secret or key. Assuming the framework does it automatically without verifying (when in fact you called the wrong method) is an easy trap. Always differentiate between parse/decode and verify.
Hardcoding Secrets and Keys in Code: Putting the HMAC secret or RSA private key directly in the source code (especially if the repository is public or widely accessible) is a serious anti-pattern. We still see defaults like secret or password used as JWT secrets in open-source projects, or API code with private static final String JWT_SECRET = "123456";. This invites brute force or immediate compromise if code leaks. Proper practice is to inject secrets via configuration, environment, or a vault, never in the codebase.
Ignoring Algorithm Flexibility Completely: While we advocate for algorithm whitelisting, another pitfall is ignoring algorithm mismatches. For instance, if you expect RS256 but the token comes in as HS256, your code should ideally flag that. Some older implementations would just try whatever key they have – which led to the confusion attacks. Modern code should treat an unexpected alg as a failed validation, not try to accommodate it. An anti-pattern is having one piece of code issuing HS256 tokens, another expecting RS256, etc., causing inconsistent behavior.
Overloading JWT with Too Much Data: It can be tempting to put a lot of information in a JWT since it’s a handy JSON container. But large tokens not only strain network and storage (and maybe exceed cookie limits), they also potentially expose more sensitive data. A token that includes an entire user profile (address, email, permissions, etc.) is riskier than one with just a user ID and a role. Also, more data means more chances that something wasn’t meant to be public (remember, JWT payload is readable if intercepted). A common anti-pattern is using JWT as a general-purpose data store between client and server – that’s not its purpose. It should contain identity claims to identify and authorize, nothing more. If you find yourself needing to update JWT content frequently or include things that change often, that indicates perhaps JWT isn’t the right place for that data.
No Logout or Long Expiration: Many JWT-based systems launch without a good story for logout or token revocation. This often leads to awkward hacks later, or ignoring the issue. For example, not providing a logout means a token remains valid until expiry; if that expiry is long, a user who stops using the app might still be considered logged in on that device, which might be stolen or sold. An anti-pattern here is setting a very long expiration (days or months) to avoid implementing refresh tokens, thus extending the window of compromise immensely. Conversely, some make tokens that never expire (“exp” claim far in the future or omitted) – which is worse. Best practice is tokens should expire, and ideally fairly soon. If the user needs to stay logged in, use a refresh token mechanism and periodically get a new JWT. Systems that don’t consider logout often scramble later to add a blacklist or force key rotation, which can be complex.
Storing JWT in Insecure Locations: A pitfall especially on the client side – for web apps, storing JWT in window.localStorage or sessionStorage is common but makes it accessible to any script on the page. If an XSS vulnerability exists, the attacker can grab it. A more secure alternative is a cookie (HttpOnly) or even better, not storing it at all if using it only in memory. On mobile, storing JWT in plain text files or insecure preferences (not using Keychain/Keystore) is similar. The anti-pattern is not considering the sensitivity of the token; treat it like a password or session ID. We see applications that put JWTs in the URL (as a query parameter or fragment) – this is particularly bad because URLs can be logged or leaked via referrer headers. That often happens in OAuth flows (id_token in redirect URL) so there are mitigation techniques (PKCE, etc.), but if you design your own flow, avoid that.
Not Handling Audience/Issuer (Accepting any JWT): Some services will accept any JWT signed by certain keys, without restricting who issued it or for what purpose. This is dangerous in multi-tenant or microservice environments. If two different systems use JWT and an attacker can trick one system into accepting the other’s token, that’s a breach. Each service (relying party) should check iss and aud. An anti-pattern is to have multiple issuers sharing a signing key but not differentiating tokens – effectively any token from any system with that key gets accepted anywhere. Unless that’s intended, it breaks the principle of least privilege.
Poor Error Handling Leading to Security Bypass: For instance, catching a token verification exception and then, instead of failing the request, proceeding as if the user is unauthenticated (which might still allow access to a public endpoint) is fine – but if your application treats lack of token and invalid token differently, be careful. One pitfall is misconfiguring frameworks such that an invalid token leads to a 500 error that might be overlooked, or conversely, treating a missing token as anonymous but an invalid token also as anonymous (thus not distinguishing a potential attack from a genuine no-login scenario). It’s safer to treat an invalid token as a potential attack – log it, and respond with a 401 to make clear that something was wrong.
Rolling Custom Crypto: JWT involves cryptography (HMAC or RSA/ECC signatures). An anti-pattern is deciding to implement your own signing or verification instead of using the libraries. There are many subtle things (padding rules in base64, Unicode normalization, side-channel resistant comparisons, etc.) that libraries handle. A custom implementation may accidentally skip critical steps (like not verifying the alg properly as we saw). Given the complexity, reinventing the wheel is a recipe for mistakes.
Ignoring Best Practices and Standards: Finally, not keeping up with the evolving best practices is a pitfall. For instance, the IETF Best Current Practices RFC 8725 outlines various recommendations that some older systems might not follow, like disallowing certain weaker RSA sizes or the “none” algorithm (www.rfc-editor.org) (www.rfc-editor.org). Sticking to older JWT patterns without revisiting them in light of newer guidance can leave an app vulnerable. Security is not static; one should review their JWT usage periodically against the latest OWASP cheat sheets or standards.
Avoiding these anti-patterns is largely about awareness and diligence. JWT is powerful but easy to get wrong when not treated as a security-sensitive component. By keeping these pitfalls in mind, developers and architects can double-check their designs and implementations for any sign of these issues, and correct course before they manifest in a breach.
References and Further Reading
RFC 7519: JSON Web Token (JWT) – The official IETF specification that defines the structure of JWTs, including the standard claims and the token format (Header, Payload, Signature). This document is the foundational reference for understanding JWT data encoding and interpretation. (Jones et al., IETF RFC 7519, 2015) – RFC 7519 (JWT Specification)
RFC 8725: JSON Web Token Best Current Practices – An IETF Best Current Practice document that updates RFC 7519 with security recommendations. It enumerates known vulnerabilities in JWT implementations (such as "alg=none" and algorithm confusion) and provides guidance to library implementers and developers on how to avoid them. (Sheffer et al., IETF RFC 8725, 2020) – RFC 8725 (JWT Best Practices)
OWASP JSON Web Token Cheat Sheet – An OWASP Cheat Sheet that provides practical security advice for using JWTs, particularly with a focus on Java implementations. It covers common issues like the none algorithm attack, token sidejacking, storage considerations, and how to securely implement logout and token revocation in a stateless context. This is a highly actionable guide for developers. (OWASP Cheat Sheet Series, 2021) – OWASP JWT Cheat Sheet
OWASP Application Security Verification Standard 4.0 – The ASVS is a comprehensive security standard for web apps. While not JWT-specific, it includes relevant requirements such as verifying the integrity of session tokens, using secure token storage, and enforcing expiration. Following ASVS guidelines ensures a robust implementation of authentication and session management controls, including those utilizing JWT. (OWASP ASVS 4.0, 2019) – OWASP ASVS 4.0 Standard
“Critical vulnerabilities in JSON Web Token libraries” – Auth0 Blog – Auth0’s security blog (article by Tim McLean) that famously exposed the “alg": "none” flaw and RSA vs HMAC confusion issues in various JWT libraries. It analyzes how these vulnerabilities occurred and lists which libraries were affected, providing insight into the importance of proper JWT validation. (Auth0 Blog, 2015) – Auth0 Blog: JWT Vulnerabilities
OWASP Web Security Testing Guide – Testing JSON Web Tokens – A section of the OWASP WSTG focused on JWT. It outlines how to test the contents of JWTs, how to identify if tokens can be tampered with, and describes the attacker-provided public key scenario. It also provides a remediation checklist (use strong keys, validate algorithms, etc.) and references tools for testing JWT security. (OWASP WSTG v4.2, 2020) – OWASP WSTG: Testing JWT
PentesterLab “Ultimate Guide to JWT Vulnerabilities and Attacks” – A detailed blog post and exercise guide by PentesterLab that walks through multiple JWT flaws with examples. It covers lack of signature verification, none algorithm, key injection, weak secrets, etc., and demonstrates how an attacker exploits each and how to fix them. This is useful for those who want a hands-on understanding of JWT attack techniques. (PentesterLab, 2025) – PentesterLab: JWT Vulnerabilities and Attacks
“The Dark Side of JWTs” – Hakai Security Blog – An article exploring pitfalls of JWT from a security perspective. It discusses why “signed != encrypted” (emphasizing that JWTs do not hide data), known attacks like x5c header abuse, and best practices such as always verifying tokens and not trusting client-stored tokens blindly.
NIST Special Publication 800-63B – Section on Session Management – While not exclusively about JWT, this NIST guideline covers session management for digital identity. It recommends practices like short session lifetime and re-authentication for sensitive transactions. These principles can inform JWT usage (for example, keeping JWT lifetimes short and renewing tokens regularly to abide by NIST guidance on session lengths for various assurance levels). (NIST SP 800-63B, June 2017) – NIST SP 800-63B – Session Management
JSON Web Tokens (JWT) Handbook by Auth0 – A comprehensive free e-book that covers JWT basics, usage scenarios, and best practices. It is less about security pitfalls and more about correct implementation, but includes sections on choosing algorithms, securing claims, and common patterns (like refresh tokens). Good for developers who want a deep understanding from a practical standpoint. (Auth0 JWT Handbook, 2019) – Available from Auth0 resources (search “JWT Handbook Auth0”).
This content is authored with assistance from OpenAI's advanced reasoning models (classified as AI-assisted content). Material is reviewed, validated, and refined by our team, but some issues may be missed and best practices evolve rapidly. Please use your best judgment when reviewing this material. We welcome corrections and improvements.
Send corrections to [email protected].
We cite sources directly where possible. Some elements may be derived from content linked to the OWASP Foundation, so this work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. You are free to share and adapt this material for any purpose, even commercially, under the terms of the license. When doing so, please reference the OWASP Foundation where relevant. JustAppSec Limited is not associated with the OWASP Foundation in any way.
