|
27 | 27 |
|
28 | 28 | logger = logging.getLogger(__name__) |
29 | 29 |
|
30 | | -def decorate_scope( |
31 | | - scopes, client_id, |
32 | | - reserved_scope=frozenset(['openid', 'profile', 'offline_access'])): |
33 | | - if not isinstance(scopes, (list, set, tuple)): |
34 | | - raise ValueError("The input scopes should be a list, tuple, or set") |
35 | | - scope_set = set(scopes) # Input scopes is typically a list. Copy it to a set. |
36 | | - if scope_set & reserved_scope: |
37 | | - # These scopes are reserved for the API to provide good experience. |
38 | | - # We could make the developer pass these and then if they do they will |
39 | | - # come back asking why they don't see refresh token or user information. |
40 | | - raise ValueError( |
41 | | - "API does not accept {} value as user-provided scopes".format( |
42 | | - reserved_scope)) |
43 | | - if client_id in scope_set: |
44 | | - if len(scope_set) > 1: |
45 | | - # We make developers pass their client id, so that they can express |
46 | | - # the intent that they want the token for themselves (their own |
47 | | - # app). |
48 | | - # If we do not restrict them to passing only client id then they |
49 | | - # could write code where they expect an id token but end up getting |
50 | | - # access_token. |
51 | | - raise ValueError("Client Id can only be provided as a single scope") |
52 | | - decorated = set(reserved_scope) # Make a writable copy |
53 | | - else: |
54 | | - decorated = scope_set | reserved_scope |
55 | | - return list(decorated) |
56 | | - |
57 | 30 |
|
58 | 31 | def extract_certs(public_cert_content): |
59 | 32 | # Parses raw public certificate file contents and returns a list of strings |
@@ -123,6 +96,7 @@ def __init__( |
123 | 96 | # despite it is currently only needed by ConfidentialClientApplication. |
124 | 97 | # This way, it holds the same positional param place for PCA, |
125 | 98 | # when we would eventually want to add this feature to PCA in future. |
| 99 | + exclude_scopes=None, |
126 | 100 | ): |
127 | 101 | """Create an instance of application. |
128 | 102 |
|
@@ -275,11 +249,28 @@ def __init__( |
275 | 249 | or provide a custom http_client which has a short timeout. |
276 | 250 | That way, the latency would be under your control, |
277 | 251 | but still less performant than opting out of region feature. |
| 252 | + :param list[str] exclude_scopes: (optional) |
| 253 | + Historically MSAL hardcodes `offline_access` scope, |
| 254 | + which would allow your app to have prolonged access to user's data. |
| 255 | + If that is unnecessary or undesirable for your app, |
| 256 | + now you can use this parameter to supply an exclusion list of scopes, |
| 257 | + such as ``exclude_scopes = ["offline_access"]``. |
278 | 258 | """ |
279 | 259 | self.client_id = client_id |
280 | 260 | self.client_credential = client_credential |
281 | 261 | self.client_claims = client_claims |
282 | 262 | self._client_capabilities = client_capabilities |
| 263 | + |
| 264 | + if exclude_scopes and not isinstance(exclude_scopes, list): |
| 265 | + raise ValueError( |
| 266 | + "Invalid exclude_scopes={}. It need to be a list of strings.".format( |
| 267 | + repr(exclude_scopes))) |
| 268 | + self._exclude_scopes = frozenset(exclude_scopes or []) |
| 269 | + if "openid" in self._exclude_scopes: |
| 270 | + raise ValueError( |
| 271 | + 'Invalid exclude_scopes={}. You can not opt out "openid" scope'.format( |
| 272 | + repr(exclude_scopes))) |
| 273 | + |
283 | 274 | if http_client: |
284 | 275 | self.http_client = http_client |
285 | 276 | else: |
@@ -326,6 +317,34 @@ def __init__( |
326 | 317 | self._telemetry_buffer = {} |
327 | 318 | self._telemetry_lock = Lock() |
328 | 319 |
|
| 320 | + def _decorate_scope( |
| 321 | + self, scopes, |
| 322 | + reserved_scope=frozenset(['openid', 'profile', 'offline_access'])): |
| 323 | + if not isinstance(scopes, (list, set, tuple)): |
| 324 | + raise ValueError("The input scopes should be a list, tuple, or set") |
| 325 | + scope_set = set(scopes) # Input scopes is typically a list. Copy it to a set. |
| 326 | + if scope_set & reserved_scope: |
| 327 | + # These scopes are reserved for the API to provide good experience. |
| 328 | + # We could make the developer pass these and then if they do they will |
| 329 | + # come back asking why they don't see refresh token or user information. |
| 330 | + raise ValueError( |
| 331 | + "API does not accept {} value as user-provided scopes".format( |
| 332 | + reserved_scope)) |
| 333 | + if self.client_id in scope_set: |
| 334 | + if len(scope_set) > 1: |
| 335 | + # We make developers pass their client id, so that they can express |
| 336 | + # the intent that they want the token for themselves (their own |
| 337 | + # app). |
| 338 | + # If we do not restrict them to passing only client id then they |
| 339 | + # could write code where they expect an id token but end up getting |
| 340 | + # access_token. |
| 341 | + raise ValueError("Client Id can only be provided as a single scope") |
| 342 | + decorated = set(reserved_scope) # Make a writable copy |
| 343 | + else: |
| 344 | + decorated = scope_set | reserved_scope |
| 345 | + decorated -= self._exclude_scopes |
| 346 | + return list(decorated) |
| 347 | + |
329 | 348 | def _build_telemetry_context( |
330 | 349 | self, api_id, correlation_id=None, refresh_reason=None): |
331 | 350 | return msal.telemetry._TelemetryContext( |
@@ -505,7 +524,7 @@ def initiate_auth_code_flow( |
505 | 524 | flow = client.initiate_auth_code_flow( |
506 | 525 | redirect_uri=redirect_uri, state=state, login_hint=login_hint, |
507 | 526 | prompt=prompt, |
508 | | - scope=decorate_scope(scopes, self.client_id), |
| 527 | + scope=self._decorate_scope(scopes), |
509 | 528 | domain_hint=domain_hint, |
510 | 529 | claims=_merge_claims_challenge_and_capabilities( |
511 | 530 | self._client_capabilities, claims_challenge), |
@@ -587,7 +606,7 @@ def get_authorization_request_url( |
587 | 606 | response_type=response_type, |
588 | 607 | redirect_uri=redirect_uri, state=state, login_hint=login_hint, |
589 | 608 | prompt=prompt, |
590 | | - scope=decorate_scope(scopes, self.client_id), |
| 609 | + scope=self._decorate_scope(scopes), |
591 | 610 | nonce=nonce, |
592 | 611 | domain_hint=domain_hint, |
593 | 612 | claims=_merge_claims_challenge_and_capabilities( |
@@ -650,7 +669,7 @@ def authorize(): # A controller in a web app |
650 | 669 | response =_clean_up(self.client.obtain_token_by_auth_code_flow( |
651 | 670 | auth_code_flow, |
652 | 671 | auth_response, |
653 | | - scope=decorate_scope(scopes, self.client_id) if scopes else None, |
| 672 | + scope=self._decorate_scope(scopes) if scopes else None, |
654 | 673 | headers=telemetry_context.generate_headers(), |
655 | 674 | data=dict( |
656 | 675 | kwargs.pop("data", {}), |
@@ -722,7 +741,7 @@ def acquire_token_by_authorization_code( |
722 | 741 | self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID) |
723 | 742 | response = _clean_up(self.client.obtain_token_by_authorization_code( |
724 | 743 | code, redirect_uri=redirect_uri, |
725 | | - scope=decorate_scope(scopes, self.client_id), |
| 744 | + scope=self._decorate_scope(scopes), |
726 | 745 | headers=telemetry_context.generate_headers(), |
727 | 746 | data=dict( |
728 | 747 | kwargs.pop("data", {}), |
@@ -1020,7 +1039,7 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it( |
1020 | 1039 | assert refresh_reason, "It should have been established at this point" |
1021 | 1040 | try: |
1022 | 1041 | result = _clean_up(self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family( |
1023 | | - authority, decorate_scope(scopes, self.client_id), account, |
| 1042 | + authority, self._decorate_scope(scopes), account, |
1024 | 1043 | refresh_reason=refresh_reason, claims_challenge=claims_challenge, |
1025 | 1044 | **kwargs)) |
1026 | 1045 | if (result and "error" not in result) or (not access_token_from_cache): |
@@ -1166,7 +1185,7 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes, **kwargs): |
1166 | 1185 | refresh_reason=msal.telemetry.FORCE_REFRESH) |
1167 | 1186 | response = _clean_up(self.client.obtain_token_by_refresh_token( |
1168 | 1187 | refresh_token, |
1169 | | - scope=decorate_scope(scopes, self.client_id), |
| 1188 | + scope=self._decorate_scope(scopes), |
1170 | 1189 | headers=telemetry_context.generate_headers(), |
1171 | 1190 | rt_getter=lambda rt: rt, |
1172 | 1191 | on_updating_rt=False, |
@@ -1197,7 +1216,7 @@ def acquire_token_by_username_password( |
1197 | 1216 | - A successful response would contain "access_token" key, |
1198 | 1217 | - an error response would contain "error" and usually "error_description". |
1199 | 1218 | """ |
1200 | | - scopes = decorate_scope(scopes, self.client_id) |
| 1219 | + scopes = self._decorate_scope(scopes) |
1201 | 1220 | telemetry_context = self._build_telemetry_context( |
1202 | 1221 | self.ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID) |
1203 | 1222 | headers = telemetry_context.generate_headers() |
@@ -1343,7 +1362,7 @@ def acquire_token_interactive( |
1343 | 1362 | telemetry_context = self._build_telemetry_context( |
1344 | 1363 | self.ACQUIRE_TOKEN_INTERACTIVE) |
1345 | 1364 | response = _clean_up(self.client.obtain_token_by_browser( |
1346 | | - scope=decorate_scope(scopes, self.client_id) if scopes else None, |
| 1365 | + scope=self._decorate_scope(scopes) if scopes else None, |
1347 | 1366 | extra_scope_to_consent=extra_scopes_to_consent, |
1348 | 1367 | redirect_uri="http://localhost:{port}".format( |
1349 | 1368 | # Hardcode the host, for now. AAD portal rejects 127.0.0.1 anyway |
@@ -1374,7 +1393,7 @@ def initiate_device_flow(self, scopes=None, **kwargs): |
1374 | 1393 | """ |
1375 | 1394 | correlation_id = msal.telemetry._get_new_correlation_id() |
1376 | 1395 | flow = self.client.initiate_device_flow( |
1377 | | - scope=decorate_scope(scopes or [], self.client_id), |
| 1396 | + scope=self._decorate_scope(scopes or []), |
1378 | 1397 | headers={msal.telemetry.CLIENT_REQUEST_ID: correlation_id}, |
1379 | 1398 | **kwargs) |
1380 | 1399 | flow[self.DEVICE_FLOW_CORRELATION_ID] = correlation_id |
@@ -1485,7 +1504,7 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, claims_challenge=No |
1485 | 1504 | response = _clean_up(self.client.obtain_token_by_assertion( # bases on assertion RFC 7521 |
1486 | 1505 | user_assertion, |
1487 | 1506 | self.client.GRANT_TYPE_JWT, # IDTs and AAD ATs are all JWTs |
1488 | | - scope=decorate_scope(scopes, self.client_id), # Decoration is used for: |
| 1507 | + scope=self._decorate_scope(scopes), # Decoration is used for: |
1489 | 1508 | # 1. Explicitly requesting an RT, without relying on AAD default |
1490 | 1509 | # behavior, even though it currently still issues an RT. |
1491 | 1510 | # 2. Requesting an IDT (which would otherwise be unavailable) |
|
0 commit comments