1414import requests
1515
1616from .oauth2cli import Client , JwtAssertionCreator
17+ from .oauth2cli .oidc import decode_part
1718from .authority import Authority
1819from .mex import send_request as mex_send_request
1920from .wstrust_request import send_request as wst_send_request
2526
2627
2728# The __init__.py will import this. Not the other way around.
28- __version__ = "1.14 .0"
29+ __version__ = "1.15 .0"
2930
3031logger = logging .getLogger (__name__ )
3132
@@ -111,6 +112,36 @@ def _preferred_browser():
111112 return None
112113
113114
115+ class _ClientWithCcsRoutingInfo (Client ):
116+
117+ def initiate_auth_code_flow (self , ** kwargs ):
118+ if kwargs .get ("login_hint" ): # eSTS could have utilized this as-is, but nope
119+ kwargs ["X-AnchorMailbox" ] = "UPN:%s" % kwargs ["login_hint" ]
120+ return super (_ClientWithCcsRoutingInfo , self ).initiate_auth_code_flow (
121+ client_info = 1 , # To be used as CSS Routing info
122+ ** kwargs )
123+
124+ def obtain_token_by_auth_code_flow (
125+ self , auth_code_flow , auth_response , ** kwargs ):
126+ # Note: the obtain_token_by_browser() is also covered by this
127+ assert isinstance (auth_code_flow , dict ) and isinstance (auth_response , dict )
128+ headers = kwargs .pop ("headers" , {})
129+ client_info = json .loads (
130+ decode_part (auth_response ["client_info" ])
131+ ) if auth_response .get ("client_info" ) else {}
132+ if "uid" in client_info and "utid" in client_info :
133+ # Note: The value of X-AnchorMailbox is also case-insensitive
134+ headers ["X-AnchorMailbox" ] = "Oid:{uid}@{utid}" .format (** client_info )
135+ return super (_ClientWithCcsRoutingInfo , self ).obtain_token_by_auth_code_flow (
136+ auth_code_flow , auth_response , headers = headers , ** kwargs )
137+
138+ def obtain_token_by_username_password (self , username , password , ** kwargs ):
139+ headers = kwargs .pop ("headers" , {})
140+ headers ["X-AnchorMailbox" ] = "upn:{}" .format (username )
141+ return super (_ClientWithCcsRoutingInfo , self ).obtain_token_by_username_password (
142+ username , password , headers = headers , ** kwargs )
143+
144+
114145class ClientApplication (object ):
115146
116147 ACQUIRE_TOKEN_SILENT_ID = "84"
@@ -174,7 +205,7 @@ def __init__(
174205 you may try use only the leaf cert (in PEM/str format) instead.
175206
176207 *Added in version 1.13.0*:
177- It can also be a completly pre-signed assertion that you've assembled yourself.
208+ It can also be a completely pre-signed assertion that you've assembled yourself.
178209 Simply pass a container containing only the key "client_assertion", like this::
179210
180211 {
@@ -481,7 +512,7 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
481512 authority .device_authorization_endpoint or
482513 urljoin (authority .token_endpoint , "devicecode" ),
483514 }
484- central_client = Client (
515+ central_client = _ClientWithCcsRoutingInfo (
485516 central_configuration ,
486517 self .client_id ,
487518 http_client = self .http_client ,
@@ -506,7 +537,7 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
506537 regional_authority .device_authorization_endpoint or
507538 urljoin (regional_authority .token_endpoint , "devicecode" ),
508539 }
509- regional_client = Client (
540+ regional_client = _ClientWithCcsRoutingInfo (
510541 regional_configuration ,
511542 self .client_id ,
512543 http_client = self .http_client ,
@@ -529,6 +560,7 @@ def initiate_auth_code_flow(
529560 login_hint = None , # type: Optional[str]
530561 domain_hint = None , # type: Optional[str]
531562 claims_challenge = None ,
563+ max_age = None ,
532564 ):
533565 """Initiate an auth code flow.
534566
@@ -559,6 +591,17 @@ def initiate_auth_code_flow(
559591 `here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code>`_ and
560592 `here <https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8>`_.
561593
594+ :param int max_age:
595+ OPTIONAL. Maximum Authentication Age.
596+ Specifies the allowable elapsed time in seconds
597+ since the last time the End-User was actively authenticated.
598+ If the elapsed time is greater than this value,
599+ Microsoft identity platform will actively re-authenticate the End-User.
600+
601+ MSAL Python will also automatically validate the auth_time in ID token.
602+
603+ New in version 1.15.
604+
562605 :return:
563606 The auth code flow. It is a dict in this form::
564607
@@ -577,7 +620,7 @@ def initiate_auth_code_flow(
577620 3. and then relay this dict and subsequent auth response to
578621 :func:`~acquire_token_by_auth_code_flow()`.
579622 """
580- client = Client (
623+ client = _ClientWithCcsRoutingInfo (
581624 {"authorization_endpoint" : self .authority .authorization_endpoint },
582625 self .client_id ,
583626 http_client = self .http_client )
@@ -588,6 +631,7 @@ def initiate_auth_code_flow(
588631 domain_hint = domain_hint ,
589632 claims = _merge_claims_challenge_and_capabilities (
590633 self ._client_capabilities , claims_challenge ),
634+ max_age = max_age ,
591635 )
592636 flow ["claims_challenge" ] = claims_challenge
593637 return flow
@@ -654,7 +698,7 @@ def get_authorization_request_url(
654698 self .http_client
655699 ) if authority else self .authority
656700
657- client = Client (
701+ client = _ClientWithCcsRoutingInfo (
658702 {"authorization_endpoint" : the_authority .authorization_endpoint },
659703 self .client_id ,
660704 http_client = self .http_client )
@@ -1178,6 +1222,10 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
11781222 key = lambda e : int (e .get ("last_modification_time" , "0" )),
11791223 reverse = True ):
11801224 logger .debug ("Cache attempts an RT" )
1225+ headers = telemetry_context .generate_headers ()
1226+ if "home_account_id" in query : # Then use it as CCS Routing info
1227+ headers ["X-AnchorMailbox" ] = "Oid:{}" .format ( # case-insensitive value
1228+ query ["home_account_id" ].replace ("." , "@" ))
11811229 response = client .obtain_token_by_refresh_token (
11821230 entry , rt_getter = lambda token_item : token_item ["secret" ],
11831231 on_removing_rt = lambda rt_item : None , # Disable RT removal,
@@ -1189,7 +1237,7 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
11891237 skip_account_creation = True , # To honor a concurrent remove_account()
11901238 )),
11911239 scope = scopes ,
1192- headers = telemetry_context . generate_headers () ,
1240+ headers = headers ,
11931241 data = dict (
11941242 kwargs .pop ("data" , {}),
11951243 claims = _merge_claims_challenge_and_capabilities (
@@ -1370,6 +1418,7 @@ def acquire_token_interactive(
13701418 timeout = None ,
13711419 port = None ,
13721420 extra_scopes_to_consent = None ,
1421+ max_age = None ,
13731422 ** kwargs ):
13741423 """Acquire token interactively i.e. via a local browser.
13751424
@@ -1415,6 +1464,17 @@ def acquire_token_interactive(
14151464 in the same interaction, but for which you won't get back a
14161465 token for in this particular operation.
14171466
1467+ :param int max_age:
1468+ OPTIONAL. Maximum Authentication Age.
1469+ Specifies the allowable elapsed time in seconds
1470+ since the last time the End-User was actively authenticated.
1471+ If the elapsed time is greater than this value,
1472+ Microsoft identity platform will actively re-authenticate the End-User.
1473+
1474+ MSAL Python will also automatically validate the auth_time in ID token.
1475+
1476+ New in version 1.15.
1477+
14181478 :return:
14191479 - A dict containing no "error" key,
14201480 and typically contains an "access_token" key.
@@ -1433,6 +1493,7 @@ def acquire_token_interactive(
14331493 port = port or 0 ),
14341494 prompt = prompt ,
14351495 login_hint = login_hint ,
1496+ max_age = max_age ,
14361497 timeout = timeout ,
14371498 auth_params = {
14381499 "claims" : claims ,
@@ -1581,6 +1642,7 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, claims_challenge=No
15811642 claims = _merge_claims_challenge_and_capabilities (
15821643 self ._client_capabilities , claims_challenge )),
15831644 headers = telemetry_context .generate_headers (),
1645+ # TBD: Expose a login_hint (or ccs_routing_hint) param for web app
15841646 ** kwargs ))
15851647 telemetry_context .update_telemetry (response )
15861648 return response
0 commit comments