Get Active Directory authentification data

Have you configured your app as a SPA on Azure? And do you run it locally or deployed? Have you added your redirect URIs to the white list?

According to Stack Overflow, it could be that you should change your platform type to SPA instead of web service. Does that make sense?

1 Like

The plugin I have made is made for frontend usage. So you get a token for your frontend (which is registered as its own platform/service as a SPA). You can then call some backend of your choice with the token in your authorization header.

Hi everyone,

The past few days I also have been working to figure out my token issues so I feel itā€™s a good time to add a few bits to the discussion. Unfortunately I cannot authoritatively speak to the security of these approaches so I welcome any comments.

@mstaal Thanks for the component! In my case (and I believe OPā€™s case), the frontend part of the login is handled by Azure, but I was still following this thread to figure out how to securely read other claims.

My use case

I assume not everyone here has the same aims. I am using the built-in App Service authentication (ā€œEasyAuthā€) configured to provide single-tenant access, for users in my organization only & restricted by group access.

In that case, the App Service provides the email of the logged in user in the HTTP headers, as mentioned by others already. I wanted to access other claims, particularly the application roles for the user to provide some more fine-grained access control. At the same time, frontend development is not my forte so I did not want to stray far from the built-in authentication module or worry about custom JS components.

The vanishing tokens

My most basic problem was the bug mentioned by @thunderbug1 . Because I am running a container from the Python Docker image, every time I updated something I lost both the tokens from the header and the contents of /.auth/me endpoint! This made it impossible to develop against these services, and for a long time I did not understand why this happened.

The solution is as follows:

  1. Make a dedicated container in a storage account.
  2. Generate a SAS token with read/write/list permissions for the contents of the container.
  3. In the App Service, go to Configuration and add a WEBSITE_AUTH_TOKEN_CONTAINER_SASURL Application Setting with the full SAS URL for the container as the value.
  4. The container now serves as the token store and tokens wonā€™t be deleted anymore.

The token approach

Now that the tokens are fixed, the dict returned by _get_websocket_headers() has an "X-Ms-Token-Aad-Id-Token" and "X-Ms-Token-Aad-Access-Token". As far as I understand, the Access token can be used to gain access to other resources, while the Id token can be used to get info on the user; in practice they both have similar contents. They might differ if custom claims are configured on the Azure side.

The contents of the ID token could be read without validation using PyJWT:

import jwt
from streamlit.web.server.websocket_headers import _get_websocket_headers
token = _get_websocket_headers()["X-Ms-Token-Aad-Id-Token"]
jwt.decode(jwt=token, options={"verify_signature": False})

The contents can also be validated by Azure signature, but the kicker is that the ID token has a 1-hour expiration window and possibly cannot be refreshed, so I am skipping the code here. I tried setting up refresh tokens and calling the /.auth/refresh endpoint from streamlit server-side with no luck, for either the ID or access token.

The /.auth/me approach

As mentioned by @thunderbug1 above, the App Service also provides an /.auth/me endpoint which can be used to directly read the claims present in the tokens, validated (I believe) by the App Service middleware.

@thunderbug1 's method uses a client-side custom JS component which is probably the more ā€œcorrectā€ solution. I made it work in the server Python code using the AppServiceAuthCookie, again from the websocket headers. It works something like this:

import requests
import streamlit as st

host = <my-app-url>
if st.button("`/.auth/me` content"):
    cookies = headers["Cookie"].split("; ")
    for cookie in cookies:
        cookie = cookie.split('=', 1)
        if cookie[0]=="AppServiceAuthSession":
            auth_cookie = {"AppServiceAuthSession": cookie[1]}
    me = requests.get(f"{host}/.auth/me", cookies=auth_cookie)
    st.write(me.json())

Then itā€™s just a matter of picking out values from the JSON response.

The Graph API

Another way is to use the Access token to query the Graph API for user details using the Access token. But since I did not entirely figure out how to refresh the Access token after itā€™s expired, I have not put too much thought into it.

Hi,

I keep getting a load error for the component. Anyonre experiencing the same?

Hey @mstaal,

Thanks a lot for the tool you built, super useful. I am currently trying to set up Azure AD authentication for a streamlit dashboard and I am having the same issue as @Akshay_rao. The pop up login page is displayed and I can authenticate properly. However, nothing happens to the streamlit page and the token is still None. Would you know where this issue comes from?

Thanks a lot!

@sofiaddj

What do you redirect to? Can you share your code?

Hey @asl ,
Thanks for getting back to me.
In my pages/login.py I have:

import streamlit as st
from msal_streamlit_authentication import msal_authentication


login_token = msal_authentication(
    auth={
        "clientId": CLIENT_ID,
        "authority": AUTHORITY,
        "redirectUri": "/login",
        "postLogoutRedirectUri": "/"
    }, # Corresponds to the 'auth' configuration for an MSAL Instance
    cache={
        "cacheLocation": "sessionStorage",
        "storeAuthStateInCookie": False
    }, # Corresponds to the 'cache' configuration for an MSAL Instance
    login_request={
        # "scopes": ["User.ReadBasic.All"]
    }, # Optional
    logout_request={}, # Optional
    login_button_text="Login", # Optional, defaults to "Login"
    logout_button_text="Logout", # Optional, defaults to "Logout"
    key=1 # Optional if only a single instance is needed
)

print(login_token)
st.write("Recevied login token:", login_token)

After I authenticate I end up with this on the login page:

@sofiaddj

What is your full redirectUri?

I actually ended up downloading the code from Github, and modifying it to my need inside my Streamlit project in VS Code. Initially, my redirect uri was just the default Streamlit webserver, http://localhost:8501, however after trying out a few things I managed to get this to work with http://localhost:5173/, which is the address of the node.js webserver.

You can try installing node.js and then run the node.js webserver npm run dev to see if it will redirect properly.

Iā€™m yet to deploy this to Azure, so Iā€™m a bit unsure of how itā€™ll behave in a ā€œreal production environmentā€.

Thanks a lot, that is super helpful!

Did you also run npm run dev on http://localhost:5173/ in parallel?

Would you mind sharing how you modified the code and what is your redirect uri now? Is it http://localhost:5173/ or is there a specific path after it?

On my side I have changed:

 _USE_WEB_DEV_SERVER = os.getenv("USE_WEB_DEV_SERVER", False)

to

_USE_WEB_DEV_SERVER = os.getenv("USE_WEB_DEV_SERVER", True)

changed the redirect URI to http://localhost:5173 and run npm run dev so that the component runs on http://localhost:5173/. However I still donā€™t get any token after authenticatingā€¦

Update: I actually get something in return when doing it your way (keeping _USE_WEB_DEV_SERVER to False and redirecting to http://localhost:5173/ and not running anything with npm ) !
The pop up tries to access http://localhost:5173/#code=some_code but cannot. I guess this is the access token? Do you know how to retrieve it programmatically?

I assume this is the auth code, which is a part of the authorization flow.

Iā€™m running the two servers in parallel, i.e. streamlit run something.py and npm run dev, having _USE_WEB_DEV_SERVER = os.getenv("USE_WEB_DEV_SERVER", True). By only running the Streamlit webserver, my component wonā€™t render for some reasonā€¦

By doing the above, Iā€™m able to retrieve the entire token claim, which is also persisted between pages, etc. However, Iā€™m not sure that itā€™s intended to work like this.

Have you set up a SPA in the App Registration?

Alright,

With setting

_USE_WEB_DEV_SERVER = os.getenv("USE_WEB_DEV_SERVER", False)
_WEB_DEV_SERVER_URL = os.getenv("WEB_DEV_SERVER_URL", "http://localhost:5173")

redirecting to http://localhost:5173/
and running only the Streamlit webserver, the component renders and I get somehow a response (even though I canā€™t make it to be stored in the login_token variable).

I have setup a webapp registration, not a SPA one, do you think that makes a difference?

Thanks again for your help!

@sofiaddj
What does your reponse look like?

And yes, the method used in the libracy (MSAL.js 2.0 - auth code flow) needs the SPA to be setup under the Authorization tab in the App Registration.

@asl
Alright, thanks I will change that.

My response looks like this in the pop up:

@sofiaddj

Did you get it to work? The response youā€™re posting is halfway there. The code parameter is used to obtain the token.

Hey @asl

Yes it ended up working with setting up the SPA, thanks a lot for pointing me to that!
I am still not quite sure how to properly validate the token but I am using the jwt python library for now. Did you manage to get it to work in the end?

@sofiaddj

Good!

I donā€™t think this method is meant for validating tokens - rather itā€™s for allowing users to access restricted resources. The token that the component returns contains both the token itself, but also some other values such as email, name, etc, which in turn can be used for client-sided logic.

And no, Iā€™m not able to get it to run in production. The component renders when I use the development server, but when I try to use the compiled frontend, the component doesnā€™t render. Initially, I thought it was a Microsoft thing, but I tried doing it in Docker on a Linux image, and the same error persists. So now Iā€™m starting to think that thereā€™s a bug somewhereā€¦

Hi everyone, sorry for the unavailability. I am new to this forum, so I may not have detected all my notifications properly. Are people still suffering from problems? As indicated by others inhere (including @asl), you need to register your app for the Authorization Code flow in Azure, if you want to use Azure as your OIDC provider. And you can alter the afore-mentioned WEB_DEV_SERVER_URL variable by setting an env variable. This was meant to be a somewhat simple design. Feel free to ask for changes made to the library on Github by e.g. raising a PR.

And as for validation, @sofiaddj : The library I made is not meant as a means of validating the token. My idea was to keep that to be dealt with however you want to that, because it would anyway depend on your use case. Think of it this way: The Microsoft MSAL library is purely meant for the front end side of things. So when your frontend (be it a React or Streamlit app) retrieves a token, it can pass this token to whatever backend you have (for instance via a REST invocation), and that backend would then have to validate the token itself. Does that make sense?
And are you now able to use the library out of the box?

Hi @mstaal ,

Thanks for your reply. Yes I am able to use the library out of the box now, the problem I had is that I registered my Streamlit application as a web app and not as a Single Page Application. And I have found a way to validate the token. Thanks again for the component, saved me lots of time!!

1 Like

Thatā€™s great to hear, @sofiaddj! And you are welcome. Just glad to know that it is indeed more genericly relevant than just for my own usage.