summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVictor Shyba <victor.shyba@gmail.com>2015-10-27 16:48:39 -0300
committerVictor Shyba <victor.shyba@gmail.com>2015-11-03 12:41:37 -0300
commit421691ef71019d0bcd4447a773efa5e9b15b0c71 (patch)
treeb768c56b2629a274cc8347107cf3c230b675fd11
parentb9de2af55ab7fbccadf1fdb92d06858c9f29acfa (diff)
[refactor] token verification moved to couch module + tests
Added tests for this token verification as it wasn't covered. Then moved it to the new couch module that implements a couch storage. The ServerState was chosen to hold the verify_token method. CouchServerState holds the current implementation, which is called on authentication middleware as the new test shows.
-rw-r--r--common/src/leap/soledad/common/couch/state.py46
-rw-r--r--common/src/leap/soledad/common/tests/test_server.py31
-rw-r--r--server/src/leap/soledad/server/auth.py52
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.