Experimental two factor authentication support - toot - Unnamed repository; edit this file 'description' to name the repository.
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) LICENSE
       ---
 (DIR) commit 7886199295bda15965999daa736a5e8e165e01d6
 (DIR) parent 3f44d560c8c1159df2cf318cc71a9d235fde6f55
 (HTM) Author: Ivan Habunek <ivan@habunek.com>
       Date:   Tue, 18 Apr 2017 16:40:26 +0200
       
       Experimental two factor authentication support
       
       issue #3
       
       Diffstat:
         README.rst                          |      11 +++++++++--
         toot/api.py                         |       2 +-
         toot/console.py                     |      83 +++++++++++++++++++++++++++++--
       
       3 files changed, 90 insertions(+), 6 deletions(-)
       ---
 (DIR) diff --git a/README.rst b/README.rst
       @@ -33,7 +33,8 @@ Running ``toot <command> -h`` shows the documentation for the given command.
        ===================  ===============================================================
         Command              Description
        ===================  ===============================================================
       - ``toot login``       Log into a Mastodon instance, saves access keys for later use.
       + ``toot login``       Log into a Mastodon instance.
       + ``toot 2fa``         Log into a Mastodon instance using two factor authentication.
         ``toot logout``      Log out, deletes stored access keys.
         ``toot auth``        Display stored authenitication tokens.
         ``toot whoami``      Display logged in user details.
       @@ -53,13 +54,19 @@ Before tooting, you need to login to a Mastodon instance:
        
            toot login
        
       +**Two factor authentication** is supported experimentally, instead of ``login``, you should instead run:
       +
       +.. code-block::
       +
       +    toot 2fa
       +
        You will be asked to chose an instance_ and enter your credentials.
        
        .. _instance: https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md
        
        The application and user access tokens will be saved in two files in your home directory:
        
       -* ``~/.config/toot/app.cfg``
       +* ``~/.config/toot/instances/<name>`` - created for each mastodon instance once
        * ``~/.config/toot/user.cfg``
        
        You can check whether you are currently logged in:
 (DIR) diff --git a/toot/api.py b/toot/api.py
       @@ -118,7 +118,7 @@ def login(app, username, password):
        
            # If auth fails, it redirects to the login page
            if response.is_redirect:
       -        raise AuthenticationError("Login failed")
       +        raise AuthenticationError()
        
            return _process_response(response)
        
 (DIR) diff --git a/toot/console.py b/toot/console.py
       @@ -2,17 +2,19 @@
        from __future__ import unicode_literals
        from __future__ import print_function
        
       +import json
       +import logging
        import os
       +import requests
        import sys
       -import logging
        
       +from argparse import ArgumentParser, FileType
        from bs4 import BeautifulSoup
        from builtins import input
        from datetime import datetime
        from future.moves.itertools import zip_longest
        from getpass import getpass
        from itertools import chain
       -from argparse import ArgumentParser, FileType
        from textwrap import TextWrapper
        
        from toot import api, config, DEFAULT_INSTANCE, User, App
       @@ -89,11 +91,65 @@ def login_interactive(app):
            return user
        
        
       +def two_factor_login_interactive(app):
       +    """Hacky implementation of two factor authentication"""
       +
       +    print("Log in to " + green(app.instance))
       +    email = input('Email: ')
       +    password = getpass('Password: ')
       +
       +    sign_in_url = app.base_url + '/auth/sign_in'
       +
       +    session = requests.Session()
       +
       +    # Fetch sign in form
       +    response = session.get(sign_in_url)
       +    response.raise_for_status()
       +
       +    soup = BeautifulSoup(response.content, "html.parser")
       +    form = soup.find('form')
       +    inputs = form.find_all('input')
       +
       +    data = {i.attrs.get('name'): i.attrs.get('value') for i in inputs}
       +    data['user[email]'] = email
       +    data['user[password]'] = password
       +
       +    # Submit form, get 2FA entry form
       +    response = session.post(sign_in_url, data)
       +    response.raise_for_status()
       +
       +    soup = BeautifulSoup(response.content, "html.parser")
       +    form = soup.find('form')
       +    inputs = form.find_all('input')
       +
       +    data = {i.attrs.get('name'): i.attrs.get('value') for i in inputs}
       +    data['user[otp_attempt]'] = input("2FA Token: ")
       +
       +    # Submit token
       +    response = session.post(sign_in_url, data)
       +    response.raise_for_status()
       +
       +    # Extract access token from response
       +    soup = BeautifulSoup(response.content, "html.parser")
       +    initial_state = soup.find('script', id='initial-state')
       +
       +    if not initial_state:
       +        raise ConsoleError("Login failed: Invalid 2FA token?")
       +
       +    data = json.loads(initial_state.get_text())
       +    access_token = data['meta']['access_token']
       +
       +    user = User(app.instance, email, access_token)
       +    path = config.save_user(user)
       +    print("Access token saved to: " + green(path))
       +
       +
        def print_usage():
            print("toot - interact with Mastodon from the command line")
            print("")
            print("Usage:")
       -    print("  toot login      - log into a Mastodon instance (stores access tokens)")
       +    print("  toot login      - log into a Mastodon instance")
       +    print("  toot 2fa        - log into a Mastodon instance using 2FA (experimental)")
            print("  toot logout     - log out (delete stored access tokens)")
            print("  toot auth       - display stored authentication tokens")
            print("  toot whoami     - display logged in user details")
       @@ -221,6 +277,24 @@ def cmd_login(args):
            return app, user
        
        
       +def cmd_2fa(args):
       +    parser = ArgumentParser(prog="toot 2fa",
       +                            description="Log into a Mastodon instance using 2 factor authentication (experimental)",
       +                            epilog="https://github.com/ihabunek/toot")
       +    parser.parse_args(args)
       +
       +    print()
       +    print(yellow("Two factor authentication is experimental."))
       +    print(yellow("If you have problems logging in, please open an issue:"))
       +    print(yellow("https://github.com/ihabunek/toot/issues"))
       +    print()
       +
       +    app = create_app_interactive()
       +    user = two_factor_login_interactive(app)
       +
       +    return app, user
       +
       +
        def cmd_logout(app, user, args):
            parser = ArgumentParser(prog="toot logout",
                                    description="Log out, delete stored access keys",
       @@ -363,6 +437,9 @@ def run_command(command, args):
            if command == 'login':
                return cmd_login(args)
        
       +    if command == '2fa':
       +        return cmd_2fa(args)
       +
            if command == 'auth':
                return cmd_auth(app, user, args)