initial revision; support basic micropub auth
This commit is contained in:
parent
4764df9460
commit
b958da9bd1
|
@ -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)
|
|
@ -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'])
|
|
@ -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',
|
||||||
|
]
|
||||||
|
)
|
Loading…
Reference in New Issue