Implement proper two factor authentication - toot - Unnamed repository; edit th… | |
Log | |
Files | |
Refs | |
LICENSE | |
--- | |
commit 62c4075fe1a8aee25c8b4a06f1521461f84e1596 | |
parent cebc88d3292cbd46d486f97d1114cff6ce879335 | |
Author: Ivan Habunek <[email protected]> | |
Date: Sat, 26 Aug 2017 14:39:53 +0200 | |
Implement proper two factor authentication | |
fixes #19, #23 | |
Diffstat: | |
CHANGELOG.md | 1 + | |
README.rst | 47 +++++++++++++++++-------------- | |
toot/api.py | 36 ++++++++++++++++++++++++++++--- | |
toot/commands.py | 61 ++++++++++++++++++++++++++----- | |
toot/console.py | 31 +++++++++++++++++++------------ | |
5 files changed, 131 insertions(+), 45 deletions(-) | |
--- | |
diff --git a/CHANGELOG.md b/CHANGELOG.md | |
@@ -4,6 +4,7 @@ Changelog | |
**0.13.0 (TBA)** | |
* Allow passing `--instance` and `--email` to login command | |
+* Add `login_browser` command for proper two factor authentication through the… | |
**0.12.0 (2016-05-08)** | |
diff --git a/README.rst b/README.rst | |
@@ -37,29 +37,30 @@ Running ``toot <command> -h`` shows the documentation for t… | |
toot - a Mastodon CLI client | |
Authentication: | |
- toot login Log into a Mastodon instance | |
- toot login_2fa Log in using two factor authentication (experimental) | |
- toot logout Log out, delete stored access keys | |
- toot auth Show stored credentials | |
+ toot login Log into a Mastodon instance, does NOT support two … | |
+ toot login_browser Log in using your browser, supports regular and two… | |
+ toot login_2fa Log in using two factor authentication in the conso… | |
+ toot logout Log out, delete stored access keys | |
+ toot auth Show stored credentials | |
Read: | |
- toot whoami Display logged in user details | |
- toot whois Display account details | |
- toot search Search for users or hashtags | |
- toot timeline Show recent items in your public timeline | |
- toot curses An experimental timeline app. | |
+ toot whoami Display logged in user details | |
+ toot whois Display account details | |
+ toot search Search for users or hashtags | |
+ toot timeline Show recent items in your public timeline | |
+ toot curses An experimental timeline app. | |
Post: | |
- toot post Post a status text to your timeline | |
- toot upload Upload an image or video file | |
+ toot post Post a status text to your timeline | |
+ toot upload Upload an image or video file | |
Accounts: | |
- toot follow Follow an account | |
- toot unfollow Unfollow an account | |
- toot mute Mute an account | |
- toot unmute Unmute an account | |
- toot block Block an account | |
- toot unblock Unblock an account | |
+ toot follow Follow an account | |
+ toot unfollow Unfollow an account | |
+ toot mute Mute an account | |
+ toot unmute Unmute an account | |
+ toot block Block an account | |
+ toot unblock Unblock an account | |
To get help for each command run: | |
toot <command> --help | |
@@ -77,19 +78,23 @@ It is possible to pipe status text into `toot post`, for ex… | |
Authentication | |
-------------- | |
-Before tooting, you need to login to a Mastodon instance: | |
+Before tooting, you need to login to a Mastodon instance. | |
+ | |
+If you don't use two factor authentication you can log in directly from the co… | |
.. code-block:: | |
toot login | |
-**Two factor authentication** is supported experimentally, instead of ``login`… | |
+You will be asked to chose an instance_ and enter your credentials. | |
+ | |
+If you do use **two factor authentication**, you need to log in through your b… | |
.. code-block:: | |
- toot login_2fa | |
+ toot login_browser | |
-You will be asked to chose an instance_ and enter your credentials. | |
+You will be redirected to your Mastodon instance to log in and authorize toot … | |
.. _instance: https://github.com/tootsuite/documentation/blob/master/Using-Mas… | |
diff --git a/toot/api.py b/toot/api.py | |
@@ -4,7 +4,7 @@ import logging | |
import re | |
import requests | |
-from future.moves.urllib.parse import urlparse | |
+from future.moves.urllib.parse import urlparse, urlencode | |
from requests import Request, Session | |
from toot import CLIENT_NAME, CLIENT_WEBSITE | |
@@ -53,10 +53,16 @@ def _process_response(response): | |
_log_response(response) | |
if not response.ok: | |
+ error = "Unknown error" | |
+ | |
try: | |
- error = response.json()['error'] | |
+ data = response.json() | |
+ if "error_description" in data: | |
+ error = data['error_description'] | |
+ elif "error" in data: | |
+ error = data['error'] | |
except: | |
- error = "Unknown error" | |
+ pass | |
if response.status_code == 404: | |
raise NotFoundError(error) | |
@@ -131,6 +137,30 @@ def login(app, username, password): | |
return _process_response(response).json() | |
+def get_browser_login_url(app): | |
+ """Returns the URL for manual log in via browser""" | |
+ return "{}/oauth/authorize/?{}".format(app.base_url, urlencode({ | |
+ "response_type": "code", | |
+ "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", | |
+ "scope": "read write follow", | |
+ "client_id": app.client_id, | |
+ })) | |
+ | |
+ | |
+def request_access_token(app, authorization_code): | |
+ url = app.base_url + '/oauth/token' | |
+ | |
+ response = requests.post(url, { | |
+ 'grant_type': 'authorization_code', | |
+ 'client_id': app.client_id, | |
+ 'client_secret': app.client_secret, | |
+ 'code': authorization_code, | |
+ 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', | |
+ }, allow_redirects=False) | |
+ | |
+ return _process_response(response).json() | |
+ | |
+ | |
def post_status(app, user, status, visibility='public', media_ids=None): | |
return _post(app, user, '/api/v1/statuses', { | |
'status': status, | |
diff --git a/toot/commands.py b/toot/commands.py | |
@@ -4,6 +4,7 @@ from __future__ import print_function | |
import json | |
import requests | |
+import webbrowser | |
from bs4 import BeautifulSoup | |
from builtins import input | |
@@ -45,6 +46,15 @@ def create_app_interactive(instance=None): | |
return config.load_app(instance) or register_app(instance) | |
+def create_user(app, email, access_token): | |
+ user = User(app.instance, email, access_token) | |
+ path = config.save_user(user) | |
+ | |
+ print_out("Access token saved to: <green>{}</green>".format(path)) | |
+ | |
+ return user | |
+ | |
+ | |
def login_interactive(app, email=None): | |
print_out("Log in to <green>{}</green>".format(app.instance)) | |
@@ -62,12 +72,7 @@ def login_interactive(app, email=None): | |
except api.ApiError: | |
raise ConsoleError("Login failed") | |
- user = User(app.instance, email, response['access_token']) | |
- path = config.save_user(user) | |
- | |
- print_out("Access token saved to: <green>{}</green>".format(path)) | |
- | |
- return user | |
+ return create_user(app, email, response['access_token']) | |
def two_factor_login_interactive(app): | |
@@ -118,9 +123,7 @@ def two_factor_login_interactive(app): | |
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_out("Access token saved to: <green>{}</green>".format(path)) | |
+ return create_user(app, email, access_token) | |
def _print_timeline(item): | |
@@ -222,6 +225,46 @@ def login_2fa(app, user, args): | |
print_out("<green>✓ Successfully logged in.</green>") | |
+BROWSER_LOGIN_EXPLANATION = """ | |
+This authentication method requires you to log into your Mastodon instance | |
+in your browser, where you will be asked to authorize <yellow>toot</yellow> to… | |
+your account. When you do, you will be given an <yellow>authorization code</ye… | |
+which you need to paste here. | |
+""" | |
+ | |
+ | |
+def login_browser(app, user, args): | |
+ app = create_app_interactive(instance=args.instance) | |
+ url = api.get_browser_login_url(app) | |
+ | |
+ print_out(BROWSER_LOGIN_EXPLANATION) | |
+ | |
+ print_out("This is the login URL:") | |
+ print_out(url) | |
+ print_out("") | |
+ | |
+ yesno = input("Open link in default browser? [Y/n]") | |
+ if not yesno or yesno.lower() == 'y': | |
+ webbrowser.open(url) | |
+ | |
+ authorization_code = "" | |
+ while not authorization_code: | |
+ authorization_code = input("Authorization code: ") | |
+ | |
+ print_out("\nRequesting access token...") | |
+ response = api.request_access_token(app, authorization_code) | |
+ | |
+ # TODO: user email is not available in this workflow, maybe change the User | |
+ # to store the username instead? Currently set to "unknown" since it's not | |
+ # used anywhere. | |
+ email = "unknown" | |
+ | |
+ create_user(app, email, response['access_token']) | |
+ | |
+ print_out() | |
+ print_out("<green>✓ Successfully logged in.</green>") | |
+ | |
+ | |
def logout(app, user, args): | |
config.delete_user() | |
diff --git a/toot/console.py b/toot/console.py | |
@@ -38,26 +38,33 @@ account_arg = (["account"], { | |
"help": "account name, e.g. 'Gargron' or '[email protected]'", | |
}) | |
+instance_arg = (["-i", "--instance"], { | |
+ "type": str, | |
+ "help": 'mastodon instance to log into e.g. "mastodon.social"', | |
+}) | |
+ | |
+email_arg = (["-e", "--email"], { | |
+ "type": str, | |
+ "help": 'email address to log in with', | |
+}) | |
+ | |
AUTH_COMMANDS = [ | |
Command( | |
name="login", | |
- description="Log into a Mastodon instance", | |
- arguments=[ | |
- (["-i", "--instance"], { | |
- "type": str, | |
- "help": 'mastodon instance to log into e.g. "mastodon.social"', | |
- }), | |
- (["-e", "--email"], { | |
- "type": str, | |
- "help": 'email address to log in with', | |
- }), | |
- ], | |
+ description="Log into a Mastodon instance, does NOT support two factor… | |
+ arguments=[instance_arg, email_arg], | |
+ require_auth=False, | |
+ ), | |
+ Command( | |
+ name="login_browser", | |
+ description="Log in using your browser, supports regular and two facto… | |
+ arguments=[instance_arg, email_arg], | |
require_auth=False, | |
), | |
Command( | |
name="login_2fa", | |
- description="Log in using two factor authentication (experimental)", | |
+ description="Log in using two factor authentication in the console (ha… | |
arguments=[], | |
require_auth=False, | |
), |