When implementing CORS with HTTP basic authentication in Nginx, you'll encounter a critical issue: browsers send OPTIONS requests as preflight checks, but these fail with 401 Unauthorized before CORS headers can be processed. This breaks the entire CORS workflow since the browser never sees the required CORS headers in the 401 response.
Here's what happens during a CORS request with authentication:
- Browser sends OPTIONS preflight request
- Nginx challenges with 401 before processing CORS headers
- Browser fails the CORS check due to missing headers
- Actual request never gets sent
We need to modify the Nginx configuration to skip authentication specifically for OPTIONS requests:
location /api/ {
# Handle OPTIONS requests separately
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# Normal authenticated requests
auth_basic "Restricted Area";
auth_basic_user_file /var/www/admin.htpasswd;
proxy_pass http://127.0.0.1:14000;
proxy_set_header Host $host;
# Include CORS headers for actual requests
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
add_header 'Access-Control-Allow-Credentials' 'true';
}
- The OPTIONS handler must return 204 (No Content) rather than 200
- CORS headers must be duplicated for both OPTIONS and regular requests
- The
$http_origin
variable dynamically echoes the request origin - Credentials are allowed with
Access-Control-Allow-Credentials: true
Verify the solution works with curl:
curl -X OPTIONS -i http://yourdomain.com/api/endpoint
You should see a 204 response with all CORS headers present, but no authentication challenge.
For enhanced security:
# Restrict allowed origins in production
map $http_origin $cors_origin {
default "";
"~^https://(allowed1.com|allowed2.com)$" $http_origin;
}
# Then use $cors_origin instead of $http_origin
When implementing both HTTP Basic Authentication and CORS in Nginx, developers often encounter a critical issue with preflight OPTIONS requests. Modern browsers automatically send these requests when making cross-origin API calls with certain HTTP methods or custom headers.
The core problem manifests when:
- The browser sends an OPTIONS preflight request
- Nginx responds with 401 Unauthorized (due to auth_basic)
- The browser sees the 401 response but no proper CORS headers
- The actual API request never gets sent
CORS preflight is a security mechanism where browsers send an OPTIONS request before the actual request to check:
1. Allowed origins
2. Supported methods
3. Permitted headers
4. Credential requirements
This preflight must succeed before the browser proceeds with the actual request. When Nginx requires authentication for OPTIONS (via auth_basic), it breaks this flow.
The most elegant solution uses Nginx's $request_method
variable to bypass authentication specifically for OPTIONS requests:
location /api/ {
# Proxy configuration
proxy_pass http://127.0.0.1:14000;
proxy_set_header Host $host;
# CORS headers
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
add_header Access-Control-Allow-Credentials true;
# Conditional authentication
if ($request_method = OPTIONS ) {
return 204;
}
auth_basic "Restricted Area";
auth_basic_user_file /var/www/admin.htpasswd;
}
1. The return 204
handles OPTIONS requests before they hit the auth_basic check
2. 204 (No Content) is the proper response for successful preflight requests
3. All CORS headers remain intact for both preflight and actual requests
For more complex setups with multiple endpoints:
# Global CORS settings
map $http_origin $cors_origin {
default "";
"~^https://(app1|app2)\.example\.com$" $http_origin;
}
server {
# ... other server config
location ~ ^/api/ {
# Handle OPTIONS requests
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' $cors_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# Regular request handling
auth_basic "API Restricted";
auth_basic_user_file /etc/nginx/.htpasswd_api;
proxy_pass http://backend;
include proxy_params;
}
}
Verify with curl commands:
# Test preflight request
curl -i -X OPTIONS http://yourdomain.com/api/endpoint \
-H "Origin: https://yourfrontend.com" \
-H "Access-Control-Request-Method: POST"
# Should return 204 with CORS headers
# Test authenticated request
curl -u username:password -i http://yourdomain.com/api/endpoint \
-H "Origin: https://yourfrontend.com"