requests-oauthlib
requests-oauthlib copied to clipboard
Scope changes with Microsoft services & `offline_access`
I'm trying to set up OAuth2 for unattended access to Microsoft IMAP servers - the refresh_token is important here.
When providing a request scope set as follows:
offline_accesshttps://outlook.office.com/User.Readhttps://outlook.office.com/IMAP.AccessAsUser.All
The service responds with the following (i.e: offline_access is removed):
https://outlook.office.com/User.Readhttps://outlook.office.com/IMAP.AccessAsUser.All
This results in a warning being raised.
Traceback
Traceback (most recent call last):
File "./oauth2-test.py", line 51, in <module>
token = oauth.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_response)
File "/usr/lib/python3.8/site-packages/requests_oauthlib/oauth2_session.py", line 366, in fetch_token
self._client.parse_request_body_response(r.text, scope=self.scope)
File "/usr/lib/python3.8/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 427, in parse_request_body_response
self.token = parse_token_response(body, scope=scope)
File "/usr/lib/python3.8/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 441, in parse_token_response
validate_token_parameters(params)
File "/usr/lib/python3.8/site-packages/oauthlib/oauth2/rfc6749/parameters.py", line 471, in validate_token_parameters
raise w
Warning: Scope has changed from "https://outlook.office.com/User.Read https://outlook.office.com/IMAP.AccessAsUser.All offline_access" to "https://outlook.office.com/User.Read https://outlook.office.com/IMAP.AccessAsUser.All".
Apparently the offline_access scope should never be returned by Microsoft services, as it's not actually a useful scope for accessing resources (ref).
My current approach (which isn't ideal), is as follows:
oauth = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scope)
authorization_url, state = oauth.authorization_url(authorize_url)
# remove the `offline_access` scope directly / by hand
oauth.scope.remove('offline_access')
# ... submit the request to authorization_url, and retrieve the token
redirect_response = ...
token = oauth.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_response)
I'm aware of OAUTHLIB_RELAX_TOKEN_SCOPE (link), but that seems perhaps a little over-permissive.
Perhaps one of the following would be a good idea?
- A more generic mechanism to permit accepting scope changes
- A way to supply the expected response set of scopes
- A list of "I don't mind if these aren't grated" scopes
We have encountered a similar issue when using incremental authorization with Google APIs. In our app there are two different login screens which request different scopes. If a user uses only one of these screens, everything is good. If they use one login screen and later access the other, the oauth callback from Google for the second login screen will return the combined scopes that the user has granted, not just the scopes requested at the second screen.
According to this commit the API in oauthlib for handling a scope change is to catch a Warning exception, check if the modified scopes are acceptable and then extract the token from the exception if they are. In the context of the fetch_token call in requests-oauthlib this is problematic because the exception is thrown in a mutating method before the token is saved to the OAuth2Session.token field. Although the token can be extracted from the Warning, it won't be saved to the session.