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
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
## Changed
### Changed
- Fix; fall back to indieauth.com when no authorization_endpoint is
specified (previous fix broke this).
## 0.2.2
## Changed
### Changed
- Fix vulnerability; re-discover the authorization_endpoint and
token_endpoint at each stage in the flow. Prevents a buggy or
malicious authorization_endpoint from giving you credentials for
another user's domain name.
## 0.2.1 - 2015-02-07
## Changed
### Changed
- Updated setup.py, no functional changes
## 0.2.0 - 2015-02-07

View File

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

View File

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