JWT Debugger & Decoder
Decode and inspect JSON Web Tokens (JWT) instantly. View the header, payload, and signature. Check expiration status. Runs in your browser.
A JSON Web Token (JWT) debugger and decoder is a critical diagnostic environment used by software developers to unpack, inspect, and verify the contents of stateless authentication tokens. Because JWTs transmit encoded—not encrypted—information between parties as a JSON object, understanding how to decode their header, payload, and cryptographic signature is essential for securing modern web applications and application programming interfaces (APIs). This comprehensive guide will transform you from a complete novice into an expert on JWT architecture, teaching you the exact mechanics of token decoding, cryptographic verification, and enterprise-grade security practices.
What It Is and Why It Matters
A JSON Web Token (JWT) is an open standard that defines a compact, self-contained way for securely transmitting information between parties as a JavaScript Object Notation (JSON) object. A JWT debugger and decoder is the analytical mechanism used to reverse-engineer these tokens, allowing developers to read the data inside them and verify that their cryptographic signatures are mathematically valid. To understand why this matters, you must first understand the problem JWTs solve: the limitations of stateful authentication. In traditional web architecture, when a user logs in, the server creates a "session" in its database and sends a small reference ID (a cookie) back to the user's browser. Every time the user clicks a link or requests data, the browser sends that cookie, and the server must look up the session ID in its database to verify the user's identity.
This stateful approach breaks down at scale. If an application grows to handle 100,000 concurrent users across a distributed network of 50 different microservices, requiring every single server to constantly query a central database just to verify a user's identity creates a massive, expensive bottleneck. JWTs solve this by being entirely stateless and self-contained. Instead of storing a session in a database, the server packages all necessary user information (like their user ID, role, and session expiration time) directly into the token, cryptographically signs it, and hands it to the user. When the user presents this token to any server in the network, the server simply performs a mathematical operation on the signature to verify that the token was created by a trusted source and has not been tampered with. A JWT decoder allows developers to see exactly what data is packed inside these tokens, ensuring that the correct user privileges, expiration timestamps, and metadata are being transmitted across the network. Without the ability to decode and debug these tokens, developers would be flying blind, unable to verify the integrity of their distributed authentication systems.
History and Origin
The conceptual foundation for JSON Web Tokens emerged in the early 2010s as the software industry underwent a massive paradigm shift from monolithic server-rendered applications to decoupled Single Page Applications (SPAs) and mobile applications. Traditional cookie-based session management, which relied heavily on web browsers automatically attaching cookies to requests, proved incredibly cumbersome for iOS and Android applications interacting with RESTful APIs. Recognizing the need for a standardized, platform-agnostic, and URL-safe token format, a group of identity and security experts formed the JSON Object Signing and Encryption (JOSE) working group under the Internet Engineering Task Force (IETF).
The primary architects of the JWT specification included Michael B. Jones of Microsoft, John Bradley of Ping Identity, and Nat Sakimura of the Nomura Research Institute. They began drafting the initial specifications in 2011, aiming to combine the lightweight nature of JSON with robust cryptographic signing mechanisms. Over several years of rigorous peer review and iteration, the working group finalized the standard. In May 2015, the IETF officially published RFC 7519, cementing the JSON Web Token as a global internet standard. Alongside RFC 7519, the working group published a suite of related standards, including RFC 7515 (JSON Web Signature or JWS) and RFC 7516 (JSON Web Encryption or JWE), which defined the exact cryptographic operations required to secure the tokens. Since its standardization in 2015, the JWT has become the undisputed backbone of modern identity protocols, serving as the default token format for OpenID Connect (OIDC) and OAuth 2.0 implementations worldwide. Understanding this history is crucial because it explains why JWTs are designed the way they are: they were explicitly engineered to be lightweight enough to fit in HTTP headers, flexible enough to carry arbitrary JSON data, and secure enough to operate in highly distributed, zero-trust network environments.
How It Works — Step by Step
To truly master JWT debugging, you must understand the exact mechanical process of how a token is constructed and subsequently decoded. A standard JWT consists of three distinct parts separated by periods (.): the Header, the Payload, and the Signature. The resulting string looks like this: xxxxx.yyyyy.zzzzz. None of this data is inherently hidden; the first two parts are simply encoded using a scheme called Base64Url. Base64Url is a binary-to-text encoding scheme that translates data into a string of 64 specific ASCII characters (A-Z, a-z, 0-9, -, and _). It is specifically designed to be "URL-safe," meaning the resulting string can be passed in web addresses and HTTP headers without breaking web protocols.
Let us walk through a complete, mathematical example of encoding and decoding a JWT. We begin with the Header, which declares the token type and the cryptographic algorithm used. For example, the JSON {"alg":"HS256","typ":"JWT"}. To encode this, we convert the text to bytes and apply Base64Url encoding, which yields the exact string: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. Next is the Payload, which contains the actual data (called claims). Suppose our payload is {"sub":"1234567890","name":"John Doe","admin":true}. Converting this JSON to Base64Url yields: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9. At this point, we concatenate the encoded header and payload with a period: eyJhbGci...J9.eyJzdWIi...ydWV9.
The final step is the Signature, which proves the token has not been tampered with. If the header specifies HS256 (HMAC with SHA-256), the server takes the concatenated string from the previous step and hashes it using a secret key known only to the server. The formula is: HMACSHA256( encoded_header + "." + encoded_payload, "your-256-bit-secret" ). If our secret is the string my_secret_key_123, the resulting mathematical hash, once Base64Url encoded, becomes RQ2r86R7zD9X2eYq-z3J5_Yw6a7P8_9_9_9_9_9_9_8 (hypothetical representation). The final JWT is the combination of all three parts: Header.Payload.Signature. When a debugger decodes this token, it reverses the process. It splits the string at the periods. It takes the first two segments, replaces any URL-safe characters (- becomes +, _ becomes /), pads the string with = characters until its length is a multiple of 4, and performs standard Base64 decoding to reveal the original JSON. Finally, the debugger takes the provided secret key, runs the exact same HMACSHA256 formula on the decoded header and payload, and checks if the resulting signature matches the third part of the token. If they match perfectly, the token is valid; if even a single space or character in the payload was altered by a hacker, the resulting hash will be completely different, and the signature verification will fail.
Key Concepts and Terminology
To navigate the world of JWTs and identity management, you must master the specific vocabulary used by security professionals. The most fundamental concept is a Claim. In JWT terminology, a claim is simply a piece of information asserted about a subject, represented as a key-value pair inside the JSON payload. Claims are categorized into three distinct types: Registered, Public, and Private.
Registered Claims are a set of predefined, standardized fields established by the IETF in RFC 7519. They are not mandatory, but they provide a universal language for token validation. The most critical registered claims include iss (Issuer, identifying the server that created the token), exp (Expiration Time, a crucial numeric timestamp defining exactly when the token becomes invalid), sub (Subject, typically the unique user ID), aud (Audience, identifying the intended recipient of the token), nbf (Not Before, a timestamp indicating when the token becomes active), and iat (Issued At, the timestamp of creation). All timestamps in JWTs are represented as NumericDate values, which are the number of seconds since the Unix Epoch (January 1, 1970, 00:00:00 UTC).
Public Claims are custom fields created by developers that are meant to be shared across different organizations. To prevent naming collisions (where two different companies use the same key for different purposes), public claims should be registered in the IANA JSON Web Token Claims Registry or formatted as collision-resistant URIs (e.g., https://example.com/claims/role). Private Claims are custom fields used exclusively within a closed system or single organization, such as {"employee_id": "8675309", "department": "engineering"}.
You must also understand the distinction between JWS (JSON Web Signature) and JWE (JSON Web Encryption). When people say "JWT," 99% of the time they are actually referring to a JWS. A JWS simply signs the data to prove its authenticity; the payload remains entirely readable to anyone who decodes the Base64Url string. A JWE, on the other hand, actually encrypts the payload using complex cryptographic algorithms, ensuring that only a party holding the correct decryption key can read the contents. Finally, you will frequently encounter JWK (JSON Web Key), which is a standardized JSON format for representing cryptographic keys. Identity providers like Google or Auth0 host a public "JWKS endpoint" (JSON Web Key Set) that applications query to download the public keys necessary to verify the signatures of incoming tokens.
Types, Variations, and Methods
The security and functionality of a JWT depend entirely on the cryptographic algorithm used to generate its signature. The header of every JWT contains an alg (algorithm) claim that instructs the decoding application on how to verify the token. These algorithms fall into two primary categories: Symmetric cryptography and Asymmetric cryptography. Understanding when to use each is the hallmark of a competent security architect.
Symmetric Algorithms (HS256, HS384, HS512) utilize a single, shared secret key for both creating the signature and verifying it. HS256 (HMAC using SHA-256) is the most common symmetric algorithm. In this model, the server generating the JWT and the server verifying the JWT must both possess the exact same secret string. This method is incredibly fast and computationally inexpensive. However, it suffers from a major architectural limitation: key distribution. If you have an authentication server issuing tokens, and 50 different microservices verifying those tokens, you must securely distribute the master secret to all 50 microservices. If any single microservice is compromised, the attacker obtains the secret key and can forge perfectly valid JWTs for any user, completely destroying the integrity of your system. Therefore, symmetric algorithms are strictly reserved for closed, monolithic applications where the same server issues and verifies the token.
Asymmetric Algorithms (RS256, ES256, PS256) solve the key distribution problem by utilizing public-key cryptography. RS256 (RSA Signature with SHA-256) is the industry standard for enterprise applications. In this model, the authentication server generates a mathematically linked pair of keys: a Private Key and a Public Key. The server uses the strictly guarded Private Key to sign the JWT. The 50 microservices are given only the Public Key. The mathematical magic of RSA allows the microservices to use the Public Key to verify that the signature was definitively created by the Private Key, but it is mathematically impossible to use the Public Key to forge a new signature. If a microservice is compromised, the attacker only gets the Public Key, which is useless for forging tokens. ES256 (ECDSA using P-256 and SHA-256) is a modern variation of asymmetric cryptography that uses Elliptic Curve mathematics. ES256 provides the exact same security level as RS256 but generates significantly smaller signature strings, making it highly desirable for bandwidth-constrained environments like mobile applications or IoT devices.
Real-World Examples and Applications
To solidify these concepts, let us examine how JWTs operate in high-scale, real-world scenarios. Consider a modern streaming platform like Netflix or Spotify, which utilizes a distributed microservices architecture. When a user enters their email and password, the request goes to a centralized Identity Provider (IdP) service. The IdP verifies the credentials against the database and generates a JWT. The payload of this token contains specific, actionable data: {"sub": "user_98765", "plan": "premium", "region": "US", "exp": 1715000000}. The IdP signs this token using its RSA Private Key and sends the resulting 600-byte string back to the user's browser.
Now, the user clicks a button to stream a 4K movie. The browser sends an HTTP request to the "Video Streaming Microservice," attaching the JWT in the Authorization: Bearer <token> HTTP header. The Video Streaming Microservice does not need to pause and ask the central database, "Is user_98765 allowed to watch 4K video?" Instead, it simply decodes the JWT using a debugger-like internal library. It checks the signature using the IdP's Public Key. Once the math checks out, the microservice trusts the payload implicitly. It reads "plan": "premium", confirms the exp timestamp has not passed, and immediately begins streaming the 4K video. By eliminating the database lookup, the microservice saves approximately 50 to 100 milliseconds of latency per request. When a platform processes 10 million requests per minute, eliminating 10 million database queries saves immense computational resources and millions of dollars in infrastructure costs.
Another critical application of JWTs is in secure password reset flows. When a user requests a password reset, the server generates a highly specific, short-lived JWT. The payload might look like: {"sub": "user_123", "action": "password_reset", "exp": 1620001800}. The expiration time is set to exactly 15 minutes from creation. The server embeds this JWT directly into the URL sent to the user's email: https://app.com/reset?token=eyJhbGci.... When the user clicks the link, the server decodes the token from the URL. Because the token is self-contained, the server doesn't need a "pending resets" database table. It simply verifies the signature, ensures the action claim is strictly password_reset, and checks that the current time is less than the exp time. If valid, it allows the password change. If the user clicks the link 16 minutes later, the cryptographic verification of the exp claim fails, and the server automatically rejects the request.
Common Mistakes and Misconceptions
The simplicity of decoding a JWT often leads to severe security vulnerabilities caused by fundamental developer misconceptions. The single most dangerous and pervasive misconception is the belief that JWTs are encrypted and therefore hide data. Because a JWT looks like a random string of gibberish (eyJhbGci...), novice developers frequently assume it is encrypted. They will place highly sensitive Personally Identifiable Information (PII)—such as Social Security Numbers, plaintext passwords, or internal database connection strings—directly into the payload. Anyone with a web browser can copy that token, paste it into a JWT decoder, and instantly read the plaintext JSON. Unless you are specifically using the complex JWE (JSON Web Encryption) standard, a standard JWS provides zero data confidentiality. It only provides data integrity.
Another catastrophic mistake is failing to validate the cryptographic signature after decoding the token. Some developers use decoding libraries simply to read the JSON payload, writing logic like if (decodedToken.admin == true) { grantAccess(); }. This is a fatal flaw. A malicious user can simply decode their own token, change "admin": false to "admin": true, re-encode it to Base64Url, and send it back to the server. If the server does not perform the mathematical signature verification using the secret key, it will blindly trust the tampered token. You must never trust the decoded payload until the signature verification algorithm returns a definitive success.
Historically, the most infamous JWT vulnerability is the alg: none attack. The JWT specification technically allows the header to specify "alg": "none", indicating that the token has no signature at all. This was originally intended for situations where the token was already secured by another layer (like mutual TLS). However, poorly written JWT decoding libraries would read the header, see "alg": "none", and bypass the signature verification step entirely. Attackers realized they could take a valid token, alter the payload to give themselves administrative rights, change the header to "alg": "none", strip off the signature, and send it to the server. The flawed libraries would accept the forged token as valid. Modern, well-maintained JWT libraries explicitly reject the none algorithm by default, but developers building custom verification logic must explicitly hardcode their systems to only accept specific, secure algorithms like RS256.
Best Practices and Expert Strategies
Professional security architects adhere to strict decision frameworks when implementing JWTs. The foundational best practice is managing token lifespans through the "Access Token / Refresh Token" pattern. Because JWTs are stateless, they cannot easily be revoked once issued. If an attacker steals a token with a 30-day expiration, they have unfettered access for 30 days. Therefore, experts mandate that JWT Access Tokens must have incredibly short expiration times—typically between 5 and 15 minutes. To prevent the user from having to log in every 15 minutes, the server simultaneously issues an opaque, stateful "Refresh Token" with a longer lifespan (e.g., 7 days). When the 15-minute JWT expires, the frontend application silently sends the Refresh Token to the authentication server. The server checks its database to ensure the user hasn't been banned or logged out; if all is well, it issues a fresh 15-minute JWT. This pattern balances the performance benefits of stateless JWTs with the security requirement of being able to cut off access.
Token storage in web browsers is another area requiring expert strategy. Novice developers often store JWTs in the browser's localStorage or sessionStorage. This is highly discouraged because any JavaScript running on the page can access localStorage. If the application suffers from a single Cross-Site Scripting (XSS) vulnerability—perhaps through a compromised third-party analytics script—the attacker's script can instantly steal the JWT and hijack the user's session. The industry standard best practice is to store JWTs in HttpOnly, Secure, SameSite=Strict cookies. An HttpOnly cookie cannot be read by any JavaScript under any circumstances; the browser automatically attaches it to outgoing HTTP requests. While this completely mitigates XSS token theft, it does introduce the risk of Cross-Site Request Forgery (CSRF), meaning developers must implement standard anti-CSRF tokens alongside their JWT architecture.
Finally, experts strictly enforce claim validation beyond just the signature and expiration. A robust JWT decoder implementation must verify the iss (Issuer) claim to ensure the token came from the expected authentication server, preventing scenarios where a token from a staging environment is accidentally accepted by production servers. Furthermore, the aud (Audience) claim must be verified. If an Identity Provider issues a token intended for the "Billing Microservice," the token's aud claim will reflect that. If a malicious user intercepts that token and tries to use it to access the "User Deletion Microservice," the deletion service must reject it because it is not the intended audience, strictly limiting the blast radius of a stolen token.
Edge Cases, Limitations, and Pitfalls
Despite their ubiquity, JWTs possess inherent architectural limitations that break down in specific edge cases. The most significant limitation is the "Revocation Problem." Because a JWT is entirely self-contained and stateless, the verifying server relies solely on the token's internal math and expiration timestamp. If a user's laptop is stolen, or if an administrator clicks "Ban User," you need to invalidate their access immediately. However, the existing JWT out in the wild is still mathematically valid until its exp timestamp passes. To solve this, developers are forced to reintroduce statefulness, defeating the primary purpose of the JWT. The standard mitigation is maintaining a "Denylist" (or blacklist) in an ultra-fast in-memory database like Redis. When a token is revoked, its unique jti (JWT ID) claim is added to Redis. Every microservice must then check Redis on every request to see if the jti is blacklisted. This reintroduces the exact database bottleneck JWTs were designed to eliminate.
Token size bloat is another severe pitfall. Because developers love the convenience of having data readily available in the decoded payload, they tend to shove more and more information into the token: user roles, permissions, department IDs, avatar URLs, and user preferences. A JWT can easily swell from 500 bytes to 8 kilobytes. When this massive token is attached to every single HTTP request via the Authorization header, it severely degrades network performance, especially on slow mobile connections. Furthermore, standard web servers like Nginx or Apache often have hard limits on HTTP header sizes (typically 4KB or 8KB). If a bloated JWT exceeds this limit, the web server will outright reject the request with a 431 Request Header Fields Too Large error, causing catastrophic application failure that is notoriously difficult to debug.
Clock skew represents a subtle but maddening edge case in distributed systems. A JWT's validity relies heavily on timestamps (exp, nbf, iat). If the authentication server generating the token has its system clock set 30 seconds faster than the API server verifying the token, bizarre errors occur. The API server might decode a brand-new token, look at the iat (Issued At) timestamp, and determine that the token was mathematically issued 30 seconds into the future. Strict decoding libraries will immediately reject this as an invalid token. To mitigate this pitfall, robust JWT verification implementations always include a "leeway" configuration—typically allowing for 30 to 60 seconds of clock skew between distributed servers to prevent false rejections.
Industry Standards and Benchmarks
The implementation of JWTs is heavily governed by stringent industry standards and accepted benchmarks. The foundational document is RFC 7519, published by the IETF, which defines the exact structural requirements of the token. However, because RFC 7519 is highly flexible, the IETF subsequently published RFC 8725, titled "JSON Web Token Best Current Practices" (BCP 225). This document is the definitive benchmark for security professionals. It explicitly mandates that applications must perform algorithm verification (preventing the alg: none attack), must validate all cryptographic signatures before reading payloads, and must utilize cryptographically secure pseudo-random number generators for key creation.
In terms of cryptographic benchmarks, the absolute minimum standard for symmetric keys (HS256) is a 256-bit secret. This equates to a purely random string of 32 bytes (or 64 hexadecimal characters). Using a simple password like my_company_secret as a symmetric key is a critical vulnerability, as modern GPU clusters can brute-force weak HMAC-SHA256 hashes in minutes. For asymmetric cryptography (RS256), the industry benchmark mandates a minimum RSA key size of 2048 bits, though 4096 bits is highly recommended for systems requiring long-term security against advancing computational power.
Payload size benchmarks dictate that a well-architected JWT should remain under 1 kilobyte (1,024 bytes) in total length. This ensures the token fits comfortably within standard HTTP header limits while minimizing network latency overhead. If the encoded string exceeds 1KB, architects are benchmarked to refactor their systems, removing non-essential claims from the token and instead fetching that supplementary data via secondary API calls. Regarding temporal benchmarks, the industry standard for Access Token expiration (exp) is strictly between 5 and 15 minutes. Any JWT access token with an expiration exceeding 1 hour is generally flagged as a high-severity security risk during professional penetration testing.
Comparisons with Alternatives
To fully master JWTs, you must understand how they compare to alternative authentication mechanisms and when to choose one over the other. The most common comparison is JWT vs. Stateful Session Cookies. Stateful sessions utilize an opaque, random string (e.g., session_id=9876xyz) stored in a cookie. The string means nothing on its own; the server must look it up in a database to find the user's data. Sessions are vastly superior to JWTs when it comes to revocation and administrative control. If you need to instantly log out a user across all devices, you simply delete the session row in the database. Sessions also completely eliminate the risk of exposing sensitive data to the client, as the data never leaves the server. However, sessions fail spectacularly in highly distributed, cross-domain architectures. If you have an API at api.example.com and a frontend at app.example.com, managing cross-origin cookie sharing is notoriously difficult. JWTs win decisively in distributed, multi-server environments because they require no central database and easily cross domain boundaries via the Authorization header.
JWT vs. SAML (Security Assertion Markup Language) is a comparison of eras. SAML is an older, XML-based standard heavily used in enterprise Single Sign-On (SSO) environments (like corporate Active Directory integrations). SAML assertions function similarly to JWTs—they are signed packages of user data. However, SAML XML payloads are massive, complex, and computationally heavy to parse. A SAML assertion might be 10 kilobytes of dense XML, whereas the equivalent JWT is 600 bytes of JSON. JWTs have largely replaced SAML in modern web and mobile development due to JSON's native compatibility with JavaScript and the lightweight nature of the tokens, though SAML remains entrenched in legacy enterprise systems.
Finally, JWT vs. PASETO (Platform-Agnostic Security Tokens) represents the future of tokenized security. PASETO was created by security researchers specifically to address the design flaws of JWT. The primary flaw of JWT is that it gives the attacker control over the cryptographic algorithm via the alg header. PASETO removes this entirely. Instead of allowing developers to choose from dozens of algorithms, PASETO defines strict "versions" and "purposes" (e.g., v4.public or v4.local) that mandate the use of specific, state-of-the-art cryptographic algorithms (like Ed25519 for signatures or XChaCha20 for encryption). While PASETO is objectively more secure and developer-proof than JWT, JWT remains the industry standard purely due to massive existing adoption, integration into OAuth/OIDC standards, and universal library support across every programming language.
Frequently Asked Questions
Can I decode a JWT without knowing the secret key? Yes, absolutely. The secret key is only required to verify the signature (the third part of the token). The header and payload (the first two parts) are merely encoded using Base64Url, not encrypted. Anyone who possesses the token string can instantly decode it and read all the JSON data inside. This is why you must never store passwords, social security numbers, or sensitive personal data inside a standard JWT payload.
If anyone can decode the token, what prevents a user from changing their user ID or role?
While anyone can decode and modify the payload, they cannot generate a valid signature for their modified payload without the server's secret key. When the server receives the token, it takes the header and payload, runs them through the hashing algorithm using its closely guarded secret key, and compares the result to the signature attached to the token. If an attacker changed "role":"user" to "role":"admin", the server's mathematical hash will not match the attacker's token signature, and the token will be immediately rejected as tampered.
What is the difference between an Access Token and an ID Token? An Access Token is used to authorize a user to access an API or resource; it is meant to be read by the API server, and its payload format is often specific to that API. An ID Token, defined by the OpenID Connect (OIDC) standard, is strictly meant to be read by the client application (like a web browser or mobile app). The ID Token contains standard claims about the user's identity (like name, email, and profile picture) so the frontend application can display "Welcome, John!" without needing to make an additional API call.
Why does my JWT decoding library throw an error about "Clock Skew"?
Clock skew occurs when the server that generated the token and the server verifying the token have slightly different system times. If Server A creates a token at 12:00:05 and sends it to Server B, but Server B's clock thinks it is currently 12:00:00, Server B will look at the iat (Issued At) claim and think the token was generated 5 seconds in the future. Most robust decoding libraries will reject "future" tokens. You fix this by configuring a "leeway" tolerance of 30-60 seconds in your verification library.
How do I invalidate or log out a JWT before it expires?
Because JWTs are stateless, you cannot "delete" them from a central database. The most common solution is to maintain a denylist (blacklist) of revoked token IDs (jti claim) in a fast, in-memory store like Redis. When a user logs out, their token's jti is added to Redis until its natural expiration time passes. Every API request must quickly check Redis to ensure the token hasn't been revoked. Alternatively, you can keep token expiration times extremely short (e.g., 5 minutes) so that revoked access naturally expires very quickly.
Is it safe to store a JWT in LocalStorage?
No, storing JWTs in the browser's localStorage or sessionStorage is strongly discouraged by security professionals. Any JavaScript executing on your website has full access to localStorage. If your site has a Cross-Site Scripting (XSS) vulnerability, a malicious script can instantly copy the JWT and send it to an attacker, granting them full access to the user's account. The recommended best practice is to store the JWT in an HttpOnly, Secure cookie, which is completely inaccessible to client-side JavaScript.