Hash authentication tokens before storing in the database.

This commit is contained in:
2017-09-21 13:04:07 +02:00
parent 389413ab8a
commit c57aefd48b
8 changed files with 86 additions and 24 deletions

View File

@@ -91,6 +91,7 @@ class PillarServer(Eve):
self.log = logging.getLogger('%s.%s' % (__name__, self.__class__.__name__))
self.log.info('Creating new instance from %r', self.app_root)
self._config_auth_token_hmac_key()
self._config_tempdirs()
self._config_git()
self._config_bugsnag()
@@ -149,6 +150,18 @@ class PillarServer(Eve):
if self.config['DEBUG']:
log.info('Pillar starting, debug=%s', self.config['DEBUG'])
def _config_auth_token_hmac_key(self):
"""Load AUTH_TOKEN_HMAC_KEY, falling back to SECRET_KEY."""
hmac_key = self.config.get('AUTH_TOKEN_HMAC_KEY')
if not hmac_key:
self.log.warning('AUTH_TOKEN_HMAC_KEY not set, falling back to SECRET_KEY')
hmac_key = self.config['AUTH_TOKEN_HMAC_KEY'] = self.config['SECRET_KEY']
if isinstance(hmac_key, str):
self.log.warning('Converting AUTH_TOKEN_HMAC_KEY to bytes')
self.config['AUTH_TOKEN_HMAC_KEY'] = hmac_key.encode('utf8')
def _config_tempdirs(self):
storage_dir = self.config['STORAGE_DIR']
if not os.path.exists(storage_dir):

View File

@@ -320,6 +320,10 @@ tokens_schema = {
'required': True,
},
'token': {
'type': 'string',
'required': False,
},
'token_hashed': {
'type': 'string',
'required': True,
},

View File

@@ -72,13 +72,16 @@ def make_token():
return jsonify(token=token['token'])
def generate_and_store_token(user_id, days=15, prefix=b''):
def generate_and_store_token(user_id, days=15, prefix=b'') -> dict:
"""Generates token based on random bits.
NOTE: the returned document includes the plain-text token.
DO NOT STORE OR LOG THIS unless there is a good reason to.
:param user_id: ObjectId of the owning user.
:param days: token will expire in this many days.
:param prefix: the token will be prefixed by these bytes, for easy identification.
:return: the token document.
:return: the token document with the token in plain text as well as hashed.
"""
if not isinstance(prefix, bytes):
@@ -90,10 +93,17 @@ def generate_and_store_token(user_id, days=15, prefix=b''):
# Use 'xy' as altargs to prevent + and / characters from appearing.
# We never have to b64decode the string anyway.
token = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=')
token_bytes = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=')
token = token_bytes.decode('ascii')
token_expiry = datetime.datetime.now(tz=tz_util.utc) + datetime.timedelta(days=days)
return store_token(user_id, token.decode('ascii'), token_expiry)
token_data = store_token(user_id, token, token_expiry)
# Include the token in the returned document so that it can be stored client-side,
# in configuration, etc.
token_data['token'] = token
return token_data
def hash_password(password: str, salt: typing.Union[str, bytes]) -> str:

View File

@@ -5,7 +5,10 @@ unique usernames from emails. Calls out to the pillar_server.modules.blender_id
module for Blender ID communication.
"""
import base64
import datetime
import hmac
import hashlib
import logging
import typing
@@ -181,8 +184,10 @@ def find_token(token, is_subclient_token=False, **extra_filters):
tokens_collection = current_app.data.driver.db['tokens']
# TODO: remove expired tokens from collection.
lookup = {'token': token,
token_hashed = hash_auth_token(token)
# TODO: remove matching on unhashed tokens once all tokens have been hashed.
lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}],
'is_subclient_token': True if is_subclient_token else {'$in': [False, None]},
'expire_time': {"$gt": datetime.datetime.now(tz=tz_util.utc)}}
lookup.update(extra_filters)
@@ -191,6 +196,19 @@ def find_token(token, is_subclient_token=False, **extra_filters):
return db_token
def hash_auth_token(token: str) -> str:
"""Returns the hashed authentication token.
The token is hashed using HMAC and then base64-encoded.
"""
hmac_key = current_app.config['AUTH_TOKEN_HMAC_KEY']
token_hmac = hmac.new(hmac_key, msg=token.encode('utf8'), digestmod=hashlib.sha256)
digest = token_hmac.digest()
return base64.b64encode(digest).decode('ascii')
def store_token(user_id, token: str, token_expiry, oauth_subclient_id=False):
"""Stores an authentication token.
@@ -201,7 +219,7 @@ def store_token(user_id, token: str, token_expiry, oauth_subclient_id=False):
token_data = {
'user': user_id,
'token': token,
'token_hashed': hash_auth_token(token),
'expire_time': token_expiry,
}
if oauth_subclient_id:

View File

@@ -24,6 +24,9 @@ DEBUG = False
# python3 -c 'import secrets; print(secrets.token_urlsafe(128))'
SECRET_KEY = ''
# Authentication token hashing key. If empty falls back to UTF8-encoded SECRET_KEY with a warning.
AUTH_TOKEN_HMAC_KEY = b''
# Authentication settings
BLENDER_ID_ENDPOINT = 'http://blender_id:8000/'