Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
    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")
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
    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!')
Ejemplo n.º 5
0
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()
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
    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!")
Ejemplo n.º 8
0
    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'])
Ejemplo n.º 9
0
    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)
Ejemplo n.º 10
0
    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)
Ejemplo n.º 11
0
    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'])
Ejemplo n.º 12
0
    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())
Ejemplo n.º 13
0
    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
Ejemplo n.º 14
0
    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)
Ejemplo n.º 15
0
    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!")