Introduction
Introduction Statistics Contact Development Disclaimer Help
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,
),
You are viewing proxied material from vernunftzentrum.de. The copyright of proxied material belongs to its original authors. Any comments or complaints in relation to proxied material should be directed to the original authors of the content concerned. Please see the disclaimer for more details.