Why JWT Works in Postman but Not in Browser
A developer tests a protected API endpoint in Postman using an Authorization: Bearer <token> header and receives HTTP 200 OK. However, when the exact same request is dispatched from a frontend web application (React, Vue, Angular, or vanilla fetch), the browser either throws a CORS error or returns HTTP 401 Unauthorized.
Access to fetch at "https://api.example.com/me" from origin "https://app.example.com" has been blocked by CORS policy. OR: 401 Unauthorized (Missing token).Why does Why JWT Works in Postman but Not in Browser occur?
Primary Root Cause
Postman is an HTTP client without a browser security sandbox. Browsers, by contrast, strictly enforce the Same-Origin Policy. The moment client JavaScript attaches a custom header like Authorization, the browser pauses the request and dispatches an HTTP OPTIONS preflight probe to verify server permission.
If the backend API lacks CORS headers, returns HTTP 401 on the unauthenticated OPTIONS probe, or fails to list Authorization in Access-Control-Allow-Headers, the browser terminates the request with a CORS error. Alternatively, if the token is stored in cookies, browsers will NOT send cookies cross-origin unless credentials: "include" is explicitly configured on the fetch call.
How to Confirm You Are Facing This Exact Issue
Postman returns HTTP 200 OK with valid user data; the browser Network tab shows red or canceled requests.
Console error: "Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response".
Backend logs show the server received an OPTIONS request instead of a GET/POST request.
Browser sends the request, but backend returns 401 Unauthorized because cookies were stripped by default.
Explicit Diagnostic Reasoning
Use this diagnostic decision tree to identify the failure point:
"If the browser Network tab shows an OPTIONS request failing with status 401, 404, or 405"
Your backend auth middleware is intercepting the preflight OPTIONS probe and rejecting it because it has no token.
→ Configure your auth middleware to bypass OPTIONS requests: if (req.method === "OPTIONS") return next().
"If you store the JWT in an HttpOnly cookie and the backend reports "Missing Token""
Your frontend fetch call is missing credentials: "include" (or withCredentials: true in Axios).
→ Add credentials: "include" to your fetch() options.
"If the browser console says "authorization is not allowed by Access-Control-Allow-Headers""
The server’s CORS configuration does not explicitly allow the Authorization header.
→ Add "Authorization" to your backend CORS allowedHeaders configuration.
Simulates the browser preflight check. Check if the response includes Access-Control-Allow-Headers: Authorization and HTTP 200/204 status.
Long-Term Engineering Solution
Align frontend transport settings and backend CORS preflight handlers so that the Authorization header or credentials cookie can navigate browser cross-origin boundaries.
Architectural Pattern
Update the API gateway / backend middleware to permit Authorization in preflight headers, and ensure frontend client requests explicitly declare credentials mode.
How do you fix this step-by-step?
Whitelist Authorization in Backend CORS
In your backend CORS middleware, explicitly add "Authorization" to the allowed headers list.
// Express / Node.js
import cors from 'cors';
app.use(cors({
origin: 'https://app.example.com',
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
}));Allow OPTIONS to Bypass Authentication Middleware
Ensure your JWT validation middleware does not require tokens on OPTIONS requests.
export function jwtAuthMiddleware(req, res, next) {
// CRITICAL: Preflight OPTIONS requests NEVER carry auth tokens!
if (req.method === 'OPTIONS') return next();
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
// Verify token...
}Configure Frontend Credentials Mode (If Using Cookies)
If your JWT is stored in an HttpOnly cookie, tell fetch or Axios to transmit cross-origin cookies.
// Native Fetch
fetch('https://api.example.com/me', {
method: 'GET',
credentials: 'include', // Sends cross-origin cookies!
});
// Axios
axios.get('https://api.example.com/me', {
withCredentials: true, // Sends cross-origin cookies!
});Verify Bearer Header Formatting
Ensure your frontend sends Authorization: `Bearer ${token}` with the word "Bearer" capitalized and a single space.
What are common misconceptions and mistakes when fixing this?
Assuming that because it works in Postman, the backend is 100% bug-free
Por qué falla: Postman does not simulate the browser security model, CORS preflights, or third-party cookie restrictions.
✓ Alternativa correcta: Always test cross-origin APIs using real browser tabs or curl with simulated Origin headers.
Omitting credentials: "include" on fetch() when using cookie authentication
Por qué falla: By default, browsers strip all cookies from cross-origin fetch requests unless credentials mode is enabled.
✓ Alternativa correcta: Always specify credentials: "include".
Can this fix introduce security vulnerabilities?
Fixing CORS preflight issues properly ensures that only approved frontend origins can access your authenticated API.
Security Risks If Ignored
- •Setting Access-Control-Allow-Origin: * while allowing credentials is fundamentally blocked by browsers and creates a false sense of security.
Hardening Best Practices
- ✓Always validate the Origin header against a strict allowlist of domains before returning Access-Control-Allow-Credentials: true.
- ✓Use short-lived JWT access tokens (15 minutes) with rotating refresh tokens.
Comparing Postman Request vs Browser Fetch
// BROKEN Frontend Call: Fails due to default credentials policy and unhandled preflight
fetch('https://api.example.com/me', {
headers: {
'Authorization': 'eyJhbGciOi...' // Missing "Bearer " prefix, triggers unhandled preflight!
}
});// FIXED Frontend Call: Proper Bearer syntax & CORS credentials
const token = getAccessToken();
const response = await fetch('https://api.example.com/me', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
credentials: 'include', // Required if session cookie is also used
});
const data = await response.json();Frequently Asked Questions About Why JWT Works in Postman but Not in Browser
Why does adding an Authorization header trigger an extra OPTIONS request?↓
The W3C CORS specification classifies Authorization as a non-simple header.
Simple headers are limited to Accept, Accept-Language, and Content-Language. Any custom or security-sensitive header requires the browser to verify with the server before dispatching data.
How can I debug the exact headers Postman is sending vs the browser?↓
In Postman, open the Postman Console (bottom left). In the browser, open the DevTools Network tab and click "Headers".
Comparing the Raw Request Headers between both clients will instantly reveal missing Origin headers, differing cookie flags, or header capitalization differences.