Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

missing "mail" in response with Microsoft SSO #81

Open
arribatec-cloud-1 opened this issue Oct 25, 2023 · 8 comments
Open

missing "mail" in response with Microsoft SSO #81

arribatec-cloud-1 opened this issue Oct 25, 2023 · 8 comments

Comments

@arribatec-cloud-1
Copy link

I have set up an application in Azure with credentials.

When I try to log in using said credentials as per the examples, the call fails with a missing key error:

ERROR: KeyError('mail')
Traceback (most recent call last):
  File "/whatever/routes/sso_microsoft.py", line 54, in microsoft_callback
    user = await microsoft_sso.verify_and_process(request)
  File "/usr/local/lib/python3.9/site-packages/fastapi_sso/sso/base.py", line 212, in verify_and_process
    return await self.process_login(
  File "/usr/local/lib/python3.9/site-packages/fastapi_sso/sso/base.py", line 292, in process_login
    return await self.openid_from_response(content)
  File "/usr/local/lib/python3.9/site-packages/fastapi_sso/sso/microsoft.py", line 45, in openid_from_response
    return OpenID(email=response["mail"], display_name=response["displayName"], provider=cls.provider)
KeyError: 'mail'

The code looks like this:

from fastapi import APIRouter, Depends
from fastapi_sso.sso.microsoft import MicrosoftSSO
from starlette.requests import Request
import logging
import os
import pprint

logger = logging.getLogger(__name__)

allow_insecure_http = ("1" == os.environ.get("OAUTHLIB_INSECURE_TRANSPORT", "0"))

# documentation https://pypi.org/project/fastapi-sso/

sso_microsoft_route = APIRouter(
	  prefix="/sso/microsoft"
	, tags = ["sso"]
	#,dependencies=[Depends(get_token_header)]
	, responses={404: {"description": "Not found"}}
)

MICROSOFT_SSO_DEBUG = os.environ.get("MICROSOFT_SSO_DEBUG")
MICROSOFT_SSO_REDIRECT_URL = os.environ.get("MICROSOFT_SSO_REDIRECT_BASE_URL")
MICROSOFT_SSO_TENANT = os.environ.get("MICROSOFT_SSO_TENANT")
MICROSOFT_SSO_CLIENT_ID = os.environ.get("MICROSOFT_SSO_CLIENT_ID")
MICROSOFT_SSO_CLIENT_SECRET = os.environ.get("MICROSOFT_SSO_CLIENT_SECRET")


if MICROSOFT_SSO_DEBUG:
	logger.info(f"  MICROSOFT_SSO_REDIRECT_URL: {MICROSOFT_SSO_REDIRECT_URL}")
	logger.info(f"        MICROSOFT_SSO_TENANT: {MICROSOFT_SSO_TENANT}")
	logger.info(f"     MICROSOFT_SSO_CLIENT_ID: {MICROSOFT_SSO_CLIENT_ID}")
	logger.info(f" MICROSOFT_SSO_CLIENT_SECRET: {MICROSOFT_SSO_CLIENT_SECRET}")

microsoft_sso = MicrosoftSSO(
	  client_id = MICROSOFT_SSO_CLIENT_ID
	, client_secret = MICROSOFT_SSO_CLIENT_SECRET
	, tenant = MICROSOFT_SSO_TENANT
	, allow_insecure_http = allow_insecure_http
	, scope = ["openid"]
)


@sso_microsoft_route.get("/login")
async def microsoft_login(request: Request):
	with microsoft_sso:
		return await microsoft_sso.get_login_redirect(redirect_uri = request.url_for("microsoft_callback"))


@sso_microsoft_route.get("/callback")
async def microsoft_callback(request: Request):
	user = None
	with microsoft_sso:
		try:
			user = await microsoft_sso.verify_and_process(request)
		except Exception as e:
			logger.exception(f"ERROR: {pprint.pformat(e)}")
	if not user:
		logger.warning("NO USER")
		return None
	return {
		"id": user.get("id"),
		"picture": user.get("picture"),
		"display_name": user.get("display_name"),
		"email": user.get("email"),
		"provider": user.get("provider"),
	}
@arribatec-cloud-1
Copy link
Author

I investigated this further and .... well let's just say MicrosoftSSO has no error handling what-so-ever. I might submit a PR at some point if I get it to work.

If you need a patch straight away, put this in the top of MicrosoftSSO.openid_from_response():

from fastapi_sso.sso.base SSOLoginError
error = response.get("error")
		if error:
			raise SSOLoginError(401, f"Error '{pprint.pformat(error)}' returned from Microsoft")

@tomasvotava
Copy link
Owner

I believe some tenants require to ask for email scope directly, it is now a default in 0.8.0 https://github.com/tomasvotava/fastapi-sso/releases/tag/0.8.0

Could you test if this resolves the problem for you?

@arribatec-cloud-1
Copy link
Author

arribatec-cloud-1 commented Nov 27, 2023 via email

@tomasvotava
Copy link
Owner

That's a good idea, I've added a simple post to guide users who struggle with this as well, thanks!

https://tomasvotava.github.io/fastapi-sso/how-to-guides/key-error/

@bolau
Copy link

bolau commented Sep 12, 2024

The key error of the OP is not a problem anymore. But still, I don't get an email address. Even with the scopes email or User.Read.All, it's not returned by MS, their "mail" field is null:

{
   '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users/$entity', 
   'businessPhones': [], 
   'displayName': 'Boris Lau', 
   'givenName': 'Boris', 
   'jobTitle': None, 
   'mail': None, 
   'mobilePhone': None, 
   'officeLocation': None, 
   'preferredLanguage': 'en-US', 
   'surname': 'Lau', 
   'userPrincipalName': 'lau@XXXXX',
   'id':XXXXX'
}

Resulting in:

id='XXXXX'
email=None
first_name='Boris'
last_name='Lau'
display_name='Boris Lau'
picture=None
provider='microsoft'

Interestingly, my mail address is contained in the field userPrincipalName.

@tomasvotava
Copy link
Owner

tomasvotava commented Sep 14, 2024

Hi @bolau! I am afraid since e-mail is really considered personal data these days, more and more openid providers will make it more difficult to retrieve it on behalf of a user. E.g. Apple has a configuration that allows the user to generate a temporary e-mail for each service, therefore you'll never actually be able to retrieve the user's real e-mail address and won't even be able to tell. Nevertheless, I believe in Microsoft's case there are lots of settings on tenant level that can play part in whether you get the e-mail address or not.
I'd recommend double-checking whether your Azure Application Registration has admin authorization granted (for some permissions you will se that it's not required, but things change when it is).
We are using the /me endpoint to retrieve the data https://github.com/tomasvotava/fastapi-sso/blob/master/fastapi_sso/sso/microsoft.py#L45
It is documented here, you can get your access token like this:

@app.get("/callback")
async def login_callback(request: Request):
    with sso:
        openid = await sso.verify_and_process(request)
    print(sso.access_token)

Then you can use the access token to play around with the API in curl / Postman or MS Graph Explorer. In the explorer, you can provide your own access token (obtained with fastapi) and try to come up with a combination of fields and params that will yield what you need. I will play around as well, but in my tenant and my app, the mail field is actually retrieved, so it's hard for me to debug.

@tomasvotava tomasvotava reopened this Sep 14, 2024
@bolau
Copy link

bolau commented Sep 14, 2024

Hi Tomas, thanks for your reply. I tried the Graph Explorer, but couldn't get the email address out of it.
Here's a temporary solution, which modifies fastapi_sso/sso/microsoft.py, but :

    async def openid_from_response(self, response: dict, session: Optional["httpx.AsyncClient"] = None) -> OpenID:
        email = response.get("mail")
        if email is None and "@" in response.get("userPrincipalName"):
            email = response.get("userPrincipalName")
        return OpenID(
            email=email,
            display_name=response.get("displayName"),
            provider=self.provider,
            id=response.get("id"),
            first_name=response.get("givenName"),
            last_name=response.get("surname"),
        )

I don't want to make this a pull request though, since this is most likely not a "correct" solution. I guess my account just doesn't have a proper e-mail address linked to it. Which seems weird, but anyhow :)

@tomasvotava
Copy link
Owner

In that case, you could try passing convert_response = False to verify_and_process. This will return the original response from /me endpoint instead of the OpenID instance and you can grab the userPrincipalName directly from there without the need to edit or subclass MicrosoftSSO.

SSOBase.verify_and_process

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants