Streamlit-Authenticator icon indicating copy to clipboard operation
Streamlit-Authenticator copied to clipboard

Cannot instantiate authenticator object in multiple pages: DuplicatedWidgetId

Open mattiatantardini opened this issue 1 year ago • 3 comments

I'm following the documentation at steps 1 and 2, which states that one needs to recreate the Authenticator object and call the login method on each page of a multipage app.

I'm testing the functionality but cannot recreate the object in a different page. The exception is the following:

DuplicateWidgetID: There are multiple widgets with the same key='init'.

To fix this, please make sure that the key argument is unique for each widget you create.

Traceback:

File "/home/mattia/develop/caritas/zaccheo/zaccheo-ui/app.py", line 46, in <module>
    anagrafica_page()
File "/home/mattia/develop/caritas/zaccheo/zaccheo-ui/app/pages/anagrafica.py", line 13, in anagrafica_page
    authenticator = stauth.Authenticate(
                    ^^^^^^^^^^^^^^^^^^^^
File "/home/mattia/venvs/venv-zaccheo-ui/lib/python3.11/site-packages/streamlit_authenticator/authenticate/__init__.py", line 53, in __init__
    self.cookie_handler             =   CookieHandler(cookie_name,
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mattia/venvs/venv-zaccheo-ui/lib/python3.11/site-packages/streamlit_authenticator/authenticate/cookie/__init__.py", line 39, in __init__
    self.cookie_manager         =   stx.CookieManager()
                                    ^^^^^^^^^^^^^^^^^^^
File "/home/mattia/venvs/venv-zaccheo-ui/lib/python3.11/site-packages/extra_streamlit_components/CookieManager/__init__.py", line 22, in __init__
    self.cookies = self.cookie_manager(method="getAll", key=key, default={})
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mattia/venvs/venv-zaccheo-ui/lib/python3.11/site-packages/streamlit_option_menu/streamlit_callback.py", line 20, in wrapper_register_widget
    return register_widget(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

I looked at the source code and it seems to me that there is no way to pass an explicit key to the CookieManager instantiated within the Authenticate object, which results for the CookieManager to always use the default init key.

My code follows.

app.py

import yaml
from yaml.loader import SafeLoader

import streamlit as st
import streamlit_authenticator as stauth
from streamlit_option_menu import option_menu

from app.pages.anagrafica import anagrafica_page


if __name__ == "__main__":

    with st.sidebar:
        st.title("My app")

    with open('./settings/users.yaml') as file:
        config = yaml.load(file, Loader=SafeLoader)

    authenticator = stauth.Authenticate(
        config['credentials'],
        config['cookie']['name'],
        config['cookie']['key'],
        config['cookie']['expiry_days'],
        config['pre-authorized']
    )

    authenticator.login()

    if st.session_state["authentication_status"]:
        with st.sidebar:
            # authenticator.logout()  
            st.write(f'Welcome *{st.session_state["name"]}*')

            page = option_menu(
                menu_title="Menù",
                options=["Anagrafica", "Tessere", "Scontrini", "Prodotti"],
                icons=["people-fill", "card-text", "receipt", "tag-fill"],
            )

        if page == "Anagrafica":
            anagrafica_page()

    elif st.session_state["authentication_status"] is False:
        st.error('Username/password is incorrect')
    elif st.session_state["authentication_status"] is None:
        st.warning('Please enter your username and password')

app.pages.anagrafica.py

import yaml
from yaml.loader import SafeLoader

import streamlit as st
import streamlit_authenticator as stauth


def anagrafica_page():

    with open('./settings/users.yaml') as file:
        config = yaml.load(file, Loader=SafeLoader)

    authenticator = stauth.Authenticate(
        config['credentials'],
        config['cookie']['name'],
        config['cookie']['key'],
        config['cookie']['expiry_days'],
        config['pre-authorized'],
        # key="anagrafica-auth"
    )

    authenticator.login()

    st.title("Anagrafica")

Environment:

  • python 3.11
  • streamlit 1.33.0
  • extra-streamlit-components 0.1.71
  • streamlit-option-menu 0.3.12

Is there some sort of bug or am I missing something?

mattiatantardini avatar May 02 '24 18:05 mattiatantardini

I think that it might be the path you're in. It could also be that there might be an init file where there shouldn't be.

AngelicSage avatar Jun 06 '24 17:06 AngelicSage

Same problem here

anderson-klabin avatar Jun 10 '24 13:06 anderson-klabin

Dear all, this issue will be resolved in the next release.

mkhorasani avatar Jun 10 '24 13:06 mkhorasani

Same problem here

lkdhy avatar Jul 09 '24 16:07 lkdhy

same problem, is there a fix?

garima-sage avatar Jul 22 '24 19:07 garima-sage

On the way!

mkhorasani avatar Jul 23 '24 07:07 mkhorasani

Same problem here

alibabadoufu avatar Jul 23 '24 12:07 alibabadoufu

I'm not sure if this will help anyone or not, but in my app, I created a dedicated page for login, home page after login, etc... I only declared the streamlit authenticator object in the subpages and not in the "entrypoint" file (in this case, app.py). I did have authenticator.login() on any page that was used after the login in case of session refresh in addition to the additional login page. I mean to say if there are any other pages that are used before the user has logged in, the line authenticator.login() was not needed.

BDesai-12 avatar Jul 24 '24 02:07 BDesai-12

Same problem here

joost-vanlawick avatar Jul 27 '24 12:07 joost-vanlawick

Fixed! Please refer to v0.3.3.

mkhorasani avatar Jul 27 '24 14:07 mkhorasani

I am still having this issue on v0.3.3. I have an authenticated decorator which creates an Authenticate object but still gives this error upon instantiation.

serkankalay avatar Sep 15 '24 21:09 serkankalay

Actually with the exact same code, it still gives the exact same error. In v0.3.3 @mkhorasani you introduced key but only to the widgets. However, the error is raised while instantiating the Authenticate class and the actual line where the duplicate widget issue arises is: return register_widget_from_metadata(metadata, ctx, widget_func_name, element_type) And when traced back, this is where it happens:

class CookieManager:
    def __init__(self, key="init"):
        self.cookie_manager = _component_func
        self.cookies = self.cookie_manager(method="getAll", key=key, default={})
    ....

and where you instantiate the CookieManager class, you do not accept a key

class CookieModel:
    """
    This class executes the logic for the cookies for password-less re-authentication, 
    including deleting, getting, and setting the cookie.
    """
    def __init__(self, cookie_name: str, cookie_key: str, cookie_expiry_days: float):
        ...
        self.cookie_manager         =   stx.CookieManager()
        ...

Hence, I think the bug problem is still there.

serkankalay avatar Sep 16 '24 07:09 serkankalay

Actually with the exact same code, it still gives the exact same error. In v0.3.3 @mkhorasani you introduced key but only to the widgets. However, the error is raised while instantiating the Authenticate class and the actual line where the duplicate widget issue arises is: return register_widget_from_metadata(metadata, ctx, widget_func_name, element_type) And when traced back, this is where it happens:

class CookieManager:
    def __init__(self, key="init"):
        self.cookie_manager = _component_func
        self.cookies = self.cookie_manager(method="getAll", key=key, default={})
    ....

and where you instantiate the CookieManager class, you do not accept a key

class CookieModel:
    """
    This class executes the logic for the cookies for password-less re-authentication, 
    including deleting, getting, and setting the cookie.
    """
    def __init__(self, cookie_name: str, cookie_key: str, cookie_expiry_days: float):
        ...
        self.cookie_manager         =   stx.CookieManager()
        ...

Hence, I think the bug problem is still there.

Hi @serkankalay instead of creating multiple Authenticate objects, can you please create it only once, save it to session state, and access it from the session state where ever you need to.

mkhorasani avatar Sep 16 '24 08:09 mkhorasani

Hi @mkhorasani. That's actually what I did, and it works perfectly fine. For others maybe to make use of it:

  • I have a decorator that creates and Authenticate if it doesn't exist in the st.session_state
def authenticated(func: Callable) -> Callable:
    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        if (
            "authentication_status" not in st.session_state
            or "my_auth_key" not in st.session_state
        ):
            my_auth_key = str(uuid.uuid4())
            st.session_state["my_auth_key"] = my_auth_key
        else:
            my_auth_key = st.session_state["my_auth_key"]

        name, authentication_status, username = _get_authenticator(
            my_auth_key
        ).login("main")
        if authentication_status is False:
            st.error("Username/password is incorrect")
        elif authentication_status is None:
            st.warning("Please enter your username and password")
        elif authentication_status:
            return func()

    return wrapper
  • I have another file that is not interacting with any streamlit component, where the _get_authenticator is defined:
from __future__ import annotations

import yaml
from streamlit_authenticator import Authenticate
from yaml.loader import SafeLoader

with open("./authentication_config.yaml") as file:
    _CONFIG = yaml.load(file, Loader=SafeLoader)

_AUTHENTICATOR_MAPPING: dict[str, Authenticate] = {}


def _make_authenticator(auth_key: str) -> None:
    if auth_key not in _AUTHENTICATOR_MAPPING:
        _AUTHENTICATOR_MAPPING[auth_key] = Authenticate(
            _CONFIG["credentials"],
            _CONFIG["cookie"]["name"],
            _CONFIG["cookie"]["key"],
            _CONFIG["cookie"]["expiry_days"],
            _CONFIG["preauthorized"],
        )


def _get_authenticator(auth_key: str) -> Authenticate:
    _make_authenticator(auth_key)
    return _AUTHENTICATOR_MAPPING[auth_key]

  • And on every page, I encapsulate all the page building in a function and decorate it with it
@authenticated
def _build() -> None:
    st.header("Hello world")
    # More awesome things built here

Hope this helps someone.

serkankalay avatar Sep 16 '24 10:09 serkankalay

Hello @serkankalay,

I am a new Streamlit user and have been struggling with an issue. My app is a multipage application, and when I try to implement your strategy, My application consists of three forms, and each user's dropdowns should be populated with different data. However, what's happening is that the first user's session instance is affecting all other users' forms, it creates a single-user session instance that is shared across the server. As a result, the details of the first user who logs in are distributed to all other users, even though they have their own instances of the app and regardless of them logging in with their credentials.

This is my separate file auth.py, where I am loading credentials from a Google Sheet. I am also defining the decorator here and calling it in the root app entry file.

import streamlit as st
from streamlit_authenticator import Authenticate
from streamlit_gsheets import GSheetsConnection
from typing import Callable, Any
from functools import wraps

conn = st.connection("gsheets", type=GSheetsConnection)
users_df = conn.read(worksheet="Users")
users = users_df.to_dict("records")

credentials = {"usernames": {}}
for user in users:
    credentials["usernames"][user["username"]] = {
        "fullname": user["name"],
        "name": user["username"],
        "email": user["email"],
        "password": user["password"],
        "role": user["role"],
        "Territory_ID": user["Territory_ID"],
    }
authenticator_mapping = {}

# AUTH_SECRET_KEY is stored in secrets.toml
def create_authenticator(AUTH_SECRET_KEY: str) -> None:
    if AUTH_SECRET_KEY not in authenticator_mapping:
        authenticator_mapping[AUTH_SECRET_KEY] = Authenticate(
            credentials=credentials,
            cookie_name="my_app_cookie",
            cookie_key=AUTH_SECRET_KEY,
            cookie_expiry_days=7,
            preauthorized=None,
        )
def get_authenticator(AUTH_SECRET_KEY: str) -> Authenticate:
    create_authenticator(AUTH_SECRET_KEY)
    return authenticator_mapping[AUTH_SECRET_KEY]


# Decorator
def authenticated(func: Callable) -> Callable:
    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        if "AUTH_SECRET_KEY" not in st.session_state:
            st.session_state["AUTH_SECRET_KEY"] = AUTH_SECRET_KEY
        AUTH_SECRET_KEY = st.session_state["AUTH_SECRET_KEY"]

        authenticator = get_authenticator(AUTH_SECRET_KEY)

        login_result = authenticator.login(location="main")

        if login_result is None:
            st.warning("Please enter your username and password.")
            return None
        auth_status = login_result

        if auth_status:
            return func(*args, **kwargs)
        elif auth_status is False:
            st.error("Invalid username or password")
        else:
            st.warning("Please log in to continue")

    return wrapper

Any guidance on how to resolve this would be greatly appreciated.

Thank you!

benson-nderitu avatar Nov 10 '24 06:11 benson-nderitu