From 2cf8467c2af38ae46de20e711959bbf32c3d8cf5 Mon Sep 17 00:00:00 2001 From: Marek Marecki Date: Sun, 25 Aug 2013 00:12:58 +0200 Subject: [PATCH] Changes in login and authentication procedure (improved security) --- Changelog.markdown | 11 ++++++ diaspy/connection.py | 74 ++++++++++++++++++-------------------- diaspy/errors.py | 4 +++ diaspy/streams.py | 2 +- manual/connection.markdown | 23 +++--------- tests.py | 4 +-- 6 files changed, 57 insertions(+), 61 deletions(-) diff --git a/Changelog.markdown b/Changelog.markdown index beb66f9..f68ae1d 100644 --- a/Changelog.markdown +++ b/Changelog.markdown @@ -23,6 +23,13 @@ up-to-date than manual and if conflicts appear they should follow the order: #### Version `0.4.1` (2013-08-): +Login and authentication procedure backend received major changes in this version. +There are no longer `username` and `password` variables in `Connection` object. +Instead, credentials are stored (together with the token) in single variable `_login_data`. +This is preserved until you call `login()` at which point credentials are erased and +only token is left -- it can be obtained by calling `repr(Connection)`. + + * __new__: `__getitem__()` in `diaspy.models.Post`, * __new__: `__dict__()` in `diaspy.models.Post`, * __new__: `guid` argument in `diaspy.models.Post.__init__()`, @@ -32,11 +39,15 @@ up-to-date than manual and if conflicts appear they should follow the order: * __new__: `setLanguage()` method in `diaspy.settings.Settings`, * __new__: `downloadPhotos()` method in `diaspy.settings.Settings`, * __new__: `backtime` argument in `more()` method in `diaspy.streams.Generic`, +* __new__: `DiaspyError` will be raised when connection is created with empty password and/or username, * __upd__: if `Post()` is created with fetched comments, data will also be fetched as a dependency, * __upd__: `id` argument type is now `int` (`diaspy.models.Post.__init__()`), * __upd__: `Search().lookup_user()` renamed to `Search().lookupUser()`, * __upd__: `diaspy.messages` renamed to `diaspy.conversations` (but will be accessible under both names for this and next release), +* __upd__: `LoginError` moved to `diaspy.errors`, +* __upd__: `TokenError` moved to `diaspy.errors`, +* __upd__: `diaspy.connection.Connection.podswitch()` gained two new positional arguments: `username` and `password`, * __fix__: fixed some bugs in regular expressions used by `diaspy` internals (html tag removal, so you get nicer notifications), diff --git a/diaspy/connection.py b/diaspy/connection.py index 8654b06..4c60783 100644 --- a/diaspy/connection.py +++ b/diaspy/connection.py @@ -5,19 +5,13 @@ import requests import json import warnings +from diaspy import errors + """This module abstracts connection to pod. """ -class LoginError(Exception): - pass - - -class TokenError(Exception): - pass - - class Connection(): """Object representing connection with the pod. """ @@ -34,16 +28,20 @@ class Connection(): :type password: str """ self.pod = pod - self.session = requests.Session() - self.login_data = {} - self.token = '' - try: self._setlogin(username, password) + self._session = requests.Session() + self._login_data = {} + self._token = '' + try: + self._setlogin(username, password) except requests.exceptions.MissingSchema: self.pod = '{0}://{1}'.format(schema, self.pod) warnings.warn('schema was missing') - finally: pass - try: self._setlogin(username, password) - except Exception as e: raise LoginError('cannot create login data (caused by: {0})'.format(e)) + finally: + pass + try: + self._setlogin(username, password) + except Exception as e: + raise errors.LoginError('cannot create login data (caused by: {0})'.format(e)) def __repr__(self): """Returns token string. @@ -51,7 +49,7 @@ class Connection(): repr(connection) instead of calling a specified method. """ - return self.get_token() + return self._token def get(self, string, headers={}, params={}): """This method gets data from session. @@ -63,7 +61,7 @@ class Connection(): :param string: URL to get without the pod's URL and slash eg. 'stream'. :type string: str """ - return self.session.get('{0}/{1}'.format(self.pod, string), params=params, headers=headers) + return self._session.get('{0}/{1}'.format(self.pod, string), params=params, headers=headers) def post(self, string, data, headers={}, params={}): """This method posts data to session. @@ -81,15 +79,15 @@ class Connection(): :type params: dict """ string = '{0}/{1}'.format(self.pod, string) - request = self.session.post(string, data, headers=headers, params=params) + request = self._session.post(string, data, headers=headers, params=params) return request def put(self, string, data=None, headers={}, params={}): """This method PUTs to session. """ string = '{0}/{1}'.format(self.pod, string) - if data is not None: request = self.session.put(string, data, headers=headers, params=params) - else: request = self.session.put(string, headers=headers, params=params) + if data is not None: request = self._session.put(string, data, headers=headers, params=params) + else: request = self._session.put(string, headers=headers, params=params) return request def delete(self, string, data, headers={}): @@ -103,7 +101,7 @@ class Connection(): :type headers: dict """ string = '{0}/{1}'.format(self.pod, string) - request = self.session.delete(string, data=data, headers=headers) + request = self._session.delete(string, data=data, headers=headers) return request def _setlogin(self, username, password): @@ -112,42 +110,42 @@ class Connection(): .. note:: It should be called before _login() function. """ - self.username, self.password = username, password - self.login_data = {'user[username]': self.username, - 'user[password]': self.password, - 'authenticity_token': self._fetchtoken()} + self._login_data = {'user[username]': username, + 'user[password]': password, + 'authenticity_token': self._fetchtoken()} def _login(self): """Handles actual login request. Raises LoginError if login failed. """ request = self.post('users/sign_in', - data=self.login_data, + data=self._login_data, headers={'accept': 'application/json'}) if request.status_code != 201: - raise LoginError('{0}: login failed'.format(request.status_code)) + raise errors.LoginError('{0}: login failed'.format(request.status_code)) def login(self, username='', password=''): """This function is used to log in to a pod. Will raise LoginError if password or username was not specified. """ if username and password: self._setlogin(username, password) - if not self.username or not self.password: raise LoginError('password or username not specified') + if not self._login_data['user[username]'] or not self._login_data['user[password]']: + raise errors.LoginError('username and/or password is not specified') self._login() + self._login_data = {} def logout(self): """Logs out from a pod. When logged out you can't do anything. """ self.get('users/sign_out') - self.username = '' self.token = '' - self.password = '' - def podswitch(self, pod): + def podswitch(self, pod, username, password): """Switches pod from current to another one. """ self.pod = pod + self._setlogin(username, password) self._login() def getUserInfo(self): @@ -169,24 +167,20 @@ class Connection(): """ request = self.get('stream') token = self._token_regex.search(request.text).group(1) - self.token = token + self._token = token return token def get_token(self, fetch=False): """This function returns a token needed for authentication in most cases. - Each time it is run a _fetchtoken() is called and refreshed token is stored. - - It is more safe to use than _fetchtoken(). - By setting new you can request new token or decide to get stored one. - If no token is stored new one will be fatched anyway. + **Notice:** using repr() is recommended method for getting token. :returns: string -- token used to authenticate """ try: if fetch: self._fetchtoken() - if not self.token: self._fetchtoken() + if not self._token: self._fetchtoken() except requests.exceptions.ConnectionError as e: warnings.warn('{0} was cought: reusing old token'.format(e)) finally: - if not self.token: raise TokenError('cannot obtain token and no previous token found for reuse') - return self.token + if not self._token: raise errors.TokenError('cannot obtain token and no previous token found for reuse') + return self._token diff --git a/diaspy/errors.py b/diaspy/errors.py index d4eac5f..bbb0350 100644 --- a/diaspy/errors.py +++ b/diaspy/errors.py @@ -16,6 +16,10 @@ class LoginError(DiaspyError): pass +class TokenError(Exception): + pass + + class UserError(DiaspyError): """Exception raised when something related to users goes wrong. """ diff --git a/diaspy/streams.py b/diaspy/streams.py index 24ab5ba..64cab0e 100644 --- a/diaspy/streams.py +++ b/diaspy/streams.py @@ -87,7 +87,7 @@ class Generic(): self._stream = stream def clear(self): - """Removes all posts from stream. + """Set stream to empty. """ self._stream = [] diff --git a/manual/connection.markdown b/manual/connection.markdown index 9d4e8af..dba2790 100644 --- a/manual/connection.markdown +++ b/manual/connection.markdown @@ -5,7 +5,6 @@ It is pushed around and used by various methods and other objects: * `Post()` and `Conversation()` objects require it to authenticate and do their work, -* `Client()` uses it for loging in to pod and other stuff, * streams use it for getting their contents, * etc. @@ -51,7 +50,6 @@ created and `login()` is called without any arguments. Both ways are valid and will result in exactly the same connection. But consider the following example: - connection = diaspy.connection.Connection(pod='https://pod.example.com', username='user', password='password') @@ -61,20 +59,9 @@ This code will result in connection with username `loser` and password `passphrase` because data passed to `login()` overrides data passed directly to object. -**Remember:** if you pass something to `login()` it will not only *override* but -also *overwrite* the username and password! - - ----- - -##### Switching pods - -`Connection()` provides functionality for switching pods on the fly. -This can be achieved with `podswitch()` method. +**Remember:** if you pass something to `login()` it will *override* credentials set when the +connection was created. -If login to a new pod is successful your connection is just changed -overwritten but if it fails everything else also fails and the current -connection is broken. ---- @@ -84,10 +71,10 @@ Authentication in Diaspora\* is done with *token*. It is a string which is passed to pod with several requests to prove you are who you are pretending to be. -`Connection` providesyou with a method to fetch this token and perform +`Connection` provides you with a method to fetch this token and perform various actions on your account. This method is called `_fetchtoken()`. -It will try to download new token for you. +It will try to download a token for you. Once a token is fetched there is no use for doing it again. You can save time by using `get_token()` method. @@ -114,7 +101,7 @@ Here is how you should create your auth flow: ##### Note for developers If you want to write your own interface or client for D\* -`Connection()` will be the only object you need. +`Connection()` is the only object you need. ---- diff --git a/tests.py b/tests.py index 83c9e39..593f075 100644 --- a/tests.py +++ b/tests.py @@ -49,11 +49,11 @@ post_text = '#diaspy test no. {0}'.format(test_count) class ConnectionTest(unittest.TestCase): def testLoginWithoutUsername(self): connection = diaspy.connection.Connection(pod=__pod__) - self.assertRaises(diaspy.connection.LoginError, connection.login, password='foo') + self.assertRaises(diaspy.errors.LoginError, connection.login, password='foo') def testLoginWithoutPassword(self): connection = diaspy.connection.Connection(pod=__pod__) - self.assertRaises(diaspy.connection.LoginError, connection.login, username='user') + self.assertRaises(diaspy.errors.LoginError, connection.login, username='user') def testGettingUserInfo(self): info = test_connection.getUserInfo() -- 2.25.1