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)