Authentication script

I have here a brief authentication script (well, a hundred lines or so of python) that just allows very simple user authentication and management, without any additional components, or even using the forms control. Itā€™s been developed in version 0.55 just to make sure that itā€™s widely compatible!

To use it, just import the auth function from the file and run it - it returns either None or the authenticated username. It runs in the sidebar, but can be made to run in the main window if you prefer.

State is stored in an on-disk sqlite database file. THERE IS NO PASSWORD HASHING and passwords are easily viewable when editing the user file. This is for simple use cases only. Additional functions (eg password hashing and salting) would be fairly easy to add, but not needed for my use case, which is a public facing, multiuser server, with no confidential data but a need to display information depending on the user viewing the site.

To set the user database up, just run the module directly as a streamlit script.

Iā€™ve included an example main.py streamlit script showing how to do the import (ideally as close to the top of the file as possible) and then use the return value to decide if the user is authenticated or not (as well as who they are!)

main.py

import streamlit as st

st.write(
    "This is a landing page designed to showcase the simple authentication library."
)
st.write(
    "This is just a single import and a function to run, and the return value is either None, or the authenticated username."
)
st.write(
    "The user login is usually based in the sidebar (though ultimately configurable by passing True or False to the sidebar parameter of the auth function"
)
st.write(
    "All the user management and username and password entry should be taken care of by the library. To automatically have creation and edit access, just run the library directly as a streamlit script."
)

from authlib import auth

data = auth()  # auth(sidebar=False) or auth(False) if you don't want the sidebar
"""This both displays authentication input in the sidebar, and then returns the credentials for use locally"""

st.write(f"{'AUTHENTICATED!' if data else 'NOT authenticated'}")
st.write(
    f"Authentication data received = {data} (which is the username that has logged in)"
)

authlib.py

import streamlit as st
import sqlite3 as sql


def auth(sidebar=True):

    try:
        conn = sql.connect("file:auth.db?mode=ro", uri=True)
    except sql.OperationalError:
        st.error(
            "Authentication Database is Not Found.\n\nConsider running authlib script in standalone mode to generate."
        )
        return None

    input_widget = st.sidebar.text_input if sidebar else st.text_input
    checkbox_widget = st.sidebar.checkbox if sidebar else st.checkbox
    user = input_widget("Username:")

    data = conn.execute("select * from users where username = ?", (user,)).fetchone()
    if user:
        password = input_widget("Enter Password:", type="password")
        if data and password == data[2]:
            if data[3]:
                if checkbox_widget("Check to edit user database"):
                    _superuser_mode()
            return user
        else:
            return None
    return None


def _list_users(conn):
    table_data = conn.execute("select username,password,su from users").fetchall()
    if table_data:
        table_data2 = list(zip(*table_data))
        st.table(
            {
                "Username": (table_data2)[0],
                "Password": table_data2[1],
                "Superuser?": table_data2[2],
            }
        )
    else:
        st.write("No entries in authentication database")


def _create_users(conn, init_user="", init_pass="", init_super=False):
    user = st.text_input("Enter Username", value=init_user)
    pass_ = st.text_input("Enter Password (required)", value=init_pass)
    super_ = st.checkbox("Is this a superuser?", value=init_super)
    if st.button("Update Database") and user and pass_:
        with conn:
            conn.execute(
                "INSERT INTO USERS(username, password, su) VALUES(?,?,?)",
                (user, pass_, super_),
            )
            st.text("Database Updated")


def _edit_users(conn):
    userlist = [x[0] for x in conn.execute("select username from users").fetchall()]
    userlist.insert(0, "")
    edit_user = st.selectbox("Select user", options=userlist)
    if edit_user:
        user_data = conn.execute(
            "select username,password,su from users where username = ?", (edit_user,)
        ).fetchone()
        _create_users(
            conn=conn,
            init_user=user_data[0],
            init_pass=user_data[1],
            init_super=user_data[2],
        )


def _delete_users(conn):
    userlist = [x[0] for x in conn.execute("select username from users").fetchall()]
    userlist.insert(0, "")
    del_user = st.selectbox("Select user", options=userlist)
    if del_user:
        if st.button(f"Press to remove {del_user}"):
            with conn:
                conn.execute("delete from users where username = ?", (del_user,))
                st.write(f"User {del_user} deleted")


def _superuser_mode():
    with sql.connect("file:auth.db?mode=rwc", uri=True) as conn:
        conn.execute(
            "create table if not exists users (id INTEGER PRIMARY KEY, username UNIQUE ON CONFLICT REPLACE, password, su)"
        )
        mode = st.radio("Select mode", ("View", "Create", "Edit", "Delete"))
        {
            "View": _list_users,
            "Create": _create_users,
            "Edit": _edit_users,
            "Delete": _delete_users,
        }[mode](
            conn
        )  # I'm not sure whether to be proud or horrified about this...


if __name__ == "__main__":
    st.write(
        "Warning, superuser mode\n\nUse this mode to initialise authentication database"
    )
    if st.checkbox("Check to continue"):
        _superuser_mode()
3 Likes

@madflier

This is pretty cool. Thanks for sharing

Nice job, and I can see this being useful for simple use cases.

Youā€™d be more proud with less typing and dependency on getting the strings to match :grinning_face_with_smiling_eyes::

modes =  {
    "View": _list_users,
    "Create": _create_users,
    "Edit": _edit_users,
    "Delete": _delete_users,
}
mode = st.radio("Select mode", modes.keys())
modes[mode](conn)

Suggestions:

  • Add _logout() so the user doesnā€™t auto-logout when the password field is empty
  • Hold the user in st.session_state
  • Add a method, e.g. authenticated() which validates the logged in user state making it easier to use in client programs
2 Likes

Thank you - some brilliant comments. I completely agree that the intermediate dictionary is a better choice - Iā€™ve already moved to this in my own code.

I am avoiding using any explicit session state functions with this script - partly for simplicity and partly so that it works even with older versions of streamlit (I have a few web apps running older versions of code that donā€™t work with current). I think it is also easier to understand for a beginner programmer (like myself) without delving too deeply into the streamlit API.

Ditto a logout function - here there is just the simplicity of clearing the password box or not.

And finally, an authenticated method is useful, but the initial call to auth() already returns either None, if unauthenticated, or a tuple of the logged in user, for use in a client app.

Thank you for the comments - Iā€™ll definitely have a think about updating the OP with your dictionary variable suggestion as I think itā€™s both significantly clearer and safer. I still dislike calling an arbitrary function from a dictionary with this syntax, but I accept that itā€™s pretty pythonic.

No worries. Thanks for sharing.

This pattern is as old as the hillsā€¦ itā€™s called a function dispatcher.

For Security
We can hide the password in database by the hash string

import hashlib

def make_hashes(password):
    return hashlib.sha256(str.encode(password)).hexdigest()


| username | password | su |

| david | 004e94270edf3e623dc969eā€¦ | 1 |

| lili | 6f14e133cd9350b8e9966a7ā€¦ | 0 |

It is hard to me , can you share the code here?

Itā€™s called a function dispatcher

Yup, I dislike the multiple dispatch syntax in python.

But thatā€™s just me!

Yes, I initially added hashing for the password, and it would be best practice. As I mentioned in the OP, there is no password hashing here, and the main reason (other than simplicity) is the streamlit execution model. The hashing would be done for every script run, and as a password hash is meant to be computationally expensive, this is a problem. You could cache the hash function, but then youā€™d still be storing the original password in the cache lookup.

If itā€™s insecure, itā€™s far better to be explicitly insecure!

You could much more correctly manage this using session state to remember login credentials and not rehash each time, but as mentioned above, the aim was to avoid esoteric streamlit functions including state.

Very important point to make about storage of passwords though, thank you. And to reiterate - this is NOT SECURE and the sqlite database stores all passwords in plaintext, for anyone with access to the server. However, with such access you could steal all the passwords upon entry anyway, so I donā€™t consider that a vulnerability.

@madflier @lt8cn

FYI - Iā€™ve built on this idea and published my work.

Thanks!

1 Like

Awesome, so great!

1 Like

Thatā€™s great news, nice one!

1 Like