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 {}
```
6 Likes

Thank you so much for this!

2 Likes

Thanks for this great info! Must one use Docker though? Or can this easily be adapted to support a typical Heroku deployment straight from github? I am looking for relatively straightforward OAuth multi-provider solution to secure my Streamlit app on either Heroku or Digital Ocean. It’s really a shame that there are really any turnkey solutions available for deploying externally deployed Streamlit apps securely. I realize that Streamlit for Teams is now available but, as a startup, the per month price is fairly steep for a project that’s just getting off the ground. Thanks!

nothing depends on docker here, it is just a way to install nginx and oauth2-proxy that is somehow platform independent. But I’m not sure if you can have this kind of flexibility to install both on hieroku.

Hertzer have server auctions that starts at 28 usd / month without upfront cost and you get a okeish bare metal server that you can use for a lot of stuff. It is what we do for our internal apps.

If you want to stick with hieroku I would have a look at the cloudflare they have some confusing products that seems to be doing authentication of other apps, that seems to be replacing oauthproxy that they were building some time ago: GitHub - cloudflare/nginx-google-oauth: Lua module to add Google OAuth to nginx.

I say confusing as I’ve spend like 20 min trying to figure out which one you can use and I failed :), but it seems to have the auth you are looking for:
https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/google

If you manage to make it work please share in this thread.

Hi PiotrCzapla,
Awesome solution! Thank you for sharing! I have a question: how does a user logout?

Logout seems to be implemented here is the commit: https://github.com/oauth2-proxy/oauth2-proxy/commit/562cc2e466415c0de133417550a55ea9b9cdf16f

We relay on the cookie expiration for logout so I don’t have snippet to implement this.

but it seems you simply go to /sign_out and you get logged out.

Thank you! I tried to redirect streamlit page to /sign_out, but it opens a new tab for user to login. The original streamlit tab is still there. Do you have any idea of redirect streamlit page to /sign_out in the same tab?

I think I figure it out by myself. Thank you!
I have another question about auth2-proxy: how to replace the auth2-proxy icon on the sign on page to my own icon?