Table of Contents
At a glance
Don't have the time to read the entire post? Our human writers will be sad, but we understand. Summarize the post with your preferred LLM here instead.
This tutorial was written by Manish Hatwalne, a developer with a knack for demystifying complex concepts and translating "geek speak" into everyday language. Visit Manish's website to see more of his work!
If you've worked with APIs, you may be familiar with the terms "JWT" and "bearer tokens". Many developers even use these terms interchangeably, thinking they're just two names for the same thing. But there's a key distinction:
A bearer token describes how you send credentials (an authentication scheme), while a JWT (JSON Web Token) describes what the token contains (a token format).
This distinction is similar to email as a delivery method versus HTML or plain text as the content format—the former describes the transport mechanism, while the latter describes how the data is structured. JWTs are frequently used as bearer tokens, but they're separate concepts that just happen to work well together.
In this article, we’ll clarify the relationship between JWTs and bearer tokens and address some of the common sources of confusion in the bearer token vs JWT debate. We’ll also look at how JWTs and bearer tokens work together in modern authentication systems, how they compare to session tokens and refresh tokens, and when you should choose JWT bearer tokens versus opaque bearer tokens.
At a glance
A bearer token is any token that a client sends with a request to prove it is allowed access, named because whoever bears the token can use it.
A JWT, or JSON Web Token, is a specific self-contained token format made of a header, payload, and signature, defined by the standard RFC 7519.
The two are not competing options: a JWT is a token format, while bearer is how a token is sent, so a JWT is commonly used as a bearer token.
The alternative to a JWT is an opaque token, a random string that means nothing on its own and must be checked against the server that issued it.
Choose a JWT when you want stateless, self-verifying tokens, and an opaque or session token when you need instant revocation and central control.
Quick facts
What a bearer token is | An authentication scheme where possession of the token grants access, defined in RFC 6750 |
What a JWT is | A self-contained token format made of a header, payload, and signature, defined in RFC 7519 |
How they relate | A JWT is a format and a bearer is a transport method, so a JWT is commonly sent as a bearer token |
The alternative to a JWT | An opaque token, a random string with no decodable meaning that requires a server-side lookup |
When to use a JWT | For stateless, scalable authentication across APIs and microservices |
What is a bearer token?
A bearer token is an authentication scheme defined in RFC 6750. The name "bearer" clarifies how it works: whoever bears (holds) the token can use it. In other words, possession equals authorization. If you have the token, you can access the protected resource. No additional password or secret is required.
This might sound risky at first, and it needs to be handled carefully. But this simplicity is exactly what makes bearer tokens so practical for API authentication. The server doesn't need to verify your identity separately. It just checks if the token you're sending is valid.
How bearer tokens are used
Bearer tokens are typically sent in the HTTP Authorization header of your API requests, like this:
GET /api/user/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer a8f5f167f44f4964e6c998dee827110cThe diagram below shows the typical flow for bearer token authentication:

You can also send bearer tokens in other ways (e.g., in the request body or as a query parameter), but the HTTP Authorization header is the standard and recommended approach.
When you make an API request with a bearer token, here's what happens behind the scenes:
Your client application sends an HTTP request with the token in the Authorization header
The API server receives the request and extracts the token
The server validates the token (checking if it's expired, properly signed, etc.)
If valid, the server authorizes access and returns the requested resource
Bearer tokens are commonly used with OAuth 2.0 and OpenID Connect (OIDC) authentication flows. When you log into an app using "Sign in with Google" or "Sign in with GitHub," you usually receive bearer tokens.
Bearer token formats
It’s important to understand that the “bearer” part of bearer tokens tells you how the token is being used, not what the token looks like inside. The bearer authentication scheme doesn’t care about the token’s format. You could use any of the following as long as they’re used according to the bearer authentication scheme (possession grants access):
JWTs with encoded data.
Opaque tokens, which are random strings or database reference keys.
Other proprietary token formats defined by your system.
Key security considerations
Since possession of a bearer token equals access, protecting the token is critical. If someone intercepts your bearer token, they can use it to access protected resources. There's no additional "proof of possession" required.
In fact, if you've used APIs from services like OpenAI or Anthropic's Claude, you've worked with bearer tokens. Those API keys you protect carefully are bearer tokens, and anyone who gets ahold of them can make API calls on your behalf, and you'll end up paying the charges.
This is why you should always:
Store tokens securely on the client side.
Use HTTPS for all communication involving bearer tokens (don't use unencrypted HTTP).
Set appropriate expiration times so tokens don't remain valid forever.
Implement token refresh mechanisms so users don't have to re-authenticate constantly.
The bearer authentication scheme prioritizes simplicity and statelessness (the server doesn't need to store session data), which makes it perfect for APIs and microservices. But that simplicity also requires you to be extra careful about token security.
What is a JWT?
A JWT is a token format defined in RFC 7519. While bearer tokens tell you how to transport credentials, JWTs tell you how to structure what's inside those credentials. Think of it as a standardized way to package information (called "claims") about a user or session into a compact, URL-safe string.
The beauty of JWTs is that they're self-contained. All the information you need to make authorization decisions is right there in the token itself. You don't need to call a database or authentication service to figure out who the user is or what permissions they have. The token carries that information with it.
The structure of a JWT (header, payload, signature)
A JWT consists of three parts separated by dots (.):
Header.payload.signatureHere's what a JWT looks like:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cIf you decode this JWT, you'll see three distinct parts:

Header
The header specifies the algorithm used to sign the token (in this case, HMAC SHA-256) and the token type:
{
"alg": "HS256",
"typ": "JWT"
}Payload
The payload contains the claims (the actual data you want to send). This might include user ID, name, roles, expiration time, and any other information you need:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}Signature
The signature ensures that the token hasn't been tampered with. The server can verify the signature using a shared secret (for HMAC) or a public key (for RSA):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
your-256-bit-secret
)Advantages of JWTs
JWTs have become immensely popular in web applications for several reasons:
Self-contained: Since all the claims are embedded in the token, your server doesn't need to query a database to check who the user is or what permissions they have. This makes JWTs perfect for stateless authentication systems.
Easily verifiable: You can verify a JWT's authenticity by checking its signature. Here's a simple example in Python:
import jwt
# Verifying a JWT
try:
decoded = jwt.decode(token, 'your-secret-key', algorithms=['HS256'])
print('User ID:', decoded['sub'])
print('User name:', decoded['name'])
except jwt.InvalidTokenError:
print('Invalid token')Widely supported: JWTs are supported by virtually every programming language and framework. There are libraries available for Python, Node.js, Java, Go, and many others.
Compact and URL-safe: JWTs are small enough to fit in HTTP headers, and their Base64URL encoding means they can be safely used in URLs if needed.
A JWT is a token format, not a token type
A JWT describes what a token looks like, while bearer describes how it’s sent, and a single token can be both a JWT and a bearer token at the same time. JWTs don’t specify how they should be transmitted; the standard defines the format and structure of the token, but it doesn’t tell you whether to send it in an HTTP header, a cookie, or another method.
In practice, JWTs are used in many ways:
As bearer tokens: (most common ) Authorization: Bearer <JWT>.
In cookies: Stored as an
httpOnlycookie (read only by the server, invisible to client code) and sent automatically with requests.In query parameters: Sometimes used for one-time links, though this is generally discouraged for security reasons.
In custom authentication schemes: Though rare, you could theoretically use JWTs with other authentication methods.
Most of the time, you'll see JWTs used as bearer tokens. That's the primary reason why many developers assume they're the same thing. But understanding that they're separate concepts (format vs. transport method) helps you make better architectural decisions.
JWT vs bearer token: the key differences
Aspect | Bearer token | JWT |
|---|---|---|
What it is | An authentication scheme: whoever holds the token can use it | A specific token format with a header, payload, and signature |
Category | Transport / usage method | Data format |
Self-contained | Depends on the token format used | Yes, claims are embedded directly in the token |
How it’s validated | Server checks the token is valid (may or may not require a lookup) | Server verifies the signature; no lookup needed if self-contained |
Typical use | Any token sent in the Authorization header | Access tokens for stateless, distributed APIs |
How JWTs and bearer tokens work together
Now that you have a good understanding of bearer tokens and JWT, let’s look at how they work together in a real authentication flow. In short, the authorization server issues a JWT after successful login, the client sends that JWT as a bearer token on every request, and the API server verifies the JWT’s signature and claims before granting access. This is the pattern you’ll encounter most often in web applications, especially those using OAuth 2.0 or OpenID Connect.
Typical authentication flow
Here's a visual overview of how JWT Bearer tokens work in a typical OAuth 2.0 or OpenID Connect flow:

In this flow, the first thing that happens is you (or your application) sends credentials to the authorization server. This could be a username and password, or an OAuth grant like an authorization code from a "Sign in with Google" flow:
import requests
# Example: Password grant (simplified)
response = requests.post('https://auth.example.com/token', data={
'grant_type': 'password',
'username': 'user@example.com',
'password': 'secure_password',
'client_id': 'your_client_id'
})
token_data = response.json()
access_token = token_data['access_token'] # This is usually a JWTIf authentication succeeds, the authorization server generates a JWT containing claims about the user (like user ID, email, roles, and expiration time) and sends it back to your client.
Your application stores this JWT and includes it in the Authorization header of subsequent API requests:
import requests
# Making an authenticated request
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get('https://api.example.com/user/profile', headers=headers)
user_profile = response.json()When your API server receives the request, it needs to validate the JWT before granting access. Here's what that validation looks like:
import jwt
def validate_jwt_bearer_token(auth_header):
# Extract the token from the Authorization header
if not auth_header or not auth_header.startswith('Bearer '):
return None
token = auth_header.split(' ')[1]
try:
# Verify the signature and decode the JWT
decoded = jwt.decode(
token,
'your-secret-key', # Or public key for RSA
algorithms=['HS256']
)
# Token is valid, return the claims
return decoded
except jwt.exceptions.InvalidTokenError:
# Handles all validation failures including token expiry
return None
# In your API endpoint
auth_header = request.headers.get('Authorization')
claims = validate_jwt_bearer_token(auth_header)
if claims:
user_id = claims['sub']
# Grant access to the resource
else:
# Return 401 UnauthorizedThe server performs these checks:
Verifies the signature: Ensures the JWT was issued by a trusted authority and hasn't been tampered with.
Checks expiration: Validates that the token hasn't expired based on the exp claim.
Reads claims: Extracts information like user ID and permissions to determine what the user can access.
The JWT format provides all the information needed for authorization, while the bearer scheme provides a simple, standardized way to transmit that token. Together, they create a stateless, scalable authentication system that works great for APIs and microservices.
Opaque tokens vs bearer tokens
So far, you've examined JWTs as bearer tokens. But, bearer is just the authentication scheme (how you send the token), not the token format itself. You can use opaque tokens as bearer tokens, and in many scenarios, you should.
What are opaque tokens
As the name suggests, opaque tokens are non-decodable, random strings that have no intrinsic meaning. Unlike JWTs, you can't decode an opaque token to see what's inside. They're typically just random identifiers like this:
a8f5f167f44f4964e6c998dee827110cWhen your API receives an opaque Bearer token, it can't decode it and read the user's information. Instead, the server must look up the token in a database or cache to retrieve the associated claims and permissions.
Here's what validation looks like on the server side:
import redis
import json
# Connect to token store (using Redis as an example)
token_store = redis.Redis(host='localhost', port=6379, db=0)
def validate_opaque_bearer_token(auth_header):
# Extract the token
if not auth_header or not auth_header.startswith('Bearer '):
return None
token = auth_header.split(' ')[1]
# Look up token in the store
token_data = token_store.get(f'token:{token}')
if not token_data:
return None # Token not found or expired
# Parse and return the stored claims
return json.loads(token_data)
# In your API endpoint
claims = validate_opaque_bearer_token(request.headers.get('Authorization'))
if claims:
user_id = claims['user_id']
# Grant access
else:
# Return 401 UnauthorizedWhen to use opaque bearer tokens vs. JWT
Both opaque bearer tokens and JWTs are valid options. The following table summarizes their typical use cases:
Use Opaque Bearer Tokens When | Use JWT Bearer Tokens When |
|---|---|
You need immediate revocation. Delete the token from your database to instantly revoke access when users log out or change passwords. | You need stateless, scalable authentication. JWTs don't require database lookups, making them perfect for distributed systems and microservices. |
You want centralized session management. Get a single source of truth for all active sessions and user activity. | You're building public APIs. The self-contained nature makes them easier for third-party developers to work with. |
Privacy of token content matters. Keep sensitive information server-side instead of in easily decoded tokens. | Performance is critical. Verifying a JWT signature is faster than database lookups. |
You need to update permissions in real-time. Changes to user roles take effect immediately without waiting for token expiration. | You need to pass claims between services in a microservices architecture without calling back to authentication services. |
The hybrid approach
Many systems use both these tokens. They issue short-lived JWT access tokens (valid for 15-60 minutes) along with long-lived opaque refresh tokens. When the JWT expires, the client uses the opaque refresh token to get a new JWT. This gives you the performance benefits of JWTs for most requests while maintaining the ability to revoke access via the refresh token.
In microservice architectures, client-side applications (built with React or similar) typically use this hybrid approach for efficient authentication across distributed API services.
JWT vs session tokens: what’s the difference?
A session token points to state the server holds and looks up on every request, while a JWT carries its own state and is validated without a lookup, which is faster but harder to revoke before it expires. Both accomplish the same goal, which is keeping a user authenticated across requests, but they store and check that state in opposite places.
Session token | JWT | |
|---|---|---|
Where state lives | On the server, in a database or cache | Inside the token itself |
Revocation | Instant; delete the session server-side | Only at expiration, unless paired with a revocation list |
Scalability | Requires a shared session store across servers | Scales easily since no lookup is needed |
Best for | Apps needing tight, centralized control over active sessions | APIs and microservices needing stateless, distributed authentication |
For a deeper look at how JWTs handle session-like state without a server-side store, see what a JWT is and how it works.
When should you use a JWT instead of session tokens?
Use a JWT when your architecture is stateless, distributed, or API- and microservice-based, and a server-side session lookup would add unnecessary latency or a shared point of failure. Use session tokens when you need instant revocation, centralized control over active sessions, or simpler server-side session management. Many teams land on a practical middle path: a short-lived JWT access token paired with a refresh token and a revocation list, which gets the performance of JWTs for everyday requests while keeping a way to cut off access quickly when needed.
If your priority is | Use | Why |
|---|---|---|
Stateless, distributed APIs or microservices | A JWT | No server-side lookup needed, so it scales cleanly across services |
Instant revocation and central control | A session token | The server can delete the session immediately |
Both performance and revocability | A short-lived JWT plus a refresh token | Combines fast, stateless requests with a way to cut off access at refresh time |
Access token vs refresh token: what’s the difference?
An access token is short-lived and sent with every request to reach a protected resource, while a refresh token is long-lived and used only to obtain a new access token once the old one expires, so the two work as a pair. A JWT is often used as the access token itself.
Access token | Refresh token | |
|---|---|---|
Lifetime | Short (minutes to about an hour) | Long (days to months) |
Purpose | Proves identity and permissions for each request | Used only to request a new access token |
Sent where | In the Authorization header with each API call | Sent only to the token endpoint, never to other services |
Read more on the differences between access tokens and refresh tokens in this guide.
Handle tokens the easy way with Descope
Managing auth tokens by hand—whether that’s a JWT token, a session token, or a refresh token—means building signing, validation, storage, and revocation logic yourself. Descope issues, validates, and rotates JWTs, and manages sessions and refresh tokens, instead of requiring your team to hand-build any of that token logic. You can configure token lifetimes, storage, and revocation without writing custom authentication code.
Sign up for a Free Forever account to start handling tokens the easy way, or book time with our auth experts if you have questions first.

