Using NGINX to redirect socket.io (Node-Red) traffic based on a subrequest result (with SSL)

March 14, 2019 677 views
Nginx Node.js Ubuntu 18.04

NGINX is acting as a reverse proxy for a number of Node-Red instances and should allow/deny access requests based on the result of a subrequest. When a client requests access to a specific Node-Red instance NGINX forwards the request to the authentication server using the httpauthrequest_module. The authentication server then allows/denies access based on the cookie contents.

If access is allowed (the subrequest replies with 200 status) NGINX forwards the request to the Node-Red instance using proxy_pass. If access is denied (the subrequest replies with 401 status) NGINX redirects the request to the login in page. This works correctly.

If a client was allowed to access a specific instance and the cookie expires/or it is deleted NGINX should then deny the request and redirect the client to the login page. This does not work correctly. Looking at the developer console in Chrome the request alternates between 302 and 304 status and specifically mentions socket.io polling. How do I redirect the socket.io traffic to the login screen if user access is denied?

The Node-Red instances are running in Docker containers. I am not providing the docker-compose file as it has not influence on the problem. The authentication server is working correctly as it replies with the correct status for both allowed and denied requests.

Below the nginx.conf file.

worker_processes 2;

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;
    listen [::]:80;
    server_name foo.bar;
    return 302 https://foo.bar$request_uri;
  }

  server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name foo.bar;

    ssl on;

    ssl_certificate       /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key   /etc/nginx/ssl/privkey.pem;

    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:20m;
    ssl_session_tickets off;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';

    ssl_stapling on;
    ssl_stapling_verify on;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-SSL on;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-NginX-Proxy true;

    #Upgrade websockets to work over http
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";

    #Redirect unauthorized attempts to login URL
    location @login {
        return 302 https://$server_name;
    }

    #NR instance for login page
    location / {
        proxy_pass http://nodered0:1880/;
        proxy_intercept_errors on;
        auth_request_set $auth_status $upstream_status;
        error_page 401 = @login;
        error_page 404 = @login;
    }

    #Admin interface of login page
    location /admin/0/ {
        proxy_pass http://nodered0:1880/admin;
        auth_request /auth;
    }

    #Authentication route
    location /auth {
        proxy_pass http://nodered1:1880/auth/;
        proxy_pass_request_body off; # no need to send the POST body
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;

        proxy_set_header Content-Length "";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    #Admin interface of authentication route
    location /admin/1/ {
        proxy_pass http://nodered1:1880/;
        auth_request /auth;
        error_page 401 = @login;
        error_page 404 = @login;
    }

    location /user1/ui/  {
        proxy_pass http://nodered100001:1880/ui/;
        auth_request /auth;
        proxy_intercept_errors on;
        auth_request_set $auth_status $upstream_status;
        error_page 401 = @login;
        error_page 404 = @login;
    }

    location /user1/worldmap/ {
        proxy_pass http://nodered100001:1880/worldmap/;
        auth_request /auth;
        proxy_intercept_errors on;
        auth_request_set $auth_status $upstream_status;
        error_page 401 = @login;
        error_page 404 = @login;
    }

    location /admin/100001/ {
        proxy_pass http://nodered100001:1880/;
        auth_request /auth;
        proxy_intercept_errors on;
        auth_request_set $auth_status $upstream_status;
        error_page 401 = @login;
        error_page 404 = @login;
    }
  }
}
1 Answer

Greetings!

I welcome anyone else weighing in on this, as it is a bit outside of my knowledge. I wanted to do my best to offer advice though, as perhaps an outsider on this particular setup can offer a refreshing perspective.

So as I understand it, Nginx forwards to the auth server, which checks for the cookie. If the cookie is there, the user is passed on to the application. If the user deletes the cookie, they continue to access the back-end application without another authentication check. Is the application server then caching login authorization in some way? Could you re-poll the client for authorization every X minutes?

If you're instructing the client to make connections to socket.io then the only way you're going to be able to alter that traffic, I would think, would be either:

  1. Instructing the client to sever the connection (perhaps a page refresh and the client isn't instructed to connect to socket.io on that refresh)
  2. Proxying socket.io connections for the client so that you can manipulate their connection to it

I have no idea if I've offered any decent perspective, and may even be misunderstanding how socket.io works, but I can try :)

Jarland

Have another answer? Share your knowledge.