From 9fdd45a0dea71435224a820f085115a9fb77fded Mon Sep 17 00:00:00 2001 From: Kyle Mahan Date: Wed, 27 Jan 2016 07:24:55 -0800 Subject: [PATCH] Do not burn auth codes by authenticating before requesting an access token This broke authorization for endpoints that only allow a code to be used once (e.g. Known). A side effect is that authorization no longer falls back to authentication if the token endpoint does not exist or returns an error. --- CHANGELOG.md | 8 ++++++++ flask_micropub.py | 41 ++++++++++++++++++++++++++++------------- setup.py | 2 +- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c389a8..3afed46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Change Log All notable changes to this project will be documented in this file. +## 0.2.5 - 2016-01-27 +### Changed +- Bugfix: authorization_handler was burning the auth code by + delegating to authentication_handler. This broke authorization for + endpoints that only allow codes to be used once. A side effect of + this is that authorization no longer falls back to authentication + when there is no token_endpoint or the token_endpoint request fails. + ## 0.2.4 - 2015-12-13 ### Changed - Replace `next_url` parameter with more general `state` diff --git a/flask_micropub.py b/flask_micropub.py index c6a6e70..e25e6aa 100644 --- a/flask_micropub.py +++ b/flask_micropub.py @@ -224,28 +224,37 @@ class MicropubClient: return AuthResponse(me=confirmed_me, state=state) def _handle_authorize_response(self): - authenticate_response = self._handle_authenticate_response() code = flask.request.args.get('code') wrapped_state = flask.request.args.get('state') me = flask.request.args.get('me') redirect_uri = flask.url_for(flask.request.endpoint, _external=True) - if authenticate_response.error: - return authenticate_response + if wrapped_state and '|' in wrapped_state: + csrf_token, state = wrapped_state.split('|', 1) + else: + csrf_token = state = None + + if not csrf_token: + return AuthResponse( + state=state, error='no CSRF token in response') + + if csrf_token != flask.session.get('_micropub_csrf_token'): + return AuthResponse( + state=state, error='mismatched CSRF token') token_url, micropub_url = self._discover_endpoints(me)[1:] if not token_url or not micropub_url: # successfully auth'ed user, no micropub endpoint return AuthResponse( - me=authenticate_response.me, - state=authenticate_response.state, + me=me, + state=state, error='no micropub endpoint found.') # request an access token token_data = { 'code': code, - 'me': authenticate_response.me, + 'me': me, 'redirect_uri': redirect_uri, 'client_id': self.client_id, 'state': wrapped_state, @@ -260,26 +269,29 @@ class MicropubClient: if token_response.status_code != 200: return AuthResponse( - me=authenticate_response.me, - state=authenticate_response.state, + me=me, + state=state, error='bad response from token endpoint: {}' .format(token_response)) tdata = parse_qs(token_response.text) if 'access_token' not in tdata: return AuthResponse( - me=authenticate_response.me, - state=authenticate_response.state, + me=me, + state=state, error='response from token endpoint missing access_token: {}' .format(tdata)) # success! access_token = tdata.get('access_token')[0] + confirmed_me = tdata.get('me')[0] + confirmed_scope = tdata.get('scope')[0] return AuthResponse( - me=authenticate_response.me, + me=confirmed_me, micropub_endpoint=micropub_url, access_token=access_token, - state=authenticate_response.state) + scope=confirmed_scope, + state=state) def _discover_endpoints(self, me): me_response = requests.get(me) @@ -311,15 +323,18 @@ class AuthResponse: micropub_endpoint (string): The endpoint to POST micropub requests to. access_token (string): The authorized user's micropub access token. state (string): The optional state that was passed to authorize. + scope (string): The scope that comes with the micropub access token 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, micropub_endpoint=None, - access_token=None, state=None, error=None): + access_token=None, state=None, scope=None, + error=None): self.me = me self.micropub_endpoint = micropub_endpoint self.access_token = access_token self.next_url = self.state = state + self.scope = scope self.error = error diff --git a/setup.py b/setup.py index fdbb250..0e70b60 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ from setuptools import setup setup( name='Flask-Micropub', - version='0.2.4', + version='0.2.5', url='https://github.com/kylewm/flask-micropub/', license='BSD', author='Kyle Mahan',