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 """
+
+
+
+
+ - me: {}
+ - token: {}
+ - next: {}
+ - error: {}
+
+
+
+ """.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',
+ ]
+)