1
0
mirror of https://github.com/ihabunek/toot.git synced 2024-11-03 04:17:21 -05:00

Implement proper two factor authentication

fixes #19, #23
This commit is contained in:
Ivan Habunek 2017-08-26 14:39:53 +02:00
parent cebc88d329
commit 62c4075fe1
5 changed files with 131 additions and 45 deletions

View File

@ -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)**

View File

@ -37,29 +37,30 @@ 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 logout Log out, delete stored access keys
toot auth Show stored credentials
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
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 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

View File

@ -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,

View File

@ -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()

View File

@ -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,
),