Streamlit Next.js Component, Auth0 Authentication, Bi-Directional Messaging & Serverless APIs

We can connect w/c 5 July. If you look at the Auth0 ā€œconnectionā€ setup docs youā€™ll see that configuring social logins is just a matter of clicking a checkbox in the connections management portal, and then the different options will be added to the Auth0 login box automatically.

Thank you for your initiative @asehmi. Streamlit is a great framework but is sorely lacking out of the box auth functionality for certain types of projects.

I have tried running your project locally but noticed that the sessionInfo data written in the Indexed Database pertaining to the auth popup window is not being added to the Indexed Database in the main tab (inspector shows the Indexed DB as empty and token data is never picked up in getSessionInfo). Am I missing something?

Hi @keith,

When the Auth0 login/logout completes, a redirect to index.js is performed. Here getServerSideProps() obtains the context session object and passes it to Auth.js which stores (on login) or deletes it (on logout) in local storage. index.js/Auth.js is just a mini-app which runs in a pop-up window on the same domain & port as the Streamlit component (localhost:3001). The component runs in the Streamlit page and its main responsibility is simply to monitor for local storage session object changes and notify the Streamlit client/host ā€“ see the one or two second heartbeat which looks for these changes (note, itā€™s allowed to do so only because it runs on the same domain + port as index.js). When the Streamlit client receives a token update, it stores it in session_state (not local storage, as I donā€™t think Streamlit has access to this anyway, and also it runs on a different port [I use 4010, I think], so canā€™t see local storage on port 3001). My published code uses the session state hack, not the latest st.session_state.

In streamlit.tsx > getSessionInfo(), you may need to de-serialize the stored session string, if and only if, its stored by Auth.js as a string.

Hereā€™s what I have in Auth.js:

  useEffect(async () => {
        if (sessionInfo.user?.email) {
            console.log('AuthApp (set session info: user, token, expiry)')
            await STORAGE.setItem('sessionInfo', JSON.stringify(sessionInfo))
        } else {
            console.log('AuthApp (remove session info: user, token, expiry)')
            await STORAGE.removeItem('sessionInfo')
        }
  }, [sessionInfo])

Correspondingly in streamlit.tsx:

  const getSessionInfo = async () => {
    const sessionInfoStr = await STORAGE.getItem('sessionInfo')
    if (sessionInfoStr) {
      const sessionInfo = JSON.parse(String(sessionInfoStr))
      const me = sessionInfo.user
      if (me) {
        const name = me.name || `${me.given_name} ${me.family_name}` || me.nickname || me.email
        const email = me.email || me.sub
        const user = { user: me, name: name, email: email }
        const token = sessionInfo.token // {value: accessToken, value_id_token: idToken, expiry: tokenExpiresAt}
        console.log('STC getSessionInfo (user): ' + JSON.stringify(user))
        console.log('STC getSessionInfo (token): ' + JSON.stringify(token))
        return {user: user, token: token}
      }
    }
    console.log('STC getSessionInfo (NULL)')
    return {user: null, token: null}
  }

streamlit.tsx > listenForAuthChangeAndNotifyHost() has not changed.

Hope that helps.

P.S. Iā€™m writing a short series of 2-3 articles for the Auth0 Developer Blog in which Iā€™ll be introducing Streamlit and components, and then implementing Auth0 authentication. For this, I wrote a slightly newer version of the example, which uses st.session_state.

After further investigation I found that it works fine on Chrome but it seems that Safari does not support the usage of an Indexed Database when embedded in an iframe. (Using local storage instead does seem to work but I have yet to find a way how to access this value from within the heartbeat.)

Login/Logout text is now being updated as expected in the UI but am now getting a AttributeError("ā€˜strā€™ object has no attribute ā€˜getā€™") from component_event_producer() or more specifically component_host(key=key, **props) or {}. Any pointers as to where I might look to fix this? Below is the last debug message that was logged.

streamlit.script_runner.RerunException: RerunData(query_string='', widget_states=widgets {
  id: "$$GENERATED_WIDGET_KEY-4884191662795434989"
  bool_value: false
}
widgets {
  id: "login"
  json_value: "{\"name\":\"onStatusUpdate\",\"data\":{\"hostname\":\"None\",\"message\":\"Logged in\",\"isError\":false,\"error\":null,\"sessioninfo\":{\"user\":{\"user\":{\"nickname\":\"username\",\"name\":\"user@hotmail.com\",\"picture\":\"https://s.gravatar.com/avatar/some_pic.png\",\"updated_at\":\"2021-07-19T22:03:29.094Z\",\"email\":\"user@hotmail.com\",\"email_verified\":true,\"sub\":\"auth0|60e45bf6011d8d006a059cca\"},\"name\":\"user@hotmail.com\",\"email\":\"user@hotmail.com\"},\"token\":{\"value\":\"TOKEN_HERE\",\"value_id_token\":\"TOKENHERE\",\"expiry\":1626818609}}}}"
}
)

(On a sidenote I had trouble finding a working ptvsd package for python 3.7 and settled for ptvsd==5.0.0a12. I also needed to perform some exception handling on ptvsd.enable_attach(address=('localhost', 6790)). This was failing when attempting to enable_attach() more than once for the same process).

Looking forward to your Auth0 articles.

Glad youā€™re making progress. I hadnā€™t tried it on Safari, so good catch! Presumably you adjusted the driver used by localforage in storage.js.

I think the code that comes after component_host(key=key, **props) or {} breaks if the returned event isnā€™t always a dict. I had the same issue so implemented some belt and braces. Please, try these lines instead of that single line:

    event_ = component_host(key=key, **props)

    event = {}
    if isinstance(event_, str):
        event = json.loads(event_)
    elif isinstance(event_, dict):
        event = event_

Below updated by author

Regarding the issue with ptvsd, I found that debugpy has replaced it.

Essentially, follow these steps:

  1. pip install debugpy
  2. Add the following snippet in your <your-app_name>.py file.
try:
    import debugpy
    debugpy.listen(("localhost", 5678))
    # debugpy.wait_for_client() # Only include this line if you always want to manually attach the debugger
except:
    # Ignore... for Heroku deployments!
    pass
  1. Then start your Streamlit app

streamlit run <your-app_name>.py

  1. From the VS Code Debug sidebar menu configure Python: Remote Attach debug server and update your launch.json file with the details below.
{
    "name": "Python: Remote Attach",
    "type": "python",
    "request": "attach",
    "port": 5678,
    "host": "localhost",
    "justMyCode": true,
    "redirectOutput": true,
    "pathMappings": [
        {
            "localRoot": "${workspaceFolder}",
            "remoteRoot": "."
        }
    ]
}
  1. Make sure you manually insert the redirectOutput setting.
  2. By default you will be debugging your own code only. If you want to debug into streamlit code, then change justMyCode setting from true to false.
  3. Finally, attach the debugger by clicking the debugger play button.

Donā€™t know where youā€™re based (Iā€™m in UK), but would be great to have a web conf call to see how youā€™re using this?

Cheers,
Arvindra

1 Like

This is very nice, I will definitely start using this in my next app, as good quality and easy to use authentication libs are rare, thank you for this nice work.

1 Like

That seems to have fixed it, thanks.

Iā€™m based in Europe, just reached out via LinkedIn.

1 Like

Iā€™ve edited the solution to include info on replacing ptvsd.

2 Likes

Great work thumbs up!, I am too exploring on user authentication. Is this only able to work in localhost?

@Tan_Yu_Da_Benjamin No, this works as a deployed service too. You just need to configure the urls/paths for your hosted domains. See this app running in Heroku where I found it easier to deploy the app and the frontend as separate services: amwtmu-demo

2 Likes

Hey folks,

Iā€™ve just built a synchronous way to access localStorage from Streamlit using websockets (streamlit-ws-localstorage), feel free to try it out.

I struggled for a couple of days with localStorage access (and authentication), and thought it would be easier to build a websocket based synchronous communication itself. The code is simple, just import the module and use it like this:

import streamlit as st
from streamlit_ws_localstorage import injectWebsocketCode, getOrCreateUID

# Main call to the api, returns a communication object
conn = injectWebsocketCode(hostPort='linode.liquidco.in', uid=getOrCreateUID())

st.write('setting into localStorage')
ret = conn.setLocalStorageVal(key='k1', val='v1')
st.write('ret: ' + ret)

st.write('getting from localStorage')
ret = conn.getLocalStorageVal(key='k1')
st.write('ret: ' + ret)

Here is a demo of fetching saved info in the browser:

Installation: pip install streamlit-ws-localstorage
Repository: GitHub - gagangoku/streamlit-ws-localstorage: A simple synchronous way of accessing localStorage from your Streamlit app.
On pypi: streamlit-ws-localstorage Ā· PyPI

@gagangoku Nice. You should probably make this a separate (new) post and register it as a component.

1 Like

Thansk @asehmi , will do.

Is there support for accessing user identity within the App? FYI: OAuth2 Authentication Support out of the box Ā· Issue #5840 Ā· streamlit/streamlit Ā· GitHub

Iā€™m using the Auth0 Next.js SDK (Auth0 Next.js SDK Quickstarts: Login) and you can configure multiple different auth protocols and identity providers in the Auth0 management portal independently of the login box that gets shown in this Streamlit Auth example.

The implementation is fully documented here if youā€™d like to customize it (and, FYI the article and implementation passed review and testing by Auth0ā€™s own editorial team and developers, so Iā€™m pretty certain it meets their best practice).

I understand that probably the right solution is to have the component returning to streamlit fields from the OIDC identity token. Two questions:

  • Does Auth0 SDK support Okta?
  • Do you also need a running NextJS to serve the component?a

Well Auth0 is Okta now! :wink:

Auth0 supports all current standards and out-of-the-box social and enterprise IdPs for login, including whatever Okta uses, assuming thatā€™s your IdP.

My implementation uses Next.js, as I think the Auth0 Next.js SDK and Next.js are easier to use than React Native, etc. I also deliberately wanted to use an enterprise grade web stack framework for the authentication server, which is separated from the Streamlit Componentā€¦ so that they can be deployed independently in any cloud. (Your milage may vary.)

Other members of this forum have implemented Auth0 components too, so worth taking a look at those.

Thnks,

do you need an Auth0 account to use the SDK?

Ed

Yes. Check out the pricing page, you get a big bucket for free, indefinitely. And if youā€™re a startup, I think thereā€™s a huge bucket based on a revenue limit.

Yes so thatā€™s not an option, we donā€™t want to onboard another vendor. Weā€™ll look if there are alternatives using the Okta SDK in the discussion forum, thanks for helping anyways!