Skip to content

Microsoft SSO User Authentication failing for some users with no errors. #11168

@ruskgo

Description

@ruskgo

Checklist

  • I have searched the existing issues for similar issues.
  • I added a very descriptive title to this issue.
  • I have provided sufficient information below to help reproduce this issue.

Summary

Following the documentation on and the tutorial I am trying to log users into the app using Microsoft SSO. It works fine for most users, just fails for some with no apparent reason. I cannot debug it for the life of me. When I try to log in the user he is always returned back to login, because the st.experimiental_user.is_logged_in is set to False. This happens without any errors from streamlit.

Below is the test code I have created to try to debug it. But nothing provides me with a clue. It seems that all the HTTP call are being returned with 200 status code and even the bytes sizes are similar for replicated calls with request library I have simulated.

Reproducible Code Example

import streamlit as st
import requests
import logging
import jwt

from urllib.parse import urlencode

logging.basicConfig(
    level=logging.DEBUG,
    )
logger = logging.getLogger("python")



def get_discovery_keys(token_response):
    tenant = st.secrets["auth"]["microsoft"]["tenant"]
    url = f"https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
    try:
        logger.info("Getting discovery keys...")
        response = requests.get(url=url)
        response.raise_for_status()
        disc_keys = response.json()
        logger.info(f"Discovery keys: {disc_keys}")
        if "id_token" in token_response:
            token = token_response["id_token"]
            decoded_payload = jwt.decode(token, options={"verify_signature": False})
            decoded_header = jwt.get_unverified_header(token)

            logger.info(f"ID Token Header:{str(decoded_header)}")
            logger.info(f"ID Token Payload:{str(decoded_payload)}")
        else:
            logger.error("No ID Token in the token_response.")

        st.write("Logged with manual set up.")
        logger.info("Manual Log in logs end.")
        st.query_params.clear()
    except requests.exceptions.RequestException as error:
        logger.error(f"Error on gettin the discovery keys: {str(error)}")

    st.stop()

def get_token_from_code(auth_code):
    tenant = st.secrets["auth"]["microsoft"]["tenant"]
    client_id = st.secrets["auth"]["microsoft"]["client_id"]
    client_secret = st.secrets["auth"]["microsoft"]["client_secret"] 
    redirect_uri = st.secrets["auth"]["microsoft"]["redirect_uri"]

    token_url = f"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    body = {
        'grant_type': 'authorization_code',
        'response_type': 'id_token',
        'code': auth_code,
        'redirect_uri': redirect_uri,
        'client_id': client_id,
        'client_secret': client_secret,
        'scope': 'openid profile email'
    }

    try:
        logger.info("Manual Log in logs start...")
        response = requests.post(token_url, headers=headers, data=body)
        response.raise_for_status()
        token_response = response.json()
        logger.info(f"Response token: {token_response}")
        logger.info(f"Context Headers: {st.context.headers.to_dict()}")
        logger.info(f"Context Cookes: {st.context.cookies.to_dict()}")
        logger.info(f"Experimental user: {st.experimental_user.to_dict()}")
        logger.info(f"Manual parameters: {st.query_params.to_dict()}")
        get_discovery_keys(token_response)
    except requests.exceptions.RequestException as error:
        logger.error(f"Error on gettin the token from code: {str(error)}")



def manual_login():
    tenant = st.secrets["auth"]["microsoft"]["tenant"]
    params = {
        "client_id": st.secrets["auth"]["microsoft"]["client_id"],
        "response_type": "code",
        "scope": "openid profile email",
        "state": "12345",
        "nonce": "678910",
        'redirect_uri': st.secrets["auth"]["microsoft"]["redirect_uri"],
        "prompt": "select_account",
    }

    url = f"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?{urlencode(params)}"

    url_link = st.button("Manual Login",type="secondary")
    if url_link:
        st.markdown(
            f'<meta http-equiv="refresh" content="0; url={url}">',
            unsafe_allow_html=True
        )

    if "code" in st.query_params:
        code = st.query_params["code"]
        logger.info("Getting token from code...")
        get_token_from_code(code)
    elif "error" in st.query_params:
        logger.error(f'Failed to get authorization code: {st.query_params.get("error")}')



def streamlit_login():

    if st.button("Streamlit Login"):
        tenant = st.secrets["auth"]["microsoft"]["tenant"]
        url = f"https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys"
        try:
            logger.info("Warming discovery keys...")
            response = requests.get(url=url)
            response.raise_for_status()
        except requests.exceptions.RequestException as error:
            logger.error(f"Error on warming the discovery keys: {str(error)}")

        st.login("microsoft")

        logger.info("Streamltit Log in logs without 'is_logged_in' start...")
        logger.info(f"Context Headers: {st.context.headers.to_dict()}")
        logger.info(f"Context Cookes: {st.context.cookies.to_dict()}")
        logger.info(f"Experimental user: {st.experimental_user.to_dict()}")
        logger.info(f"Streamlit parameters: {st.query_params.to_dict()}")
        logger.info("Streamltit Log in logs without 'is_logged_in' end.")
    
    if st.experimental_user.is_logged_in:
        st.write("Logged in with Streamlit Log In.")
        logger.info("Streamltit Log in logs with 'is_logged_in' start...")
        logger.info(f"Context Headers: {st.context.headers.to_dict()}")
        logger.info(f"Context Cookes: {st.context.cookies.to_dict()}")
        logger.info(f"Experimental user: {st.experimental_user.to_dict()}")
        logger.info(f"Streamlit parameters: {st.query_params.to_dict()}")
        logger.info("Streamltit Log in logs with 'is_logged_in' end.")

    if st.button("clear cookies"):
        st.logout()

    st.stop()


def main():
    pg = st.navigation(
        {"Home":[
                st.Page(
                streamlit_login,
                title="Streamlit Login",
                ),
                st.Page(
                    manual_login,
                    title="Manual Authorization Setup"
                )
            ]
        }
    )
    pg.run()

Steps To Reproduce

  1. Log In with Streamlit Login
  2. Check the logs.

Expected Behavior

I expect the st.experimental_user.is_logged_in being set to True with all the user's details inside the JSON.

Current Behavior

There is no error message, but st.experimental_user.is_logged_in being set to False. Also the st.contex.cookies does not contain the _streamlit_user key that would be used for st.experimental_user.

Is this a regression?

  • Yes, this used to work in a previous version.

Debug info

  • Streamlit version: 1.44.0
  • Python version: 3.11
  • Operating System: Windows
  • Browser: Edge
  • Authlib>=1.3.2

Additional Information

No response

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions