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
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
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:
@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,