How to Disable HTTP Authentication for CORS Preflight OPTIONS Requests in Nginx


2 views

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:

  1. Browser sends OPTIONS preflight request
  2. Nginx challenges with 401 before processing CORS headers
  3. Browser fails the CORS check due to missing headers
  4. 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"