Get Active Directory authentification data

At least the first version works in reading only the username.
The other one always fails, even if I delete all cookies and user incognito browsing mode.
But I am good for now with only the username!

for the second one to work your web-app will need to be configured with the correct permissions, otherwise you will always get an empty list from the /.auth/me endpoint.

Relying on a HTTP header such as X-Ms-Client-Principal-Name to carry identity information is not secure - they can be faked so, so easily, and in effect impersonate a user.

If you are using Azure Active Directory you can configure your App Registration to include user identification in the Access Token - see Provide optional claims to Azure AD apps - Microsoft Entra | Microsoft Docs - specifically the email claim.

Then, as long as you have a valid token from the issuer you are expecting (i.e. Microsoft and specifically your tenant), you can get the email claim from the access token returned by acquire_token_by_auth_code_flow.

In the code you quote above, acquire_token_for_client doesn’t, I think, get the token you are expecting - this method is for offline daemon apps (e.g. system components) to authenticate, NOT authenticating on behalf of a user. You should be using acquire_token_by_auth_code_flow if you are expecting to authorize on behalf of an authenticated user.

Hi @Kevin_Dixon, thank you very much for your input.
While I also don’t like relying on http headers, it seemed to be the most robust solution for my usecase due to the Microsoft bug mentioned before.

Regarding the fakability of the X-Ms-Client-Principal-Name header, I’m no specialist regarding that but it isn’t set by the client but by the app-service-auth-session wrapper based on the Active Directory login that the user is using. Can you describe how you would fake that?

Hi @thunderbug1 without getting too much into the dark arts, its very simple with browser development tools or indeed cli tools as simple as curl to modify headers. With token in hand for a user, it would be a simple deal to use that token but modify the header and thus pretend to be another user (all this outside of Streamlit).

Not tried it, so you never know I might be wrong. But… you have a secure and verifiable token, so why not use that to carry your identity information.

yeah, I think just setting it in the client request wouldn’t work. This is what the documentation says about it:

Access user claims in app code

For all language frameworks, App Service makes the claims in the incoming token (whether from an authenticated end user or a client application) available to your code by injecting them into the request headers. External requests aren’t allowed to set these headers, so they are present only if set by App Service. Some example headers include:

X-MS-CLIENT-PRINCIPAL-NAME
X-MS-CLIENT-PRINCIPAL-ID

Code that is written in any language or framework can get the information that it needs from these headers.

So I am still feeling good about the solution with the http header.

Did anyone find a sustainable solution for this?

Check the second to last item in the announcement of Streamlit 1.14.0.

Hey Goyo - thanks for your prompt reply. However, I can’t really figure out, how this helps me authentication the end user with Microsoft Active Directory? Can you be a little me specific?

Thanks,
Mikkel

IIUC you can find the required data in the request headers, and that has been addressed (to a certain extent) in Streamlit 1.14.

There are other issues discussed in the thread but they are not directly related to Streamlit.

Hi Mikkel,

If you just want something basic like the logged in user email this should be enough (in version 1.14+):

from streamlit.web.server.websocket_headers import _get_websocket_headers

headers = _get_websocket_headers()

if "X-Ms-Client-Principal-Name" in headers:
    user_email = headers["X-Ms-Client-Principal-Name"]

st.write(headers) # have a look at what else is in the dict

I have not yet figured out how to access app roles and such though. I assume you would have to delve into the Azure AD / MSAL documentation, maybe someone else here is up to speed.

I know I am a little late to the party, but since I wanted an authentication component for Streamlit myself (and could not immediately find something that did what I wanted), I ended up making one myself:

  1. GitHub - mstaal/msal_streamlit_authentication: MSAL Streamlit Component
  2. msal-streamlit-authentication · PyPI

It’s based on Microsoft’s own MSAL.JS library, and it can be configured like its Javascript equivalent from the Python side. In particular, it supports Azure AD (as well as the Open ID Connect protocol in general). Once a user is logged in, the component returns the entire login token to Streamlit, allowing users to use whatever pieces (like the access token) if one wants to do this. I guess it can also be used to manipulate the Streamlit session.

I have updated the Readme of the above mentioned repo so that it is clearer how it is used.

Thanks for this one, but I have trouble getting a token from this. Upon clicking the Login button, it opens a new tab and authenticates, but the streamlit page doesn’t get updated to show the token. Am I doing something wrong with the redirect URI? or it should be set to '' as it is.

As illustrated in the README on Github, the token will (once it is set after a valid login) live in the dictionary corresponding to the python component object. You can then use the contents of the the token however you want (for instance to display your username or to invoke an API that requries an Access token).

Do you know what could be the reason for getting an error AADSTS9002326: Cross-origin token redemption is permitted only for the ‘Single-Page Application’ client-type. Request origin: when trying to login?

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?