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