diff --git a/README.md b/README.md
index 739921c..54a2fe0 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,28 @@
-# flask-micropub
-Flask extension to support IndieAuth and Micropub clients.
+# Flask-Micropub
+
+A Flask extension to support IndieAuth and Micropub clients.
+
+
+```python
+from flask import Flask, request, url_for
+from flask.ext.micropub import Micropub
+
+app = Flask(__name__)
+micropub = Micropub(app)
+
+
+@app.route('/login')
+def login():
+ return micropub.authorize(
+ me, redirect_url=url_for('micropub_callback', _external=True),
+ scope=request.args.get('scope'))
+
+
+@app.route('/micropub-callback')
+@micropub.authorized_handler
+def micropub_callback(resp):
+ print('success!', resp.me, resp.access_token, resp.next_url, resp.error)
+
+```
+
+See details at https://indiewebcamp.com/IndieAuth and https://indiewebcamp.com/Micropub
diff --git a/example.py b/example.py
index 071e7ea..7cd2577 100644
--- a/example.py
+++ b/example.py
@@ -1,28 +1,28 @@
-
from flask import Flask, request, url_for
from flask.ext.micropub import Micropub
app = Flask(__name__)
+app.config['SECRET_KEY'] = 'my super secret key'
micropub = Micropub(app)
@app.route('/micropub-callback')
@micropub.authorized_handler
-def micropub_callback(me, token, next_url, error):
+def micropub_callback(resp):
return """
-
-
- - me: {}
- - token: {}
- - next: {}
- - error: {}
-
-
+
+
+ - me: {}
+ - token: {}
+ - next: {}
+ - error: {}
+
+
- """.format(me, token, next_url, error)
+ """.format(resp.me, resp.access_token, resp.next_url, resp.error)
@app.route('/')
@@ -30,16 +30,22 @@ def index():
me = request.args.get('me')
if me:
return micropub.authorize(
- me, url_for('micropub_callback', _external=True))
+ me, redirect_url=url_for('micropub_callback', _external=True),
+ scope=request.args.get('scope'))
return """
-
-
-
+
+
+
"""
diff --git a/flask_micropub.py b/flask_micropub.py
index f1b7c7e..66a55fd 100644
--- a/flask_micropub.py
+++ b/flask_micropub.py
@@ -1,6 +1,11 @@
-"""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.
+# -*- coding: utf-8 -*-
+"""
+ 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.
"""
import requests
@@ -9,61 +14,114 @@ import flask
import functools
import sys
-if sys.version > '3':
- from urllib.parse import urlencode, parse_qs
-else:
+if sys.version < '3':
from urlparse import parse_qs
from urllib import urlencode
+else:
+ from urllib.parse import urlencode, parse_qs
class Micropub:
- def __init__(self, app):
+ """Flask-Micropub provides support for IndieAuth/Micropub
+ authentication and authorization.
+
+ Args:
+ app (flask.Flask, optional): the flask application to extend.
+ client_id (string, optional): the IndieAuth client id, will be displayed
+ when the user is asked to authorize this client.
+ """
+ def __init__(self, app=None, client_id=None):
self.app = app
if app is not None:
- self.init_app(app)
+ self.init_app(app, client_id)
- def init_app(self, app):
- self.client_id = app.name
+ def init_app(self, app, client_id=None):
+ """Initialize the Micropub extension if it was not given app
+ in the constructor.
- def authorize(self, me, redirect_uri, next=None, scope='read'):
+ Args:
+ app (flask.Flask): the flask application to extend.
+ client_id (string, optional): the IndieAuth client id, will be
+ displayed when the user is asked to authorize this client. If not
+ provided, the app name will be used.
+ """
+ if client_id:
+ self.client_id = client_id
+ else:
+ self.client_id = app.name
+
+ def authorize(self, me, redirect_url, next_url=None, scope='read'):
+ """Authorize a user via Micropub.
+
+ Args:
+ me (string): the authing user's URL. if it does not begin with
+ https?://, http:// will be prepended.
+ redirect_url (string): the URL that IndieAuth should redirect to
+ when it's finished; This should be the authorized_handler
+ next_url (string, optional): passed through the whole auth process,
+ useful if you want to redirect back to a starting page when auth
+ is complete.
+ scope (string, optional): a space-separated string of micropub
+ scopes. 'read' by default.
+
+ Returns:
+ a redirect to the user's specified authorization url, or
+ https://indieauth.com/auth if none is provided.
+ """
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'
+ # save the endpoints so we don't have to scrape the target page again
+ # right awway
+ try:
+ flask.session['_micropub_endpoints'] = (auth_url, token_url,
+ micropub_url)
+ except RuntimeError:
+ pass # we'll look it up again later
+
auth_params = {
'me': me,
'client_id': self.client_id,
- 'redirect_uri': redirect_uri,
+ 'redirect_uri': redirect_url,
'scope': scope,
}
- if next:
- auth_params['state'] = next
+ if next_url:
+ auth_params['state'] = next_url
return flask.redirect(
auth_url + '?' + urlencode(auth_params))
def authorized_handler(self, f):
+ """Decorates the authorization callback endpoint. The endpoint should
+ take one argument, a flask.ext.micropub.AuthResponse.
+ """
@functools.wraps(f)
- def decorated(*args, **kwargs):
- data = self._handle_response()
- return f(*(data + args), **kwargs)
+ def decorated():
+ resp = self._handle_response()
+ return f(resp)
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 '_micropub_endpoints' in flask.session:
+ auth_url, token_url, micropub_url \
+ = flask.session['_micropub_endpoints']
+ del flask.session['_micropub_endpoints']
+ else:
+ 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')
+ return AuthResponse(
+ next_url=next_url, error='no authorization endpoint')
code = flask.request.args.get('code')
client_id = ''
@@ -78,16 +136,24 @@ class Micropub:
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')))
+ return AuthResponse(
+ next_url=next_url,
+ error='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')
+ return AuthResponse(
+ next_url=next_url,
+ error='missing "me" in response')
confirmed_me = rdata.get('me')[0]
+ if not token_url or not micropub_url:
+ # successfully auth'ed user, no micropub endpoint
+ return AuthResponse(
+ me=confirmed_me, next_url=next_url,
+ error='no micropub endpoint found.')
+
# request an access token
token_response = requests.post(token_url, data={
'code': code,
@@ -98,18 +164,22 @@ class Micropub:
})
if token_response.status_code != 200:
- return (confirmed_me, access_token, next_url,
- 'bad response from token endpoint: {}'
- .format(token_response))
+ return AuthResponse(
+ me=confirmed_me, next_url=next_url,
+ error='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))
+ return AuthResponse(
+ me=confirmed_me, next_url=next_url,
+ error='response from token endpoint missing access_token: {}'
+ .format(tdata))
+ # success!
access_token = tdata.get('access_token')[0]
- return confirmed_me, access_token, next_url, None
+ return AuthResponse(me=confirmed_me, access_token=access_token,
+ next_url=next_url)
def _discover_endpoints(self, me):
me_response = requests.get(me)
@@ -124,3 +194,23 @@ class Micropub:
return (auth_endpoint and auth_endpoint['href'],
token_endpoint and token_endpoint['href'],
micropub_endpoint and micropub_endpoint['href'])
+
+
+class AuthResponse:
+ """Authorization response, passed to the authorized_handler endpoint.
+
+ Attributes:
+ me (string): The authenticated user's URL. This will be non-None if and
+ only if the user was successfully authenticated.
+ access_token (string): The authorized user's micropub access token.
+ next_url (string): The optional URL that was passed to authorize.
+ error (string): describes the error encountered if any. It is possible
+ that the authentication step will succeed but the access token step
+ will fail, in which case me will be non-None, and error will describe
+ this condition.
+ """
+ def __init__(self, me=None, access_token=None, next_url=None, error=None):
+ self.me = me
+ self.access_token = access_token
+ self.next_url = next_url
+ self.error = error