Google sign in with landing page works fine - issue on refresh - session lost and GetAccessTokenError: {'error': 'invalid_grant', 'error_description': 'Bad Request'}

Summary

I have built a google sign in, as a must when anyone access the landing page - the code is incorporated in the landing page - it works fine - logs to google - comes back with user details and runs the functionality right - however - when I refresh the page I get - GetAccessTokenError: {‘error’: ‘invalid_grant’, ‘error_description’: ‘Bad Request’}

Steps to reproduce

Code snippet:

#imports here
import streamlit as st
import asyncio
from httpx_oauth.clients.google import GoogleOAuth2

# OAuth 2.0 Clients and secrets details
CLIENT_ID = "XXXX"
CLIENT_SECRET = "XXXXXX"
REDIRECT_URI = "http://localhost:8501"

async def get_authorization_url(client: GoogleOAuth2, redirect_uri: str):
    authorization_url = await client.get_authorization_url(
        redirect_uri, scope=["profile", "email"]
    )
    return authorization_url

async def get_access_token(client: GoogleOAuth2, redirect_uri: str, code: str):
    token = await client.get_access_token(code, redirect_uri)
    return token

async def get_email(client: GoogleOAuth2, token: str):
    user_id, user_email = await client.get_id_email(token)
    return user_id, user_email

client: GoogleOAuth2 = GoogleOAuth2(CLIENT_ID, CLIENT_SECRET)

# Check if user session exists
if "user_id" in st.session_state and "user_email" in st.session_state:
    user_id = st.session_state.user_id
    user_email = st.session_state.user_email
else:
    user_id = None

# Check if access token is available in session state
if st.session_state.get("access_token", None) is None:
    try:
        # Get the code from the URL
        code = st.experimental_get_query_params().get("code")
        if code is None:
            raise KeyError
        # Get the access token
        token = asyncio.run(get_access_token(client, REDIRECT_URI, code[0]))
        st.session_state.access_token = token["access_token"]    # Store access_token in session state
        user_id, user_email = asyncio.run(get_email(client, token["access_token"]))
        st.session_state.user_id = user_id
        st.session_state.user_email = user_email
        st.write(f"You're logged in as {user_email} and your id is {user_id}")

        # Clear URL parameters
        st.markdown(
            """
            <script>
            const url = new URL(window.location);
            url.searchParams.delete("code");
            window.history.replaceState({}, '', url);
            </script>
            """,
            unsafe_allow_html=True,
        )
    except KeyError:
        if user_id is None:
            authorization_url = asyncio.run(
                get_authorization_url(client, REDIRECT_URI)
            )
            st.sidebar.markdown(
                f'<a target="_self" href="{authorization_url}">Login with Google</a>',
                unsafe_allow_html=True,
            )
else:
    user_id, user_email = asyncio.run(get_email(client, st.session_state.access_token))
    st.write(f"You're logged in as {user_email} and your id is {user_id}")

if user_id is None:
    st.warning("Please log in to continue.")
    st.stop()
# Rest of the code here that forms the landing page

Explain what you expect to happen when you run the code above.

  • I would like refresh to maintain the session and open the landing page clear of any test inputs

Explain the undesired behavior or error you see when you run the code above.

GetAccessTokenError: {‘error’: ‘invalid_grant’, ‘error_description’: ‘Bad Request’}

Traceback:

File "C:\Users\bkant\AppData\Local\Programs\Python\Python39\lib\site-packages\streamlit\runtime\scriptrunner\script_runner.py", line 552, in _run_script
    exec(code, module.__dict__)File "D:\AVA\SID-Project-Py\landing.py", line 54, in <module>
    token = asyncio.run(get_access_token(client, REDIRECT_URI, code[0]))File "C:\Users\bkant\AppData\Local\Programs\Python\Python39\lib\asyncio\runners.py", line 44, in run
    return loop.run_until_complete(main)File "C:\Users\bkant\AppData\Local\Programs\Python\Python39\lib\asyncio\base_events.py", line 647, in run_until_complete
    return future.result()File "D:\AVA\SID-Project-Py\landing.py", line 27, in get_access_token
    token = await client.get_access_token(code, redirect_uri)File "C:\Users\bkant\AppData\Local\Programs\Python\Python39\lib\site-packages\httpx_oauth\oauth2.py", line 148, in get_access_token
    raise GetAccessTokenError(data)

Debug info

  • Streamlit version: Streamlit, version 1.24.1
  • Python version: Python 3.9.13
  • Using Conda? PipEnv? PyEnv? Pex? - PIP
  • OS version: Windows 11
  • Browser version: Chrome Version 114.0.5735.199 (Official Build) (64-bit)

Hey @Brijesh_Srikanth1,

Thanks for sharing this question!

You mentioned that you want the session to be maintained across refreshes of the browser. Unfortunately, refreshing your browser ends your session and creates a new one, so st.session_state will be refreshed.

I think what most folks do in this scenario is keep a token as a parameter in the URL, so that when a user refreshes the browser, you can then grab that same token from the URL and establish that they’ve already authenticated with Google (relevant thread). I implemented some similar functionality a few months ago and that’s what I ended up doing to keep users logged in across browser refreshes.