Search code examples
flasknginxjupyternginx-reverse-proxy

Adding a Flask Authentication layer infront of Juptyer


I am wanting to add my a flask authentication layer in front of Juptyer to enable my access from anywhere.

Both Flask and Jupyter on the same server so after authentication is should pass the connection.

I am using Juptyer server with the following config

c.ServerApp.ip = '127.0.0.1'
c.ServerApp.port = 8888
c.ServerApp.open_browser = False
c.ServerApp.token = ''  # Disable token authentication
c.ServerApp.password = ''  # Disable password authentication
c.ServerApp.disable_check_xsrf = True  # Disable XSRF checks
c.ServerApp.trust_xheaders = True  # Allow reverse proxy headers
c.ServerApp.allow_remote_access = True
c.ServerApp.allow_origin = '*'

below is my current nginx configuration:

server {
    listen 443 ssl;
    server_name jupyter.mywebsite.com;

    ssl_certificate /etc/letsencrypt/live/jupyter.mywebsite.com/fullchain.pem; # Certbot
    ssl_certificate_key /etc/letsencrypt/live/jupyter.mywebsite.com/privkey.pem; # Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # Certbot SSL options
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://127.0.0.1:7000/;  # Flask server
        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-Proto $scheme;

        # WebSocket support for Jupyter
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

server {
    listen 80;
    server_name jupyter.mywebsite.com mywebsite.com www.mywebsite.com;

    return 301 https://$host$request_uri;
}


server {
    listen 443 ssl;
    server_name mywebsite.com www.mywebsite.com;

    ssl_certificate /etc/letsencrypt/live/mywebsite.com/fullchain.pem; # Certbot
    ssl_certificate_key /etc/letsencrypt/live/mywebsite.com/privkey.pem; # Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # Certbot SSL options
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://127.0.0.1:7000;  # Flask app
        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-Proto $scheme;

        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

This is my current flask config

# Proxy Jupyter Requests
@app.after_request
def adjust_csp_for_jupyter(response):
    if "/jupyter/" in request.path:
        response.headers["Content-Security-Policy"] = (
            "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net 'unsafe-eval'; "
            "style-src 'self' https://cdnjs.cloudflare.com; img-src 'self' data:;"
        )
    return response


JUPYTER_BASE_URL = "http://127.0.0.1:8888"  # Internal Jupyter address

@app.route('/jupyter/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
@login_required
def proxy_to_jupyter(path):
    jupyter_url = f"{JUPYTER_BASE_URL}/{path}"
    try:
        response = requests.request(
            method=request.method,
            url=jupyter_url,
            headers={key: value for key, value in request.headers.items() if key.lower() not in ['host', 'cookie']},
            data=request.get_data(),
            cookies=request.cookies,
            allow_redirects=False,  # Handle redirects manually
        )
        excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
        headers = [(name, value) for name, value in response.headers.items() if name.lower() not in excluded_headers]
        return Response(response.content, response.status_code, headers)
    except requests.RequestException as e:
        abort(502, f"Failed to connect to Jupyter: {str(e)}")

@app.route('/api/kernels/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
@login_required
def proxy_websocket_to_jupyter(path):
    # WebSocket-specific proxying code (use `flask-sock` or similar)
    pass

Solution

  • So I figured it out! I had a few things wrong and was being over controlling with the NGINX config: Below is the accurate config:

    Key Changes

    • removed the subdomain configuration
    • created a custom location called auth-check, this is called in the below jupyter configuration. this way flask as function to interact with.
    • only one jupyter configuration
    • then i have have the flask app that does the authentication last. This ensure that the configuration for the other protected sites happens before it loads the index.
        server {
        listen 443 ssl;
        server_name mywebsite.com www.mywebsite.com;
    
        ssl_certificate /etc/letsencrypt/live/mywebsite.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/mywebsite.com/privkey.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    
        location = /auth-check {
            internal;
            proxy_pass http://127.0.0.1:7000/auth-check;
            proxy_set_header Cookie $http_cookie;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    
        # Jupyter main routes
        location /jupyter/ {
            auth_request /auth-check;
            proxy_pass http://127.0.0.1:8888/jupyter/;
            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-Proto $scheme;
    
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 3600s;
            proxy_send_timeout 3600s;
        }
    
        # Flask application (if separate from Jupyter)
        location / {
            proxy_pass http://127.0.0.1:7000;
            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-Proto $scheme;
    
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 3600s;
            proxy_send_timeout 3600s;
        }
    }
    

    Changes to Flask:

    • addition of the auth-check method
    • addition of csp headers to ensure the passing of the sockets worked.
    @app.route('/auth-check')
    def auth_check():
        if current_user.is_authenticated:
            return '', 200
        return '', 401
    
    # Adjust CSP for Jupyter pages if desired.
    @app.after_request
    def adjust_csp_for_jupyter(response):
        if "/jupyter/" in request.path:
            response.headers["Content-Security-Policy"] = (
              "default-src 'self'; "
              "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net 'unsafe-eval'; "
              "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com; "
              "font-src 'self' https://fonts.gstatic.com; "
              "img-src 'self' data:; "
              "connect-src 'self' wss:;"
              "connect-src 'self' wss: https://mywebsite.com;"
            )
        return response
    
    # Internal Jupyter server base URL (no auth). Ensure you started Jupyter with --ServerApp.token=''
    JUPYTER_BASE_URL = "http://127.0.0.1:8888/jupyter"
    
    @app.route('/jupyter/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
    @login_required
    def proxy_to_jupyter(path):
        jupyter_url = f"{JUPYTER_BASE_URL}/{path}"
        try:
            # Forward the request to the Jupyter server
            response = requests.request(
                method=request.method,
                url=jupyter_url,
                headers={k: v for k, v in request.headers.items() if k.lower() not in ['host', 'cookie']},
                data=request.get_data(),
                cookies=request.cookies,
                allow_redirects=False
            )
            excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
            headers = [(name, value) for name, value in response.headers.items() if name.lower() not in excluded_headers]
            return Response(response.content, response.status_code, headers)
        except requests.RequestException as e:
            abort(502, f"Failed to connect to Jupyter: {str(e)}")
    
    @app.route('/api/kernels/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
    @login_required
    def proxy_websocket_to_jupyter(path):
        # WebSocket proxying is more complex. Consider:
        # - Using a reverse proxy like Nginx for WebSocket traffic
        # - Using flask-sock or a similar package to handle upgrade requests
        # This is a placeholder.
        pass
    

    Key changes to Jupyter,

    • i moved to lab instead of server since it is just me accessing it and server allows more for multiuser and what not but i dont need that.
    • Also removed origin statement from config. as it is more secure that way.