chainlit
chainlit copied to clipboard
no user role with Azure AD authentication
Although authentication works, with the below I couldn't find any info on user roles that are assigned to users in the App Registration.
def oauth_callback(provider_id: str, token: str, raw_user_data: dict, default_user: cl.User): print(f"{provider_id=}") print(f"{token=}") print(f"{raw_user_data=}") print(f"{default_user=}") return default_user
You need azure-ad-hybrid authentication for that. It was just added a few weeks ago.
that's great news @hayescode, but how/where? I'm on the latest 1.1.400r1
ah, the env vars. Got it!
having troubles after replacing the OAUTH_AZURE_AD_* keys with OAUTH_AZURE_AD_HYBRID_*. Get double login buttons, one works, the other seems to have no call_back uri. But that button shouldn't be there in the first place, i guess.
AADSTS700016: Application with identifier 'None' was not found in the directory 'Signify'. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant.
@hayescode I looked at the raw user data (provider_id='azure-ad-hybrid'), but can't find the roles. These are the keys (no nested values):
raw_user_data.keys()=dict_keys(['@odata.context', 'businessPhones', 'displayName', 'givenName', 'jobTitle', 'mail', 'mobilePhone', 'officeLocation', 'preferredLanguage', 'surname', 'userPrincipalName', 'id', 'image'])
Add id_token to your params in the decorated login params, then decode it using jwt. ChatGPT will show you how to decode/extract the app role.
Not very familiar with this. When not using msal, it gets tricky very quickly. This what I used:
@cl.oauth_callback
def oauth_callback(provider_id: str, token: str, raw_user_data: dict, default_user: cl.User):
from msal.oauth2cli.oidc import decode_id_token
a = decode_id_token(id_token=token, client_id=os.environ['OAUTH_AZURE_AD_HYBRID_CLIENT_ID'])
print(a)
this is what it returns:
msal.oauth2cli.oidc.IdTokenAudienceError: 3. The aud (audience) claim must contain this client's client_id "---GUID---", case-sensitively. Was your client_id in wrong casing? Current epoch = 2024-07-19 18:51:19. The id_token was approximately: { "aud": "https://graph.microsoft.com", ...
it's https://graph.microsoft.com instead. I can fill that in and it will not complain, but that doesn't make any sense. Furthermore, no roles.
Add id_token params at the end.
@cl.oauth_callback
def oauth_callback(provider_id: str, token: str, raw_user_data: dict, default_user: cl.User, id_token: str):
Then decode the id_token. You don't need any other API calls you already have the token with app roles here.
Yes, that does it @hayescode! Last issue, do you know why there are 2 auth "flows"/buttons presented? The one not working calls a microsoft authorize with:
- client_id: None
- tenant: None
- redirect_uri=http://localhost:8000/auth/oauth/azure-ad/callback
It seems it is the "azure-ad" flow, for which I didn't provide the env vars of course. I guess I need a way to disable it somehow?
For those with the same problem, I have a temporary work around. Add this to stylesheet.css:
/* Temporary patch for "duplicated" continue with Microsoft */
.MuiStack-root .MuiButtonBase-root:first-of-type:has(span > svg[data-testid="MicrosoftIcon"]) {
display: none;
}
could u guys tell how did u authorize access to the users based on their roles? it seems like im having a problem after decoding the id_tokens. the id_token wont store anywhere.
It has been a while, but I did it this way quick and dirty. i'm not an expert. There are other ways to decode the token and at this stage i don't if you need to verify the the token first for security, @hayescode do you know?
from msal.oauth2cli.oidc import decode_id_token
@cl.oauth_callback # type: ignore new api not official yet
def oauth_callback(
provider_id: str,
token: str,
raw_user_data: dict,
default_user: cl.User,
id_token: str, # new parameter
):
dec_id_token = decode_id_token(
id_token=id_token, client_id=os.environ["OAUTH_AZURE_AD_HYBRID_CLIENT_ID"]
)
default_user.metadata["roles"] = dec_id_token["roles"]
return default_user
Now you access the user data via the chainlit api like e.g.:
@cl.set_chat_profiles
def set_chat_profiles(user: Optional[cl.User]):
if "SomeRole" in user.metadata["roles"]:
apps = all_apps
else:
apps = public_apps
return [cast(cl.ChatProfile, app.profile()) for app in apps]
Also, i'm on an old version ^1.1.402, so things might have changed since.
@weipienlee yes this is basically what I do too
thankyou so much, this metadata """default_user.metadata["roles"]""" was missing in my code. i had no idea what to do with the id_token coz it wont pass in the chat profile function.
@weipienlee yes this is basically what I do too
@hayescode, is token verification still required or has that been done by the chainlit framework? meaning, after the user logs in via an auth provider let's say microsoft AAD, is the callback first handled by the framework and so performing a token verification with the returned auth code and such, before it "hands it over" to "@cl.oauth_callback"?
thankyou so much, this metadata """default_user.metadata["roles"]""" was missing in my code. i had no idea what to do with the id_token coz it wont pass in the chat profile function.
hope that solves your problem, remember, metadata["roles"] is just dict key-value defined by you.
Thanks for your directions, here is a complete solution that verifies the signature too
import os
import time
import jwt
import requests
from jwt.algorithms import RSAAlgorithm
CLIENT_ID = os.environ["OAUTH_AZURE_AD_HYBRID_CLIENT_ID"]
TENANT_ID = os.environ.get("OAUTH_AZURE_AD_HYBRID_TENANT_ID")
JWKS_URL = f"https://login.microsoftonline.com/{TENANT_ID}/discovery/v2.0/keys"
JWKS_CACHE = {"keys": None, "last_fetch": 0, "ttl": 3600}
def fetch_jwks():
global JWKS_CACHE
current_time = time.time()
if JWKS_CACHE["keys"] and (current_time - JWKS_CACHE["last_fetch"]) < JWKS_CACHE["ttl"]:
return JWKS_CACHE["keys"]
response = requests.get(JWKS_URL)
JWKS_CACHE["keys"] = response.json()
JWKS_CACHE["last_fetch"] = current_time
return JWKS_CACHE["keys"]
def validate_and_get_azure_id_token(id_token):
jwks = fetch_jwks()
header = jwt.get_unverified_header(id_token)
kid = header["kid"]
key = next((key for key in jwks["keys"] if key["kid"] == kid), None)
if not key:
raise ValueError("❌ Key still not found after refresh.")
try:
decoded_token = jwt.decode(
id_token,
RSAAlgorithm.from_jwk(key),
algorithms=["RS256"],
audience=f"{CLIENT_ID}",
issuer=f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
)
return decoded_token
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
@cl.oauth_callback
def oauth_callback(
provider_id: str,
token: str,
raw_user_data: Dict[str, str],
default_user: cl.User,
id_token: str = None,
) -> Optional[cl.User]:
if provider_id == "azure-ad-hybrid":
default_user.display_name = raw_user_data.get("displayName", default_user.identifier)
decoded_id_token = validate_and_get_azure_id_token(id_token)
default_user.metadata["roles"] = decoded_id_token["groups"] # todo: map guid's to roles
return default_user
return None
tnx @wizhippo, I also tried packages like msal or authlib which might also be interesting.
This issue is stale because it has been open for 14 days with no activity.
This issue was closed because it has been inactive for 7 days since being marked as stale.