Skip to content

Commit d6bf21e

Browse files
committed
Add exclude_scopes:Optional[list]
1 parent 06f2abb commit d6bf21e

File tree

1 file changed

+56
-37
lines changed

1 file changed

+56
-37
lines changed

msal/application.py

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,6 @@
2727

2828
logger = logging.getLogger(__name__)
2929

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-
5730

5831
def extract_certs(public_cert_content):
5932
# Parses raw public certificate file contents and returns a list of strings
@@ -123,6 +96,7 @@ def __init__(
12396
# despite it is currently only needed by ConfidentialClientApplication.
12497
# This way, it holds the same positional param place for PCA,
12598
# when we would eventually want to add this feature to PCA in future.
99+
exclude_scopes=None,
126100
):
127101
"""Create an instance of application.
128102
@@ -275,11 +249,28 @@ def __init__(
275249
or provide a custom http_client which has a short timeout.
276250
That way, the latency would be under your control,
277251
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"]``.
278258
"""
279259
self.client_id = client_id
280260
self.client_credential = client_credential
281261
self.client_claims = client_claims
282262
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+
283274
if http_client:
284275
self.http_client = http_client
285276
else:
@@ -326,6 +317,34 @@ def __init__(
326317
self._telemetry_buffer = {}
327318
self._telemetry_lock = Lock()
328319

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+
329348
def _build_telemetry_context(
330349
self, api_id, correlation_id=None, refresh_reason=None):
331350
return msal.telemetry._TelemetryContext(
@@ -505,7 +524,7 @@ def initiate_auth_code_flow(
505524
flow = client.initiate_auth_code_flow(
506525
redirect_uri=redirect_uri, state=state, login_hint=login_hint,
507526
prompt=prompt,
508-
scope=decorate_scope(scopes, self.client_id),
527+
scope=self._decorate_scope(scopes),
509528
domain_hint=domain_hint,
510529
claims=_merge_claims_challenge_and_capabilities(
511530
self._client_capabilities, claims_challenge),
@@ -587,7 +606,7 @@ def get_authorization_request_url(
587606
response_type=response_type,
588607
redirect_uri=redirect_uri, state=state, login_hint=login_hint,
589608
prompt=prompt,
590-
scope=decorate_scope(scopes, self.client_id),
609+
scope=self._decorate_scope(scopes),
591610
nonce=nonce,
592611
domain_hint=domain_hint,
593612
claims=_merge_claims_challenge_and_capabilities(
@@ -650,7 +669,7 @@ def authorize(): # A controller in a web app
650669
response =_clean_up(self.client.obtain_token_by_auth_code_flow(
651670
auth_code_flow,
652671
auth_response,
653-
scope=decorate_scope(scopes, self.client_id) if scopes else None,
672+
scope=self._decorate_scope(scopes) if scopes else None,
654673
headers=telemetry_context.generate_headers(),
655674
data=dict(
656675
kwargs.pop("data", {}),
@@ -722,7 +741,7 @@ def acquire_token_by_authorization_code(
722741
self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID)
723742
response = _clean_up(self.client.obtain_token_by_authorization_code(
724743
code, redirect_uri=redirect_uri,
725-
scope=decorate_scope(scopes, self.client_id),
744+
scope=self._decorate_scope(scopes),
726745
headers=telemetry_context.generate_headers(),
727746
data=dict(
728747
kwargs.pop("data", {}),
@@ -1020,7 +1039,7 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
10201039
assert refresh_reason, "It should have been established at this point"
10211040
try:
10221041
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,
10241043
refresh_reason=refresh_reason, claims_challenge=claims_challenge,
10251044
**kwargs))
10261045
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):
11661185
refresh_reason=msal.telemetry.FORCE_REFRESH)
11671186
response = _clean_up(self.client.obtain_token_by_refresh_token(
11681187
refresh_token,
1169-
scope=decorate_scope(scopes, self.client_id),
1188+
scope=self._decorate_scope(scopes),
11701189
headers=telemetry_context.generate_headers(),
11711190
rt_getter=lambda rt: rt,
11721191
on_updating_rt=False,
@@ -1197,7 +1216,7 @@ def acquire_token_by_username_password(
11971216
- A successful response would contain "access_token" key,
11981217
- an error response would contain "error" and usually "error_description".
11991218
"""
1200-
scopes = decorate_scope(scopes, self.client_id)
1219+
scopes = self._decorate_scope(scopes)
12011220
telemetry_context = self._build_telemetry_context(
12021221
self.ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID)
12031222
headers = telemetry_context.generate_headers()
@@ -1343,7 +1362,7 @@ def acquire_token_interactive(
13431362
telemetry_context = self._build_telemetry_context(
13441363
self.ACQUIRE_TOKEN_INTERACTIVE)
13451364
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,
13471366
extra_scope_to_consent=extra_scopes_to_consent,
13481367
redirect_uri="http://localhost:{port}".format(
13491368
# 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):
13741393
"""
13751394
correlation_id = msal.telemetry._get_new_correlation_id()
13761395
flow = self.client.initiate_device_flow(
1377-
scope=decorate_scope(scopes or [], self.client_id),
1396+
scope=self._decorate_scope(scopes or []),
13781397
headers={msal.telemetry.CLIENT_REQUEST_ID: correlation_id},
13791398
**kwargs)
13801399
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
14851504
response = _clean_up(self.client.obtain_token_by_assertion( # bases on assertion RFC 7521
14861505
user_assertion,
14871506
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:
14891508
# 1. Explicitly requesting an RT, without relying on AAD default
14901509
# behavior, even though it currently still issues an RT.
14911510
# 2. Requesting an IDT (which would otherwise be unavailable)

0 commit comments

Comments
 (0)