Пример #1
0
    def _refresh_oauth2(self) -> None:
        """Refresh an oauth2 token.

        Raises:
            AuthenticationError: If refresh_token is missing or if there is an error
                when trying to refresh a token.

        """
        if self.refresh_token is None:
            raise AuthenticationError(
                "Cannot refresh login with unset refresh token")
        relogin_payload = {
            "grant_type": "refresh_token",
            "refresh_token": self.refresh_token,
            "scope": "internal",
            "client_id": CLIENT_ID,
            "expires_in": EXPIRATION_TIME,
        }
        self.session.headers.pop("Authorization", None)
        res = self.post(OAUTH_TOKEN_URL,
                        data=relogin_payload,
                        auto_login=False,
                        raise_errors=False)
        if "error" in res:
            raise AuthenticationError("Failed to refresh token")
        self._process_auth_body(res)
Пример #2
0
    def _refresh_oauth2(self) -> None:
        """Refresh an oauth2 token.

        Raises:
            AuthenticationError: If refresh_token is missing or if there is an error
                when trying to refresh a token.

        """
        if not self.oauth.is_valid:
            raise AuthenticationError(
                "Cannot refresh login with unset refresh token")
        relogin_payload = {
            "grant_type": "refresh_token",
            "refresh_token": self.oauth.refresh_token,
            "scope": "internal",
            "client_id": CLIENT_ID,
            "expires_in": EXPIRATION_TIME,
        }
        self.session.headers.pop("Authorization", None)
        try:
            oauth = self.post(
                urls.OAUTH,
                data=relogin_payload,
                auto_login=False,
                schema=OAuthSchema(),
            )
        except HTTPError:
            raise AuthenticationError("Failed to refresh token")

        self._configure_manager(oauth)
Пример #3
0
    def _challenge_oauth2(self, oauth: OAuth, oauth_payload: JSON) -> OAuth:
        """Process the ouath challenge flow.

        Args:
            oauth: An oauth response model from a login request.
            oauth_payload: The payload to use once the challenge has been processed.

        Returns:
            An OAuth response model from the login request.

        Raises:
            AuthenticationError: If there is an error in the initial challenge response.

        .. # noqa: DAR202
        .. https://github.com/terrencepreilly/darglint/issues/81

        """
        # login challenge
        challenge_url = urls.build_challenge(oauth.challenge.id)
        print(
            f"Input challenge code from {oauth.challenge.type.capitalize()} "
            f"({oauth.challenge.remaining_attempts}/"
            f"{oauth.challenge.remaining_retries}):"
        )
        challenge_code = input()
        challenge_payload = {"response": str(challenge_code)}
        challenge_header = CaseInsensitiveDict(
            {"X-ROBINHOOD-CHALLENGE-RESPONSE-ID": str(oauth.challenge.id)}
        )
        oauth_inner, res = self.post(
            challenge_url,
            data=challenge_payload,
            raise_errors=False,
            headers=challenge_header,
            auto_login=False,
            return_response=True,
            schema=OAuthSchema(),
        )
        if res.status_code == requests.codes.ok:
            try:
                # the cast is required for mypy
                return cast(
                    OAuth,
                    self.post(
                        urls.OAUTH,
                        data=oauth_payload,
                        headers=challenge_header,
                        auto_login=False,
                        schema=OAuthSchema(),
                    ),
                )
            except HTTPError:
                raise AuthenticationError("Error in finalizing auth token")
        elif oauth_inner.is_challenge and oauth_inner.challenge.can_retry:
            print("Invalid code entered")
            return self._challenge_oauth2(oauth, oauth_payload)
        else:
            raise AuthenticationError("Exceeded available attempts or code expired")
Пример #4
0
    def _login_oauth2(self) -> None:
        """Create a new oauth2 token.

        Raises:
            AuthenticationError: If the login credentials are not set, if a challenge
                wasn't accepted, or if an mfa code is not accepted.

        """
        self.session.headers.pop("Authorization", None)
        oauth_payload = {
            "password": self.password,
            "username": self.username,
            "grant_type": "password",
            "client_id": CLIENT_ID,
            "expires_in": EXPIRATION_TIME,
            "scope": "internal",
            "device_token": "4cf700d5-933b-4aff-86b5-33ddd0e93cb4"
        }
        if self.challenge_type != "none":
            oauth_payload = {
                "password": self.password,
                "username": self.username,
                "grant_type": "password",
                "client_id": CLIENT_ID,
                "expires_in": EXPIRATION_TIME,
                "scope": "internal",
                "device_token": self.device_token,
                "challenge_type": self.challenge_type
            }

        oauth = self.post(
            urls.OAUTH,
            data=oauth_payload,
            raise_errors=False,
            auto_login=False,
            schema=OAuthSchema(),
        )

        if oauth.is_challenge:
            oauth = self._challenge_oauth2(oauth, oauth_payload)
        elif oauth.is_mfa:
            oauth = self._mfa_oauth2(oauth_payload)

        if not oauth.is_valid:
            if hasattr(oauth, "error"):
                msg = f"{oauth.error}"
            elif hasattr(oauth, "detail"):
                msg = f"{oauth.detail}"
            else:
                msg = "Unknown login error"
            raise AuthenticationError(msg)
        else:
            self._configure_manager(oauth)
Пример #5
0
    def logout(self) -> None:
        """Logout from the session.

        Raises:
            AuthenticationError: If there is an error when logging out.

        """
        logout_payload = {"client_id": CLIENT_ID, "token": self.oauth.refresh_token}
        try:
            self.post(urls.OAUTH_REVOKE, data=logout_payload, auto_login=False)
            self.oauth = OAuth()
            self.session.headers.pop("Authorization", None)
        except HTTPError:
            raise AuthenticationError("Could not log out")
Пример #6
0
    def logout(self) -> None:
        """Logout from the session.

        Raises:
            AuthenticationError: If there is an error when logging out.

        """
        logout_payload = {
            "client_id": CLIENT_ID,
            "token": self.refresh_token,
        }
        res = self.post(OAUTH_REVOKE_URL,
                        data=logout_payload,
                        raise_errors=False,
                        auto_login=False)
        if len(res) == 0:
            self.access_token = None
            self.refresh_token = None
        else:
            raise AuthenticationError("Could not log out")
Пример #7
0
    def _process_auth_body(self, res: Dict) -> None:
        """Process an authentication response dictionary.

        This method updates the internal state of the session based on a login or
        token refresh request.

        Args:
            res: A response dictionary from a login request.

        Raises:
            AuthenticationError: If the input dictionary is malformed.

        """
        try:
            self.access_token = res["access_token"]
            self.refresh_token = res["refresh_token"]
            self.expires_at = datetime.now() + timedelta(
                seconds=EXPIRATION_TIME)
            self.session.headers.update(
                {"Authorization": f"Bearer {self.access_token}"})
        except KeyError:
            raise AuthenticationError(
                "Authorization result body missing required responses.")
Пример #8
0
    def _mfa_oauth2(self, oauth_payload: JSON, attempts: int = 3) -> OAuth:
        """Mfa auth flow.

         For people with 2fa.

        Args:
            oauth_payload: JSON payload to send on mfa approval.
            attempts: The number of attempts to allow for mfa approval.

        Returns:
            An OAuth response model object.

        Raises:
            AuthenticationError: If the mfa code is incorrect more than specified \
                number of attempts.

        """
        print(f"Input mfa code:")
        mfa_code = input()
        oauth_payload["mfa_code"] = mfa_code
        oauth, res = self.post(
            urls.OAUTH,
            data=oauth_payload,
            raise_errors=False,
            auto_login=False,
            return_response=True,
            schema=OAuthSchema(),
        )
        attempts -= 1
        if (res.status_code != requests.codes.ok) and (attempts > 0):
            print("Invalid mfa code")
            return self._mfa_oauth2(oauth_payload, attempts)
        elif res.status_code == requests.codes.ok:
            # TODO: Write mypy issue on why this needs to be casted?
            return cast(OAuth, oauth)
        else:
            raise AuthenticationError("Too many incorrect mfa attempts")
Пример #9
0
    def _login_oauth2(self) -> None:
        """Create a new oauth2 token.

        Raises:
            AuthenticationError: If the login credentials are not set, if a challenge
                wasn't accepted, or if an mfa code is not accepted.

        """
        if not self.login_set:
            raise AuthenticationError(
                "Username and password must be passed to constructor or must be loaded "
                "from json")
        self.session.headers.pop("Authorization", None)

        oauth_payload = {
            "password": self.password,
            "username": self.username,
            "grant_type": "password",
            "client_id": CLIENT_ID,
            "expires_in": EXPIRATION_TIME,
            "scope": "internal",
            "device_token": self.device_token,
            "challenge_type": self.challenge_type,
        }

        res = self.post(OAUTH_TOKEN_URL,
                        data=oauth_payload,
                        raise_errors=False,
                        auto_login=False)

        if res is None or "error" in res:
            raise AuthenticationError("Unknown login error")
        elif "detail" in res and any(k in res["detail"]
                                     for k in ["Invalid", "Unable"]):
            raise AuthenticationError(f"{res['detail']}")
        elif "challenge" in res:
            challenge_id = res["challenge"]["id"]
            # TODO: use api module
            challenge_url = (
                f"https://api.robinhood.com/challenge/{challenge_id}/respond/")
            print(
                f"Input challenge code from {self.challenge_type.capitalize()}:"
            )
            challenge_code = input()
            challenge_payload = {"response": str(challenge_code)}
            challenge_header = CaseInsensitiveDict(
                {"X-ROBINHOOD-CHALLENGE-RESPONSE-ID": challenge_id})
            challenge = self.post(
                challenge_url,
                data=challenge_payload,
                raise_errors=False,
                headers=challenge_header,
                auto_login=False,
            )
            if challenge is None or challenge.get("status", "") != "validated":
                raise AuthenticationError(
                    "Challenge response was not accepted")
            res = self.post(
                OAUTH_TOKEN_URL,
                data=oauth_payload,
                headers=challenge_header,
                auto_login=False,
            )
        elif "mfa_required" in res:
            print(f"Input mfa code:")
            mfa_code = input()
            oauth_payload["mfa_code"] = mfa_code
            res = self.post(
                OAUTH_TOKEN_URL,
                data=oauth_payload,
                raise_errors=False,
                auto_login=False,
            )
            if res is None or (res.get("detail", "")
                               == "Please enter a valid code."):
                raise AuthenticationError("Mfa code was not accepted")

        self._process_auth_body(res)