diff --git a/CHANGES.rst b/CHANGES.rst index 7adf24a..fee6bef 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +1.1.2 +----- +- Login and Logout actions are performed via POST and has protection + against CSRF attacks + 1.1.1 ----- - Fix ``BaseHandler`` obscuring ``AttributeError`` during dispatch diff --git a/README.rst b/README.rst index 103c198..a5bedbc 100644 --- a/README.rst +++ b/README.rst @@ -127,7 +127,11 @@ You can provide custom ``forbidden.jinja2`` template by overriding asset in your See template example in `pyramid_odesk/templates/forbidden.jinja2`_. +The "Logout" action is done also via POST request with CSRF protection, +see example of "Logout" buttion in `pyramid_odesk_example/templates/layout.jinja2`_. + .. _`pyramid_odesk/templates/forbidden.jinja2`: https://github.com/kipanshi/pyramid_odesk/tree/master/pyramid_odesk/templates/forbidden.jinja2 +.. _`pyramid_odesk_example/templates/layout.jinja2`: https://github.com/kipanshi/pyramid_odesk_example/blob/master/pyramid_odesk_example/templates/layout.jinja2 Contacts diff --git a/pyramid_odesk/__init__.py b/pyramid_odesk/__init__.py index 7e0c8a1..a01c7be 100644 --- a/pyramid_odesk/__init__.py +++ b/pyramid_odesk/__init__.py @@ -4,7 +4,7 @@ from pyramid.authorization import ACLAuthorizationPolicy -from .views import login, logout, oauth_callback, forbidden +from .views import Login, Logout, OauthCallback, forbidden def includeme(config): @@ -48,22 +48,22 @@ def includeme(config): config.registry['odesk.login_route'] = login_route login_path = settings.get('odesk.login_path', '/odesk-auth/login') config.add_route(login_route, login_path) - config.add_view(login, route_name=login_route, - permission='login') + config.add_view(Login, route_name=login_route, + permission='login', check_csrf=True) logout_route = settings.get('odesk.logout_route', 'logout') config.registry['odesk.logout_route'] = logout_route logout_path = settings.get('odesk.logout_path', '/odesk-auth/logout') config.add_route(logout_route, logout_path) - config.add_view(logout, route_name=logout_route, - permission=NO_PERMISSION_REQUIRED) + config.add_view(Logout, route_name=logout_route, + permission=NO_PERMISSION_REQUIRED, check_csrf=True) callback_route = settings.get('odesk.callback_route', 'oauth_callback') config.registry['odesk.logout_route'] = callback_route callback_path = settings.get('odesk.callback_path', '/odesk-auth/callback') config.add_route(callback_route, callback_path) - config.add_view(oauth_callback, route_name=callback_route, + config.add_view(OauthCallback, route_name=callback_route, permission='login') # A simple 403 view, with a login button. diff --git a/pyramid_odesk/templates/forbidden.jinja2 b/pyramid_odesk/templates/forbidden.jinja2 index 1c16d74..220d5f3 100644 --- a/pyramid_odesk/templates/forbidden.jinja2 +++ b/pyramid_odesk/templates/forbidden.jinja2 @@ -2,9 +2,15 @@

You are not authorized to access this page.

{% if authenticated %} -

Log out

+
+ + +
{% else %} -

Login with oDesk account here

+
+ + +
{% endif %} diff --git a/pyramid_odesk/utils.py b/pyramid_odesk/utils.py new file mode 100644 index 0000000..58eecfb --- /dev/null +++ b/pyramid_odesk/utils.py @@ -0,0 +1,26 @@ +import odesk + + +def get_odesk_client(request, **attrs): + """Construct an oDesk client. + + *Parameters:* + :attrs: keyword arguments that will be + attached to the ``client.auth`` as attributes + (``request_token``, etc.) + """ + client_kwargs = { + 'oauth_access_token': attrs.pop('oauth_access_token', None), + 'oauth_access_token_secret': attrs.pop( + 'oauth_access_token_secret', None) + + } + + settings = request.registry.settings + client = odesk.Client(settings['odesk.api.key'], + settings['odesk.api.secret'], **client_kwargs) + + for key, value in attrs.items(): + setattr(client.auth, key, value) + + return client diff --git a/pyramid_odesk/views.py b/pyramid_odesk/views.py index 47078a7..bea4004 100644 --- a/pyramid_odesk/views.py +++ b/pyramid_odesk/views.py @@ -1,7 +1,7 @@ from pyramid.security import remember, forget, unauthenticated_userid from pyramid.httpexceptions import HTTPMethodNotAllowed, HTTPFound -import odesk +from .utils import get_odesk_client class BaseHandler(object): @@ -36,91 +36,66 @@ def delete(self): raise NotImplementedError -def _get_odesk_client(request, **attrs): - """Construct an oDesk client. - - *Parameters:* - :attrs: keyword arguments that will be - attached to the ``client.auth`` as attributes - (``request_token``, etc.) - """ - client_kwargs = { - 'oauth_access_token': attrs.pop('oauth_access_token', None), - 'oauth_access_token_secret': attrs.pop( - 'oauth_access_token_secret', None) - - } +class Login(BaseHandler): + def post(self): + """The login view performs following actions: - settings = request.registry.settings - client = odesk.Client(settings['odesk.api.key'], - settings['odesk.api.secret'], **client_kwargs) + - Redirects user to oDesk. If user is logged in, callback url + is invoked, otherwise user is asked to login to oDesk. - for key, value in attrs.items(): - setattr(client.auth, key, value) + """ + client = get_odesk_client(self.request) + authorize_url = client.auth.get_authorize_url() + # Save request tokens in the session + self.request.session['odesk_request_token'] = client.auth.request_token + self.request.session['odesk_request_token_secret'] = \ + client.auth.request_token_secret - return client + return HTTPFound(location=authorize_url) -def login(request): - """The login view performs following actions: +class Logout(BaseHandler): + def post(self): + # Forget user + forget(self.request) + self.request.session.invalidate() + return HTTPFound('/') - - Redirects user to oDesk. If user is logged in, callback url - is invoked, otherwise user is asked to login to oDesk. - """ - client = _get_odesk_client(request) - authorize_url = client.auth.get_authorize_url() - # Save request tokens in the session - request.session['odesk_request_token'] = client.auth.request_token - request.session['odesk_request_token_secret'] = \ - client.auth.request_token_secret - - redirect_url = '{0}&callback_url={1}'.format( - authorize_url, - request.route_url('oauth_callback', - _query={'next': request.GET.get('next', '/')}) - ) - return HTTPFound(location=redirect_url) - - -def logout(request): - # Forget user - forget(request) - request.session.invalidate() - return HTTPFound('/') - - -def oauth_callback(request): - verifier = request.GET.get('oauth_verifier') - next_url = request.GET.get('next', '/') - - request_token = request.session.pop('odesk_request_token', None) - request_token_secret = request.session.pop( - 'odesk_request_token_secret', None) - - if verifier: - client = _get_odesk_client(request, request_token=request_token, - request_token_secret=request_token_secret) - oauth_access_token, oauth_access_token_secret = \ - client.auth.get_access_token(verifier) - - client = _get_odesk_client( - request, - oauth_access_token=oauth_access_token, - oauth_access_token_secret=oauth_access_token_secret) - - # Get user info - user_info = client.auth.get_info() - user_uid = user_info['auth_user']['uid'] - - # Store the user in session - remember(request, user_uid) - # Store oauth access token in session - request.session['auth.access_token'] = oauth_access_token - request.session['auth.access_token_secret'] = oauth_access_token_secret - - # Redirect to ``next`` url - return HTTPFound(location=next_url) +class OauthCallback(BaseHandler): + def get(self): + request = self.request + verifier = request.GET.get('oauth_verifier') + + request_token = request.session.pop('odesk_request_token', None) + request_token_secret = request.session.pop( + 'odesk_request_token_secret', None) + + if verifier: + client = get_odesk_client( + request, request_token=request_token, + request_token_secret=request_token_secret) + oauth_access_token, oauth_access_token_secret = \ + client.auth.get_access_token(verifier) + + client = get_odesk_client( + request, + oauth_access_token=oauth_access_token, + oauth_access_token_secret=oauth_access_token_secret) + + # Get user info + user_info = client.auth.get_info() + user_uid = user_info['auth_user']['uid'] + + # Store the user in session + remember(request, user_uid) + # Store oauth access token in session + request.session['auth.access_token'] = oauth_access_token + request.session['auth.access_token_secret'] = \ + oauth_access_token_secret + + # Redirect to ``next`` url + return HTTPFound(location='/') def forbidden(request): diff --git a/setup.py b/setup.py index 5298129..8e6a0fc 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ ] setup(name='pyramid_odesk', - version='1.1.1', + version='1.1.2', description='pyramid_odesk', long_description=README + '\n\n' + CHANGES, classifiers=[