Implement proper two factor authentication

fixes #19, #23
pull/31/head
Ivan Habunek 2017-08-26 14:39:53 +02:00
rodzic cebc88d329
commit 62c4075fe1
5 zmienionych plików z 131 dodań i 45 usunięć

Wyświetl plik

@ -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 browser (#19, #23)
**0.12.0 (2016-05-08)**

Wyświetl plik

@ -37,8 +37,9 @@ Running ``toot <command> -h`` shows the documentation for the given command.
toot - a Mastodon CLI client
Authentication:
toot login Log into a Mastodon instance
toot login_2fa Log in using two factor authentication (experimental)
toot login Log into a Mastodon instance, does NOT support two factor authentication
toot login_browser Log in using your browser, supports regular and two factor authentication
toot login_2fa Log in using two factor authentication in the console (hacky, experimental)
toot logout Log out, delete stored access keys
toot auth Show stored credentials
@ -77,19 +78,23 @@ It is possible to pipe status text into `toot post`, for example:
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 command line:
.. code-block::
toot login
**Two factor authentication** is supported experimentally, instead of ``login``, you should instead run ``login_2fa``:
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 browser:
.. 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 to access your account, and will be given an **authorization code** in return which you need to enter to log in.
.. _instance: https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md

Wyświetl plik

@ -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,11 +53,17 @@ def _process_response(response):
_log_response(response)
if not response.ok:
try:
error = response.json()['error']
except:
error = "Unknown error"
try:
data = response.json()
if "error_description" in data:
error = data['error_description']
elif "error" in data:
error = data['error']
except:
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,

Wyświetl plik

@ -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 access
your account. When you do, you will be given an <yellow>authorization code</yellow>
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()

Wyświetl plik

@ -38,26 +38,33 @@ account_arg = (["account"], {
"help": "account name, e.g. 'Gargron' or 'polymerwitch@toot.cat'",
})
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 authentication",
arguments=[instance_arg, email_arg],
require_auth=False,
),
Command(
name="login_browser",
description="Log in using your browser, supports regular and two factor authentication",
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 (hacky, experimental)",
arguments=[],
require_auth=False,
),