Skip to content

Commit

Permalink
Add two-factor authentication support
Browse files Browse the repository at this point in the history
  • Loading branch information
niuware committed Aug 13, 2019
1 parent d9d1a2f commit 72d143d
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 22 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions examples/two_factor_auth.py
Original file line number Diff line number Diff line change
@@ -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]
)
47 changes: 32 additions & 15 deletions instpector/apis/instagram/authenticate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {
Expand All @@ -73,27 +73,44 @@ 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"),
"X-Instagram-AJAX": self._app_info.get("ig_ajax_id", ""),
"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")
7 changes: 3 additions & 4 deletions instpector/instpector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import configparser
import atexit
import pytest
from pyotp import TOTP
from instpector import Instpector

CONFIG = configparser.ConfigParser()
CONFIG.read("tests/pytest.ini")

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:
Expand Down

0 comments on commit 72d143d

Please sign in to comment.