How to handle JWT in JavaScript
A practical guide to creating, sending, and validating JSON Web Tokens in modern JavaScript.
JSON Web Tokens, or JWTs, are a small, self-contained way to say who someone is and what they’re allowed to do, in a form that can be safely passed between systems without shared state, which makes them a natural fit for the distributed world we build in. If you’ve worked with logins, APIs, mobile apps, or single-page applications, you’ve almost certainly used JWTs, whether you realized it or not.
This guide will walk you through everything you need to know to safely consume, validate, and work with JWTs in JavaScript. We’ll cover what JWTs are, how they’re structured, how to create and validate them, and the best practices you should follow when using them in production JavaScript applications.
JWT 101
A JSON Web Token is a compact, URL-safe token format used to securely transmit information between systems. At a high level, a JWT lets one system make a signed statement about a user or service, and lets another system verify that statement without needing to look anything up in a database.
They are typically used to indicate a user’s identity and/or assert permissions and roles.
A JWT is composed of three parts, each Base64URL-encoded and separated by dots:
Header
The header contains metadata about the token, most importantly the signing algorithm used to create the signature (e.g., HMAC, RSA, or ECDSA). This tells the verifier how the token was signed and how it should be validated.
A typical header might look like this before encoding:
In this example, the alg (algorithm) is set to RS256, representing the RSA SHA-256 hashing algorithm and the typ is set to JWT (type of token).
Payload
The second part of the token is the payload. It contains the actual data of the token, which are called claims.
Claims are pieces of information about the subject of the token and additional context about how it should be used.
Some claims are registered and standardized, like iss, sub, aud, and exp (for the full list check the JWT claims registry). Others are custom and application-specific.
Example payload:
In this example, we see the following JWT claims:
- sub (subject): Represents the subject of the JWT, for example, a user ID.
- email: A custom claim specifying the user’s email.
- admin: A custom claim specifying the user’s role.
- iat (issued at): Contains the exact time you issued the JWT. Expressed in seconds since the Unix epoch.
- iss (issuer): Identifies who issued the token which is often you or your server.
- exp (expiration): Specifies the exact time when the JWT expires. Expressed in seconds since the Unix epoch.
It’s important to note that the payload is not encrypted. Anyone who has the token can decode it and read the claims.
Signature
The signature is the most critical part, ensuring the token's integrity and confirming that it was issued by a trusted source. It’s created by hashing the Base64URL-encoded header and payload with a secret key (for symmetric algorithms like HMAC-SHA256) or a private key (for asymmetric algorithms like RSA); the resulting hash is then Base64URL-encoded and appended to the token.
The signature is what makes JWTs trustworthy. When a JWT is received, the verifier recomputes the signature using the appropriate key and compares it to the signature included in the token. If they don’t match, the token has been tampered with and must be rejected.
Because JWTs are self-contained, the recipient can validate and interpret them without making a database call. This statelessness is what makes JWTs so popular for APIs and distributed systems, but it also means that incorrect validation logic can lead to serious security issues.
JWT validation library for JavaScript
While you can technically implement JWT parsing and validation yourself, you shouldn’t. Cryptography is sharp, and mistakes are easy.
For JavaScript, we recommend using jose. It is actively maintained, standards-compliant, and works well in both Node.js and modern runtimes.
Install it with:
We’ll use jose throughout the rest of this tutorial.
Generating your keys
First, you need to have a set of cryptographic keys in order to sign your tokens.
!!If you need something fast for a proof-of-concept, you can use a tool like mkjwk.org to generate a JWK.!!
In this tutorial, we will be using RS256. This asymmetric algorithm requires two keys: a private key to sign the token and a public key to verify it. If you already have them move along to the next section.
There are many ways to generate your keys. You could generate your keys using OpenSSL and save them as raw PEM files that your code would read.
However, this is not a best practice. You should use JSON Web Key Sets (JWKS), especially in distributed or cloud environments.
!!JWKS vs PEM: JWKS simplifies key rotation by allowing services to fetch the latest keys from a central endpoint, making updates easier and reducing the risk of errors. In contrast, PEM files require manual distribution and updates, which can be cumbersome in large systems. JWKS also centralizes key distribution, ensuring that all services or clients always have the correct keys without the need for constant manual updates.!!
Here’s how to generate an RSA key pair using Node.js:
- The private key is used to sign tokens.
- The public key is used to verify them.
Generating a key pair is only the first step. In a real system, you’ll also need to:
- Create a JWKS endpoint: You'll need to expose the public keys as part of a JWKS endpoint. This endpoint (
/well-known/jwks.json) will serve the JSON Web Key Set, which clients and services can use to validate JWTs. - Handle key rotation and management: You need to generate new key pairs and update the JWKS periodically. For example, you can rotate keys every few months or whenever you suspect key compromise. When you rotate keys, you can add a new key to your JWKS and mark the old one as inactive. Use a key identifier (
kid) to distinguish between active and inactive keys. - Secure your private keys: It's crucial to keep your private keys secure. They should never be exposed through your API or any public endpoint. Store them securely, for instance, in an encrypted file or a secure EKM like WorkOS Vault.
If you are using a third-party identity provider (like WorkOS), they automatically generate and expose a JWKS endpoint for you. This allows clients to dynamically fetch the public keys needed for JWT verification without you having to manage the keys manually. WorkOS offers a public JWKS endpoint:
Clients and APIs can use this endpoint to retrieve the public keys needed to validate JWTs signed by WorkOS. Key rotation, expiration, and distribution are handled automatically by WorkOS.
Creating a JWT in JavaScript
Once you have your RSA keys, you can create and sign a token using the private key.
This snippet creates a JWT with the following characteristics:
- Sets two custom claims for role and department.
- Sets the algorithm to RS256.
- Sets the issuer and audience to scope where the token is valid.
- Sets the subject of the token to uniquely identify the user.
- Adds issued-at and expiration timestamps to limit the token’s lifetime.
- Signs the token using the private RSA key.
At this point, jwt is a string you can return to a client or another service.
Sending the token as a Bearer token in JavaScript
Once the app has the JWT, it can use it to authenticate itself when calling another service, typically by sending it in the Authorization header as a Bearer token.
The Bearer prefix tells the API: “Whoever bears this token can use it.”
Client-side example using fetch:
On the server, you’ll typically extract the token from this header before validating it.
Adding standard and custom claims
JWT claims fall into two main categories:
Standard claims
Common registered claims include:
iss(issuer)sub(subject)aud(audience)exp(expiration time)iat(issued at)
You can add these using helper methods:
Custom claims
Custom claims are application-specific data:
Be careful not to include sensitive information. JWT payloads are encoded, not encrypted.
Decoding a JWT
Decoding a JWT without verifying it can be useful for debugging, but it should never be used for authorization decisions.
This simply parses the token and returns the payload.
Verifying a JWT
Verification ensures the token:
- Has a valid signature.
- Is not expired.
- Matches expected issuer and audience.
If verification fails, an error is thrown. Always handle these errors explicitly.
In a Next.js app, this verification typically lives in a Route Handler, API Route, or shared authentication middleware. The same logic applies regardless of whether you’re running in Node.js or a serverless environment.
Handling custom claims
Once verified, custom claims are available directly on the payload:
Treat claims as immutable facts for the lifetime of the token. If permissions change frequently, consider short-lived tokens.
JWTs best practices (JavaScript edition)
JWTs are a great building block, but they’re also easy to misuse. Here are the practices that matter most in production JavaScript apps.
- Always verify the signature. Don’t trust a token just because it decodes cleanly. Only use claims for authorization decisions after verification succeeds. With
jose, preferjwtVerify(...)(not justdecodeJwt(...)). - Enforce the expected algorithm. “Accept whatever the header says” is how algorithm confusion bugs are born. Configure verification so you only accept the algorithm you expect (for example, RS256), and reject anything else.
- Validate critical standard claims. At minimum, validate:
exp(Expiration)nbf(Not Before)iat(Issued At)iss(Issuer)aud(Audience)
If you deal with clock drift between systems, allow a small amount of leeway rather than loosening validation.
- Use a JWKS endpoint when possible. If your tokens are issued by an identity provider, verify against their JWKS so you can automatically select the right public key by
kid. Cache JWKS responses and handle refreshes safely, especially if you’re verifying tokens at high request volume. - Plan for key rotation. Rotation is not a “later” problem. If you manage your own keys, publish new keys before you start signing with them, keep old keys available until issued tokens expire, and use
kidto distinguish active vs retired keys. - Enforce Bearer token format. Require tokens in the
Authorizationheader in this exact format:Authorization: Bearer <jwt>. Treat tokens in query params as a smell (they leak into logs, browser history, and referrers). - Don’t put sensitive data in JWTs. JWTs are not encrypted by default. Anything in the payload can be read by anyone who can access the token. Avoid passwords, secrets, and high-risk PII.
- Keep access tokens short-lived. Short
expvalues reduce the blast radius of a leaked token. If you need long sessions, use refresh tokens and rotate them as well. - Be intentional about token storage in the browser. If you’re building a web app:
- Prefer HTTP-only, Secure cookies for session tokens to reduce XSS exposure.
- If you store tokens in JavaScript-accessible storage (like
localStorage), understand the tradeoff: it’s convenient, but XSS can steal tokens. - If you use cookies, you also need to think about CSRF protections.
- Handle verification errors explicitly. Verification can fail for many reasons (expired token, bad signature, wrong
iss, wrongaud). Map these to clean HTTP responses:401 Unauthorizedfor missing/invalid/expired tokens403 Forbiddenfor valid tokens that lack required permissions
- Use HTTPS everywhere. JWTs are bearer credentials. If someone can intercept the request, they can replay the token.
- Centralize JWT logic. Put verification in reusable middleware (Express/Koa/Fastify) so every protected route enforces the same checks. Avoid sprinkling partial verification logic across handlers.
- Log failures carefully. Logging is useful, but tokens are sensitive. Log high-level context (like
kid,iss, and the reason verification failed) and never log full tokens or entire payloads. - Test with bad tokens. Make sure your test suite covers:
- Expired tokens
- Tokens with wrong
issoraud - Tokens signed with the wrong key
- Tokens with tampered payloads
- Missing required claims
- Wrong algorithm or
none-style edge cases
JWTs are simple in structure, but security lives in the details you enforce.
Let WorkOS handle the heavy lifting
While handling JWTs with libraries like jose is often necessary at the API layer, it’s worth stepping back and looking at the bigger picture: how those tokens are issued in the first place.
If you’re building authentication flows, especially ones that involve Single Sign-On (SSO), SCIM provisioning, or multi-tenant identity, there’s a lot more to solve than just signing and verifying tokens. You need to support different identity providers, manage users and directories, rotate keys safely, and issue tokens that downstream services can trust.
WorkOS provides a modern API for enterprise ready authentication features, letting you integrate SSO (SAML, OIDC, and more), manage users and directories, and issue secure tokens without building and maintaining a full auth stack from scratch. It’s especially useful if you need to support enterprise customers or want to offer a “Login with your company” experience. And it’s free for up to 1,000,000 monthly active users.
If you’re tired of stitching together SSO flows or wrestling with SAML metadata by hand, it’s worth checking out WorkOS.