Google OAuth2 authlayer using nginx proxy in just 127 lines of code

We deployed our streamlit apps behind the oauth2-proxy that gives us a secure layer in front of streamlit and is easy to setup.

The oauth2 provider is Google Workspace in our case. But you can use anything that is compatible with oauth2-proxy azure, facebook, github etc. The docs are good enough to guide you through setup on google / azure end.
We are using certbot webroot plugin for SSL certs see the docs for the exact command, the certbot is installed via snap on the host system.

Here is our docker-compose and nginx config, search for YOUR… to find places where you need to get the config changed.

docker-compose.yml

version: "3"
services:
  nginx:
    image: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - /var/www/html:/var/www/html:ro # for certbot webroot plugin
      - /tmp/empty:/docker-entrypoint.d/
    ports:
      - 443:443
      - 80:80
    command:  nginx-debug -g 'daemon off;'
    environment:
      - TZ=Europe/Warsaw
  
  oauth2:
    image: quay.io/oauth2-proxy/oauth2-proxy:latest
    expose:
      - 4180
    environment:
    
      - TZ=Europe/Warsaw
      - OAUTH2_PROXY_COOKIE_SECURE=false
      - OAUTH2_PROXY_UPSTREAMS=http://streamlit:8501/

      - OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
      # change that to your google domain
      - OAUTH2_PROXY_EMAIL_DOMAINS=YOURDOMAIN.com
      # - OAUTH2_PROXY_AUTHENTICATED_EMAILS_FILE=/your-file-with-emails.txt
      
      # set this up to values you get from google auth
      - OAUTH2_PROXY_REDIRECT_URL=https://apps.YOURDOMAIN.com/oauth2/callback
      - OAUTH2_PROXY_COOKIE_SECRET=YOUR-COOKIE-SECRET
      - OAUTH2_PROXY_CLIENT_ID=YOUR-CLIENT-ID.apps.googleusercontent.com
      - OAUTH2_PROXY_CLIENT_SECRET=YOUR-CLIENT-SECRET


      - OAUTH2_PROXY_SET_XAUTHREQUEST=true
      - OAUTH2_PROXY_PASS_ACCESS_TOKEN=true
      - OAUTH2_PROXY_COOKIE_REFRESH=1h

  streamlit:
    image: "YOUR-streamlit-app"
    command: streamlit run /src/streamlit_entrypoint.py
    environment:
      - TZ=Europe/Warsaw

nginx.conf

user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 768;
}

http {

	##
	# copied from default nginx setup

	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 65;
	types_hash_max_size 2048;
	include /etc/nginx/mime.types;
	default_type application/octet-stream;
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
	ssl_prefer_server_ciphers on;
	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;
	gzip on;

	server {
        # set up SSL
		listen 80 default_server;
		listen [::]:80 default_server;
		listen 443 ssl; # managed by Certbot

		server_name "YOUR-DOMAIN.com";
		ssl_certificate /etc/letsencrypt/live/apps.YOUR-DOMAIN.com/fullchain.pem;
		ssl_certificate_key /etc/letsencrypt/live/apps.YOUR-DOMAIN.com/privkey.pem;
		include /etc/letsencrypt/options-ssl-nginx.conf;
		ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

		root /var/www/html;
		index index.html index.htm;

		location /.well-known {
			try_files $uri $uri/ =404;
		}

		location /stream {
			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_set_header Host $host;

			proxy_http_version 1.1;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection "upgrade";

			proxy_pass http://backend;
		}

		location / {
			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_set_header Host $host;

			proxy_pass http://backend;
			proxy_http_version 1.1;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection "upgrade";
		}
    }

	upstream backend {
		server oauth2:4180;
	}
}

Once you have the proxy deployed you can access a logged in user email address as follows:


def get_loggedin_user():
    """return logged in user email adress or None if no headers were found"""
    headers = get_request_headers()  # your way to access request headers
    return headers.get('X-Forwarded-Email', None) 

I’m not sure if it is the best way nowadays to access the request headers but it is what worked for us:

def get_session_id():
    ctx = st.report_thread.get_report_ctx()
    if ctx:
        return ctx.session_id

def get_session_info():
    session_id = get_session_id()
    if get_session_id():
        return st.server.server.Server.get_current()._get_session_info(session_id)
    raise RuntimeError('No streamlit session')

def get_request_headers():
    try:
        return get_session_info().ws.request.headers
    except AttributeError:
        return {}
```
2 Likes

Thank you so much for this!

1 Like