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#

  1. Understanding the Problem
  2. Common Causes of the Issue
  3. Client-Side Fixes: Ensuring the Token is Sent
  4. Server-Side Fixes: Configuring Spring Boot Correctly
  5. Testing the Fix
  6. Troubleshooting Tips
  7. Conclusion
  8. 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 an OPTIONS preflight 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 Authorization header or fails to respond correctly to the preflight request, the browser blocks the actual request (or strips the Authorization header), leading to a null header 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 Authorization header in cross-origin requests.
  • Handle OPTIONS preflight requests properly.

2. Incorrect Client-Side Token Handling#

The frontend may:

  • Forget to attach the Authorization header to the request.
  • Use incorrect syntax (e.g., missing Bearer prefix or typos like authorization instead of Authorization).
  • 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:3000 for development). Use * cautiously (not recommended for production).
    • allowedHeaders: Must include Authorization to 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 allows OPTIONS preflight requests to bypass authentication.
    • Order matters: Add the CorsFilter before 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 Authorization header.
  • Browser Extensions: Ad blockers or CORS-disabling extensions may interfere. Test in incognito mode.
  • Spring Security Filter Order: Ensure CorsFilter is 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:

  1. Ensuring the frontend explicitly sends Authorization: Bearer <token>,
  2. Configuring Spring Boot to allow the Authorization header and handle preflight requests,
  3. Verifying the header reaches the backend via logging or network tools,

you can resolve the 403 error and secure your cross-origin requests.

References#