AzureAD / microsoft-authentication-library-for-python

Microsoft Authentication Library (MSAL) for Python makes it easy to authenticate to Microsoft Entra ID. General docs are available here https://learn.microsoft.com/entra/msal/python/ Stable APIs are documented here https://msal-python.readthedocs.io. Questions can be asked on www.stackoverflow.com with tag "msal" + "python".
https://stackoverflow.com/questions/tagged/azure-ad-msal+python
Other
757 stars 192 forks source link

"openid" scope cannot be excluded when trying to use the XboxLive scopes instead #528

Closed PanicRide closed 10 months ago

PanicRide commented 1 year ago

Describe the bug "openid" scope cannot be excluded

My app must only authorize these scopes for accessing a person gaming profile using their Microsoft account: ['XboxLive.signin', 'Xboxlive.offline_access']

However, when attempting to exclude the three default scopes, this error is returned: ValueError: Invalid exclude_scopes=['openid', 'profile', 'offline_access']. You can not opt out "openid" scope

A small block of code in application.py seems to intentionally cause the error. When I comment out those lines of code, the library successfully works for my use-case.

There may be a feature I'm not using somewhere else in the library that requires the "openid" scope, but I can't include it because it prompts the user to grant me unnecessary access to their account. This includes being able to retrieve their real name and email address which isn't appropriate in this context.

To Reproduce Steps to reproduce the behavior:

from msal import ConfidentialClientApplication

app = ConfidentialClientApplication( APP_CLIENT_ID, authority='https://login.microsoftonline.com/consumers/', client_credential=APP_CLIENT_SECRET_VALUE, exclude_scopes=['openid', 'profile', 'offline_access'] )

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/user/env/lib/python3.10/site-packages/msal/application.py", line 500, in __init__
    raise ValueError(
ValueError: Invalid exclude_scopes=['openid', 'profile', 'offline_access']. You can not opt out "openid" scope

Expected behavior The scope should be allowed to be excluded without error

The MSAL Python version you are using 1.20.0

rayluo commented 1 year ago

Describe the bug "openid" scope cannot be excluded ......

It is not a bug, it is a feature. :-)

Half-joking aside, all MSAL libraries are written to work with Microsoft identity platform which is compliant with OpenID Connect (OIDC) specs. The OIDC specs says, "OpenID Connect requests MUST contain the openid scope value. If the openid scope value is not present, the behavior is entirely unspecified."

However, when attempting to exclude the three default scopes, this error is returned: ValueError: Invalid exclude_scopes=['openid', 'profile', 'offline_access']. You can not opt out "openid" scope

A small block of code in application.py seems to intentionally cause the error.

MSAL Python errs on the safe side, to make sure that "openid" scope is used.

... When I comment out those lines of code, the library successfully works for my use-case.

There may be a feature I'm not using somewhere else in the library that requires the "openid" scope,

When you hack to remove the "openid" scope, the Microsoft identity platform backend operates in OAuth2 mode, not in OIDC mode. MSAL Python's underlying implementation seems to tolerate the absence of an ID token, based on your report, yet we certainly did not test that usage pattern before. On top of my head, I wonder whether MSAL's acquire_token_silent() would still work in such a case, when the OAuth2 mode does not really provide an explicit identity/account concept. Your app may consequently be forced to operate in a single-user only mode. I'm not going to speculate more on this.

... but I can't include it ("openid" scope) because it prompts the user to grant me unnecessary access to their account. This includes being able to retrieve their real name and email address which isn't appropriate in this context. My app must only authorize these scopes for accessing a person gaming profile using their Microsoft account: ['XboxLive.signin', 'Xboxlive.offline_access']

Can you revisit your choice to remove "openid" scope? If your concern is privacy, you can exclude the "profile" scope, which was the reason to prompt user to provide their user name, email, etc.. The app will just need some hash value to differentiate the current login user from other login users. Personally, I think that would be very reasonable. When I'm playing an online game, I may not want the game to know my real identity, but I would certainly need the game to keep my save&load entries for me.

PanicRide commented 1 year ago

I wonder whether MSAL's acquire_token_silent() would still work in such a case, when the OAuth2 mode does not really provide an explicit identity/account concept. Your app may consequently be forced to operate in a single-user only mode.

You're right. When I remove the "openid" scope, accounts are not added to the app from the perspective of the library and I can't use functions that require specifying an account. My implementation just gets each account's initial token as needed using this function: app.acquire_token_by_authorization_code(code=AUTH_CODE, scopes=SCOPES)

Can you revisit your choice to remove "openid" scope?

After testing it again, it turns out I was wrong about the "openid" scope including the real name and email address of the user in the token claims. However, it does give the user an extra non-Xbox Live permission to approve that shouldn't need to be there. This is what it look like:

image

Perhaps I should be using a generic OAuth2 library instead of using MSAL at all. OIDC vs OAuth2 is an implimentation detail I wasn't aware I needed to consider and I just assumed MSAL would be the best option for working with Microsoft accounts. 🤷

Thank you for your help. If you choose not to implement support for this use-case, I would understand.

PanicRide commented 1 year ago

One more thing...

I noticed that when using the three default scopes the consent screen looks like this:

image

Notice how it only prompts for approval of the "profile" and "offline_access" scopes. It seems that the user is not prompted to approve the "openid" scope when the "profile" scope is already being requested.

Perhaps my real issue here is that the extra "openid" consent shouldn't be required when the "XboxLive.signin" scope is requested since it's the XboxLive version of "profile". If it weren't asking the user for additional unnecessary consent, I wouldn't have an issue using the "openid" scope.

Do you think my assumption is valid? If so, do you know how would we go about requesting such a change in Microsoft's consent flow?

rayluo commented 1 year ago

Thanks for testing all those combinations and giving us (the community) detailed screenshots.

It seems that the user is not prompted to approve the "openid" scope when the "profile" scope is already being requested.

The prompt behavior is controlled by the identity server, outside of the scope of MSAL Python. If I'm going to speculate, I would say the Microsoft identity platform optimizes the "openid+profile+..." consent experience by omitting the unnecessary prompt for openid i.e. "Sign you in ... an anonymous ID", in a sense that the profile is conceptually a superset of the openid. Such an optimization is nice to have, and understandably not applicable when the requested scope is "openid" without "profile", so you will see that "Sign you in ... an anonymous ID" prompt.

Unfortunately, that optimization does not seem to apply to "openid+XboxLive.signin+...".

Perhaps my real issue here is that the extra "openid" consent shouldn't be required when the "XboxLive.signin" scope is requested since it's the XboxLive version of "profile". If it weren't asking the user for additional unnecessary consent, I wouldn't have an issue using the "openid" scope.

Given that you already realized that your app requests much more than an anonymous ID, I would go a step further and say omitting "openid" does not make any real difference here. If an end user already trusts your app to access their gamertag and friends list etc., they shouldn't mind your app also get an anonymous ID. If they do not want to give your app their anonymous ID, no way they would give your app their gamertag and friends list.

Do you consider just keep the "openid" in your app, and see whether your end users really mind it? A good experiment to try. If it works out, your app can stay with MSAL, the official auth library provided by Microsoft.

My implementation just gets each account's initial token as needed using this function: app.acquire_token_by_authorization_code(code=AUTH_CODE, scopes=SCOPES)

Off topic. Is your app a daemon/service running in the cloud, or is it a desktop app running on end user's computer? For the latter, you should try acquire_token_interactive(...) which is much more convenient than acquire_token_by_authorization_code(...), and more secure by default.

PanicRide commented 1 year ago

Is your app a daemon/service running in the cloud, or is it a desktop app running on end user's computer?

It's a web-based app which is why it needs the auth code flow

they shouldn't mind your app also get an anonymous ID

It's true that they shouldn't mind if they understand what is happening. The problem is, no other apps in this space suffer the same issue and the user is normally only asked to authorize the Xbox Live scopes. Seeing a third thing to approve on the screen is already suspicious and it's made worse that it shows the Windows logo next to it. If it's true that the extra anonymous ID doesn't give me any extra access or a way to identify the non-gaming aspects of their account, then the identity provider shouldn't be asking them for it.

It's for those reasons that I've decided to stick with OAuth2 instead. However, I'm glad we were able to document this issue for the next person who runs into it.

Thank you for your help! :)