JWT Bearer Token — Format, Headers, and Best Practices
I was debugging an API integration for a client. Their mobile app was sending the JWT in a custom header called X-Auth-Token. The server accepted it. Everything worked — until they tried to use a third-party API gateway that only recognized the standard Authorization: Bearer format.
The fix was changing one header name. But the root issue was a team that had never read RFC 6750, the specification that defines how bearer tokens should be transmitted in HTTP requests.
The Bearer token scheme is not complicated. It is one HTTP header with a specific format. But I keep seeing implementations that get the details wrong — missing the word "Bearer", sending the token in the wrong header, or mishandling the parsing logic.
This article covers exactly how JWT Bearer tokens work, how to implement them correctly, and the edge cases that cause production bugs.
The Bearer Token Format
The Bearer authentication scheme is defined in RFC 6750. The format is:
Authorization: Bearer <token>
Three parts:
- The header name:
Authorization - The scheme identifier:
Bearer(case-insensitive, but conventionally capitalized) - The credentials:
<token>— in our case, a JWT
A complete header looks like:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
That is it. No quotes around the token. No additional parameters. Just Bearer followed by a space and the token string.
Parsing the Bearer Header
The parsing logic is straightforward but has edge cases that matter.
Correct Parsing (Node.js)
function extractBearerToken(req) {
const authHeader = req.headers.authorization;
// 1. Check header exists
if (!authHeader) {
return null;
}
// 2. Split on space — 'Bearer' and the token
const parts = authHeader.split(' ');
// 3. Must have exactly 2 parts
if (parts.length !== 2) {
return null;
}
// 4. Check scheme (case-insensitive)
const [scheme, token] = parts;
if (scheme.toLowerCase() !== 'bearer') {
return null;
}
// 5. Validate token is not empty
if (!token) {
return null;
}
return token;
}
// Usage
app.use((req, res, next) => {
const token = extractBearerToken(req);
if (!token) {
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
}
try {
req.user = jwt.verify(token, SECRET);
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
});
Common Parsing Mistakes
Mistake 1: Case-sensitive scheme check
// BAD: Only accepts "Bearer" exactly
if (parts[0] !== 'Bearer') { /* reject */ }
// GOOD: Case-insensitive
if (parts[0].toLowerCase() !== 'bearer') { /* reject */ }
The RFC says the scheme is case-insensitive. bearer, BEARER, and Bearer are all valid. Real-world clients send all three variants.
Mistake 2: Assuming the scheme is always present
// BAD: Crashes if the header has no space
const token = authHeader.split(' ')[1]; // undefined if no space
// GOOD: Check length first
const parts = authHeader.split(' ');
if (parts.length !== 2) return null;
Mistake 3: Splitting on multiple spaces
// BAD: Split on whitespace — "Bearer token" produces 3 parts
const parts = authHeader.split(/\s+/);
// GOOD: Split on single space
const parts = authHeader.split(' ');
Mistake 4: Trimming the token
// BAD: Trimming could accidentally fix a malformed token
const token = parts[1].trim();
// GOOD: Exact match — tokens should not have whitespace
const token = parts[1];
JWTs should not have leading or trailing whitespace. If you need to trim, the token is malformed and should be rejected.
Python Implementation
from typing import Optional
def extract_bearer_token(auth_header: Optional[str]) -> Optional[str]:
"""Extract a Bearer token from the Authorization header."""
if not auth_header:
return None
parts = auth_header.split(" ")
if len(parts) != 2:
return None
scheme, token = parts
if scheme.lower() != "bearer":
return None
if not token:
return None
return token
# FastAPI dependency example
from fastapi import Header, HTTPException
async def get_current_user(authorization: Optional[str] = Header(None)):
token = extract_bearer_token(authorization)
if not token:
raise HTTPException(status_code=401, detail="Invalid authorization header")
try:
payload = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])
return payload
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail="Invalid token")
Express Middleware (Complete)
Here is a production-grade middleware that handles all the edge cases:
const jwt = require('jsonwebtoken');
const BEARER_REGEX = /^Bearer\s+(.+)$/i;
function authMiddleware(options = {}) {
const {
secret = process.env.JWT_SECRET,
algorithms = ['HS256'],
extractToken = null, // Custom token extraction function
} = options;
return function (req, res, next) {
let token = null;
// Try custom extractor first
if (extractToken) {
token = extractToken(req);
if (token) {
req.tokenSource = 'custom';
}
}
// Fall back to Bearer header
if (!token) {
const authHeader = req.headers.authorization;
if (authHeader) {
const match = authHeader.match(BEARER_REGEX);
if (match) {
token = match[1];
req.tokenSource = 'bearer';
}
}
}
// Fall back to query parameter (optional, less secure)
if (!token && options.allowQueryParam) {
token = req.query.access_token;
req.tokenSource = 'query';
}
if (!token) {
return res.status(401).json({
error: 'UNAUTHENTICATED',
message: 'Authorization header with Bearer token is required',
});
}
try {
req.user = jwt.verify(token, secret, { algorithms });
req.token = token;
next();
} catch (err) {
let status = 401;
let message = 'Invalid token';
if (err.name === 'TokenExpiredError') {
message = 'Token has expired';
} else if (err.name === 'JsonWebTokenError') {
message = 'Token is malformed or signature is invalid';
} else if (err.name === 'NotBeforeError') {
message = 'Token is not yet active';
}
return res.status(status).json({ error: 'UNAUTHENTICATED', message });
}
};
}
Bearer Token in Different Contexts
WebSocket Connections
Bearer tokens in WebSocket connections require special handling because the initial handshake is an HTTP upgrade request:
// Client-side WebSocket with token
const ws = new WebSocket('wss://api.example.com/socket', {
headers: {
Authorization: `Bearer ${token}`,
},
});
// Server-side WebSocket verification (ws library)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
const token = extractBearerToken(req);
if (!token) {
ws.close(4001, 'Unauthorized');
return;
}
try {
const user = jwt.verify(token, SECRET);
ws.user = user;
} catch {
ws.close(4001, 'Invalid token');
}
});
Not all WebSocket libraries support custom headers. In that case, send the token as the first message after connection:
// Client sends token as first message
ws.on('open', () => {
ws.send(JSON.stringify({ type: 'auth', token: jwt }));
});
// Server expects auth message first
let authenticated = false;
ws.on('message', (data) => {
if (!authenticated) {
const msg = JSON.parse(data);
if (msg.type === 'auth') {
try {
const user = jwt.verify(msg.token, SECRET);
ws.user = user;
authenticated = true;
ws.send(JSON.stringify({ type: 'auth_ok' }));
} catch {
ws.close(4001, 'Invalid token');
}
}
return;
}
// Handle normal messages...
});
Server-Sent Events (SSE)
// Client
const token = localStorage.getItem('auth_token');
const eventSource = new EventSource(`/api/events?access_token=${token}`);
// Or via custom header (not natively supported by EventSource API)
Since the EventSource API does not support custom headers, you need to pass the token as a query parameter or use a cookie.
Fetch API
// Standard usage
const response = await fetch('/api/user', {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
Axios
// Global interceptor for all requests
import axios from 'axios';
axios.interceptors.request.use((config) => {
const token = getToken(); // Your token retrieval logic
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Or per-request
const response = await axios.get('/api/user', {
headers: {
Authorization: `Bearer ${token}`,
},
});
Security Considerations
HTTPS Is Mandatory
The Authorization header is sent in plaintext over HTTP. Without TLS, anyone on the network can read the token. HTTPS is non-negotiable for any system using Bearer tokens. If you are seeing unexpected token errors, Why Your JWT Token Is Invalid And How to Fix It covers the most common causes and debugging steps.
// BAD: Allowing HTTP
res.cookie('token', jwt, { secure: false }); // Token sent in the clear
// GOOD: Requiring HTTPS
res.cookie('token', jwt, { secure: true }); // Only sent over TLS
Token in Logs
The Authorization header commonly appears in server logs:
// BAD: Logging the full header
app.use((req, res, next) => {
console.log(`${req.method} ${req.path} — Auth: ${req.headers.authorization}`);
next();
});
// GOOD: Logging only that auth exists
app.use((req, res, next) => {
const hasAuth = !!req.headers.authorization;
console.log(`${req.method} ${req.path} — Authenticated: ${hasAuth}`);
next();
});
Token in URL Parameters
Some developers pass tokens as query parameters: ?token=... or ?access_token=.... This is less secure because:
- URLs are logged by proxies, load balancers, and CDNs
- URLs appear in browser history
- URLs are sent in the Referer header
- URLs are cached by browsers and intermediate systems
The Authorization header is the correct place for Bearer tokens.
CORS and Preflight Requests
When a Bearer token is sent in a cross-origin request, the browser sends a preflight OPTIONS request. This request does NOT include the Authorization header.
// Server must handle OPTIONS without authentication
app.options('/api/*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
res.sendStatus(204);
});
// The actual request includes the Authorization header
app.get('/api/user', authMiddleware, (req, res) => {
res.json({ user: req.user });
});
Error Responses
RFC 6750 specifies standard error responses for Bearer token failures:
Missing Token
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token is missing"
Expired Token
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token has expired"
Invalid Token
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token is malformed or signature is invalid"
Implementation
function sendUnauthorized(res, description) {
res.setHeader(
'WWW-Authenticate',
`Bearer realm="api.example.com", error="invalid_token", error_description="${description}"`
);
res.status(401).json({ error: 'UNAUTHENTICATED', message: description });
}
// Usage
app.use((req, res, next) => {
const token = extractBearerToken(req);
if (!token) {
return sendUnauthorized(res, 'The access token is missing');
}
try {
req.user = jwt.verify(token, SECRET);
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return sendUnauthorized(res, 'The access token has expired');
}
return sendUnauthorized(res, 'The access token is invalid');
}
});
The WWW-Authenticate header is important for API clients. Browser-based applications read it to show appropriate error messages. API clients use it to trigger token refresh flows.
Alternatives to the Bearer Header
The Authorization: Bearer header is the standard, but there are alternatives for specific scenarios:
Cookie-Based Transmission
// Set token as a cookie
res.cookie('auth_token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
});
The token is sent automatically with every request to the same domain. No need to manually attach the Authorization header. This is common in server-rendered applications. See Where to Store JWT in Browser for a detailed comparison.
Custom Header
Some applications use custom headers like X-Auth-Token or X-API-Key:
X-Auth-Token: eyJhbGciOiJIUzI1NiIs...
This is non-standard. Avoid it unless you have a specific reason (legacy system, framework constraint). Custom headers are not recognized by standard middleware, API gateways, or OAuth libraries.
Query Parameter
GET /api/user?access_token=eyJhbGciOiJIUzI1NiIs...
As mentioned above, this is less secure than the Authorization header. Only use it when you have no other option (e.g., WebSocket connections or SSE that cannot set custom headers).
FAQ
What is the correct format for the Bearer Authorization header?
Authorization: Bearer <token> — exactly three parts: the header name, the word "Bearer" (case-insensitive), and the token value, separated by a space.
Should I validate the Bearer token format case-sensitively?
No. RFC 6750 specifies that the scheme is case-insensitive. Bearer, bearer, and BEARER are all valid. Your parsing should handle all cases.
Can I send the JWT in a cookie instead of an Authorization header?
Yes. Cookies are a valid alternative, especially for server-rendered applications. The main tradeoff is CSRF vulnerability (mitigated by SameSite=Strict) and the inability to send cookies cross-origin without additional configuration.
How do I handle Bearer tokens in WebSocket connections?
Pass the token as a query parameter in the WebSocket URL (wss://api.example.com?token=...) or as the first message after connection. Not all WebSocket libraries support custom headers in the initial HTTP upgrade request.
Should the Bearer token be in the request body?
No. The Authorization header is the correct location. Passing the token in the request body makes it harder to authenticate at the middleware level, and it does not follow the HTTP standard.
What HTTP status code should I return for a missing Bearer token?
Return 401 Unauthorized with a WWW-Authenticate: Bearer header. The 401 status tells the client to re-authenticate. Do not return 403 (Forbidden) — that indicates the request is authenticated but not authorized.
How do I avoid logging the Bearer token?
Do not log the full Authorization header. Log only the presence or absence of authentication. If you need to correlate requests with specific users, log a hash of the token or the user ID from the verified payload.
Summary
The JWT Bearer token is transmitted via the Authorization: Bearer <token> header as specified in RFC 6750. The implementation is simple but has several edge cases:
- Parse the header case-insensitively
- Expect exactly two parts (scheme and token)
- Return 401 with
WWW-Authenticateheader on failure - Never log the full token
- Always require HTTPS
When you need to debug a Bearer token — check what claims it contains, verify the expiration, or inspect the algorithm — the JWT Decoder decodes any JWT instantly. Paste the token, see the full payload, and find the issue without writing code.