What Is JWT? JSON Web Tokens Explained

JSON Web Tokens appear in login flows, APIs, mobile applications, single sign-on systems, and communication between backend services. Their compact format makes them convenient, but that convenience has also produced a surprising number of insecure implementations.
In our experience working with authentication systems, the hardest part of JWT is rarely generating the token. The difficult part is deciding what the token should represent, validating it correctly, storing it safely, and handling expiration and revocation without weakening the entire authentication flow.
This guide explains how JWT works, what it protects, where it fits, and the security decisions that matter in a production application.
What Is a JWT?
A JSON Web Token, commonly called a JWT, is a compact, URL-safe format for transmitting a set of claims between parties. A claim is a statement such as the identity of a user, the system that issued the token, its intended recipient, or the time at which it expires.
The official JWT standard is RFC 7519. JWT defines the structure of the token, while related JOSE standards define how its contents can be signed, authenticated, or encrypted.
A common signed JWT contains three Base64URL-encoded sections separated by periods:
header.payload.signatureA token might look like this:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMyIsImlzcyI6Imh0dHBzOi8vYXV0aC5leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tIn0.
signature-bytesThe periods are part of the compact serialization. Line breaks have been added above only to make the example easier to read.
The Most Important JWT Fact
A signed JWT is normally readable by anyone who obtains it.
Base64URL encoding is an encoding scheme, not encryption. Anyone can decode the header and payload without possessing the signing key. The signature protects the token against undetected modification, but it does not make the claims private.
Sensitive information such as passwords, private keys, payment details, medical records, or unnecessary personal data should therefore stay out of an ordinary signed JWT.
Encryption requires a JSON Web Encryption token, commonly called JWE, or another secure transport and data-protection design. Most access tokens encountered in application development are signed JWS tokens rather than encrypted JWEs.
The Three Parts of a JWT
1. Header
The header describes how the token is protected. A typical header contains an algorithm and a token type:
{
"alg": "RS256",
"typ": "JWT"
}The alg value identifies the signature or message-authentication algorithm. RS256, for example, uses an RSA private key to sign the token and the corresponding public key to verify it.
The verifier must use a predefined allowlist of acceptable algorithms. Trusting whichever algorithm arrives in the token header allows an attacker to influence a security decision that belongs to the application.
2. Payload
The payload contains the JWT claims set:
{
"iss": "https://auth.example.com",
"sub": "user_123",
"aud": "https://api.example.com",
"iat": 1786600000,
"exp": 1786600900,
"scope": "orders:read"
}Claims can be registered, public, or private. Registered claims provide standardized names, while private claims support application-specific information.
Common registered claims include:
| Claim | Meaning | Typical validation |
iss | Issuer | Must match the trusted authorization server |
sub | Subject | Identifies the user or entity represented by the token |
aud | Audience | Must include the API or service processing the token |
exp | Expiration time | Token must be rejected at or after this time |
nbf | Not before | Token must not be accepted before this time |
iat | Issued at | Helps determine the token's age |
jti | JWT ID | Provides a unique identifier that can support replay controls |
RFC 7519 makes many registered claims optional at the generic format level. A real application or protocol should define which claims are required and reject tokens that do not satisfy that profile.
3. Signature
The signature covers the encoded header and payload:
signingInput = base64url(header) + "." + base64url(payload)
signature = sign(signingInput, signingKey)The receiving service reconstructs the signing input and verifies the signature using the appropriate key. A changed payload produces a failed verification result because it no longer matches the signature.
Signature verification proves that the protected content was produced by a party holding the expected signing key and that the protected bytes have not changed. Trust still depends on selecting the correct issuer, key, algorithm, audience, and token type.
How JWT Authentication Works
A typical JWT authentication flow begins when a user sends credentials or completes an identity-provider login. The authorization server verifies the login and issues an access token. The client then presents that token when calling a protected API.
Authorization: Bearer <access-token>The API extracts the bearer token and performs cryptographic and claims validation. A valid token allows the API to use the approved claims when making its authorization decision.
The complete flow is:
- The user authenticates with an authorization server.
- The server issues a short-lived access token.
- The client sends the token to the intended API over HTTPS.
- The API verifies the signature and required claims.
- The API applies its own authorization rules.
- The API accepts or rejects the request.
Authentication and authorization remain separate concerns. A valid token may identify the caller, but the API must still decide whether that caller can perform the requested action on the requested resource.
Decoding Is Not Verification
Many JWT libraries provide a function that decodes a token without verifying it. Decoding is useful for inspection, logging, or selecting the key that might verify the signature, but decoded claims are still untrusted input.
During code reviews, we have encountered applications that decoded a token, read an admin claim, and treated the result as authenticated. An attacker could construct a new payload containing "admin": true because creating Base64URL text requires no secret.
Production authorization must use a verification function that validates the signature and required claims. The node-jsonwebtoken documentation makes the same distinction and warns that even a verified payload should be treated as untrusted input whose expected properties must be checked.
JWT Signing Algorithms
JWT implementations commonly use symmetric or asymmetric signing.
HMAC: HS256, HS384, and HS512
HMAC uses the same secret to sign and verify tokens. It can work well when one trusted system both issues and verifies the tokens, or when every verifier is fully trusted to issue tokens too.
Sharing an HMAC secret with several services expands the security boundary. Every service able to verify the token can also create a valid token. A strong, randomly generated secret must be stored in a secret manager rather than committed to source code.
RSA and RSA-PSS: RS256 and PS256
RSA uses a private key for signing and a public key for verification. APIs can verify tokens without gaining the ability to issue them.
Let’s Build a Reliable Backend Together
F22 Labs developers secure backend systems that handle growth, protect user data, and integrate modern standards like JWT.
This separation is useful in systems with one authorization server and multiple resource servers. Public keys can be distributed through a trusted JSON Web Key Set, while the private signing key remains restricted to the issuer.
Elliptic Curve and EdDSA
Elliptic-curve and Edwards-curve algorithms can provide strong signatures with smaller keys or signatures than traditional RSA choices. Library, platform, identity-provider, and compliance support should guide the final selection.
Algorithm choice belongs in a fixed server configuration. RFC 8725, JSON Web Token Best Current Practices, requires libraries to let callers specify supported algorithms and prohibits using algorithms outside that set during cryptographic processing.
A Secure Node.js Example
The following example uses the jose library and an asymmetric signing key. Production keys should come from a managed key service, hardware security module, or protected secret store rather than being generated every time the process starts.
import {
SignJWT,
generateKeyPair,
jwtVerify,
} from "jose";
const issuer = "https://auth.example.com";
const audience = "https://api.example.com";
const algorithm = "RS256";
const { privateKey, publicKey } = await generateKeyPair(algorithm);
const accessToken = await new SignJWT({
scope: "orders:read",
})
.setProtectedHeader({ alg: algorithm, typ: "JWT" })
.setIssuer(issuer)
.setSubject("user_123")
.setAudience(audience)
.setIssuedAt()
.setJti(crypto.randomUUID())
.setExpirationTime("15m")
.sign(privateKey);
const { payload, protectedHeader } = await jwtVerify(
accessToken,
publicKey,
{
algorithms: [algorithm],
issuer,
audience,
},
);
console.log(protectedHeader.alg);
console.log(payload.sub);The important part is not the call that creates the token. Security comes from fixing the permitted algorithm, verifying the trusted issuer and intended audience, enforcing expiration, protecting the private key, and authorizing the request using expected claims.
The jose project supports JWT signing, signature verification, claims validation, JWK, JWKS, JWS, and JWE across supported JavaScript runtimes. Its current capabilities are documented in the official jose repository.
Access Tokens, ID Tokens, and Refresh Tokens
JWT describes a token format; it does not define one universal token purpose. Two tokens can both be JWTs while carrying different meanings and requiring different validation rules.
Access Token
An access token authorizes calls to a resource server or API. The API should validate that it is the token's intended audience and apply the included scope or permissions according to its authorization policy.
OAuth does not require every access token to be a JWT. An authorization server may issue an opaque access token instead. RFC 9068 defines an interoperable profile specifically for OAuth 2.0 access tokens that use JWT format.
ID Token
An OpenID Connect ID token communicates information about an authentication event to the client application. It is intended for the client identified by its audience, not as a general-purpose credential for arbitrary APIs.
Using an ID token as an API access token mixes two different security purposes and can result in incorrect validation.
Refresh Token
A refresh token allows a client to request another access token without making the user complete the full login process again. Refresh tokens usually live longer than access tokens and require stronger storage, rotation, revocation, and reuse-detection controls.
A refresh token does not have to be a JWT. An opaque, high-entropy value backed by server-side state is often easier to revoke and rotate safely.
JWT vs Session-Based Authentication
JWT and server-side sessions are both valid architectural choices. JWT did not make sessions obsolete, and neither approach is automatically more secure or scalable.
| Consideration | JWT-Based Access Token | Server-Side Session |
| State | Claims can be verified without a central session lookup | Session data is stored by the server or a shared session service |
| Revocation | Harder before token expiration unless state is added | Usually straightforward by deleting or invalidating the session |
| Payload exposure | Signed payload is readable | Browser normally receives only an opaque identifier |
| Cross-service use | Convenient with carefully scoped audiences and public keys | Requires shared session access or introspection |
| Token size | Usually larger | Session identifier can be small |
| Permission changes | Old claims can remain valid until expiration | Server-side state can reflect changes immediately |
| Complexity | Key management and strict validation | Session storage, replication, and CSRF controls |
Session stores can scale horizontally using Redis, databases, or managed session services. JWT verification can reduce a central lookup, but applications frequently reintroduce state for refresh tokens, logout, revocation, risk controls, or permission changes.
In our experience, a secure server-rendered web application often benefits from a conventional session cookie. JWT becomes more compelling when independent APIs need verifiable, short-lived claims issued by a dedicated authorization service.
Advantages of JWT
Compact and Transport-Friendly
JWT compact serialization works conveniently in HTTP headers and other URL-safe contexts. Token size still matters because every request may carry the complete token, so claims should remain minimal.
Standardized Claims
Registered claim names such as iss, sub, aud, and exp give issuers and consumers a shared vocabulary. Protocol profiles can then define exactly how those claims must be used.
Distributed Verification
Asymmetric signatures let many services verify tokens using public keys without giving those services the private key needed to issue tokens.
Reduced Dependence on a Session Lookup
An API can often validate a signed access token locally. This capability may reduce latency and dependence on a central session service, although revocation and frequently changing authorization data may still require shared state.
Broad Ecosystem Support
Mature JWT and JOSE libraries exist across common programming languages and platforms. Standards-based key discovery also supports integration with identity providers and API gateways.
JWT Limitations
Revocation Is Not Built In
A valid signed token normally remains valid until it expires. Logout, account suspension, password changes, and permission removal may require short token lifetimes, a denylist, a token-version check, sender-constrained tokens, or an introspection-based design.
Claims Can Become Stale
Roles and permissions embedded in a token represent a snapshot from the time of issuance. A user whose access changes may retain old privileges until the token expires or the verifier performs an additional state check.
Bearer Tokens Can Be Replayed
A bearer token can generally be used by whoever possesses it. A valid signature does not prove that the current presenter is the user or application to which the token was originally issued.
Short lifetimes, secure transport, careful storage, refresh-token rotation, and sender-constraining technologies reduce this risk.
Tokens Increase Request Size
Large claims create large HTTP headers, increase bandwidth, and may exceed proxy or server limits. JWT payloads should contain identifiers and essential authorization data rather than complete user profiles.
Key Management Remains Difficult
Signing keys require secure generation, storage, access control, rotation, publication, and retirement. JWT moves part of the trust decision into cryptography; it does not remove operational security work.
JWT Security Best Practices
Validate More Than the Signature
A successful signature check is only one validation step. The verifier should check the expected algorithm, issuer, audience, expiration, not-before time when used, token type or purpose, and any application-required claims.
RFC 8725 also recommends mutually exclusive validation rules for different kinds of JWTs. Access tokens, ID tokens, email-verification tokens, and password-reset tokens should not become interchangeable simply because they use the same format.
Pin the Allowed Algorithms
The application should explicitly configure the acceptable algorithm or small algorithm set. The token's alg header describes the token but must not determine security policy by itself.
Support for unsecured alg: none tokens should remain disabled unless an exceptionally specific protocol requires it and supplies protection elsewhere.
Validate the Issuer and Audience
The issuer identifies the authority that created the token. The audience identifies the intended recipient. Validating both prevents a correctly signed token issued for another tenant, environment, or API from being accepted in the wrong place.
Keep Access Tokens Short-Lived
A short expiration limits the useful lifetime of a stolen bearer token. The appropriate duration depends on risk and user experience, but access tokens commonly live for minutes rather than days.
Small clock-skew allowances can account for minor time differences between systems. Broad allowances weaken expiration enforcement and should be avoided.
Protect Signing Keys
Private keys and HMAC secrets should remain in a secret manager, key-management service, or hardware-backed system with limited access and audited use. Source repositories, mobile applications, browser code, logs, and ordinary configuration files are unsuitable locations for signing secrets.
Key rotation should use stable key identifiers and a controlled overlap period. Verifiers need the new public key before new tokens use it, and the old verification key must remain available until previously issued tokens expire.
Use HTTPS Everywhere
TLS protects tokens while they travel across the network. JWT signatures do not prevent an attacker from stealing and replaying a bearer token observed over an insecure connection.
Keep Sensitive Data Out of the Payload
Signed JWT claims are usually visible to the holder. Minimal claims reduce privacy exposure, token size, and the chance of stale data influencing authorization.
Design Browser Storage Deliberately
Browser token storage involves trade-offs. JavaScript-readable storage is exposed to successful cross-site scripting, while cookies can be sent automatically and therefore require appropriate HttpOnly, Secure, SameSite, and cross-site request forgery protections.
A backend-for-frontend pattern can keep OAuth tokens away from browser JavaScript and expose only a hardened session cookie to the browser. The right choice depends on the application architecture and threat model.
Let’s Build a Reliable Backend Together
F22 Labs developers secure backend systems that handle growth, protect user data, and integrate modern standards like JWT.
Rotate Refresh Tokens
Refresh-token rotation issues a new refresh token when the previous one is used and invalidates or tracks the old token. Reuse of an already rotated token can then signal theft and terminate the token family.
Refresh tokens should be scoped to the client, stored securely, and revoked when risk events occur.
Avoid Writing Tokens to Logs or URLs
Tokens in query strings can leak through browser history, server logs, analytics, copied links, and referrer headers. Authorization headers or protected cookies are safer transport mechanisms.
Logging systems should redact authorization headers, cookies, refresh tokens, and token values from errors and traces.
Common JWT Mistakes We See in Practice
Trusting Decoded Claims
Decoded claims provide no authenticity guarantee. Every authorization decision must follow successful verification using trusted configuration.
Using One Token for Every Purpose
A token issued to confirm an email address should not authorize API calls. Distinct audiences, token types, keys, claims, and validation paths reduce cross-token confusion.
Issuing Long-Lived Access Tokens
Long validity periods make theft and authorization changes harder to contain. Short access-token lifetimes paired with a controlled renewal mechanism provide a safer balance.
Storing Complete User Records in Tokens
User profiles increase token size and become stale. Stable subject identifiers and limited authorization claims are generally more appropriate.
Placing Secrets in Frontend Code
A browser or mobile application cannot safely hold a shared signing secret distributed to every installation. Public clients should rely on an authorization server rather than minting trusted access tokens themselves.
Sharing an HMAC Secret Too Widely
Every holder of an HMAC verification secret can create valid signatures. Asymmetric signing provides a safer trust boundary when many independent services need verification access.
Assuming Logout Invalidates Every Token
Removing a token from one browser does not invalidate copies elsewhere. Effective logout may require refresh-token revocation, short-lived access tokens, session termination, or a server-side revocation mechanism.
When Should You Use JWT?
JWT is a good fit when a trusted issuer needs to send compact, verifiable claims to one or more independent services. API access tokens, service-to-service identity, short-lived single-use flows, and federated identity protocols are common examples.
JWT is less compelling when one application controls both the browser and backend, immediate revocation is essential, authorization data changes frequently, or a small opaque session identifier would meet the requirement more safely.
The decision should begin with the trust model and lifecycle requirements rather than a desire to make the application “stateless.”
Frequently Asked Questions
What does JWT stand for?
JWT stands for JSON Web Token. It is a standardized format for carrying claims between parties in a compact and URL-safe representation.
Is a JWT encrypted?
A typical signed JWT is not encrypted. Its header and payload can be decoded by anyone who obtains it. Encryption requires JWE or another suitable protection mechanism.
Is Base64URL encoding secure?
Base64URL encoding makes binary data safe for URL-oriented transport. It provides no secrecy, authenticity, or tamper protection by itself.
Can someone change a JWT payload?
Anyone can create a modified payload, but a correctly implemented verifier will reject it because the signature no longer matches. This protection depends on strict signature and claims validation.
Where should a JWT be stored?
Storage depends on the client and threat model. Browser applications commonly use hardened cookies or an in-memory design, while native apps should use platform-provided secure storage.
What happens when a JWT expires?
The receiving service must reject the token at or after its exp time. The client may authenticate again or use an approved refresh mechanism to obtain another access token.
Can a JWT be revoked?
JWT itself does not define revocation. Applications can use short expiration times, deny lists, token versions, refresh-token revocation, introspection, or key rotation depending on the incident and design.
What is the difference between JWT and OAuth?
JWT is a token format. OAuth is an authorization framework. OAuth access tokens may be JWTs or opaque values, depending on the authorization server and deployment.
What is the difference between JWT and JWS?
JWT defines a claims format. JWS defines how content is protected with a digital signature or message authentication code. A commonly encountered signed JWT uses JWS compact serialization.
Should permissions be stored in a JWT?
Stable, narrowly scoped permissions can be included when the API validates them correctly. Frequently changing or highly sensitive authorization decisions often need a current server-side check as well.
Conclusion
JWT is a compact format for carrying claims, not a complete authentication system and not an automatic security upgrade. Its signature can protect integrity and authenticate the issuer, while the token's claims communicate identity, audience, validity, and authorization context.
Secure JWT implementations keep payloads minimal, use short-lived access tokens, protect signing keys, pin allowed algorithms, validate issuer and audience, separate token purposes, and plan for refresh, revocation, and key rotation.
The most useful mindset is straightforward: treat every token as untrusted input until its signature, claims, purpose, and lifecycle have all been validated.



