Skip to content

Commit f22cee7

Browse files
committed
Unable to login using 2FA [#276] Added TOTP, TOTPMixin and TOTPTestCase
1 parent e40d33a commit f22cee7

File tree

6 files changed

+154
-4
lines changed

6 files changed

+154
-4
lines changed

instagrapi/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626
from instagrapi.mixins.story import StoryMixin
2727
from instagrapi.mixins.timeline import ReelsMixin
28+
from instagrapi.mixins.totp import TOTPMixin
2829
from instagrapi.mixins.user import UserMixin
2930
from instagrapi.mixins.video import DownloadVideoMixin, UploadVideoMixin
3031

@@ -59,6 +60,7 @@ class Client(
5960
UploadClipMixin,
6061
ReelsMixin,
6162
BloksMixin,
63+
TOTPMixin,
6264
):
6365
proxy = None
6466
logger = logging.getLogger("instagrapi")

instagrapi/mixins/account.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ def account_change_picture(self, path: Path) -> UserShort:
162162
return extract_user_short(result["user"])
163163

164164
def news_inbox_v1(self, mark_as_seen: bool = False) -> dict:
165-
"""Get old and new stories as is
165+
"""
166+
Get old and new stories as is
166167
167168
Parameters
168169
----------

instagrapi/mixins/auth.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -334,12 +334,12 @@ def login(self, username: str, password: str, relogin: bool = False, verificatio
334334
----------
335335
username: str
336336
Instagram Username
337-
338337
password: str
339338
Instagram Password
340-
341339
relogin: bool
342340
Whether or not to re login, default False
341+
verification_code: str
342+
2FA verification code
343343
344344
Returns
345345
-------

instagrapi/mixins/totp.py

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import base64
2+
import datetime
3+
import hashlib
4+
import hmac
5+
import time
6+
from typing import Any, List, Optional
7+
8+
9+
class TOTP:
10+
"""
11+
Base class for OTP handlers.
12+
"""
13+
def __init__(self, s: str, digits: int = 6, digest: Any = hashlib.sha1, name: Optional[str] = None,
14+
issuer: Optional[str] = None) -> None:
15+
self.digits = digits
16+
self.digest = digest
17+
self.secret = s
18+
self.name = name or 'Secret'
19+
self.issuer = issuer
20+
self.interval = 30
21+
22+
def generate_otp(self, input: int) -> str:
23+
"""
24+
:param input: the HMAC counter value to use as the OTP input.
25+
Usually either the counter, or the computed integer based on the Unix timestamp
26+
"""
27+
if input < 0:
28+
raise ValueError('input must be positive integer')
29+
hasher = hmac.new(self.byte_secret(), self.int_to_bytestring(input), self.digest)
30+
hmac_hash = bytearray(hasher.digest())
31+
offset = hmac_hash[-1] & 0xf
32+
code = ((hmac_hash[offset] & 0x7f) << 24 |
33+
(hmac_hash[offset + 1] & 0xff) << 16 |
34+
(hmac_hash[offset + 2] & 0xff) << 8 |
35+
(hmac_hash[offset + 3] & 0xff))
36+
str_code = str(code % 10 ** self.digits)
37+
while len(str_code) < self.digits:
38+
str_code = '0' + str_code
39+
return str_code
40+
41+
def byte_secret(self) -> bytes:
42+
secret = self.secret
43+
missing_padding = len(secret) % 8
44+
if missing_padding != 0:
45+
secret += '=' * (8 - missing_padding)
46+
return base64.b32decode(secret, casefold=True)
47+
48+
@staticmethod
49+
def int_to_bytestring(i: int, padding: int = 8) -> bytes:
50+
"""
51+
Turns an integer to the OATH specified
52+
bytestring, which is fed to the HMAC
53+
along with the secret
54+
"""
55+
result = bytearray()
56+
while i != 0:
57+
result.append(i & 0xFF)
58+
i >>= 8
59+
# It's necessary to convert the final result from bytearray to bytes
60+
# because the hmac functions in python 2.6 and 3.3 don't work with
61+
# bytearray
62+
return bytes(bytearray(reversed(result)).rjust(padding, b'\0'))
63+
64+
def code(self):
65+
"""
66+
Generate TOTP code
67+
"""
68+
now = datetime.datetime.now()
69+
timecode = int(time.mktime(now.timetuple()) / self.interval)
70+
return self.generate_otp(timecode)
71+
72+
73+
class TOTPMixin:
74+
75+
def totp_generate_seed(self) -> str:
76+
"""
77+
Generate 2FA TOTP seed
78+
79+
Returns
80+
-------
81+
str
82+
TOTP seed (token, secret key)
83+
"""
84+
result = self.private_request(
85+
"accounts/generate_two_factor_totp_key/",
86+
data=self.with_default_data({})
87+
)
88+
return result["totp_seed"]
89+
90+
def totp_enable(self, verification_code: str) -> List[str]:
91+
"""
92+
Enable TOTP 2FA
93+
94+
Parameters
95+
----------
96+
verification_code: str
97+
2FA verification code
98+
99+
Returns
100+
-------
101+
List[str]
102+
Backup codes
103+
"""
104+
result = self.private_request(
105+
"accounts/enable_totp_two_factor",
106+
data=self.with_default_data({'verification_code': verification_code})
107+
)
108+
return result["backup_codes"]
109+
110+
def totp_disable(self) -> bool:
111+
"""
112+
Disable TOTP 2FA
113+
114+
Returns
115+
-------
116+
bool
117+
"""
118+
result = self.private_request(
119+
"accounts/disable_totp_two_factor",
120+
data=self.with_default_data({})
121+
)
122+
return result["status"] == "ok"
123+
124+
def totp_generate_code(self, seed: str) -> str:
125+
"""
126+
Disable TOTP 2FA
127+
128+
Parameters
129+
----------
130+
seed: str
131+
TOTP seed (token, secret key)
132+
133+
Returns
134+
-------
135+
str
136+
TOTP code
137+
"""
138+
return TOTP(seed).code()

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
setup(
3232
name='instagrapi',
33-
version='1.11.1',
33+
version='1.12.0',
3434
author='Mikhail Andreev',
3535
author_email='[email protected]',
3636
license='MIT',

tests.py

+9
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,15 @@ def test_story_info(self):
14471447
# }
14481448
# self.assertTrue(self.api.bloks_change_password("2r9j20r9j4230t8hj39tHW4"))
14491449

1450+
class TOTPTestCase(ClientPrivateTestCase):
1451+
1452+
def test_totp_code(self):
1453+
seed = self.api.totp_generate_seed()
1454+
code = self.api.totp_generate_code(seed)
1455+
self.assertIsInstance(code, str)
1456+
self.assertTrue(code.isdigit())
1457+
self.assertEqual(len(code), 6)
1458+
14501459

14511460
if __name__ == '__main__':
14521461
unittest.main()

0 commit comments

Comments
 (0)