What Is a JWT and Why You Should Never Put Secrets in One

A Token That Looks Encrypted but Is Not

A JSON Web Token, usually called a JWT, is a compact way to pass information between two parties. You may see one after you sign in to a website or an application. The server gives your browser or app a token, and the app sends that token with later requests to show who you are. JWTs are popular because they are self-contained. The server does not need to look up a session in a database for every request. It can read the information inside the token itself.

That convenience creates a common misunderstanding. A JWT looks like a long string of random characters. It is tempting to assume that those characters are encrypted and that nobody can read them. That assumption is wrong, and it is dangerous. The payload of a JWT, which is the middle part, is encoded, not encrypted. Encoding is not the same as encryption. Anyone who has the token can decode the payload and read what is inside.

This guide explains what a JWT is, how its three parts fit together, and why you should never put secrets in one. It includes a worked example with common claims, a checklist for handling tokens safely, and answers to questions that come up when developers first meet JWTs. You do not need to have built an authentication system to follow it.

The Three Parts: Header, Payload, Signature

A JWT has three parts, separated by dots. The format is header.payload.signature. Each part is encoded with Base64URL, which is a variant of Base64 that is safe to use in web addresses. Base64URL encoding changes binary data into text characters, but it does not hide the meaning. Decoding is a simple, reversible step that anyone can perform with a standard tool.

The header describes the token itself. It usually says that the token is a JWT and names the signing algorithm, such as HS256 or RS256. The header is small and does not contain user data. Its job is to tell the receiver how to interpret and verify the token.

The payload contains claims, which are statements about the user or the session. A claim is a key and value pair, such as a user identifier, an issued-at time, or an expiry time. The payload is the part that causes the most confusion, because it is readable. If you put an email address, a role, or a permission in the payload, anyone with the token can read it.

The signature is the part that protects the token from tampering. It is created by taking the encoded header and payload, joining them with a dot, and signing the result with a secret key or a private key. When a server receives the token, it can recompute the signature and compare it with the signature in the token. If they match, the header and payload have not been changed since the token was issued. If they do not match, the token has been altered or forged, and the server should reject it.

Encoded Is Not Encrypted

The distinction between encoding and encryption is the central idea of this guide. Encoding changes the representation of data so it can be stored or transmitted. It is designed to be reversible by anyone. Base64 is a good example. It turns bytes into text so they can travel safely in a URL or a header. It does not require a key, and it does not protect the data from being read.

Encryption, by contrast, changes data so that only someone with the correct key can read it. It is designed to keep information secret. A JWT payload is not encrypted by default. It is only Base64URL-encoded. That means the payload is readable to anyone who obtains the token. If you would not write a piece of information on a postcard, do not put it in a JWT payload.

This does not mean JWTs are useless. It means they have a specific purpose. A JWT is a way to carry claims that can be verified, not a way to hide information. The signature proves that the claims have not been changed. It does not prove that the claims are secret. Those are two different jobs, and a JWT only does the first one by default.

If you genuinely need to keep the contents of a token private, you need a different mechanism, such as an encrypted token format or a server-side session where the sensitive data stays on the server. Do not try to make a standard JWT secret by obscuring it. A determined reader can decode it in seconds.

How to Read a JWT Safely

  1. Split the token on dots. You should get three parts: header, payload, and signature. If you get a different number, the token is malformed.
  2. Decode the header. Use a Base64URL decoder, not a guess. Check the algorithm. If the algorithm is unexpected, treat the token with suspicion.
  3. Decode the payload. Read the claims. Note the subject, issued-at, and expiry values. Remember that decoding only shows you the contents. It does not prove the token is valid.
  4. Verify the signature. Recompute the signature using the correct key and algorithm, and compare it with the signature in the token. Only a matching signature gives you reason to trust the contents.
  5. Check expiry and scope. Confirm that the token has not expired, that it was issued for the expected audience, and that it grants only the permissions the user should have.

Steps one to three tell you what the token says. Step four tells you whether to trust it. Step five tells you whether it is still usable. Skipping step four is one of the most serious JWT mistakes. A token that is not verified is just a claim made by whoever sent it.

Worked Example: Claims in a Payload

Consider a payload that contains three common claims. The claim sub, short for subject, identifies the user. The claim iat, short for issued at, records when the token was created, as a count of seconds since a standard reference point. The claim exp, short for expiry, records when the token stops being valid, using the same time format.

An example payload, shown here as JSON before encoding, might look like this:

{
  "sub": "1234567890",
  "name": "Asha",
  "iat": 1516239022,
  "exp": 1516242622
}

In this example, sub identifies the user as 1234567890. The name is Asha. The token was issued at the time represented by 1516239022, and it expires at the time represented by 1516242622, which is one hour later. The exact dates do not matter for this guide. What matters is the structure: a small set of claims, each with a clear meaning.

When this payload is Base64URL-encoded and placed in a token, it becomes a long string that looks unreadable. Do not be fooled by that appearance. Anyone with the token can decode the middle part and recover the JSON above. That is why the payload should contain only information you are comfortable with the token holder reading, such as a user identifier and a role. It should never contain a password, a secret key, a payment detail, or any other sensitive value.

Notice also that the example includes an expiry. A token without an expiry is risky, because if it is leaked, it can be reused until the signing key is changed. A short expiry limits the damage from a leaked token. It does not remove the need to keep tokens safe, but it reduces the window in which a stolen token can be used.

What Never to Put in a JWT

Do not store passwords in a JWT. Passwords belong in a properly secured credential store on the server, with strong hashing. They should never travel in a token that the client can read, and they should never be stored in a place where a leaked token exposes them.

Do not store API keys, secret keys, or private tokens in a JWT. A JWT is not a vault. If a secret is in the payload, anyone who sees the token can copy it. That defeats the purpose of keeping the secret in the first place.

Do not store payment details, government identifiers, health information, or other sensitive personal data in a JWT. Even if the token is sent over an encrypted connection, it may be logged, cached, or copied. Once a token leaves the server, the server loses control over where it goes. Keep sensitive data on the server, and use the token only to refer to it by an identifier.

Do not store permissions that are hard to change. If you put a role or a permission in a token with a long expiry, you cannot easily take that permission away before the token expires, unless you also keep a server-side list of revoked tokens. For permissions that change often, keep them on the server and check them on each request.

Verify the Signature Server-Side

Signature verification must happen on the server, using a key that the client does not know. If verification happens only in the browser or in a mobile app, an attacker can bypass it by changing the app or by sending requests directly. The server is the authority. It must check the signature on every request that relies on the token.

Verification means more than checking that the token has three parts. The server must recompute the signature with the correct key and algorithm, compare it in a safe way, and reject the token if the signature does not match. It must also check the algorithm. A well-known attack involves changing the algorithm to none, which means no signature, and hoping the server accepts it. A server that is configured correctly will reject a token with an unexpected algorithm.

The server should also check the expiry, the issuer, and the audience, if those claims are used. It should reject expired tokens, even if the signature is valid. It should reject tokens issued by an unexpected party. These checks are part of verification, not optional extras. A token that passes the signature check but fails the expiry check should not be accepted.

Keep the signing key secret. If the key leaks, anyone can create tokens that the server will accept. Rotate keys when needed, and use a strong, randomly generated key for symmetric signing. For asymmetric signing, keep the private key private and share only the public key for verification.

Limitations of JWTs

JWTs are stateless, which is both their strength and their weakness. The server does not need to look up a session for every request, but it also cannot easily revoke a single token before it expires. If a token is stolen, it can be used until it expires, unless the server keeps a revocation list. That list adds back some of the state that JWTs were meant to avoid.

JWTs can also grow large. If you add many claims, the token becomes long, and it is sent with every request. That adds overhead. Keep payloads small. Include only the claims you need, and keep sensitive or detailed data on the server.

JWTs do not by themselves prove that the person presenting the token is the person it was issued to. If the token is copied, the copy works just as well as the original. That is why tokens must be sent over encrypted connections, stored carefully, and given short expiries. A JWT is a bearer token. Whoever bears it can use it. Protect it accordingly.

Finally, JWTs do not replace good authentication design. They are one tool for carrying verified claims. You still need secure sign-in, secure storage, secure transport, and sensible expiry and revocation policies. A JWT used badly can be worse than a simpler session, because it creates a false sense of security.

Frequently Asked Questions

Is a JWT encrypted?

No, not by default. A standard JWT payload is Base64URL-encoded, which means it is readable by anyone who has the token. Encoding is not encryption. If you need the contents to be private, use an encrypted token format or keep the sensitive data on the server.

Can I decode a JWT without the key?

Yes, you can decode the header and payload without the key, because decoding only reverses the Base64URL encoding. The key is needed to verify the signature, not to read the payload. That is exactly why you should never put secrets in the payload.

What does sub, iat, and exp mean?

The claim sub, or subject, identifies the user or entity the token is about. The claim iat, or issued at, records when the token was created. The claim exp, or expiry, records when the token stops being valid. Together, they help the server decide whether the token is still usable and whom it refers to.

Should I verify a JWT in the browser?

You can decode a JWT in the browser to display information, but you should not rely on browser-side verification for security. The server must verify the signature on every request. Anything checked only in the browser can be bypassed by an attacker who sends requests directly.

Conclusion

A JWT is a useful way to carry claims that can be verified, in the form header.payload.signature. The payload is readable Base64, not encrypted, so it must never contain passwords, secrets, or sensitive personal data. Use short expiries, keep the signing key secret, and verify the signature server-side on every request. When you treat a JWT as a verifiable claim rather than a hiding place, you get its benefits without falling for its most dangerous misunderstanding.