special credit: flaticon.com for the login icon

Hack the Framework: Implement Basic Authentication in Streamlit for Multiple Section (Multi Page)Apps

MrDataPsycho
Towards Dev
Published in
8 min readJan 2, 2023

--

It is very easy to create Streamlit applications for data science projects but so far I have not found any native way to implement authentication for single or multi-page applications. So I am going to discuss an Application Factory Design Pattern I have been using since 2021 for any Streamlit frontend. The best thing is it is compatible with any third-party auth provider (azure ad, auth0, etc.) which supports oauth2-based authentication. Though in this blog I will only discuss some basic authentication. I will discuss adding AzureAd as auth provider in another post.

I will explain the whole implementation in three steps. In the first step, I will create a very basic app. Then I will decouple the app using Page Factory Pattern for modularity. In the last step, I will implement authentication components into the application.

Step1: A Basic Streamlit App Template:

Let's have a look at the current project directory structure:

.
├── pdm.lock
├── __pypackages__
│ └── 3.10
├── pyproject.toml
└── src
└── main.py

I am using PDM as a python dependency manager but the project should be working with any dependency manager. Our main interest is the files inside the src directory. The main.py is the entry point of our application. Here is the content of the main file:

import streamlit as st
import pandas as pd
import numpy as np


def welcome():
st.write("# Welcome to the Auth App")

st.markdown(
"""
This is a demo multipage app, you need to log in first to see the dashboard view.
"""
)

def dashboard():
st.write("# Welcome to the Dashboard")
chart_data = pd.DataFrame(np.random.randn(20, 3), columns=['a', 'b', 'c'])
st.line_chart(chart_data)

if __name__ == "__main__":
welcome()
dashboard()

There are two functions representing two sections of the app. One has some welcome text and the other has the main dashboard. Our goal is to put the dashboard section under authentication. Now we can run the application with streamlit cli:

PYTHONPATH=src streamlit run src/main.py

Here is what the page looks like:

Nothing special so far. I guess you already know about it. Now, let’s have a look at something new.

Step 2: Implement Multisection with Application/Page Factory Pattern

Requirements:
1) App will have 2 separate sections (Welcome and Dashboard)
2) User can navigate between the section using Radio Button

To be able to fulfill the requirements we will have to restructure the app. Here is what the directory structure should look like:

.
├── pdm.lock
├── __pypackages__
...
├── pyproject.toml
└── src
├── main.py
└── views
├── dashboard.py
└── welcome.py

I have moved the code of the dashboard and welcome section to their respective py file. This is how the content looks like after the refactoring:

# welcome.py
import streamlit as st

def main():
st.write("# Welcome to the Auth App")

st.markdown(
"""
This is a demo multipage app, you need to log in first to see the dashboard view.
"""
)
# dashboard.py
import streamlit as st
import pandas as pd
import numpy as np


def main():
st.write("# Welcome to the Dashboard")
chart_data = pd.DataFrame(np.random.randn(20, 3), columns=['a', 'b', 'c'])
st.line_chart(chart_data)

Notice that I have changed the function name to main from dashboard and welcome, which will help us to create a generic application/page factory.

From now we will mostly be working with the main.py file. Now let’s build a Page factory with python default dictionary.

import streamlit as st
from views import dashboard, welcome


RADIO_BUTTON_FACTORY = {
"🚩 Welcome": {'func': welcome.main, 'activate': True},
"📈 Dashboard": {'func': dashboard.main, 'activate': True}
}


def run_app():
# 1. filter and keep only the section where active is True
active_sections = {key: value for key, value in RADIO_BUTTON_FACTORY.items() if value['activate']}
st.sidebar.header(":open_book: Menu")
# 2. Take the key of the factory to create the radio button
option = st.sidebar.radio("", list(active_sections.keys()))
# 3. Based on the user selection, the function will be selected
func = active_sections[option]["func"]
# 4. Finally the function will be executed
func()


if __name__ == "__main__":
run_app()

Here first I have created a dictionary of functions, where each key-value pair represent a radio button section. I have also added an active flag in the factory using which I can quickly filter the section I want to hide during development. Using the run app function I am able to navigate between pages 💡.

That’s what it looks like after the changes. Now we can navigate between the welcome page and the dashboard.

Step 3: Implement basic Authentication using Streamlit Session

By default, Streamlit has a session state mechanism. which enables a way to share variables between reruns, for each user session. So first we will ask the user to provide a username and password and can query a database. if the combination exists we can add a session variable authenticated and set it to true. Then every time the user navigates to a section if the session state exists in the session and it is set to true we will let the user view the content otherwise we will print some generic instructions that how they can authenticate themselves. Let’s create some authentication helper functions first, here is the content of src/authentication.py looks like:

import streamlit as st
import logging


def add_auth_to_session():
try:
st.session_state["authenticated"] = True
except Exception as e:
logging.error(e)


def user_authenticated(username: str, password: str) -> bool:
if username == "welcome" and password == "1111":
add_auth_to_session()
return True
return False

The user_authenticated function matches the provided username and password with hard-coded values. If it matches with default values I set a session variable authenticated to true using the add_auth_to_session function. This is just for prototyping purposes. You have to have your own implementation instead, either querying a database or by connecting to an authentication provider for your organization.

Now let's use the authentication function on the welcome page, which will save the authentication status into the session variable.

# src/views/welcome.py
import streamlit as st
from authentication import user_authenticated

def main():
st.write("# Welcome to the Auth App")

with st.form(key="welcome", clear_on_submit=True):
with st.sidebar:
st.markdown("---")
user = st.text_input("Username", value="", type="password")
password = st.text_input("Password", value="", type="password")
submitted = st.form_submit_button("LogIn")

if submitted:
result = user_authenticated(username=user, password=password)
if result:
st.sidebar.success("Logged in successfully!")

st.markdown(
"""
This is a demo multipage app, you need to log in first to see the dashboard view.
"""
)

Here is how the we welcome page sould looks like after the implementation.

Now if the user authenticated successfully we should have a session variable that we can use in the dashboard view. If there will be a session variable in the current session we will show the dashboard other wise will show some general instructions.

# src/views/dashboard.py
import streamlit as st
import pandas as pd
import numpy as np


def main():
if 'authenticated' in st.session_state and st.session_state['authenticated']:
st.write("# Welcome to the Dashboard")
chart_data = pd.DataFrame(np.random.randn(20, 3), columns=['a', 'b', 'c'])
st.line_chart(chart_data)
else:
st.markdown("🚫 __You must have to authenticate yourself "
"to view the content of the dashbaord. "
"Please visit the welcome section and authenticate yourself.__"
)

So that's how the app looks like if we do not authenticate when visiting the dashboard section.

🎉 That’s all about basic authentication. But let's do a little bit of refactoring.

We might have many sections in the app and we want some of them to be protected and some of them without authentication. In such cases, we will have to repeat the if else condition again and again inside the main function of each views. Instead, we can create a decorator for authentication and wrap the main function of a particular view we want to protect.

Let’s add a new decorator function in the src/authentication.py file:

import typing as t
import functools
# ... more imports
# ... Old codes

def session_handler(func: t.Callable) -> t.Callable:
generic_text = (
"🚫 __You must have to authenticate yourself "
"to view the content of the dashbaord. "
"Please visit the welcome section and authenticate yourself.__"
)
@functools.wraps(func)
def wrapper():
if 'authenticated' in st.session_state and st.session_state['authenticated']:
func()
else:
st.markdown(generic_text)
return wrapper

Now we can remove the boilerplate code from the dashboard.py file and decorate the main function as follows:

# ... old imports
from authentication import session_handler

@session_handler
def main():
st.write("# Welcome to the Dashboard")
chart_data = pd.DataFrame(np.random.randn(20, 3), columns=['a', 'b', 'c'])
st.line_chart(chart_data)

Pretty clever! From now any new section we will create which needs authentication we can just use the decorator. Let’s say we add a new section called report and want that section to be protected, we can add a new file src/views/report.py and add the main function decorated with the session handler. Here is an example:

# src/views/report.py
# ... other import
from authentication import session_handler

@session_handler
def main():
st.write("# Welcome to the Report")
# Code for Report part

The way I am matching username and password in the app is stupid and should never be used in production. Proper authentication by querying a database or using a 3rd party authentication provider should be used. But the main concepts are covered here. If you want to implement proper authentication you will just need to replace the user_authenticated function with a proper implementation. At my work, I was able to integrate oauth2-based single sign-on using AzureAd.

But the latest Streamlit has a real multipage feature, why I am not using it?

The latest Streamlit has default multipage support which does not need any hacking (the page factory I used at the beginning.) You can read more details here. But it is like magic and I have not found a way to implement authentication using the default design pattern provided by Streamlit. So I usually use the Page Factory Pattern I showed in all of my apps deployed in production.

You can find the full code of this blog in my GitLab. Let me know by comment if you are interested to see how I implemented AzureAd single sign-on in a Streamlit app. If the post reaches 100 claps I will write about AzureAd integration anyway.

Happy New Year! Keep coding, Keep learning. I hope ChatGPT still does not know this technique.

Hi,
If you already know pandas the popular data frame framework and quickly want to learn PySpark a bigdata data frame framework, I got you covered. I just launched my first online course called Pandas to PySpark DataFrame. Which is available at Educaive here and also as a book form in learnpub here. These courses are for quick learners like me, who are more interested in project-based learning.

https://www.educative.io/courses/pandas-to-pyspark-dataframe
https://leanpub.com/pandas-to-pyspark

--

--

Data Science | Dev | Author @ Learnpub, Educative: Pandas to Pyspark