def _windows_live_token_refresh(self, refresh_token): """ Internal method to refresh Windows Live Token, called by `self.authenticate` Raises: AuthenticationException: When provided Refresh-Token is invalid. Args: refresh_token (:class:`RefreshToken`): Refresh token Returns: tuple: If authentication succeeds, `tuple` of (AccessToken, RefreshToken) is returned """ if not refresh_token or not refresh_token.is_valid: raise AuthenticationException("No valid RefreshToken") resp = self.__window_live_token_refresh_request(refresh_token) response = json.loads(resp.content.decode('utf-8')) if 'access_token' not in response: raise AuthenticationException("Could not refresh token via RefreshToken") access_token = AccessToken(response['access_token'], response['expires_in']) refresh_token = RefreshToken(response['refresh_token']) return access_token, refresh_token
def _windows_live_authenticate(self, email_address, password): """ Internal method to authenticate with Windows Live, called by `self.authenticate` In case of required two-factor-authentication the respective routine is initialized and user gets asked for input of verification details. Args: email_address (str): Microsoft Account Email address password (str): Microsoft Account password Raises: AuthenticationException: When returned headers do not contain Access-/Refresh-Tokens. TwoFactorAuthRequired: If 2FA is required for this account Returns: tuple: If authentication succeeds, `tuple` of (AccessToken, RefreshToken) is returned """ response = self.__window_live_authenticate_request(email_address, password) proof_type = self.extract_js_object(response.content, "PROOF.Type") if proof_type: log.debug('Following 2fa proof-types gathered: {!s}'.format(proof_type)) server_data = self.extract_js_object(response.content, "ServerData") raise TwoFactorAuthRequired("Two Factor Authentication is required", server_data) try: # the access token is included in fragment of the location header return self.parse_redirect_url(response.headers.get('Location')) except Exception as e: log.debug('Parsing redirection url failed, error: {0}'.format(str(e))) raise AuthenticationException("Could not log in with supplied credentials")
def _xbox_live_authorize(self, user_token, device_token=None, title_token=None): """ Internal method to authorize with Xbox Live, called by `self.authenticate` Args: user_token (:class:`UserToken`): User token device_token (:class:`DeviceToken`): Optional Device token title_token (:class:`TitleToken`): Optional Title token Raises: AuthenticationException: When provided User-Token is invalid Returns: tuple: If authentication succeeds, returns tuple of (:class:`XSTSToken`, :class:`XboxLiveUserInfo`) """ if not user_token or not user_token.is_valid: raise AuthenticationException("No valid UserToken") json_data = self.__xbox_live_authorize_request(user_token, device_token, title_token).json() userinfo = json_data['DisplayClaims']['xui'][0] userinfo = XboxLiveUserInfo.from_dict(userinfo) xsts_token = XSTSToken(json_data['Token'], json_data['IssueInstant'], json_data['NotAfter']) return xsts_token, userinfo
def parse_auth_strategies(server_data): """ Parses the list of supported authentication strategies Auth variants position changes from time to time, so instead of accessing a fixed, named field, heuristic detection is used Example node: [{ data:'<some data>', type:1, display:'*****@*****.**', otcEnabled:true, otcSent:false, isLost:false, isSleeping:false, isSADef:true, isVoiceDef:false, isVoiceOnly:false, pushEnabled:false }, { data:'<some data>', type:3, clearDigits:'69', ctryISO:'DE', display:'*********69', otcEnabled:true, otcSent:false, voiceEnabled:true, isLost:false, isSleeping:false, isSADef:false, isVoiceDef:false, isVoiceOnly:false, pushEnabled:false }, { data:'2342352452523414114', type:14, display:'2342352452523414114', otcEnabled:false, otcSent:false, isLost:false, isSleeping:false, isSADef:false, isVoiceDef:false, isVoiceOnly:false, pushEnabled:true }] Returns: list: List of available auth strategies """ for k, v in server_data.items(): if isinstance(v, list) and len(v) > 0 and isinstance( v[0], dict) and 'otcEnabled' in v[0] and 'data' in v[0]: return v raise AuthenticationException('No 2fa auth strategies found!')
def two_factor_auth(auth_mgr, server_data): otc = None proof = None two_fa = TwoFactorAuthentication(auth_mgr.session, auth_mgr.email_address, server_data) strategies = two_fa.auth_strategies entries = ['{!s}, Name: {}'.format( TwoFactorAuthMethods(strategy.get('type', 0)), strategy.get('display')) for strategy in strategies ] index = int(__input_prompt('Choose desired auth method', entries)) if index < 0 or index >= len(strategies): raise AuthenticationException('Invalid auth strategy index chosen!') verification_prompt = two_fa.get_method_verification_prompt(index) if verification_prompt: proof = __input_prompt(verification_prompt) need_otc = two_fa.check_otc(index, proof) if need_otc: otc = __input_prompt('Enter One-Time-Code (OTC)') access_token, refresh_token = two_fa.authenticate(index, proof, otc) auth_mgr.access_token = access_token auth_mgr.refresh_token = refresh_token auth_mgr.authenticate()
def check_otc(self, strategy_index, proof): """ Check if OneTimeCode is required. If it's required, request it. Args: strategy_index (int): Index of chosen auth strategy proof (str/NoneType): Verification / proof of chosen auth strategy Returns: bool: `True` if OTC is required, `False` otherwise """ strategy = self.auth_strategies[strategy_index] auth_type = strategy.get('type') auth_data = strategy.get('data') response = None if auth_type != TwoFactorAuthMethods.TOTPAuthenticator: ''' TOTPAuthenticator V1 works without requesting anything (offline OTC generation) TOTPAuthenticator V2 needs a cached `Session Lookup Key`, not OTC, we handle it here ''' response = self._request_otc(auth_type, proof, auth_data) if response.status_code != 200: raise AuthenticationException( "Error requesting OTC, HTTP Code: %i" % response.status_code) state = response.json() log.debug('State from Request OTC: %s' % state.get('State')) if auth_type == TwoFactorAuthMethods.TOTPAuthenticatorV2: # Smartphone push notification self.session_lookup_key = response.json().get('SessionLookupKey') return False else: return True
def authenticate(self, strategy_index, proof, otc): """ Perform chain of Two-Factor-Authentication (2FA) with the Windows Live Server. Args: strategy_index (int): Index of chosen auth strategy server_data (dict): Parsed javascript-object `serverData`, obtained from Windows Live Auth Request otc (str): One Time Code Returns: tuple: If authentication succeeds, `tuple` of (AccessToken, RefreshToken) is returned """ strategy = self.auth_strategies[strategy_index] auth_type = strategy.get('type') auth_data = strategy.get('data') log.debug('Using Method: {!s}'.format(TwoFactorAuthMethods(auth_type))) if TwoFactorAuthMethods.TOTPAuthenticatorV2 == auth_type: if not self.session_lookup_key: raise AuthenticationException( 'Did not receive SessionLookupKey from Authenticator V2 request!' ) session_state = self._poll_session_state() if session_state != AuthSessionState.APPROVED: raise AuthenticationException( 'Authentication by Authenticator V2 failed!' ' State: %s' % AuthSessionState(session_state)) # Do not send auth_data when finishing TOTPv2 authentication auth_data = None response = self._finish_auth(auth_type, auth_data, otc, proof) try: return AuthenticationManager.parse_redirect_url( response.headers.get('Location')) except Exception as e: log.debug('Parsing redirection url failed, error: {0}'.format( str(e))) raise AuthenticationException( "2FA: Location header does not hold access/refresh tokens!")
def authenticate_with_service(self, authorization_url): """ Authenticate with partnered service, requires a successful Windows Live Authentication. It works because stored cookies from the Windows Live Auth are used, entering the credentials again is unnecessary. Args: authorization_url (str): Authorization URL Returns: requests.Response: Response of the final request """ if not self.authenticated: raise AuthenticationException("Not authenticated with Windows Live, please do that first, " "before attempting a service authentication") response = self.session.get(authorization_url, allow_redirects=False) if response.status_code != 302: raise AuthenticationException("Failed to authenticate with partner service") return self.session.get(response.headers['Location'])
def _finish_auth(self, auth_type, auth_data, otc, proof_confirmation): """ Finish the Two-Factor-Authentication. If it succeeds we are provided with Access and Refresh-Token. Args: auth_type (TwoFactorAuthMethods): Member of :class:`TwoFactorAuthMethods` auth_data (str/NoneType): Authentication data for this provided, specific authorization method otc (str/NoneType): One-Time-Code, required for every method except MS Authenticator v2 proof_confirmation (str/NoneType): Confirmation of Email or mobile phone number, if that method was chosen Returns: requests.Response: Instance of :class:`requests.Response` """ if TwoFactorAuthMethods.SMS == auth_type or \ TwoFactorAuthMethods.Voice == auth_type or \ TwoFactorAuthMethods.Email == auth_type: post_type = '18' general_verify = False elif TwoFactorAuthMethods.TOTPAuthenticator == auth_type: post_type = '19' general_verify = False elif TwoFactorAuthMethods.TOTPAuthenticatorV2 == auth_type: post_type = '22' general_verify = None else: raise AuthenticationException('Unhandled case for submitting OTC') post_data = { 'login': self.email, 'PPFT': self.flowtoken, 'SentProofIDE': auth_data, 'sacxt': '1', 'saav': '0', 'GeneralVerify': general_verify, 'type': post_type, 'purpose': 'eOTT_OneTimePassword', 'i18': '__DefaultSAStrings|1,__DefaultSA_Core|1,__DefaultSA_Wizard|1' } if otc: post_data.update(dict(otc=otc)) if self.session_lookup_key: post_data.update(dict(slk=self.session_lookup_key)) if proof_confirmation: post_data.update(dict(ProofConfirmation=proof_confirmation)) return self.session.post(self.post_url, data=post_data, allow_redirects=False)
def _request_otc(self, auth_type, proof, auth_data): """ Request OTC (One-Time-Code) if 2FA via Email, Mobile phone or MS Authenticator v2 is desired. Args: auth_type (TwoFactorAuthMethods): Member of :class:`TwoFactorAuthMethods` proof (str/NoneType): Proof Verification, used by mobile phone and email-method, for MS Authenticator provide `None` auth_data (str): Authentication data for this provided, specific authorization method Raises: AuthenticationException: If requested 2FA Authentication Type is unsupported Returns: requests.Response: Instance of :class:`requests.Response` """ get_onetime_code_url = 'https://login.live.com/pp1600/GetOneTimeCode.srf' if TwoFactorAuthMethods.Email == auth_type: channel = 'Email' post_field = 'AltEmailE' elif TwoFactorAuthMethods.SMS == auth_type: channel = 'SMS' post_field = 'MobileNumE' elif TwoFactorAuthMethods.Voice == auth_type: channel = 'Voice' post_field = 'MobileNumE' elif TwoFactorAuthMethods.TOTPAuthenticatorV2 == auth_type: channel = 'PushNotifications' post_field = 'SAPId' else: raise AuthenticationException( 'Unsupported TwoFactor Auth-Type: %s' % TwoFactorAuthentication(auth_type)) post_data = { 'login': self.email, 'flowtoken': self.flowtoken, 'purpose': 'eOTT_OneTimePassword', 'UIMode': '11', 'channel': channel, post_field: auth_data, } if proof: post_data.update(dict(ProofConfirmation=proof)) return self.session.post(get_onetime_code_url, data=post_data, allow_redirects=False)
def _xbox_live_authenticate(self, access_token): """ Internal method to authenticate with Xbox Live, called by `self.authenticate` Args: access_token (:class:`AccessToken`): Access token Raises: AuthenticationException: When provided Access-Token is invalid Returns: object: If authentication succeeds, returns :class:`UserToken` """ if not access_token or not access_token.is_valid: raise AuthenticationException("No valid AccessToken") json_data = self.__xbox_live_authenticate_request(access_token).json() return UserToken(json_data['Token'], json_data['IssueInstant'], json_data['NotAfter'])
async def request_xsts_token(self, relying_party: str = "http://xboxlive.com" ) -> XSTSResponse: """Authorize via user token and receive final X token.""" url = "https://xsts.auth.xboxlive.com/xsts/authorize" headers = {"x-xbl-contract-version": "1"} data = { "RelyingParty": relying_party, "TokenType": "JWT", "Properties": { "UserTokens": [self.user_token.token], "SandboxId": "RETAIL", }, } resp = await self.session.post(url, json=data, headers=headers) if (resp.status == 401): # if unauthorized print( 'Failed to authorize you! Your password or username may be wrong or you are trying to use child account (< 18 years old)' ) raise AuthenticationException() resp.raise_for_status() return XSTSResponse.parse_raw(await resp.text())
def _poll_session_state(self): """ Poll MS Authenticator v2 SessionState. Polling happens for maximum of 120 seconds if Authorization is not approved by the Authenticator App. It will return earlier if request gets approved/rejected. Returns: AuthSessionState: Current Session State """ polling_url = None for k, v in self.server_data.items(): if isinstance(v, str) and v.startswith( 'https://login.live.com/GetSessionState.srf'): polling_url = v if not polling_url: raise AuthenticationException( 'Cannot find polling URL for TOTPv2 session state') max_time_seconds = 120.0 time_now = time.time() time_end = time_now + max_time_seconds params = dict(slk=self.session_lookup_key) log.info('Polling Authenticator v2 Verification for {} seconds'.format( max_time_seconds)) session_state = AuthSessionState.PENDING while time_now < time_end: gif_resp = self.session.get(polling_url, params=params) session_state = self.verify_authenticator_v2_gif(gif_resp) time.sleep(1) time_now = time.time() if session_state != AuthSessionState.PENDING: break return session_state
def __window_live_authenticate_request(self, email, password): """ Authenticate with Windows Live Server. First, the Base-URL gets queried by HTTP-GET from a static URL. The resulting response holds a javascript-object containing Post-URL and PPFT parameter - both get used by the following HTTP-POST to attempt authentication by sending user-credentials in the POST-data. If the final POST-Response holds a 'Location' field in it's headers, the authentication can be considered successful and Access-/Refresh-Token are available. Args: email (str): Microsoft account email-address password (str): Corresponding password Returns: requests.Response: Response of the final POST-Request """ authorization_url = AuthenticationManager.generate_authorization_url() resp = self.session.get(authorization_url, allow_redirects=False) if resp.status_code == 302 and \ resp.headers['Location'].startswith('https://login.live.com/oauth20_desktop.srf'): # We are already authenticated by cached cookies return resp # Extract ServerData javascript-object via regex, convert it to proper JSON server_data = self.extract_js_object(resp.content, "ServerData") # Extract PPFT value (flowtoken) ppft = server_data.get('sFTTag') ppft = minidom.parseString(ppft).getElementsByTagName("input")[0].getAttribute("value") credential_type_url = None for k, v in server_data.items(): if isinstance(v, str) and v.startswith('https://login.live.com/GetCredentialType.srf'): credential_type_url = v if not credential_type_url: raise AuthenticationException('Did not find GetCredentialType URL') post_data = { 'username': email, 'uaid': self.session.cookies['uaid'], 'isOtherIdpSupported': False, 'checkPhones': False, 'isRemoteNGCSupported': True, 'isCookieBannerShown': False, 'isFidoSupported': False, 'flowToken': ppft } resp = self.session.post(credential_type_url, json=post_data, headers=dict(Referer=resp.url)) credential_type = resp.json() post_data = { 'login': email, 'passwd': password, 'PPFT': ppft, 'PPSX': 'Passpor', 'SI': 'Sign in', 'type': '11', 'NewUser': '******', 'LoginOptions': '1' } if 'Credentials' not in credential_type: raise AuthenticationException('Did not find Credentials in CredentialType respose, auth likely failed!') elif credential_type['Credentials']['HasRemoteNGC'] == 1: ngc_params = credential_type['Credentials']['RemoteNgcParams'] post_data.update({ 'ps': 2, 'psRNGCEntropy': ngc_params['SessionIdentifier'], 'psRNGCDefaultType': ngc_params['DefaultType'] }) return self.session.post(server_data.get('urlPost'), data=post_data, allow_redirects=False)
def authenticate(self, do_refresh=True): """ Authenticate with Xbox Live using either tokens or user credentials. Args: do_refresh (bool): Refresh Access- and Refresh Token even if still valid, default: True Raises: AuthenticationException: When neither token and credential authentication is successful TwoFactorAuthRequired: If 2FA is required for this account """ full_authentication_required = False try: # Refresh and Access Token if not do_refresh and self.access_token and self.refresh_token and \ self.access_token.is_valid and self.refresh_token.is_valid: pass else: self.access_token, self.refresh_token = self._windows_live_token_refresh(self.refresh_token) # User Token if self.user_token and self.user_token.is_valid: pass else: self.user_token = self._xbox_live_authenticate(self.access_token) ''' TODO: Fix # Device Token if ts.device_token and ts.device_token.is_valid: pass else: ts.device_token = self._xbox_live_device_auth(ts.access_token) # Title Token if ts.title_token and ts.title_token.is_valid: pass else: ts.title_token = self._xbox_live_title_auth(ts.device_token, ts.access_token) ''' # XSTS Token if self.xsts_token and self.xsts_token.is_valid and self.userinfo: pass else: self.xsts_token, self.userinfo = self._xbox_live_authorize(self.user_token) except AuthenticationException as e: log.warning('Token Auth failed: %s. Attempting auth via credentials' % e) full_authentication_required = True # Authentication via credentials if full_authentication_required and self.email_address and self.password: log.info('Attempting user credentials auth') self.access_token, self.refresh_token = self._windows_live_authenticate(self.email_address, self.password) self.user_token = self._xbox_live_authenticate(self.access_token) ''' TODO: Fix ts.device_token = self._xbox_live_device_auth(ts.access_token) ts.title_token = self._xbox_live_title_auth(ts.device_token, ts.access_token) ''' self.xsts_token, self.userinfo = self._xbox_live_authorize(self.user_token) if not self.authenticated: raise AuthenticationException("AuthenticationManager was not able to authenticate " "with provided tokens or user credentials!")