1
0
mirror of https://github.com/instaloader/instaloader.git synced 2024-11-04 09:22:29 +01:00

Implement two-factor authentication

Closes #200.
This commit is contained in:
André Koch-Kramer 2018-12-21 21:52:17 +01:00
parent edba6959d9
commit 4ee867c61b
5 changed files with 90 additions and 18 deletions

View File

@ -188,6 +188,10 @@ Exceptions
.. autoexception:: LoginRequiredException .. autoexception:: LoginRequiredException
.. autoexception:: TwoFactorAuthRequiredException
.. versionadded:: 4.2
.. autoexception:: InvalidArgumentException .. autoexception:: InvalidArgumentException
.. autoexception:: BadResponseException .. autoexception:: BadResponseException

View File

@ -8,7 +8,8 @@ from argparse import ArgumentParser, SUPPRESS
from typing import List, Optional from typing import List, Optional
from . import (Instaloader, InstaloaderException, InvalidArgumentException, Post, Profile, ProfileNotExistsException, from . import (Instaloader, InstaloaderException, InvalidArgumentException, Post, Profile, ProfileNotExistsException,
StoryItem, __version__, load_structure_from_file) StoryItem, __version__, load_structure_from_file, TwoFactorAuthRequiredException,
BadCredentialsException)
from .instaloader import get_default_session_filename from .instaloader import get_default_session_filename
from .instaloadercontext import default_user_agent from .instaloadercontext import default_user_agent
@ -84,7 +85,16 @@ def _main(instaloader: Instaloader, targetlist: List[str],
instaloader.context.log("Session file does not exist yet - Logging in.") instaloader.context.log("Session file does not exist yet - Logging in.")
if not instaloader.context.is_logged_in or username != instaloader.test_login(): if not instaloader.context.is_logged_in or username != instaloader.test_login():
if password is not None: if password is not None:
instaloader.login(username, password) try:
instaloader.login(username, password)
except TwoFactorAuthRequiredException:
while True:
try:
code = input("Enter 2FA verification code: ")
instaloader.two_factor_login(code)
break
except BadCredentialsException:
pass
else: else:
instaloader.interactive_login(username) instaloader.interactive_login(username)
instaloader.context.log("Logged in as %s." % username) instaloader.context.log("Logged in as %s." % username)

View File

@ -33,6 +33,10 @@ class LoginRequiredException(InstaloaderException):
pass pass
class TwoFactorAuthRequiredException(InstaloaderException):
pass
class InvalidArgumentException(InstaloaderException): class InvalidArgumentException(InstaloaderException):
pass pass

View File

@ -350,9 +350,20 @@ class Instaloader:
:raises InvalidArgumentException: If the provided username does not exist. :raises InvalidArgumentException: If the provided username does not exist.
:raises BadCredentialsException: If the provided password is wrong. :raises BadCredentialsException: If the provided password is wrong.
:raises ConnectionException: If connection to Instagram failed.""" :raises ConnectionException: If connection to Instagram failed.
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call :meth:`Instaloader.two_factor_login`."""
self.context.login(user, passwd) self.context.login(user, passwd)
def two_factor_login(self, two_factor_code) -> None:
"""Second step of login if 2FA is enabled.
Not meant to be used directly, use :meth:`Instaloader.two_factor_login`.
:raises InvalidArgumentException: No two-factor authentication pending.
:raises BadCredentialsException: 2FA verification code invalid.
.. versionadded:: 4.2"""
self.context.two_factor_login(two_factor_code)
def format_filename(self, item: Union[Post, StoryItem], target: Optional[str] = None): def format_filename(self, item: Union[Post, StoryItem], target: Optional[str] = None):
"""Format filename of a :class:`Post` or :class:`StoryItem` according to ``filename-pattern`` parameter. """Format filename of a :class:`Post` or :class:`StoryItem` according to ``filename-pattern`` parameter.
@ -1041,11 +1052,20 @@ class Instaloader:
:raises ConnectionException: If connection to Instagram failed.""" :raises ConnectionException: If connection to Instagram failed."""
if self.context.quiet: if self.context.quiet:
raise LoginRequiredException("Quiet mode requires given password or valid session file.") raise LoginRequiredException("Quiet mode requires given password or valid session file.")
password = None try:
while password is None: password = None
password = getpass.getpass(prompt="Enter Instagram password for %s: " % username) while password is None:
try: password = getpass.getpass(prompt="Enter Instagram password for %s: " % username)
self.login(username, password) try:
except BadCredentialsException as err: self.login(username, password)
print(err, file=sys.stderr) except BadCredentialsException as err:
password = None print(err, file=sys.stderr)
password = None
except TwoFactorAuthRequiredException:
while True:
try:
code = input("Enter 2FA verification code: ")
self.two_factor_login(code)
break
except BadCredentialsException:
pass

View File

@ -59,6 +59,7 @@ class InstaloaderContext:
self._graphql_page_length = 50 self._graphql_page_length = 50
self.graphql_count_per_slidingwindow = graphql_count_per_slidingwindow or 200 self.graphql_count_per_slidingwindow = graphql_count_per_slidingwindow or 200
self._root_rhx_gis = None self._root_rhx_gis = None
self.two_factor_auth_pending = None
# error log, filled with error() and printed at the end of Instaloader.main() # error log, filled with error() and printed at the end of Instaloader.main()
self.error_log = [] self.error_log = []
@ -180,25 +181,32 @@ class InstaloaderContext:
:raises InvalidArgumentException: If the provided username does not exist. :raises InvalidArgumentException: If the provided username does not exist.
:raises BadCredentialsException: If the provided password is wrong. :raises BadCredentialsException: If the provided password is wrong.
:raises ConnectionException: If connection to Instagram failed.""" :raises ConnectionException: If connection to Instagram failed.
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call :meth:`Instaloader.two_factor_login`."""
import http.client import http.client
# pylint:disable=protected-access # pylint:disable=protected-access
http.client._MAXHEADERS = 200 http.client._MAXHEADERS = 200
session = requests.Session() session = requests.Session()
session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1', session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1',
'ig_vw': '1920', 'csrftoken': '', 'ig_vw': '1920', 'ig_cb': '1', 'csrftoken': '',
's_network': '', 'ds_user_id': ''}) 's_network': '', 'ds_user_id': ''})
session.headers.update(self._default_http_header()) session.headers.update(self._default_http_header())
session.headers.update({'X-CSRFToken': self.get_json('', {})['config']['csrf_token']}) session.get('https://www.instagram.com/web/__mid/')
csrf_token = session.cookies.get_dict()['csrftoken']
session.headers.update({'X-CSRFToken': csrf_token})
# Not using self.get_json() here, because we need to access csrftoken cookie # Not using self.get_json() here, because we need to access csrftoken cookie
self._sleep() self._sleep()
login = session.post('https://www.instagram.com/accounts/login/ajax/', login = session.post('https://www.instagram.com/accounts/login/ajax/',
data={'password': passwd, 'username': user}, allow_redirects=True) data={'password': passwd, 'username': user}, allow_redirects=True)
if login.status_code != 200:
if login.status_code == 400 and login.json().get('two_factor_required', None):
raise ConnectionException("Login error: Two factor authorization not yet supported.")
raise ConnectionException("Login error: {} {}".format(login.status_code, login.reason))
resp_json = login.json() resp_json = login.json()
if resp_json.get('two_factor_required'):
two_factor_session = copy_session(session)
two_factor_session.headers.update({'X-CSRFToken': csrf_token})
two_factor_session.cookies.update({'csrftoken': csrf_token})
self.two_factor_auth_pending = (two_factor_session,
user,
resp_json['two_factor_info']['two_factor_identifier'])
raise TwoFactorAuthRequiredException("Login error: two-factor authentication required.")
if resp_json['status'] != 'ok': if resp_json['status'] != 'ok':
if 'message' in resp_json: if 'message' in resp_json:
raise ConnectionException("Login error: \"{}\" status, message \"{}\".".format(resp_json['status'], raise ConnectionException("Login error: \"{}\" status, message \"{}\".".format(resp_json['status'],
@ -220,6 +228,32 @@ class InstaloaderContext:
self._session = session self._session = session
self.username = user self.username = user
def two_factor_login(self, two_factor_code):
"""Second step of login if 2FA is enabled.
Not meant to be used directly, use :meth:`Instaloader.two_factor_login`.
:raises InvalidArgumentException: No two-factor authentication pending.
:raises BadCredentialsException: 2FA verification code invalid.
.. versionadded:: 4.2"""
if not self.two_factor_auth_pending:
raise InvalidArgumentException("No two-factor authentication pending.")
(session, user, two_factor_id) = self.two_factor_auth_pending
login = session.post('https://www.instagram.com/accounts/login/ajax/two_factor/',
data={'username': user, 'verificationCode': two_factor_code, 'identifier': two_factor_id},
allow_redirects=True)
resp_json = login.json()
if resp_json['status'] != 'ok':
if 'message' in resp_json:
raise BadCredentialsException("Login error: {}".format(resp_json['message']))
else:
raise BadCredentialsException("Login error: \"{}\" status.".format(resp_json['status']))
session.headers.update({'X-CSRFToken': login.cookies['csrftoken']})
self._session = session
self.username = user
self.two_factor_auth_pending = None
def _sleep(self): def _sleep(self):
"""Sleep a short time if self.sleep is set. Called before each request to instagram.com.""" """Sleep a short time if self.sleep is set. Called before each request to instagram.com."""
if self.sleep: if self.sleep: