14
14
import requests
15
15
16
16
from .oauth2cli import Client , JwtAssertionCreator
17
+ from .oauth2cli .oidc import decode_part
17
18
from .authority import Authority
18
19
from .mex import send_request as mex_send_request
19
20
from .wstrust_request import send_request as wst_send_request
25
26
26
27
27
28
# The __init__.py will import this. Not the other way around.
28
- __version__ = "1.14 .0"
29
+ __version__ = "1.15 .0"
29
30
30
31
logger = logging .getLogger (__name__ )
31
32
@@ -111,6 +112,36 @@ def _preferred_browser():
111
112
return None
112
113
113
114
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
+
114
145
class ClientApplication (object ):
115
146
116
147
ACQUIRE_TOKEN_SILENT_ID = "84"
@@ -174,7 +205,7 @@ def __init__(
174
205
you may try use only the leaf cert (in PEM/str format) instead.
175
206
176
207
*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.
178
209
Simply pass a container containing only the key "client_assertion", like this::
179
210
180
211
{
@@ -481,7 +512,7 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
481
512
authority .device_authorization_endpoint or
482
513
urljoin (authority .token_endpoint , "devicecode" ),
483
514
}
484
- central_client = Client (
515
+ central_client = _ClientWithCcsRoutingInfo (
485
516
central_configuration ,
486
517
self .client_id ,
487
518
http_client = self .http_client ,
@@ -506,7 +537,7 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
506
537
regional_authority .device_authorization_endpoint or
507
538
urljoin (regional_authority .token_endpoint , "devicecode" ),
508
539
}
509
- regional_client = Client (
540
+ regional_client = _ClientWithCcsRoutingInfo (
510
541
regional_configuration ,
511
542
self .client_id ,
512
543
http_client = self .http_client ,
@@ -529,6 +560,7 @@ def initiate_auth_code_flow(
529
560
login_hint = None , # type: Optional[str]
530
561
domain_hint = None , # type: Optional[str]
531
562
claims_challenge = None ,
563
+ max_age = None ,
532
564
):
533
565
"""Initiate an auth code flow.
534
566
@@ -559,6 +591,17 @@ def initiate_auth_code_flow(
559
591
`here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code>`_ and
560
592
`here <https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8>`_.
561
593
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
+
562
605
:return:
563
606
The auth code flow. It is a dict in this form::
564
607
@@ -577,7 +620,7 @@ def initiate_auth_code_flow(
577
620
3. and then relay this dict and subsequent auth response to
578
621
:func:`~acquire_token_by_auth_code_flow()`.
579
622
"""
580
- client = Client (
623
+ client = _ClientWithCcsRoutingInfo (
581
624
{"authorization_endpoint" : self .authority .authorization_endpoint },
582
625
self .client_id ,
583
626
http_client = self .http_client )
@@ -588,6 +631,7 @@ def initiate_auth_code_flow(
588
631
domain_hint = domain_hint ,
589
632
claims = _merge_claims_challenge_and_capabilities (
590
633
self ._client_capabilities , claims_challenge ),
634
+ max_age = max_age ,
591
635
)
592
636
flow ["claims_challenge" ] = claims_challenge
593
637
return flow
@@ -654,7 +698,7 @@ def get_authorization_request_url(
654
698
self .http_client
655
699
) if authority else self .authority
656
700
657
- client = Client (
701
+ client = _ClientWithCcsRoutingInfo (
658
702
{"authorization_endpoint" : the_authority .authorization_endpoint },
659
703
self .client_id ,
660
704
http_client = self .http_client )
@@ -1178,6 +1222,10 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
1178
1222
key = lambda e : int (e .get ("last_modification_time" , "0" )),
1179
1223
reverse = True ):
1180
1224
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 ("." , "@" ))
1181
1229
response = client .obtain_token_by_refresh_token (
1182
1230
entry , rt_getter = lambda token_item : token_item ["secret" ],
1183
1231
on_removing_rt = lambda rt_item : None , # Disable RT removal,
@@ -1189,7 +1237,7 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
1189
1237
skip_account_creation = True , # To honor a concurrent remove_account()
1190
1238
)),
1191
1239
scope = scopes ,
1192
- headers = telemetry_context . generate_headers () ,
1240
+ headers = headers ,
1193
1241
data = dict (
1194
1242
kwargs .pop ("data" , {}),
1195
1243
claims = _merge_claims_challenge_and_capabilities (
@@ -1370,6 +1418,7 @@ def acquire_token_interactive(
1370
1418
timeout = None ,
1371
1419
port = None ,
1372
1420
extra_scopes_to_consent = None ,
1421
+ max_age = None ,
1373
1422
** kwargs ):
1374
1423
"""Acquire token interactively i.e. via a local browser.
1375
1424
@@ -1415,6 +1464,17 @@ def acquire_token_interactive(
1415
1464
in the same interaction, but for which you won't get back a
1416
1465
token for in this particular operation.
1417
1466
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
+
1418
1478
:return:
1419
1479
- A dict containing no "error" key,
1420
1480
and typically contains an "access_token" key.
@@ -1433,6 +1493,7 @@ def acquire_token_interactive(
1433
1493
port = port or 0 ),
1434
1494
prompt = prompt ,
1435
1495
login_hint = login_hint ,
1496
+ max_age = max_age ,
1436
1497
timeout = timeout ,
1437
1498
auth_params = {
1438
1499
"claims" : claims ,
@@ -1581,6 +1642,7 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, claims_challenge=No
1581
1642
claims = _merge_claims_challenge_and_capabilities (
1582
1643
self ._client_capabilities , claims_challenge )),
1583
1644
headers = telemetry_context .generate_headers (),
1645
+ # TBD: Expose a login_hint (or ccs_routing_hint) param for web app
1584
1646
** kwargs ))
1585
1647
telemetry_context .update_telemetry (response )
1586
1648
return response
0 commit comments