Skip to main contentArrow Right

Table of Contents

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 a8f5f167f44f4964e6c998dee827110c

The diagram below shows the typical flow for bearer token authentication:

Bearer Token Illustration
Fig: Diagram illustrating how a bearer token is used for 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.signature

Here's what a JWT looks like:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

If you decode this JWT, you'll see three distinct parts:

Fig: Decoding a JWT with a JWT decoder / encoder
Fig: Decoding a JWT with a JWT decoder / encoder

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 httpOnly cookie (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:

Fig: Diagram of how JWT Bearer tokens work in typical OAuth or OIDC flows
Fig: Diagram of how JWT Bearer tokens work in typical OAuth or OIDC flows

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 JWT

If 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 Unauthorized

The 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:

a8f5f167f44f4964e6c998dee827110c

When 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 Unauthorized

When 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.