How to Fix Bearer Token Authorization Header Null in JavaScript When Calling Spring Boot Endpoints (CORS 403 Error)
In modern web development, integrating frontend applications (built with JavaScript frameworks like React, Vue, or vanilla JS) with backend APIs (often built with Spring Boot) is a common scenario. A critical part of this integration is secure authentication, typically using JSON Web Tokens (JWT) passed via the Authorization header as a Bearer token. However, developers often encounter a frustrating issue: the Bearer token sent from the frontend is null or missing on the Spring Boot backend, leading to a 403 Forbidden error. This is frequently caused by misconfigured Cross-Origin Resource Sharing (CORS) policies or incorrect client-side token handling.
In this blog, we’ll demystify why the Authorization header goes missing, break down the root causes, and provide step-by-step solutions to fix the issue—covering both client-side (JavaScript) and server-side (Spring Boot) configurations.
Table of Contents#
- Understanding the Problem
- Common Causes of the Issue
- Client-Side Fixes: Ensuring the Token is Sent
- Server-Side Fixes: Configuring Spring Boot Correctly
- Testing the Fix
- Troubleshooting Tips
- Conclusion
- References
Understanding the Problem#
When your JavaScript frontend sends a request to a Spring Boot backend with a Bearer token in the Authorization header, the backend may receive a null value for this header. This triggers a 403 Forbidden error because the backend cannot validate the user’s identity without the token.
What’s Happening Under the Hood?#
- CORS Preflight Requests: Browsers enforce CORS to prevent unauthorized cross-origin requests. For "non-simple" requests (e.g., those with custom headers like
Authorization), the browser first sends anOPTIONSpreflight request to the backend to check if the origin, method, and headers are allowed. - Missing Headers in Preflight Response: If the backend’s CORS configuration does not explicitly allow the
Authorizationheader or fails to respond correctly to the preflight request, the browser blocks the actual request (or strips theAuthorizationheader), leading to anullheader on the backend.
Common Causes of the Issue#
Let’s break down the most likely culprits:
1. CORS Misconfiguration in Spring Boot#
The backend may not be configured to:
- Allow the frontend’s origin (e.g.,
http://localhost:3000). - Permit the
Authorizationheader in cross-origin requests. - Handle
OPTIONSpreflight requests properly.
2. Incorrect Client-Side Token Handling#
The frontend may:
- Forget to attach the
Authorizationheader to the request. - Use incorrect syntax (e.g., missing
Bearerprefix or typos likeauthorizationinstead ofAuthorization). - Send the request before the token is retrieved (e.g., async timing issues).
3. Spring Security Blocking Preflight Requests#
Spring Security may intercept and block OPTIONS preflight requests, preventing the browser from proceeding with the actual request.
Client-Side Fixes: Ensuring the Token is Sent#
First, verify that your JavaScript frontend is correctly attaching the Bearer token to the Authorization header.
1. Attach the Token Explicitly#
Use the Authorization header with the format: Bearer <token>.
Example with fetch:#
async function fetchProtectedData() {
const token = localStorage.getItem('jwtToken'); // Retrieve token from storage
if (!token) {
throw new Error("Token not found");
}
const response = await fetch('http://localhost:8080/api/protected', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // Critical: Attach Bearer token
}
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
}Example with Axios:#
import axios from 'axios';
async function fetchProtectedData() {
const token = localStorage.getItem('jwtToken');
if (!token) {
throw new Error("Token not found");
}
try {
const response = await axios.get('http://localhost:8080/api/protected', {
headers: {
'Authorization': `Bearer ${token}` // Bearer token here
}
});
return response.data;
} catch (error) {
console.error("Request failed:", error);
}
}2. Ensure Token Availability#
If the token is fetched asynchronously (e.g., from an API or secure storage), ensure it’s retrieved before sending the request. Use async/await or promises to avoid race conditions:
// Bad: Token may not be loaded yet
const token = localStorage.getItem('jwtToken'); // Might be null if not saved yet
fetch('...', { headers: { 'Authorization': `Bearer ${token}` } });
// Good: Wait for token retrieval
async function init() {
const token = await getTokenFromSecureStorage(); // Async token fetch
fetch('...', { headers: { 'Authorization': `Bearer ${token}` } });
}3. Avoid Typos and Case Sensitivity#
The Authorization header is case-sensitive. Use the exact casing:
- ✅ Correct:
'Authorization': 'Bearer <token>' - ❌ Incorrect:
'authorization': 'Bearer <token>'(lowercase "a")
Server-Side Fixes: Configuring Spring Boot Correctly#
Even if the frontend is sending the token, the backend must be configured to accept cross-origin requests with the Authorization header.
1. Configure CORS to Allow Authorization Header#
Spring Boot requires explicit CORS configuration to permit custom headers like Authorization. Use one of these approaches:
Option 1: Global CORS Configuration with WebMvcConfigurer#
Create a configuration class to define CORS rules for all endpoints:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // Apply to all endpoints
.allowedOrigins("http://localhost:3000") // Allow frontend origin
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // Allow all methods
.allowedHeaders("Authorization", "Content-Type") // Allow Authorization header
.allowCredentials(true) // If using cookies (optional)
.maxAge(3600); // Cache preflight response for 1 hour
}
}- Key Settings:
allowedOrigins: Replace with your frontend’s URL (e.g.,http://localhost:3000for development). Use*cautiously (not recommended for production).allowedHeaders: Must includeAuthorizationto permit the Bearer token header.
Option 2: Per-Endpoint CORS with @CrossOrigin#
Use the @CrossOrigin annotation on individual controllers or methods (less recommended for global rules):
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
@CrossOrigin(
origins = "http://localhost:3000",
allowedHeaders = "Authorization",
methods = {RequestMethod.GET, RequestMethod.OPTIONS}
)
public class ProtectedController {
@GetMapping("/protected")
public String getProtectedData() {
return "Secure data";
}
}2. Handle Preflight OPTIONS Requests#
Browsers send OPTIONS preflight requests for non-simple requests. Ensure Spring Boot allows these requests:
Configure CorsConfigurationSource (Advanced)#
For more control, use CorsConfigurationSource to define CORS rules, including preflight handling:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfigurationSourceConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:3000"); // Frontend origin
config.addAllowedHeader("Authorization"); // Allow Authorization header
config.addAllowedHeader("Content-Type");
config.addAllowedMethod("*"); // Allow all HTTP methods
config.setMaxAge(3600L); // Preflight cache duration
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); // Apply to all endpoints
return new CorsFilter(source);
}
}3. Spring Security Configuration#
If using Spring Security, it may override CORS settings. Explicitly enable CORS and permit OPTIONS requests in your security config:
Example SecurityConfig.java:#
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.cors.CorsFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CorsFilter corsFilter; // Inject the CORS filter from earlier
public SecurityConfig(CorsFilter corsFilter) {
this.corsFilter = corsFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(corsFilter, ChannelProcessingFilter.class) // Add CORS filter first
.cors().and().csrf().disable() // Enable CORS and disable CSRF (for testing; enable in production)
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // Permit OPTIONS preflight requests
.anyRequest().authenticated() // Secure all other endpoints
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // For JWT
return http.build();
}
}- Key Notes:
http.cors(): Ensures Spring Security uses your CORS configuration.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll(): Explicitly allowsOPTIONSpreflight requests to bypass authentication.- Order matters: Add the
CorsFilterbefore other security filters (e.g.,ChannelProcessingFilter).
Testing the Fix#
After applying the fixes, verify the Authorization header is reaching the backend:
1. Browser Network Tab#
- Open Chrome DevTools (F12) → Network tab.
- Trigger the request from the frontend.
- Select the request and check the Request Headers section. Ensure
Authorization: Bearer <token>is present.
2. Backend Logging#
Add logging in your Spring Boot controller to print the Authorization header:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ProtectedController {
@GetMapping("/protected")
public String getProtectedData(@RequestHeader(value = "Authorization", required = false) String authHeader) {
System.out.println("Authorization Header: " + authHeader); // Log the header
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("Unauthorized");
}
return "Secure data";
}
}If authHeader is now populated with Bearer <token>, the fix works!
3. Test with curl or Postman#
Simulate the request to rule out browser-specific issues:
# Using curl
curl -X GET http://localhost:8080/api/protected \
-H "Authorization: Bearer <your-token>" \
-H "Origin: http://localhost:3000"If the response is successful, the backend is now accepting the header.
Troubleshooting Tips#
If the issue persists, check these:
- Proxy Servers: If using a proxy (e.g., Nginx), ensure it’s not stripping the
Authorizationheader. - Browser Extensions: Ad blockers or CORS-disabling extensions may interfere. Test in incognito mode.
- Spring Security Filter Order: Ensure
CorsFilteris registered before other filters (e.g.,UsernamePasswordAuthenticationFilter). - Token Expiry: The token may be invalid/expired (check backend logs for "invalid token" errors).
- Multiple CORS Configs: Avoid conflicting CORS settings (e.g.,
@CrossOrigin+ global config).
Conclusion#
The "Bearer Token Authorization Header Null" issue is almost always caused by misconfigured CORS in Spring Boot or incorrect client-side token handling. By:
- Ensuring the frontend explicitly sends
Authorization: Bearer <token>, - Configuring Spring Boot to allow the
Authorizationheader and handle preflight requests, - Verifying the header reaches the backend via logging or network tools,
you can resolve the 403 error and secure your cross-origin requests.