Experimental two factor authentication support - toot - Unnamed repository; edi… | |
Log | |
Files | |
Refs | |
LICENSE | |
--- | |
commit 7886199295bda15965999daa736a5e8e165e01d6 | |
parent 3f44d560c8c1159df2cf318cc71a9d235fde6f55 | |
Author: Ivan Habunek <[email protected]> | |
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(-) | |
--- | |
diff --git a/README.rst b/README.rst | |
@@ -33,7 +33,8 @@ Running ``toot <command> -h`` shows the documentation for the… | |
=================== =========================================================… | |
Command Description | |
=================== =========================================================… | |
- ``toot login`` Log into a Mastodon instance, saves access keys for late… | |
+ ``toot login`` Log into a Mastodon instance. | |
+ ``toot 2fa`` Log into a Mastodon instance using two factor authentica… | |
``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`… | |
+ | |
+.. 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-Mas… | |
The application and user access tokens will be saved in two files in your home… | |
-* ``~/.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: | |
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) | |
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 tok… | |
+ print(" toot login - log into a Mastodon instance") | |
+ print(" toot 2fa - log into a Mastodon instance using 2FA (experim… | |
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 … | |
+ 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) | |