@@ -92,11 +92,11 @@ def __init__(
9292 ) -> None :
9393 self .__username = username
9494 self .__password = password
95+ self .__logger = logger or get_logger (app = self .__class__ .__name__ )
9596 self .__identity_url = self .__resolve_fqdn_from_username_or_subdomain (identity_url , identity_tenant_subdomain )
9697 if not self .__identity_url .startswith ('https://' ):
9798 self .__identity_url = f'https://{ self .__identity_url } '
9899 self .__mfa_type = mfa_type or 'email'
99- self .__logger = logger or get_logger (app = self .__class__ .__name__ )
100100 self .__interaction_process : Optional [Process ] = None
101101 self .__is_polling : bool = False
102102 self .__keyring = ArkKeyring (self .__class__ .__name__ .lower ()) if cache_authentication else None
@@ -204,17 +204,20 @@ def __start_authentication(self) -> StartAuthResponse:
204204 return parsed_res
205205
206206 def __advance_authentication (
207- self , mechanism_id : str , session_id : str , answer : str , action : str
208- ) -> Union [AdvanceAuthMidResponse , AdvanceAuthResponse ]:
207+ self , mechanism_id : str , session_id : str , answer : str , action : str , is_idp_auth : bool = False
208+ ) -> Union [AdvanceAuthMidResponse , AdvanceAuthResponse , IdpAuthStatusResponse ]:
209209 self .__logger .info (f'Advancing authentication with user { self .__username } and fqdn { self .__identity_url } and action { action } ' )
210210 response = self .__session .post (
211211 url = f'{ self .__identity_url } /Security/AdvanceAuthentication' ,
212212 json = {'SessionId' : session_id , 'MechanismId' : mechanism_id , 'Action' : action , 'Answer' : answer },
213213 )
214214 try :
215- parsed_res : AdvanceAuthMidResponse = AdvanceAuthMidResponse .model_validate_json (response .text )
216- if parsed_res .result .summary == 'LoginSuccess' :
217- parsed_res : AdvanceAuthResponse = AdvanceAuthResponse .model_validate_json (response .text )
215+ if is_idp_auth :
216+ parsed_res : IdpAuthStatusResponse = IdpAuthStatusResponse .model_validate_json (response .text )
217+ else :
218+ parsed_res : AdvanceAuthMidResponse = AdvanceAuthMidResponse .model_validate_json (response .text )
219+ if parsed_res .result .summary == 'LoginSuccess' :
220+ parsed_res : AdvanceAuthResponse = AdvanceAuthResponse .model_validate_json (response .text )
218221 except (ValidationError , TypeError ) as ex :
219222 raise ArkException (f'Identity advance authentication failed to be parsed / validated [{ response .text } ]' ) from ex
220223 return parsed_res
@@ -281,14 +284,16 @@ def __poll_authentication(
281284 if output_conn .poll ():
282285 mfa_code = output_conn .recv ()
283286 advance_resp = self .__advance_authentication (
284- mechanism .mechanism_id , start_auth_response .result .session_id , mfa_code , 'Answer'
287+ mechanism .mechanism_id , start_auth_response .result .session_id , mfa_code , 'Answer' , False
285288 )
286289 if isinstance (advance_resp , AdvanceAuthResponse ):
287290 input_conn .send ('DONE' )
288291 else :
289292 input_conn .send ('CONTINUE' )
290293 else :
291- advance_resp = self .__advance_authentication (mechanism .mechanism_id , start_auth_response .result .session_id , '' , 'Poll' )
294+ advance_resp = self .__advance_authentication (
295+ mechanism .mechanism_id , start_auth_response .result .session_id , '' , 'Poll' , False
296+ )
292297 if isinstance (advance_resp , AdvanceAuthResponse ):
293298 # Done here, save the token
294299 self .__is_polling = False
@@ -340,6 +345,37 @@ def __pick_mechanism(self, challenge: Challenge) -> Mechanism:
340345 self .__mfa_type = next (filter (lambda f : factors [f ] == answers ['mfa' ], factors .keys ()))
341346 return next (filter (lambda m : factors [m .name .lower ()] == answers ['mfa' ], supported_mechanisms ))
342347
348+ def __perform_pin_code_idp_authentication (
349+ self , start_auth_response : StartAuthResponse , profile : Optional [ArkProfile ] = None , interactive : bool = False
350+ ) -> None :
351+ if not interactive :
352+ raise ArkException ('Non-interactive mode is not supported for OOB PIN code authentication' )
353+ answers = inquirer .prompt (
354+ [inquirer .Password ('answer' , message = 'Please enter the PIN code displayed after you logged in to your identity provider' )],
355+ render = ArkInquirerRender (),
356+ )
357+ if not answers :
358+ raise ArkAuthException ('Canceled by user' )
359+ pin_code = answers ['answer' ]
360+ result = self .__advance_authentication ('OOBAUTHPIN' , start_auth_response .result .idp_login_session_id , pin_code , 'Answer' , True )
361+ if (
362+ not result
363+ or not result .success
364+ or not isinstance (result , IdpAuthStatusResponse )
365+ or not result .result .summary
366+ or result .result .summary != 'LoginSuccess'
367+ or not result .result .token
368+ ):
369+ raise ArkAuthException ('Failed to perform idp authentication with OOB PIN' )
370+ # We managed to successfully authenticate
371+ # Done here, save the token
372+ self .__session_details = result .result
373+ self .__session .headers .update ({'Authorization' : f'Bearer { result .result .token } ' , ** ArkIdentityFQDNResolver .default_headers ()})
374+ delta = self .__session_details .token_lifetime or DEFAULT_TOKEN_LIFETIME_SECONDS
375+ self .__session_exp = datetime .now () + timedelta (seconds = delta )
376+ if self .__cache_authentication :
377+ self .__save_cache (profile )
378+
343379 def __perform_idp_authentication (
344380 self , start_auth_response : StartAuthResponse , profile : Optional [ArkProfile ] = None , interactive : bool = False
345381 ) -> None :
@@ -356,6 +392,11 @@ def __perform_idp_authentication(
356392 # Error can be ignored
357393 webbrowser .open (start_auth_response .result .idp_redirect_short_url , new = 0 , autoraise = True )
358394
395+ # Pin code flow
396+ if start_auth_response .result .idp_oob_auth_pin_required :
397+ self .__perform_pin_code_idp_authentication (start_auth_response , profile , interactive )
398+ return
399+
359400 # Start polling for idp auth
360401 self .__is_polling = True
361402 start_time = datetime .now ()
@@ -400,7 +441,7 @@ def __perform_up_authentication(
400441 raise ArkAuthException ('Canceled by user' )
401442 self .__password = answers ['answer' ]
402443 advance_resp = self .__advance_authentication (
403- mechanism .mechanism_id , start_auth_response .result .session_id , self .__password , 'Answer'
444+ mechanism .mechanism_id , start_auth_response .result .session_id , self .__password , 'Answer' , False
404445 )
405446 if isinstance (advance_resp , AdvanceAuthResponse ) and len (start_auth_response .result .challenges ) == 1 :
406447 # Done here, save the token
@@ -568,7 +609,7 @@ def auth_identity(self, profile: Optional[ArkProfile] = None, interactive: bool
568609 return
569610 else :
570611 oob_advance_resp = self .__advance_authentication (
571- mechanism .mechanism_id , start_auth_response .result .session_id , '' , 'StartOOB'
612+ mechanism .mechanism_id , start_auth_response .result .session_id , '' , 'StartOOB' , False
572613 )
573614 self .__poll_authentication (profile , mechanism , start_auth_response , oob_advance_resp , interactive )
574615 if self .__session_details :
@@ -591,7 +632,7 @@ def auth_identity(self, profile: Optional[ArkProfile] = None, interactive: bool
591632 for mechanism in start_auth_response .result .challenges [current_challenge_idx ].mechanisms :
592633 if mechanism .name .lower () == self .__mfa_type .lower ():
593634 oob_advance_resp = self .__advance_authentication (
594- mechanism .mechanism_id , start_auth_response .result .session_id , '' , 'StartOOB'
635+ mechanism .mechanism_id , start_auth_response .result .session_id , '' , 'StartOOB' , False
595636 )
596637 self .__poll_authentication (profile , mechanism , start_auth_response , oob_advance_resp , interactive )
597638 return
@@ -602,7 +643,9 @@ def auth_identity(self, profile: Optional[ArkProfile] = None, interactive: bool
602643 # Handle the rest of the challenges, might also handle the first challenge if no password is in the mechanisms
603644 for challenge in start_auth_response .result .challenges [current_challenge_idx :]:
604645 mechanism = self .__pick_mechanism (challenge )
605- oob_advance_resp = self .__advance_authentication (mechanism .mechanism_id , start_auth_response .result .session_id , '' , 'StartOOB' )
646+ oob_advance_resp = self .__advance_authentication (
647+ mechanism .mechanism_id , start_auth_response .result .session_id , '' , 'StartOOB' , False
648+ )
606649 self .__poll_authentication (profile , mechanism , start_auth_response , oob_advance_resp , interactive )
607650
608651 # pylint: disable=unused-argument
0 commit comments