Streamlit Authentication

Streamlit is an open-source Python library that allows you to create and share beautiful, interactive data applications and dashboards with minimal code. Streamlit is particularly useful for creating data science and machine learning applications, dashboards, and data visualization tools.

That being said, out-of-the-box authentication options with Streamlit are limited. That's where PropelAuth comes in.

This guide will cover how to build and deploy a Streamlit app using PropelAuth for authentication.

Setup in PropelAuth

Lets start by navigating to your Frontend Integrations page in your PropelAuth dashboard. We'll be using a proxy to protect your Streamlit app (more on that later), so make sure to set a port that's different from where your Streamlit app is located. In this guide, we'll be using http://localhost:8000.

Frontend integrations page

You can choose whichever URL you want for the Application URL, but you must use /api/auth/callback for login and /api/auth/logout for logout. You will still be redirected back to your main page, but those will handle logging the user in/out.

Installation and Initialization

In your Streamlit project, install the propelauth-py library:

pip install propelauth_py

Now create a new file in your root directory and name it propelauth.py. Copy the following code into it, making sure to edit YOUR_AUTH_URL and YOUR_API_KEY with the values found in your Backend Integration page in PropelAuth. You likely want to load them as environment variables instead of hardcoding them.

propelauth.py

import streamlit as st
from propelauth_py import init_base_auth, UnauthorizedException
from streamlit.web.server.websocket_headers import _get_websocket_headers
import requests

# Replace me
AUTH_URL = "YOUR_AUTH_URL"
API_KEY = "YOUR_API_KEY"

class Auth:
    def __init__(self, auth_url, integration_api_key):
        self.auth = init_base_auth(auth_url, integration_api_key)
        self.auth_url = auth_url
        self.integration_api_key = integration_api_key

    def get_user(self):
        access_token = get_access_token()
        if not access_token:
            return None
        try:
            return self.auth.validate_access_token_and_get_user("Bearer " + access_token)
        except UnauthorizedException as err:
            print("Error validating access token", err)
            return None

    def get_account_url(self):
        return self.auth_url + "/account"
    
    def logout(self):
        refresh_token = get_refresh_token()
        if not refresh_token:
            return False
        logout_body = {"refresh_token": refresh_token}
        url = f"{self.auth_url}/api/backend/v1/logout"
        headers = {
            "Content-Type": "application/json",
            "Authorization": "Bearer " + self.integration_api_key,
        }
        response = requests.post(url, json=logout_body, headers=headers)
        return response.ok


def get_access_token():
    try: 
        return st.context.cookies["__pa_at"]
    except:
        return None

def get_refresh_token():
    try: 
        return st.context.cookies["__pa_rt"]
    except:
        return None

auth = Auth(
    AUTH_URL,
    API_KEY
)

This is a helper class which checks to see if the user is logged in or not.

Lets now attempt to load PropelAuth user data from your Streamlit application. Import the helper class from above and use the get_user() function to retrieve user data. If the user is not logged in, display an error message that the user is unauthorized.

import streamlit as st

from propelauth import auth

user = auth.get_user()
if user is None:
    st.error('Unauthorized')
    st.stop()

with st.sidebar:
    st.link_button('Account', auth.get_account_url(), use_container_width=True)

st.text("Logged in as " + user.email + " with user ID " + user.user_id)

You should be seeing the Unauthorized error! This is because the user is not logged in. To fix this, let's now set up the auth proxy mentioned earlier.

Unauthorized error

Adding the Authentication Proxy

We'll be using a proxy to protect your Streamlit app. Get started by creating a new repository that is separate from your Streamlit app. We'll eventually be deploying both the proxy and Streamlit app as two separate deployments.

In the new repository, install the proxy from PropelAuth:

npm i @propelauth/auth-proxy

Within the proxy directory create a new file called proxy.mjs and add the following:

proxy.mjs

import { initializeAuthProxy } from '@propelauth/auth-proxy'

// Replace with your configuration
await initializeAuthProxy({
    authUrl: "YOUR_AUTH_URL",
    integrationApiKey: "YOUR_API_KEY",
    proxyPort: 8000,
    urlWhereYourProxyIsRunning: 'http://localhost:8000',
    target: {
        host: 'localhost',
        port: 8501,
        protocol: 'http:'
    },
})
  • Make sure to replace YOUR_AUTH_URL and YOUR_API_KEY with the same values we used in the propelauth.py file from earlier.

  • The proxyPort and urlWhereYourProxyIsRunning should match the Application URL that you set in your Frontend Integrations page in PropelAuth.

  • The target parameters are where your Streamlit app is located. Port 8501 is the default port so you'll likely not have to change this.

Running Your Application Locally

You should be ready to test your app locally! First, run your Streamlit app:

streamlit run your_script.py

Once that's running, it's time to start the proxy server:

node proxy.mjs

If we navigate straight to your Streamlit app (http://localhost:8501 by default), we'll still run into the Unauthorized error. However, if we navigate to the proxy server instead (http://localhost:8000), you'll be redirected to login to PropelAuth and then back to the proxy which will now be displaying your user information:

Displaying user information

Logging Users Out

As you may have noticed, the propelauth.py script above includes a logout function. This function will invalidate the user's refresh token which in turn prevents the user from generating new access tokens.

While this is a critical step in logging a user out, there is still another step required to ensure the user is no longer logged in. In your Streamlit app, you can include a logout button like so:

with st.sidebar:
    if st.button("Logout"):
        auth.logout()
        st.markdown(
            """
        <meta http-equiv="refresh" content="0; URL='/api/auth/logout'" /> 
            """,
            unsafe_allow_html=True,
        )

This will run the logout function and also redirect the user to /api/auth/logout, which is handled by the @propelauth/auth-proxy library. The auth-proxy will clear the necessary cookies from the browser before redirecting the user back to your login page.

Deploying Your Streamlit Application

This part of the guide will cover how to deploy your Streamlit app to Google Cloud Run using Docker. Similar to above, we'll use a proxy to protect our Streamlit app.

The proxy is where your users will navigate to when they want to visit your application. The proxy will then check if the user is authenticated, direct the user to login if necessary, and then fetch your Streamlit application to display to the user.

Due to this, the proxy should be located at the URL where you want your visitors to navigate to when visiting your application. On the other hand, since users will be navigating to the proxy URL, your Streamlit application can be located at a random URL.

Streamlit Proxy Visual

Lets start by navigating to the Go Live page in the PropelAuth dashboard and verifying your domain.

Once verified, we need to set our Frontend Integration locations. We still need to use /api/auth/callback for login and /api/auth/logout for logout. You can also navigate to the Backend Integrations page to retrieve your YOUR_AUTH_URL and YOUR_API_KEY.

From now on in the guide, anytime you see YOUR_AUTH_URL and YOUR_API_KEY, populate these fields with your production environment variables.

Starting with your Streamlit app, let's adjust your Dockerfile so CORS is disabled by adding the --server.enableCORS=false command. Here's an example:

FROM python:3.8
ENV auth_url=YOUR_AUTH_URL
ENV auth_api_key=YOUR_API_KEY
EXPOSE 8501
ADD requirements.txt requirements.txt
RUN pip install -r requirements.txt
WORKDIR /app
COPY . ./
ENTRYPOINT ["streamlit", "run", "app.py",  "--server.address=0.0.0.0", "--server.enableCORS=false"]

When we create a service in Cloud Run from this container, make sure to edit the Container Port field in Cloud Run to 8501, or whichever port you exposed in your Docker container.

Cloud Run Port

Since users will access your app via the proxy, the URL for the Streamlit part of your app is not important and can simply be the URL that Cloud Run generates for you.

Deploying the Proxy

Once a URL is generated, we can update the proxy.mjs file with the new target host. While we're here, we can also update the proxy domain. Remember, users will be accessing your app via the proxy, so the URL for your proxy will be the one your users will be navigating to. It should also match the Application URL that you used in the Frontend Integration page in PropelAuth.

proxy.mjs

import { initializeAuthProxy } from '@propelauth/auth-proxy'

// Replace with your configuration
await initializeAuthProxy({
    authUrl: "YOUR_AUTH_URL",
    integrationApiKey: "YOUR_API_KEY",
    proxyPort: 8000,
    urlWhereYourProxyIsRunning: "YOUR_PROXY_URL", // https://proxy-example.com
    target: {
        host: "YOUR_APP_URL", // streamlit-example.com
        protocol: 'https:'
    },
})

Lets now build a Docker container using the following Dockerfile:

FROM node:20-slim
ENV auth_url=YOUR_AUTH_URL
ENV auth_api_key=YOUR_API_KEY
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
WORKDIR /home/node/app
USER root
COPY package*.json ./
RUN npm install
COPY --chown=node:node . .
EXPOSE 8000
CMD [ "node", "proxy.mjs" ]

When we create a service in Cloud Run for the proxy container, make sure to edit the Container Port field in Cloud Run to 8000.

Google Cloud Proxy Port

As mentioned earlier, users will access your app via the proxy so it's crucial that the URL of your proxy is whats set for your production environment in the Frontend Integration page.

Once deployed, navigate to your proxy's URL and login. You're now live!