Streamlit Authentication

Streamlit Authentication

Streamlit is a powerful tool for creating “data apps.” The easiest way to understand it is with this picture (from Streamlit’s marketing site):

You write python code ⇒ You get an interactive web application.

It integrates easily with libraries you already know, like pandas and numpy. And the interactivity is way too easy, like this application where the age variable updates as the user changes the slider:

import streamlit as st

age = st.slider('How old are you?', 0, 130, 25)
st.write("I'm ", age, 'years old')

There’s no useEffect or worrying about re-renders or anything like that.

One small quirk… Authentication is difficult

Streamlit does have a few quirks, however, and one of them is that the out-of-the-box authentication options are limited.

We wanted an approach to authentication that was as simple as Streamlit is, but still powerful enough to include features like 2FA (including enrolling) and organizations (for your B2B apps). Ultimately, on the Streamlit side, it looks like this:

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)

If the user isn’t logged in, nothing else runs. If they are logged in, you’ll get an object with their information.

So… how does this work? And what is the account page button on the sidebar?

To make this work, we rely on two PropelAuth features:

  • PropelAuth’s hosted pages. These are a set of UIs that we host for you on your domain. This includes everything you need for authentication (signup, login, forgot password, etc) but also extra UIs like an account page so your users can enroll in 2FA or update their information.
  • PropelAuth’s authentication proxy. This is a lightweight service which sits on top of Streamlit which will handle auth. If the user isn’t logged in, it’ll send them over to the login page. If they are logged in, it’ll pass their information securely to Streamlit.

Setting it up

The only thing you’ll need is a PropelAuth account. We’ll first look at how to set this up locally.

PropelAuth Test Environment Setup

The only unique thing to Streamlit is you’ll need the following configuration on the Frontend Integration page of the dashboard:

You can choose whichever port, 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.

You can then modify any of the other settings, like which login methods you want to support or if you allow signups or if you will be manually onboarding users.

Protecting Streamlit - Initialization

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

pip install propelauth_py

Then create a new file called propelauth.py and copy the following code in. This is a helper class which checks to see if the user is logged in or not.

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

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():
    return get_cookie("__pa_at")

def get_refresh_token():
    return get_cookie("__pa_rt")

def get_cookie(cookie_name):
    headers = _get_websocket_headers()
    if headers is None:
        return None

    cookies = headers.get("Cookie") or headers.get("cookie") or ""
    for cookie in cookies.split(";"):
        split_cookie = cookie.split("=")
        if len(split_cookie) == 2 and split_cookie[0].strip() == cookie_name:
            return split_cookie[1].strip()

    return None

At the bottom or in a separate file, add the following:

# Configuration, please edit
auth = Auth(
    "YOUR_AUTH_URL",
    "YOUR_API_KEY"
)

You can find these in the Backend Integration settings of your PropelAuth dashboard. You likely want to load them as environment variables instead of hardcoding them.

Protecting Streamlit - Usage

In your application, you can now use that snippet of code we had above:

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)

If you go to test your app, you should see that you are never logged in and will get an Unauthorized error. We’ll fix that now.

Adding the Authentication Proxy

Next, we’re going to set up the proxy. Create a new directory called proxy and then install the proxy:

npm i @propelauth/auth-proxy

Then, create a file called proxy.mjs and add the following contents:

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:'
    },
})

The authUrl and API key are the same ones we used above (from the Backend Integration page of the dashboard). In this example, we were running the proxy locally on port 8000 (as you can see from the config), and we were running Streamlit locally on port 8501.

Once you have your streamlit app running, all you have to do is run it:

node proxy.mjs

Testing

If we go directly to http://localhost:8051, we will still see an unauthorized error, as we should always go through the proxy.

When we go to http://localhost:8000, however, we’ll be redirected to a login page. After logging in, we’ll be redirected back to our Streamlit app, and we’ll see our user’s information.

If we click the account button in the sidebar, we are redirected to a dedicated page for managing our account:

Deploying to Production with Docker

To go to production, you’ll just need a way to host the authentication proxy. If you are already deploying Streamlit with Docker, you can deploy the authentication proxy with Docker as well with this Dockerfile:

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

Your users will then go to the URL where your authentication proxy is hosted, and not the URL where Streamlit is hosted.

Depending on your infrastructure setup, you can actually keep Streamlit entirely private and only accessible from the proxy, but this isn’t a requirement. Users that go directly to Streamlit will always be met with an “Unauthorized” page.

And now you should be good to go! Just make sure you don’t forget to update your configuration with your production values from PropelAuth as well.