Table of Contents
Bearer tokens
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. We'll also look at how JWTs and bearer tokens work together in modern authentication systems, and when you should choose JWT bearer tokens versus opaque bearer tokens.
Bearer tokens
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 tokens can have different formats
As mentioned previously, 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.
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
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 robust 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.
JWTs are just a token format
It's important to understand that 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.
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. 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 as 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.
Conclusion
Bearer tokens and JWTs aren't competing concepts. They work at different layers of your authentication system. Bearer tokens define how you transmit credentials (the authentication scheme), and JWTs define what those credentials contain (the token format).
While JWTs are often used as bearer tokens, that's not the only option. Opaque tokens work as bearer tokens as well, and the choice between them depends on your specific needs. If you prefer stateless scalability and performance, go with JWTs, but if you want instant revocation and centralized control, opaque tokens are the better option. But neither choice is inherently better; what matters is that you now understand why you're choosing one over the other.
For breakdowns of more developer concepts, subscribe to the Descope blog or follow us on LinkedIn, X, and Bluesky.

