From d9d1a2fe3541c6c713a872c00361ef8e8bebe6b9 Mon Sep 17 00:00:00 2001 From: Erik Lopez Date: Tue, 13 Aug 2019 21:41:45 +0900 Subject: [PATCH 1/2] Update 'ajax id' regular expression --- instpector/apis/instagram/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instpector/apis/instagram/utilities.py b/instpector/apis/instagram/utilities.py index a625eee..83b76ea 100644 --- a/instpector/apis/instagram/utilities.py +++ b/instpector/apis/instagram/utilities.py @@ -1,7 +1,7 @@ import re def get_ajax_id(content): - match = re.findall(r"\"rollout_hash\":\"([\w]+)\"", content) + match = re.findall(r"\"rollout_hash\":\"([\w-]+)\"", content) if match: return match[0] return "" From 72d143dcf25a6dbb197bddafba01240e60851c84 Mon Sep 17 00:00:00 2001 From: Erik Lopez Date: Tue, 13 Aug 2019 23:12:58 +0900 Subject: [PATCH 2/2] Add two-factor authentication support --- README.md | 18 ++++++++- examples/two_factor_auth.py | 34 ++++++++++++++++ instpector/apis/instagram/authenticate.py | 47 +++++++++++++++-------- instpector/instpector.py | 7 ++-- setup.py | 2 +- tests/conftest.py | 6 ++- 6 files changed, 92 insertions(+), 22 deletions(-) create mode 100644 examples/two_factor_auth.py diff --git a/README.md b/README.md index 4915687..24ba452 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Instpector -A simple Instagram's web API library written in Python. No selenium or webdriver required. +A simple Instagram's web API library written in Python. No selenium or webdriver required. + +Login with two-factor authentication enabled is supported. # Installation @@ -34,6 +36,20 @@ for follower in followers.of_user(insta_profile.id): instpector.logout() ``` +## Using 2FA +For login in using two-factor authentication, generate your 2fa key on Instagram's app and provide the code when logging in with `instpector`. The following example uses `pytop` to demonstrate the usage: + +```python +from pyotp import TOTP +from instpector import Instpector, endpoints + +instpector = Instpector() +totp = TOTP("my_2fa_key") # Input without spaces + +# Login into Instagram's web +instpector.login("my_username", "my_password", totp.now()) +``` + Check more in the `examples` directory. # Available endpoints diff --git a/examples/two_factor_auth.py b/examples/two_factor_auth.py new file mode 100644 index 0000000..d31338f --- /dev/null +++ b/examples/two_factor_auth.py @@ -0,0 +1,34 @@ +from sys import argv +from pyotp import TOTP +from context import Instpector, endpoints + +def get_profile(**options): + + instpector = Instpector() + # Using pyotp for getting the two-factor authentication code + totp = TOTP(options.get("two_factor_key")) + if not instpector.login(user=options.get("user"), + password=options.get("password"), + two_factor_code=totp.now()): + return + + profile = endpoints.factory.create("profile", instpector) + + print(profile.of_user("dwgran")) + + instpector.logout() + +if __name__ == '__main__': + if len(argv) < 6: + print(( + "Missing arguments: " + "--user {user} " + "--password {password} " + "--two_factor_key {two_factor_key}" + )) + exit(1) + get_profile( + user=argv[2], + password=argv[4], + two_factor_key=argv[6] + ) diff --git a/instpector/apis/instagram/authenticate.py b/instpector/apis/instagram/authenticate.py index 2f796d3..2faa242 100644 --- a/instpector/apis/instagram/authenticate.py +++ b/instpector/apis/instagram/authenticate.py @@ -6,10 +6,11 @@ class Authenticate(HttpRequest): - def __init__(self, browser_session, user, password, app_info=None): + def __init__(self, browser_session, user, password, two_factor_code): self._user = user self._password = password - self._app_info = app_info + self._two_factor_code = two_factor_code + self._app_info = None self._auth_headers = {} self._auth_cookies = {} super().__init__("https://www.instagram.com", browser_session) @@ -59,12 +60,11 @@ def _login_prepare(self): if not response: return self._auth_headers = response.cookies.get_dict(".instagram.com") - if not self._app_info: - app_id, ajax_id = self._lookup_headers() - self._app_info = { - "ig_app_id": app_id, - "ig_ajax_id": ajax_id - } + app_id, ajax_id = self._lookup_headers() + self._app_info = { + "ig_app_id": app_id, + "ig_ajax_id": ajax_id + } def _login_execute(self): data = { @@ -73,6 +73,18 @@ def _login_execute(self): "queryParams": "{\"source\":\"auth_switcher\"}", "optIntoOneTap": "true" } + self._attempt_login("/accounts/login/ajax/", data, self._two_factor_code is not None) + + def _login_two_factor(self, identifier): + data = { + "username": self._user, + "verificationCode": self._two_factor_code, + "queryParams": "{\"source\":\"auth_switcher\", \"next\":\"/\"}", + "identifier": identifier + } + self._attempt_login("/accounts/login/ajax/two_factor/", data) + + def _attempt_login(self, url, data, use_2fa=False): headers = { "Referer": "https://www.instagram.com/accounts/login/?source=auth_switcher", "X-CSRFToken": self._auth_headers.get("csrftoken"), @@ -80,20 +92,25 @@ def _login_execute(self): "X-IG-App-ID": self._app_info.get("ig_app_id", ""), "Content-Type": "application/x-www-form-urlencoded", } - response = self.post("/accounts/login/ajax/", + response = self.post(url, data=data, headers=headers, redirects=True) try: data = json.loads(response.text) self._auth_cookies = response.cookies.get_dict(".instagram.com") - return self._parse_login(data) + self._parse_login(data, use_2fa) except (json.decoder.JSONDecodeError, AttributeError): raise AuthenticateFailException("Unexpected login response.") - def _parse_login(self, data): - if data and data.get("authenticated"): - user_id = data.get("userId") - print(f"Logged in as {self._user} (Id: {user_id})") - return + def _parse_login(self, data, use_2fa): + if data: + if data.get("authenticated"): + user_id = data.get("userId") + print(f"Logged in as {self._user} (Id: {user_id})") + return + if use_2fa and data.get("two_factor_required")\ + and data.get("two_factor_info"): + self._login_two_factor(data.get("two_factor_info").get("two_factor_identifier")) + return raise AuthenticateFailException("Login unsuccessful") diff --git a/instpector/instpector.py b/instpector/instpector.py index 151f514..b0eecc0 100644 --- a/instpector/instpector.py +++ b/instpector/instpector.py @@ -3,9 +3,8 @@ from .apis.exceptions import AuthenticateFailException, AuthenticateRevokeException class Instpector: - def __init__(self, app_info=None): + def __init__(self): self._auth = None - self._app_info = app_info self._browser_session = requests.session() def __del__(self): @@ -15,8 +14,8 @@ def __del__(self): def session(self): return self._browser_session - def login(self, user, password): - self._auth = Authenticate(self._browser_session, user, password, self._app_info) + def login(self, user, password, two_factor_code=None): + self._auth = Authenticate(self._browser_session, user, password, two_factor_code) try: self._auth.login() return True diff --git a/setup.py b/setup.py index 9eda3bc..0380338 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="instpector", - version="0.1.6", + version="0.2.0", description="A simple Instagram's web API library", author="Erik Lopez", long_description=README, diff --git a/tests/conftest.py b/tests/conftest.py index 51072d6..4a96256 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import configparser import atexit import pytest +from pyotp import TOTP from instpector import Instpector CONFIG = configparser.ConfigParser() @@ -8,7 +9,10 @@ INSTANCE = Instpector() USERNAME = CONFIG["account"]["username"] -INSTANCE.login(user=USERNAME, password=CONFIG["account"]["password"]) +TOTP_INSTANCE = TOTP(CONFIG["account"]["tf_key"]) +INSTANCE.login(user=USERNAME, + password=CONFIG["account"]["password"], + two_factor_code=TOTP_INSTANCE.now()) def on_finish(): if INSTANCE: