Different Nginx redirects based on upstream proxy response

Solution 1:

The exact solution to the question is to use the Lua capabilities of Nginx.

On Ubuntu 16.04 you can install a version of Nginx supporting Lua with:

$ apt install nginx-extra

On other systems it might be different. You can also opt for installing OpenResty.

With Lua you have full access to the upstream response. Note that you appear to have access to the upstream status via the $upstream_status variable. And in a way you do but due to the way 'if' statements are evaluated in Nginx you can not use $upstream_status in the 'if' statement conditional.

With Lua your configuration will then look like:

    location = /login { # the POST target of your login form
           rewrite_by_lua_block {
                    ngx.req.read_body()
                    local res = ngx.location.capture("/login_proxy", {method = ngx.HTTP_POST})
                    if res.status == 200 then
                            ngx.header.Set_Cookie = res.header["Set-Cookie"] # pass along the cookie set by the backend
                            return ngx.redirect("/shows/")
                    else
                            return ngx.redirect("/login.html")
                    end
            }
    }

    location = /login_proxy {
            internal;
            proxy_pass http://localhost:8080/login;
    }

Pretty straight forward. The only two quirks are the reading of the request body in order to pass along the POST parameters and the setting of the cookie in the final response to the client.


What I actually ended up doing, after a lot of prodding from the community, is that I handled the upstream stream responses on the client side. This left the upstream server unchanged and my Nginx configuration simple:

location = /login {
       proxy_pass http://localhost:8080;
}

The client initialing the request handles the upstream response:

  <body>
    <form id='login-form' action="/login" method="post">
      <input type="text" name="username">
      <input type="text" name="password">
      <input type="submit">
    </form>
    <script type='text/javascript'>
      const form = document.getElementById('login-form');
      form.addEventListener('submit', (event) => {
        const data = new FormData(form);
        const postRepresentation = new URLSearchParams(); // My upstream auth server can't handle the "multipart/form-data" FormData generates.
        postRepresentation.set('username', data.get('username'));
        postRepresentation.set('password', data.get('password'));

        event.preventDefault();

        fetch('/login', {
          method: 'POST',
          body: postRepresentation,
        })
          .then((response) => {
            if (response.status === 200) {
              console.log('200');
            } else if (response.status === 401) {
              console.log('401');
            } else {
              console.log('we got an unexpected return');
              console.log(response);
            }
          });
      });
    </script>
  </body>

The solution above achieves my goal of having a clear separation of concerns. The authentication server is oblivious to the use cases the callers want to support.

Solution 2:

While I completely agree with @michael-hampton, i.e., that this issue should not be handled by nginx, have you tried moving error_page into the location block:

{
    location @error401 {
        return 302 /login.html # this page holds the login form
    }

    location = /login { # this is the POST target of the login form
        proxy_pass http://localhost:8080;
        proxy_intercept_errors on;
        error_page 401 = @error401;
        return 302 /secure/; # without this line, failures work. With it failed logins (401 upstream response) still get 302 redirected
    }
}