From b958da9bd1c9d07b6152f7ed92bf75d7c8ca18a1 Mon Sep 17 00:00:00 2001 From: Kyle Mahan Date: Sun, 18 Jan 2015 23:10:03 -0800 Subject: [PATCH] initial revision; support basic micropub auth --- example.py | 48 ++++++++++++++++++ flask_micropub.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 40 +++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 example.py create mode 100644 flask_micropub.py create mode 100644 setup.py diff --git a/example.py b/example.py new file mode 100644 index 0000000..071e7ea --- /dev/null +++ b/example.py @@ -0,0 +1,48 @@ + +from flask import Flask, request, url_for +from flask.ext.micropub import Micropub + + +app = Flask(__name__) +micropub = Micropub(app) + + +@app.route('/micropub-callback') +@micropub.authorized_handler +def micropub_callback(me, token, next_url, error): + return """ + + + + + + + """.format(me, token, next_url, error) + + +@app.route('/') +def index(): + me = request.args.get('me') + if me: + return micropub.authorize( + me, url_for('micropub_callback', _external=True)) + return """ + + + +
+ + +
+ + + """ + + +if __name__ == '__main__': + app.run(debug=True) diff --git a/flask_micropub.py b/flask_micropub.py new file mode 100644 index 0000000..f1b7c7e --- /dev/null +++ b/flask_micropub.py @@ -0,0 +1,126 @@ +"""This extension adds the ability to login to a Flask-based website +using [IndieAuth](https://indiewebcamp.com/IndieAuth), and to request +an [Micropub](https://indiewebcamp.com/Micropub) access token. +""" + +import requests +import bs4 +import flask +import functools + +import sys +if sys.version > '3': + from urllib.parse import urlencode, parse_qs +else: + from urlparse import parse_qs + from urllib import urlencode + + +class Micropub: + def __init__(self, app): + self.app = app + if app is not None: + self.init_app(app) + + def init_app(self, app): + self.client_id = app.name + + def authorize(self, me, redirect_uri, next=None, scope='read'): + if not me.startswith('http://') and not me.startswith('https://'): + me = 'http://' + me + auth_url, token_url, micropub_url = self._discover_endpoints(me) + if not auth_url: + auth_url = 'https://indieauth.com/auth' + + auth_params = { + 'me': me, + 'client_id': self.client_id, + 'redirect_uri': redirect_uri, + 'scope': scope, + } + + if next: + auth_params['state'] = next + + return flask.redirect( + auth_url + '?' + urlencode(auth_params)) + + def authorized_handler(self, f): + @functools.wraps(f) + def decorated(*args, **kwargs): + data = self._handle_response() + return f(*(data + args), **kwargs) + return decorated + + def _handle_response(self): + redirect_uri = flask.url_for(flask.request.endpoint, _external=True) + confirmed_me = None + access_token = None + state = flask.request.args.get('state') + next_url = state + auth_url, token_url, micropub_url = self._discover_endpoints( + flask.request.args.get('me')) + + if not auth_url: + return (confirmed_me, access_token, next_url, + 'no authorization endpoint') + + code = flask.request.args.get('code') + client_id = '' + + # validate the authorization code + response = requests.post(auth_url, data={ + 'code': code, + 'client_id': client_id, + 'redirect_uri': redirect_uri, + 'state': state, + }) + + rdata = parse_qs(response.text) + if response.status_code != 200: + return (confirmed_me, access_token, next_url, + 'authorization failed. {}: {}'.format( + rdata.get('error'), rdata.get('error_description'))) + + if 'me' not in rdata: + return (confirmed_me, access_token, next_url, + 'missing "me" in response') + + confirmed_me = rdata.get('me')[0] + + # request an access token + token_response = requests.post(token_url, data={ + 'code': code, + 'me': confirmed_me, + 'redirect_uri': redirect_uri, + 'client_id': client_id, + 'state': state, + }) + + if token_response.status_code != 200: + return (confirmed_me, access_token, next_url, + 'bad response from token endpoint: {}' + .format(token_response)) + + tdata = parse_qs(token_response.text) + if 'access_token' not in tdata: + return (confirmed_me, access_token, next_url, + 'response from token endpoint missing access_token: {}' + .format(tdata)) + + access_token = tdata.get('access_token')[0] + return confirmed_me, access_token, next_url, None + + def _discover_endpoints(self, me): + me_response = requests.get(me) + if me_response.status_code != 200: + return None, None, None + + soup = bs4.BeautifulSoup(me_response.text) + auth_endpoint = soup.find('link', {'rel': 'authorization_endpoint'}) + token_endpoint = soup.find('link', {'rel': 'token_endpoint'}) + micropub_endpoint = soup.find('link', {'rel': 'micropub'}) + + return (auth_endpoint and auth_endpoint['href'], + token_endpoint and token_endpoint['href'], + micropub_endpoint and micropub_endpoint['href']) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6111913 --- /dev/null +++ b/setup.py @@ -0,0 +1,40 @@ +""" +Flask-Micropub +-------------- + +This extension adds the ability to login to a Flask-based website +using [IndieAuth](https://indiewebcamp.com/IndieAuth), and to request +an [Micropub](https://indiewebcamp.com/Micropub) access token. +""" +from setuptools import setup + + +setup( + name='Flask-Micropub', + version='0.1', + url='https://indiewebcamp.com/Flask-Micropub/', + license='BSD', + author='Kyle Mahan', + author_email='kyle@kylewm.com', + description='Adds support for Micropub clients.', + long_description=__doc__, + py_modules=['flask_micropub'], + zip_safe=False, + include_package_data=True, + platforms='any', + install_requires=[ + 'Flask', + 'requests', + 'BeautifulSoup4', + ], + classifiers=[ + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules', + ] +)