Adding Azure Active Directory sign in/sign out using MSAL Python (handling redirects)

Hi,

Has anyone managed to succesfully implement the Azure Active Directory authentication to your Streamlit app? More specifically, I’m trying to implement the initiate_auth_code_flow() method from the MSAL library to start a user-driven authentication process (as-per Microsofts’ recommendation). I’ve made an App Registratoin in Azure, added a redirect URI (as localhost:8501 for dev. purposes), and added SPA as a platform under Authentication.

Currently, I’m able to use the library by hand (in the backend), but I’m struggling a lot with handling it using the Streamlit frontend.

The issue arrises when clikcing a button that triggers the login(). This correctly imposes the initiate_auth_code_flow() method, which returns a flow. If the auth_uri exists in the flow dict, the user is redirected to Microsofts sign in page. Upon succesful sign in, the user is redirected back to the localhost:8501 with the auth code as a query parameter. However, I don’t know how to pick up this parameter and use it in the acquire_token_by_authorization_code() while maintaining the session, because the auth_uri is generated in one tab, but then auth code parameter is returned upon redirect in a new tab, which ultimately creates a new session/state, thus I get a mismatch.

How do I manage this mismatch? Any ideas?

One idea could be to create a custom endpoint using FastAPI/Flask/etc so receive the parameters, and then login the user. But is there an easier/more Streamlit-native way?

import streamlit as st
import msal
import webbrowser
import requests



client_id = "xxxx"
tenant_id = "yyy"
redirect_uri = "http://localhost:8501/"
scopes = ["https://graph.microsoft.com/.default"]
authority = f"https://login.microsoftonline.com/{tenant_id}"
endpoint = "https://graph.microsoft.com/v1.0/me"

app = msal.PublicClientApplication(
    client_id, authority=authority, verify=False
)

def get_token_from_cache():
    accounts = app.get_accounts()
    if not accounts:
        return None
    
    result = app.acquire_token_silent(scopes, account=accounts[0])
    if "access_token" in result:
        return result["access_token"]
    else:
        return None

def login():
    flow = app.initiate_auth_code_flow(scopes=scopes, state=['somestupidstate'])
    
    if "auth_uri" not in flow:
        return st.write("Failed with token")
    
    auth_uri = flow['auth_uri']
    webbrowser.open(auth_uri, new=0)
    auth_code = st.experimental_get_query_params()
    
    if 'code' not in auth_code:
        return st.write("Failed with token")
    
    result = app.acquire_token_by_authorization_code(auth_code, scopes=scopes)
    if "access_token" in result:
        return result["access_token"]
    else:
        return st.write("No token found")


if st.button("Login"):
    get_token_from_cache()
    token = login()

    st.write(st.experimental_get_query_params())
    if token:
        st.write("Logged in successfully!")
        st.write(token)
    else:
        st.write("Failed to login")

Okay, so I managed to solve this myself, partly thanks to altering my code, and partly thanks to Stackoverflow.

I opted for Selenium for handling my browser object, so my login() method looks like the one below. Furthermore, according to this post, the documentation on MSAL is a bit confusing. Especially regarding what platform to add in the App Registration. Using Selenium might be redundant however, since I added that before changing the platform settings.


def login():
    flow = app.initiate_auth_code_flow(
        scopes=scopes)

    if "auth_uri" not in flow:
        return st.write("Failed with token")

    auth_uri = flow["auth_uri"]

    browser = webdriver.Chrome()
    browser.get(auth_uri)

    WebDriverWait(browser, 200).until(
        EC.url_contains(redirect_uri))
    
    redirected_url = browser.current_url
    url = urllib.parse.urlparse(redirected_url)
    # parse the query string to get a dictionary of {key: value}

    query_params = dict(urllib.parse.parse_qsl(url.query))
    

    #code = query_params.get('code')[0]
    
    result = app.acquire_token_by_auth_code_flow(flow,query_params, scopes=scopes)
    
    browser.quit()
    return result
1 Like

I recently made this Streamlit Component that builds on top of Microsofts own MSAL.JS library: msal-streamlit-authentication: (PyPI). You can find the Github source code via the link if you want.
I have at least managed to get login working using a popup instead of redirect, but I have not testet if it also works with redirects.

1 Like

Hi, I give your component a try but I cannot get the login token back. I tried the snippet in the readme and the pop-up windows seems to work ok (It pops up and redirects to the redirect uri without error and closes itself) but login token is always None.

Have you registered your redirect URLs in your login provider? We had a similar discussion here: Get Active Directory authentification data

Thanks for the response! I will have a look at the other thread.

And yes I did, I’m developing on a local machine so it is redirected to localhost url for streamlit app.

Does the redirect uri has to be the same as the one that starts the login process?

In my own experience, if you develop on a local machine, you will have to add e.g. ‘http://localhost:1234’ as an accepted redirect URL to your login provider if your local streamlit app runs on port 1234 and on HTTP. You will of course have to adjust the exact port and protocol for your needs. The thread in question covers how to do this in the Azure portal, if your app uses Azure AD, but similar approaches should be possible via other OIDC providers. Which provider do you use? Please let me know how it ends.
I will try to see if I can update the component so that it does not fail silently but rather extends whatever error message the auth provider sends you. I think (at least) that would allow for more transparency and troubleshooting.

1 Like

And do not hesitate to give the project a star on Github if you deem it worthy of this. :stuck_out_tongue:

1 Like

I just got it sorted. I was using the wrong platform setup for Azure AD. It works as expected as soon as I change the platform type to SPA. Thanks for your help!

2 Likes

You are welcome - sounds like a similar issue also described in the other thread. Great that it works! Feel free to share feedback if you want, it is still in an early stage.

I think you can make it a bit more flexible by making a fuction which can be easily linked to other component such as st.button.

for example:

from msal_streamlit_authentication import msal_authentication_v2
msal_auth = msal_authentication_v2(...)
if st.button('login'):
    login_token = msal_auth.login()
2 Likes

Would love that kind of feature :+1:

I think it is a good suggestion. I will try to find some time to make that adjustment. I thought there was some “beauty” in the current simplicity, but I will try to consider what I think makes the most sense to do. For starters, this was actually meant as a POC on how to mak a custom Streamlit Component, and my team just happened to want some authentication mechanism.

1 Like

Hi @asl how did you deploy it? Is it contanerized? I cannot make chrome run in the docker.

Great, this solved my problem to !

Sorry, I am very new to the Python and Streamlit. Could anyone please explain how to use the CSS class name and HTML id for the login and logout button style customization with this custom component? I couldn’t able to understand from the documents.

Thanks,
Vivek