Implementing Conditional Nginx Redirects Based on Upstream HTTP Status Codes (401 vs 200)


4 views

When implementing login flows with Nginx as a reverse proxy, we often need to handle different redirect scenarios based on upstream server responses. A common pattern is:

  • Successful login (200 OK) → Redirect to secure area
  • Failed login (401 Unauthorized) → Redirect back to login form

The key issue lies in Nginx's processing order. When both proxy_pass and return directives exist in the same location block:


location = /login {
    proxy_pass http://backend;
    return 302 /secure/;  # This short-circuits the proxy flow
}

The return statement takes precedence, making Nginx ignore the upstream response entirely.

Here's the correct approach using Nginx's error handling mechanism:


server {
    # Handle failed logins (401)
    error_page 401 = @handle_401;
    
    location @handle_401 {
        return 302 /login.html?error=1;
    }

    location = /login {
        proxy_pass http://upstream_server;
        proxy_intercept_errors on;
        
        # Successful login processing
        proxy_set_header X-Login-Success "true";
        
        # Only set cookies/session if upstream returns 200
        proxy_ignore_headers Set-Cookie;
        add_header Set-Cookie "session=$upstream_http_set_cookie" if=$upstream_status = 200;
        
        # Successful login redirect
        return 302 /secure/ if=$upstream_status = 200;
    }
}

For more complex scenarios, we can use the map directive:


map $upstream_status $login_redirect {
    200     "/secure/";
    401     "/login.html";
    default "/error.html";
}

server {
    location = /login {
        proxy_pass http://upstream;
        proxy_intercept_errors on;
        return 302 $login_redirect;
    }
}

When implementing this in production:

  • Always test with curl -v to verify status codes
  • Consider adding CSRF protection to your login form
  • Set appropriate cache headers for the login page
  • Log both successful and failed attempts for security monitoring

This approach adds minimal overhead since:

  • Nginx's error handling is very efficient
  • No additional subrequests are made for the redirect logic
  • The map directive evaluation happens at runtime but is optimized

When implementing authentication flows with Nginx as a reverse proxy, we often need to handle redirects differently based on upstream server responses. A common scenario involves:

Successful login (200) → Redirect to secure area
Failed login (401) → Redirect back to login form

The initial configuration attempts to solve this with:

error_page 401 = @error401;
location @error401 {
    return 302 /login.html;
}

location = /login {
    proxy_pass http://localhost:8080;
    proxy_intercept_errors on;
    return 302 /secure/; # This overrides everything
}

The critical issue here is that return directives in Nginx terminate processing immediately, preventing the proxy response from being evaluated.

Here's an effective approach using custom headers from your upstream server:

location = /login {
    proxy_pass http://localhost:8080;
    proxy_intercept_errors on;
    proxy_pass_request_headers on;
    
    # Store upstream response status
    proxy_store_access user:rw group:rw all:r;
    proxy_set_header X-Upstream-Status $upstream_status;
    
    error_page 401 = @error401;
    
    # Evaluate after proxy completes
    proxy_redirect off;
    proxy_ignore_headers X-Accel-Redirect;
    
    if ($upstream_status = 200) {
        return 302 /secure/;
    }
}

location @error401 {
    return 302 /login.html?error=1;
}

For more sophisticated authentication flows, consider Nginx's auth_request module:

location = /auth {
    internal;
    proxy_pass http://auth-backend;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
}

location /login {
    auth_request /auth;
    auth_request_set $auth_status $upstream_status;
    
    error_page 401 = @error401;
    
    if ($auth_status = 200) {
        return 302 /secure/;
    }
}

When implementing this in production:

  • Always test with curl -v to verify status codes
  • Consider adding CSRF protection to your login flow
  • Implement rate limiting on the login endpoint
  • Use secure cookies for session management

Add these to your configuration for troubleshooting:

log_format upstream_log '$remote_addr - $remote_user [$time_local] '
                       '"$request" $status $body_bytes_sent '
                       '"$http_referer" "$http_user_agent" '
                       'ups_status: $upstream_status ups_resp: "$upstream_http_x_custom_header"';

access_log /var/log/nginx/upstream.log upstream_log;