Replace next_url parameter with the more general and useful state

This commit is contained in:
Kyle Mahan 2015-12-13 19:50:54 -08:00
parent 69eb9ffb9a
commit 3282bd76ba
3 changed files with 48 additions and 39 deletions

View File

@ -1,20 +1,25 @@
# Change Log # Change Log
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## 0.2.4 - 2015-12-13
### Changed
- Replace `next_url` parameter with more general `state`
(though we're keeping `next_url` for backward compatibility for now)
## 0.2.3 ## 0.2.3
## Changed ### Changed
- Fix; fall back to indieauth.com when no authorization_endpoint is - Fix; fall back to indieauth.com when no authorization_endpoint is
specified (previous fix broke this). specified (previous fix broke this).
## 0.2.2 ## 0.2.2
## Changed ### Changed
- Fix vulnerability; re-discover the authorization_endpoint and - Fix vulnerability; re-discover the authorization_endpoint and
token_endpoint at each stage in the flow. Prevents a buggy or token_endpoint at each stage in the flow. Prevents a buggy or
malicious authorization_endpoint from giving you credentials for malicious authorization_endpoint from giving you credentials for
another user's domain name. another user's domain name.
## 0.2.1 - 2015-02-07 ## 0.2.1 - 2015-02-07
## Changed ### Changed
- Updated setup.py, no functional changes - Updated setup.py, no functional changes
## 0.2.0 - 2015-02-07 ## 0.2.0 - 2015-02-07

View File

@ -55,16 +55,17 @@ class MicropubClient:
else: else:
self.client_id = app.name self.client_id = app.name
def authenticate(self, me, next_url=None): def authenticate(self, me, state=None, next_url=None):
"""Authenticate a user via IndieAuth. """Authenticate a user via IndieAuth.
Args: Args:
me (string): the authing user's URL. if it does not begin with me (string): the authing user's URL. if it does not begin with
https?://, http:// will be prepended. https?://, http:// will be prepended.
next_url (string, optional): passed through the whole auth process, state (string, optional): passed through the whole auth process,
useful if you want to redirect back to a starting page when auth useful if you want to maintain some state, e.g. the starting page
is complete. to return to when auth is complete.
scopes. 'read' by default. next_url (string, optional): deprecated and replaced by the more
general "state". still here for backward compatibility.
Returns: Returns:
a redirect to the user's specified authorization url, or a redirect to the user's specified authorization url, or
@ -73,17 +74,19 @@ class MicropubClient:
redirect_url = flask.url_for( redirect_url = flask.url_for(
self.flask_endpoint_for_function(self._authenticated_handler), self.flask_endpoint_for_function(self._authenticated_handler),
_external=True) _external=True)
return self._start_indieauth(me, redirect_url, next_url, None) return self._start_indieauth(me, redirect_url, state or next_url, None)
def authorize(self, me, next_url=None, scope='read'): def authorize(self, me, state=None, next_url=None, scope='read'):
"""Authorize a user via Micropub. """Authorize a user via Micropub.
Args: Args:
me (string): the authing user's URL. if it does not begin with me (string): the authing user's URL. if it does not begin with
https?://, http:// will be prepended. https?://, http:// will be prepended.
next_url (string, optional): passed through the whole auth process, state (string, optional): passed through the whole auth process,
useful if you want to redirect back to a starting page when auth useful if you want to maintain some state, e.g. the starting page
is complete. to return to when auth is complete.
next_url (string, optional): deprecated and replaced by the more
general "state". still here for backward compatibility.
scope (string, optional): a space-separated string of micropub scope (string, optional): a space-separated string of micropub
scopes. 'read' by default. scopes. 'read' by default.
@ -94,9 +97,10 @@ class MicropubClient:
redirect_url = flask.url_for( redirect_url = flask.url_for(
self.flask_endpoint_for_function(self._authorized_handler), self.flask_endpoint_for_function(self._authorized_handler),
_external=True) _external=True)
return self._start_indieauth(me, redirect_url, next_url, scope) return self._start_indieauth(
me, redirect_url, state or next_url, scope)
def _start_indieauth(self, me, redirect_url, next_url, scope): def _start_indieauth(self, me, redirect_url, state, scope):
"""Helper for both authentication and authorization. Kicks off """Helper for both authentication and authorization. Kicks off
IndieAuth by fetching the authorization endpoint from the user's IndieAuth by fetching the authorization endpoint from the user's
homepage and redirecting to it. homepage and redirecting to it.
@ -105,9 +109,9 @@ class MicropubClient:
me (string): the authing user's URL. if it does not begin with me (string): the authing user's URL. if it does not begin with
https?://, http:// will be prepended. https?://, http:// will be prepended.
redirect_url: the callback URL that we pass to the auth endpoint. redirect_url: the callback URL that we pass to the auth endpoint.
next_url (string, optional): passed through the whole auth process, state (string, optional): passed through the whole auth process,
useful if you want to redirect back to a starting page when auth useful if you want to maintain some state, e.g. the url to return
is complete. to when the process is complete.
scope (string): a space-separated string of micropub scopes. scope (string): a space-separated string of micropub scopes.
Returns: Returns:
@ -128,7 +132,7 @@ class MicropubClient:
'me': me, 'me': me,
'client_id': self.client_id, 'client_id': self.client_id,
'redirect_uri': redirect_url, 'redirect_uri': redirect_url,
'state': '{}|{}'.format(csrf_token, next_url or ''), 'state': '{}|{}'.format(csrf_token, state or ''),
} }
if scope: if scope:
auth_params['scope'] = scope auth_params['scope'] = scope
@ -162,22 +166,22 @@ class MicropubClient:
def _handle_authenticate_response(self): def _handle_authenticate_response(self):
code = flask.request.args.get('code') code = flask.request.args.get('code')
state = flask.request.args.get('state') wrapped_state = flask.request.args.get('state')
me = flask.request.args.get('me') me = flask.request.args.get('me')
redirect_uri = flask.url_for(flask.request.endpoint, _external=True) redirect_uri = flask.url_for(flask.request.endpoint, _external=True)
if state and '|' in state: if wrapped_state and '|' in wrapped_state:
csrf_token, next_url = state.split('|', 1) csrf_token, state = wrapped_state.split('|', 1)
else: else:
csrf_token = next_url = None csrf_token = state = None
if not csrf_token: if not csrf_token:
return AuthResponse( return AuthResponse(
next_url=next_url, error='no CSRF token in response') state=state, error='no CSRF token in response')
if csrf_token != flask.session.get('_micropub_csrf_token'): if csrf_token != flask.session.get('_micropub_csrf_token'):
return AuthResponse( return AuthResponse(
next_url=next_url, error='mismatched CSRF token') state=state, error='mismatched CSRF token')
auth_url = self._discover_endpoints(me)[0] auth_url = self._discover_endpoints(me)[0]
if not auth_url: if not auth_url:
@ -188,7 +192,7 @@ class MicropubClient:
'code': code, 'code': code,
'client_id': self.client_id, 'client_id': self.client_id,
'redirect_uri': redirect_uri, 'redirect_uri': redirect_uri,
'state': state, 'state': wrapped_state,
} }
flask.current_app.logger.debug( flask.current_app.logger.debug(
'Flask-Micropub: checking code against auth url: %s, data: %s', 'Flask-Micropub: checking code against auth url: %s, data: %s',
@ -203,23 +207,23 @@ class MicropubClient:
error_vals = rdata.get('error') error_vals = rdata.get('error')
error_descs = rdata.get('error_description') error_descs = rdata.get('error_description')
return AuthResponse( return AuthResponse(
next_url=next_url, state=state,
error='authorization failed. {}: {}'.format( error='authorization failed. {}: {}'.format(
error_vals[0] if error_vals else 'Unknown Error', error_vals[0] if error_vals else 'Unknown Error',
error_descs[0] if error_descs else 'Unknown Error')) error_descs[0] if error_descs else 'Unknown Error'))
if 'me' not in rdata: if 'me' not in rdata:
return AuthResponse( return AuthResponse(
next_url=next_url, state=state,
error='missing "me" in response') error='missing "me" in response')
confirmed_me = rdata.get('me')[0] confirmed_me = rdata.get('me')[0]
return AuthResponse(me=confirmed_me, next_url=next_url) return AuthResponse(me=confirmed_me, state=state)
def _handle_authorize_response(self): def _handle_authorize_response(self):
authenticate_response = self._handle_authenticate_response() authenticate_response = self._handle_authenticate_response()
code = flask.request.args.get('code') code = flask.request.args.get('code')
state = flask.request.args.get('state') wrapped_state = flask.request.args.get('state')
me = flask.request.args.get('me') me = flask.request.args.get('me')
redirect_uri = flask.url_for(flask.request.endpoint, _external=True) redirect_uri = flask.url_for(flask.request.endpoint, _external=True)
@ -232,7 +236,7 @@ class MicropubClient:
# successfully auth'ed user, no micropub endpoint # successfully auth'ed user, no micropub endpoint
return AuthResponse( return AuthResponse(
me=authenticate_response.me, me=authenticate_response.me,
next_url=authenticate_response.next_url, state=authenticate_response.state,
error='no micropub endpoint found.') error='no micropub endpoint found.')
# request an access token # request an access token
@ -241,7 +245,7 @@ class MicropubClient:
'me': authenticate_response.me, 'me': authenticate_response.me,
'redirect_uri': redirect_uri, 'redirect_uri': redirect_uri,
'client_id': self.client_id, 'client_id': self.client_id,
'state': state, 'state': wrapped_state,
} }
flask.current_app.logger.debug( flask.current_app.logger.debug(
'Flask-Micropub: requesting access token from: %s, data: %s', 'Flask-Micropub: requesting access token from: %s, data: %s',
@ -254,7 +258,7 @@ class MicropubClient:
if token_response.status_code != 200: if token_response.status_code != 200:
return AuthResponse( return AuthResponse(
me=authenticate_response.me, me=authenticate_response.me,
next_url=authenticate_response.next_url, state=authenticate_response.state,
error='bad response from token endpoint: {}' error='bad response from token endpoint: {}'
.format(token_response)) .format(token_response))
@ -262,7 +266,7 @@ class MicropubClient:
if 'access_token' not in tdata: if 'access_token' not in tdata:
return AuthResponse( return AuthResponse(
me=authenticate_response.me, me=authenticate_response.me,
next_url=authenticate_response.next_url, state=authenticate_response.state,
error='response from token endpoint missing access_token: {}' error='response from token endpoint missing access_token: {}'
.format(tdata)) .format(tdata))
@ -272,7 +276,7 @@ class MicropubClient:
me=authenticate_response.me, me=authenticate_response.me,
micropub_endpoint=micropub_url, micropub_endpoint=micropub_url,
access_token=access_token, access_token=access_token,
next_url=authenticate_response.next_url) state=authenticate_response.state)
def _discover_endpoints(self, me): def _discover_endpoints(self, me):
me_response = requests.get(me) me_response = requests.get(me)
@ -303,16 +307,16 @@ class AuthResponse:
only if the user was successfully authenticated. only if the user was successfully authenticated.
micropub_endpoint (string): The endpoint to POST micropub requests to. micropub_endpoint (string): The endpoint to POST micropub requests to.
access_token (string): The authorized user's micropub access token. access_token (string): The authorized user's micropub access token.
next_url (string): The optional URL that was passed to authorize. state (string): The optional state that was passed to authorize.
error (string): describes the error encountered if any. It is possible error (string): describes the error encountered if any. It is possible
that the authentication step will succeed but the access token step 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 will fail, in which case me will be non-None, and error will describe
this condition. this condition.
""" """
def __init__(self, me=None, micropub_endpoint=None, def __init__(self, me=None, micropub_endpoint=None,
access_token=None, next_url=None, error=None): access_token=None, state=None, error=None):
self.me = me self.me = me
self.micropub_endpoint = micropub_endpoint self.micropub_endpoint = micropub_endpoint
self.access_token = access_token self.access_token = access_token
self.next_url = next_url self.next_url = self.state = state
self.error = error self.error = error

View File

@ -11,7 +11,7 @@ from setuptools import setup
setup( setup(
name='Flask-Micropub', name='Flask-Micropub',
version='0.2.3', version='0.2.4',
url='https://github.com/kylewm/flask-micropub/', url='https://github.com/kylewm/flask-micropub/',
license='BSD', license='BSD',
author='Kyle Mahan', author='Kyle Mahan',