mkhorasani / Streamlit-Authenticator

A secure authentication module to validate user credentials in a Streamlit application.
Apache License 2.0
1.37k stars 229 forks source link

Refreshing page does not use cookie to re-authenticate #149

Open yhavin opened 3 months ago

yhavin commented 3 months ago

Hello,

I have a multipage app and am having re-authentication problems when refreshing pages. I have updated to 0.3.2, but this problem occurred before and still remains.

Problem 1: refreshing on same page as authentication occurs

I log in on the home.py page (entry point), and that works fine. I can see the cookie gets stored in the browser devtools. If I refresh the page, I can see the st.session_state start off initially blank for a fraction of a second, then populate with the username, logout, and authentication_status attributes all set to NULL. I can see, however, that the init attribute contains the cookie key-value pair, so at least I can use that. Should the authentication_status and username attributes be NULL? I've been using custom logic that checks if the cookie key-value pair exists inside st.session_state.init, and if it does, to count the user as "logged in", but it seems like extra work. Intuitively, I'd expect the authentication_status and username attributes to use the cookie and have proper values.

Problem 2: refreshing on another page

After logging in on home.py entry point page (and whether I refresh that page or not), if I navigate to another page, say report.py, I can see the authentication_status and username attributes are correctly set. So I can easily use those to determine authentication status for my application. But if I then refresh while on report.py, the session state becomes a completely blank dictionary, not even including the cookie key-value pair. So now, how is my application meant to know if the user is logged in or not? I can see the cookie in the browser devtools, but not in st.session_state.init, as st.session_state is empty.

What is the recommended way to handle authentication status across multiple pages and taking into account reloads? Reloads are actually very prevalent, especially if someone wants to bookmark a specific page of my application. Whenever they open that bookmark, it is akin to a reload, and the st.session_state is empty and my application doesn't know if they're logged in.

Thanks in advance!

mkhorasani commented 3 months ago

Dear @yhavin, if you are using Streamlit-Authenticator with multi-page apps, you will have to recreate the authenticator object on each and every page and invoke the login method as shown below:

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

authenticator.login()

This is to ensure that when a user hard refreshes the page and the session state variables related to re-authentication are lost, the authenticator object is there to re-initialize them from the cookie saved on the browser. Please let me know if this solves your issues and I will close this issue.

yhavin commented 3 months ago

Thank you, that does solve the problem. However, is there a way to make the login widget unrendered? I'd prefer that if the user navigates to a report page and they are not logged in, for them to see a warning with a button that redirects them to the home page which has the login widget. Currently, however, if I'm calling authenticator.login() on each page, it shows the login widget directly on that page. It's not a bug, it's just a UI preference for me.

mkhorasani commented 3 months ago

@yhavin by default the login function will always check to see if there is a valid re-authentication cookie available on the browser, if there is it will log in without rendering the login form. However, in the event that a user refers to a subpage without ever having logged in, perhaps you can use the following code to redirect them to the main login page:

if 'authentication_status' not in st.session_state:
   st.warning('Please log in')
   if st.button("Login"):
       st.switch_page("login.py")
yhavin commented 3 months ago

I have something similar to that already, however there are still issues.

Logically, I need to put the authenticator.login() function before checking if they're logged in (if "authentication_status" not in st.session_state), because authenticator.login() is the thing that re-authenticates if there is a cookie. Otherwise, the logged in check is not logically valid.

When the user is logged in, this is fine, because the login widget doesn't render and the page loads its contents normally.

But if the user is not logged in, then the login widget still shows up on the page because I have called authenticator.login(). For my UI, I don't want it to show up; I just want the warning and switch page suggestion.

So it's a bit of a catch 22. If I check for logged in status before calling authenticator.login(), then it won't re-authenticate using the cookie. And if I check for logged in status after calling authenticator.login(), then when they are not logged in, it shows the login widget, even though this doesn't make UX sense because I am prompting them to switch to the login (home) page.

Hence my question of whether I can make the login widget unrendered. That way, I can use it to check for re-authentication without it actually showing a login widget.

Please let me know if you understand this problem, and if you have a suggestion.

mkhorasani commented 3 months ago

Gotcha! Sure, will try to fix this in the next release. Thank you for bringing it to my attention.

yhavin commented 3 months ago

Thank you, I really appreciate your responsiveness! It gives me peace of mind in relying on this package for my work.

LeeOSO commented 1 month ago

@mkhorasani HI, I have a problem here, I have two separate pages: login, webui. Login is the default page that requires authorization to log in. The code logic is as follows:

authenticator = stauth.Authenticate(
    config['credentials'],
    config['cookie']['name'],
    config['cookie']['key'],
    config['cookie']['expiry_days'],
    config['preauthorized']
name, authentication_status, username = authenticator.login()
if st.session_state["authentication_status"]:
  switch_page("webui")

After the login is successful, enter the webui page, the code is as follows:

 authenticator = stauth.Authenticate(
        config['credentials'],
        config['cookie']['name'],
        config['cookie']['key'],
        config['cookie']['expiry_days'],
        config['preauthorized']
    )
    name, authentication_status, username = authenticator.login()
    print(f'webui->authentication_status={authentication_status}, name={name}, username={username}')
    if not st.session_state["authentication_status"]:
        switch_page("login")

The above code can be successfully jumped to the webui page after the first login. But when I refresh on the webui page, the page was switched to the login page. Obviously, the page is lost the login status if refreshed. Please help me see if I am using it correctly and what is causing the problem.


Another problem was discovered during the test. I successfully logged in as a user and clicked the logout button to switch to the login page. When I refreshed it, it jumped to the webui page again. It means that this situation means that the login status cannot be cleared after logging out.

qiang-yu commented 1 month ago

See my issue https://github.com/mkhorasani/Streamlit-Authenticator/issues/159

Just code two lines would fix this

mkhorasani commented 1 month ago

See my issue #159

Just code two lines would fix this

Please, note that both lines are already implemented in the current version of Streamlit-Authenticator.

LeeOSO commented 1 month ago

See my issue #159 Just code two lines would fix this

Please, note that both lines are already implemented in the current version of Streamlit-Authenticator.

In fact this change I see is already in the current version, but there are still the problems mentioned above.

qiang-yu commented 1 month ago

See my issue #159 Just code two lines would fix this

Please, note that both lines are already implemented in the current version of Streamlit-Authenticator.

In fact this change I see is already in the current version, but there are still the problems mentioned above.

I just follow the document pip install and get 0.3.2 version , which has not implement these code

after fix it , Rresh page works perfect in 0.3.2

So, the current version is not pip install ? but install from github master ?

mkhorasani commented 1 month ago

See my issue #159 Just code two lines would fix this

Please, note that both lines are already implemented in the current version of Streamlit-Authenticator.

In fact this change I see is already in the current version, but there are still the problems mentioned above.

I just follow the document pip install and get 0.3.2 version , which has not implement these code

after fix it , Rresh page works perfect in 0.3.2

So, the current version is not pip install ? but install from github master ?

I just checked v0.3.2 again, and I can guarantee that your fixes are already implemented.

qiang-yu commented 1 month ago

authenticator = stauth.Authenticate( config['credentials'], config['cookie']['name'], config['cookie']['key'], config['cookie']['expiry_days'], config['preauthorized'] ) name, authentication_status, username = authenticator.login()

I Confirm, the code is there

The issue i met is that , i saved authenticator in self.authenticator

self.authenticator = stauth.Authenticate( config['credentials'], config['cookie']['name'], config['cookie']['key'], config['cookie']['expiry_days'], config['preauthorized'] )

on each page, i only call login use the saved variable

on each page, just call self.authenticator.login() name, authentication_status, username = self.authenticator.login()

So, the correct way is , DO Not Save Authenticator For Future Use, Re-Instantiate Authenticator every time, and use this instance to login

I saved the authenticator , and use it in every page, so i need my fix to make it work