initial revision; support basic micropub auth

This commit is contained in:
Kyle Mahan 2015-01-18 23:10:03 -08:00
parent 4764df9460
commit b958da9bd1
3 changed files with 214 additions and 0 deletions

48
example.py Normal file
View File

@ -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 """
<!DOCTYPE html>
<html>
<body>
<ul>
<li>me: {}</li>
<li>token: {}</li>
<li>next: {}</li>
<li>error: {}</li>
</ul>
</body>
</html>
""".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 """
<!DOCTYPE html>
<html>
<body>
<form action="" method="GET">
<input type="text" name="me" placeholder="your domain.com"/>
<button type="submit">Authorize</button>
</form>
</body>
</html>
"""
if __name__ == '__main__':
app.run(debug=True)

126
flask_micropub.py Normal file
View File

@ -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'])

40
setup.py Normal file
View File

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