diff options
| -rw-r--r-- | common/src/leap/soledad/common/couch/state.py | 46 | ||||
| -rw-r--r-- | common/src/leap/soledad/common/tests/test_server.py | 31 | ||||
| -rw-r--r-- | server/src/leap/soledad/server/auth.py | 52 | 
3 files changed, 79 insertions, 50 deletions
| diff --git a/common/src/leap/soledad/common/couch/state.py b/common/src/leap/soledad/common/couch/state.py index 39d49fa0..40c0d55b 100644 --- a/common/src/leap/soledad/common/couch/state.py +++ b/common/src/leap/soledad/common/couch/state.py @@ -19,11 +19,14 @@ Server state using CouchDatabase as backend.  """  import re  import logging +import time  from urlparse import urljoin +from hashlib import sha512  from u1db.remote.server_state import ServerState  from leap.soledad.common.command import exec_validated_cmd  from leap.soledad.common.couch import CouchDatabase +from leap.soledad.common.couch import couch_server  from u1db.errors import Unauthorized @@ -50,6 +53,12 @@ class CouchServerState(ServerState):      Inteface of the WSGI server with the CouchDB backend.      """ +    TOKENS_DB_PREFIX = "tokens_" +    TOKENS_DB_EXPIRE = 30 * 24 * 3600  # 30 days in seconds +    TOKENS_TYPE_KEY = "type" +    TOKENS_TYPE_DEF = "Token" +    TOKENS_USER_ID_KEY = "user_id" +      def __init__(self, couch_url, create_cmd=None):          """          Initialize the couch server state. @@ -112,3 +121,40 @@ class CouchServerState(ServerState):                               delete databases.          """          raise Unauthorized() + +    def verify_token(self, uuid, token): +        """ +        Query couchdb to decide if C{token} is valid for C{uuid}. + +        @param uuid: The user uuid. +        @type uuid: str +        @param token: The token. +        @type token: str +        """ +        with couch_server(self.couch_url) as server: +            # the tokens db rotates every 30 days, and the current db name is +            # "tokens_NNN", where NNN is the number of seconds since epoch divided +            # by the rotate period in seconds. When rotating, old and new tokens +            # db coexist during a certain window of time and valid tokens are +            # replicated from the old db to the new one. See: +            # https://leap.se/code/issues/6785 +            dbname = self._tokens_dbname() +            db = server[dbname] +        # lookup key is a hash of the token to prevent timing attacks. +        token = db.get(sha512(token).hexdigest()) +        if token is None: +            return False +        # we compare uuid hashes to avoid possible timing attacks that +        # might exploit python's builtin comparison operator behaviour, +        # which fails immediatelly when non-matching bytes are found. +        couch_uuid_hash = sha512(token[self.TOKENS_USER_ID_KEY]).digest() +        req_uuid_hash = sha512(uuid).digest() +        if token[self.TOKENS_TYPE_KEY] != self.TOKENS_TYPE_DEF \ +                or couch_uuid_hash != req_uuid_hash: +            return False +        return True + +    def _tokens_dbname(self): +        dbname = self.TOKENS_DB_PREFIX + \ +            str(int(time.time() / self.TOKENS_DB_EXPIRE)) +        return dbname diff --git a/common/src/leap/soledad/common/tests/test_server.py b/common/src/leap/soledad/common/tests/test_server.py index d75275d6..e1129a9f 100644 --- a/common/src/leap/soledad/common/tests/test_server.py +++ b/common/src/leap/soledad/common/tests/test_server.py @@ -24,6 +24,7 @@ import time  import binascii  from pkg_resources import resource_filename  from uuid import uuid4 +from hashlib import sha512  from urlparse import urljoin  from twisted.internet import defer @@ -46,6 +47,36 @@ from leap.soledad.server import LockResource  from leap.soledad.server import load_configuration  from leap.soledad.server import CONFIG_DEFAULTS  from leap.soledad.server.auth import URLToAuthorization +from leap.soledad.server.auth import SoledadTokenAuthMiddleware + + +class ServerAuthenticationMiddlewareTestCase(CouchDBTestCase): + +    def setUp(self): +        super(ServerAuthenticationMiddlewareTestCase, self).setUp() +        app = mock.Mock() +        self._state = CouchServerState(self.couch_url) +        app.state = self._state +        self.auth_middleware = SoledadTokenAuthMiddleware(app) +        self._authorize('valid-uuid', 'valid-token') + +    def _authorize(self, uuid, token): +        token_doc = {} +        token_doc['_id'] = sha512(token).hexdigest() +        token_doc[self._state.TOKENS_USER_ID_KEY] = uuid +        token_doc[self._state.TOKENS_TYPE_KEY] = \ +            self._state.TOKENS_TYPE_DEF +        dbname = self._state._tokens_dbname() +        db = self.couch_server.create(dbname) +        db.save(token_doc) +        self.addCleanup(self.delete_db, db.name) + +    def test_authorized_user(self): +        is_authorized = self.auth_middleware._verify_authentication_data +        self.assertTrue(is_authorized('valid-uuid', 'valid-token')) +        self.assertFalse(is_authorized('valid-uuid', 'invalid-token')) +        self.assertFalse(is_authorized('invalid-uuid', 'valid-token')) +        self.assertFalse(is_authorized('eve', 'invalid-token'))  class ServerAuthorizationTestCase(BaseSoledadTest): diff --git a/server/src/leap/soledad/server/auth.py b/server/src/leap/soledad/server/auth.py index 02b54cca..01baf1ce 100644 --- a/server/src/leap/soledad/server/auth.py +++ b/server/src/leap/soledad/server/auth.py @@ -21,20 +21,16 @@ Authentication facilities for Soledad Server.  """ -import time  import httplib  import json  from u1db import DBNAME_CONSTRAINTS, errors as u1db_errors  from abc import ABCMeta, abstractmethod  from routes.mapper import Mapper -from couchdb.client import Server  from twisted.python import log -from hashlib import sha512  from leap.soledad.common import SHARED_DB_NAME  from leap.soledad.common import USER_DB_PREFIX -from leap.soledad.common.errors import InvalidAuthTokenError  class URLToAuthorization(object): @@ -193,6 +189,7 @@ class SoledadAuthMiddleware(object):          @type prefix: str          """          self._app = app +        self._state = app.state      def _error(self, start_response, status, description, message=None):          """ @@ -351,12 +348,6 @@ class SoledadTokenAuthMiddleware(SoledadAuthMiddleware):      Token based authentication.      """ -    TOKENS_DB_PREFIX = "tokens_" -    TOKENS_DB_EXPIRE = 30 * 24 * 3600  # 30 days in seconds -    TOKENS_TYPE_KEY = "type" -    TOKENS_TYPE_DEF = "Token" -    TOKENS_USER_ID_KEY = "user_id" -      TOKEN_AUTH_ERROR_STRING = "Incorrect address or token."      def _verify_authentication_scheme(self, scheme): @@ -391,50 +382,11 @@ class SoledadTokenAuthMiddleware(SoledadAuthMiddleware):          """          token = auth_data  # we expect a cleartext token at this point          try: -            return self._verify_token_in_couch(uuid, token) -        except InvalidAuthTokenError: -            raise +            return self._state.verify_token(uuid, token)          except Exception as e:              log.err(e)              return False -    def _verify_token_in_couch(self, uuid, token): -        """ -        Query couchdb to decide if C{token} is valid for C{uuid}. - -        @param uuid: The user uuid. -        @type uuid: str -        @param token: The token. -        @type token: str - -        @raise InvalidAuthTokenError: Raised when token received from user is -                                      either missing in the tokens db or is -                                      invalid. -        """ -        server = Server(url=self._app.state.couch_url) -        # the tokens db rotates every 30 days, and the current db name is -        # "tokens_NNN", where NNN is the number of seconds since epoch divided -        # by the rotate period in seconds. When rotating, old and new tokens -        # db coexist during a certain window of time and valid tokens are -        # replicated from the old db to the new one. See: -        # https://leap.se/code/issues/6785 -        dbname = self.TOKENS_DB_PREFIX + \ -            str(int(time.time() / self.TOKENS_DB_EXPIRE)) -        db = server[dbname] -        # lookup key is a hash of the token to prevent timing attacks. -        token = db.get(sha512(token).hexdigest()) -        if token is None: -            raise InvalidAuthTokenError() -        # we compare uuid hashes to avoid possible timing attacks that -        # might exploit python's builtin comparison operator behaviour, -        # which fails immediatelly when non-matching bytes are found. -        couch_uuid_hash = sha512(token[self.TOKENS_USER_ID_KEY]).digest() -        req_uuid_hash = sha512(uuid).digest() -        if token[self.TOKENS_TYPE_KEY] != self.TOKENS_TYPE_DEF \ -                or couch_uuid_hash != req_uuid_hash: -            raise InvalidAuthTokenError() -        return True -      def _get_auth_error_string(self):          """          Get the error string for token auth. | 
