No longer hash auth tokens + store the token scopes

This partially reverts commit c57aefd48b10ca3cabc9df162bc32efa62a6a21e.
The code to check against hashed tokens remains, because existing tokens
should still work.

The unhashed tokens are necessary for fetching badges from Blender ID.
This commit is contained in:
Sybren A. Stüvel 2018-09-11 16:11:44 +02:00
parent a753637e70
commit 85eab0c6cb
8 changed files with 27 additions and 43 deletions

View File

@ -368,11 +368,11 @@ tokens_schema = {
}, },
'token': { 'token': {
'type': 'string', 'type': 'string',
'required': False, 'required': True,
}, },
'token_hashed': { 'token_hashed': {
'type': 'string', 'type': 'string',
'required': True, 'required': False,
}, },
'expire_time': { 'expire_time': {
'type': 'datetime', 'type': 'datetime',

View File

@ -94,17 +94,10 @@ def generate_and_store_token(user_id, days=15, prefix=b'') -> dict:
# Use 'xy' as altargs to prevent + and / characters from appearing. # Use 'xy' as altargs to prevent + and / characters from appearing.
# We never have to b64decode the string anyway. # We never have to b64decode the string anyway.
token_bytes = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=') token = prefix + base64.b64encode(random_bits, altchars=b'xy').strip(b'=')
token = token_bytes.decode('ascii')
token_expiry = utcnow() + datetime.timedelta(days=days) token_expiry = utcnow() + datetime.timedelta(days=days)
token_data = store_token(user_id, token, token_expiry) return store_token(user_id, token.decode('ascii'), 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: def hash_password(password: str, salt: typing.Union[str, bytes]) -> str:

View File

@ -200,7 +200,7 @@ def remove_token(token: str):
tokens_coll = current_app.db('tokens') tokens_coll = current_app.db('tokens')
token_hashed = hash_auth_token(token) token_hashed = hash_auth_token(token)
# TODO: remove matching on unhashed tokens once all tokens have been hashed. # TODO: remove matching on hashed tokens once all hashed tokens have expired.
lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}]} lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}]}
del_res = tokens_coll.delete_many(lookup) del_res = tokens_coll.delete_many(lookup)
log.debug('Removed token %r, matched %d documents', token, del_res.deleted_count) log.debug('Removed token %r, matched %d documents', token, del_res.deleted_count)
@ -212,7 +212,7 @@ def find_token(token, is_subclient_token=False, **extra_filters):
tokens_coll = current_app.db('tokens') tokens_coll = current_app.db('tokens')
token_hashed = hash_auth_token(token) token_hashed = hash_auth_token(token)
# TODO: remove matching on unhashed tokens once all tokens have been hashed. # TODO: remove matching on hashed tokens once all hashed tokens have expired.
lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}], lookup = {'$or': [{'token': token}, {'token_hashed': token_hashed}],
'is_subclient_token': True if is_subclient_token else {'$in': [False, None]}, 'is_subclient_token': True if is_subclient_token else {'$in': [False, None]},
'expire_time': {"$gt": utcnow()}} 'expire_time': {"$gt": utcnow()}}
@ -246,7 +246,7 @@ def store_token(user_id, token: str, token_expiry, oauth_subclient_id=False,
token_data = { token_data = {
'user': user_id, 'user': user_id,
'token_hashed': hash_auth_token(token), 'token': token,
'expire_time': token_expiry, 'expire_time': token_expiry,
} }
if oauth_subclient_id: if oauth_subclient_id:

View File

@ -29,6 +29,7 @@ DEBUG = False
SECRET_KEY = '' SECRET_KEY = ''
# Authentication token hashing key. If empty falls back to UTF8-encoded SECRET_KEY with a warning. # Authentication token hashing key. If empty falls back to UTF8-encoded SECRET_KEY with a warning.
# Not used to hash new tokens, but it is used to check pre-existing hashed tokens.
AUTH_TOKEN_HMAC_KEY = b'' AUTH_TOKEN_HMAC_KEY = b''
# Authentication settings # Authentication settings

View File

@ -114,15 +114,16 @@ class AuthenticationTests(AbstractPillarTest):
past = now - datetime.timedelta(days=1) past = now - datetime.timedelta(days=1)
subclient = self.app.config['BLENDER_ID_SUBCLIENT_ID'] subclient = self.app.config['BLENDER_ID_SUBCLIENT_ID']
with self.app.test_request_context():
auth.store_token(user_id, 'nonexpired-main', future, None) auth.store_token(user_id, 'nonexpired-main', future, None)
auth.store_token(user_id, 'nonexpired-sub', future, subclient) auth.store_token(user_id, 'nonexpired-sub', future, subclient)
token3 = auth.store_token(user_id, 'expired-sub', past, subclient) token3 = auth.store_token(user_id, 'expired-sub', past, subclient)
# We should not find the given tokens as unhashed tokens. # We should find the given tokens as unhashed tokens.
tokens_coll = self.app.db('tokens') tokens_coll = self.app.db('tokens')
self.assertIsNone(tokens_coll.find_one({'token': 'nonespired-main'})) self.assertIsNotNone(tokens_coll.find_one({'token': 'nonexpired-main'}))
self.assertIsNone(tokens_coll.find_one({'token': 'nonespired-sub'})) self.assertIsNotNone(tokens_coll.find_one({'token': 'nonexpired-sub'}))
self.assertIsNone(tokens_coll.find_one({'token': 'expired-sub'})) self.assertIsNotNone(tokens_coll.find_one({'token': 'expired-sub'}))
with self.app.test_request_context( with self.app.test_request_context(
headers={'Authorization': self.make_header('nonexpired-main')}): headers={'Authorization': self.make_header('nonexpired-main')}):
@ -206,19 +207,19 @@ class AuthenticationTests(AbstractPillarTest):
with self.app.test_request_context(): with self.app.test_request_context():
from pillar.api.utils import authentication as auth from pillar.api.utils import authentication as auth
tokdat_le = auth.store_token(user_id, 'long-expired', auth.store_token(user_id, 'long-expired',
now - datetime.timedelta(days=365), None) now - datetime.timedelta(days=365), None)
tokdat_se = auth.store_token(user_id, 'short-expired', auth.store_token(user_id, 'short-expired',
now - datetime.timedelta(seconds=5), None) now - datetime.timedelta(seconds=5), None)
tokdat_ne = auth.store_token(user_id, 'not-expired', auth.store_token(user_id, 'not-expired',
now + datetime.timedelta(days=1), None) now + datetime.timedelta(days=1), None)
# Validation should clean up old tokens. # Validation should clean up old tokens.
auth.validate_this_token('je', 'moeder') auth.validate_this_token('je', 'moeder')
token_coll = self.app.data.driver.db['tokens'] token_coll = self.app.data.driver.db['tokens']
self.assertEqual({tokdat_se['token_hashed'], tokdat_ne['token_hashed']}, self.assertEqual({'short-expired', 'not-expired'},
{item['token_hashed'] for item in token_coll.find()}) {item['token'] for item in token_coll.find()})
def test_token_hashing_cli(self): def test_token_hashing_cli(self):
from dateutil.parser import parse from dateutil.parser import parse

View File

@ -42,8 +42,6 @@ class BlenderIdSubclientTest(AbstractPillarTest):
@responses.activate @responses.activate
def test_store_multiple_tokens(self): def test_store_multiple_tokens(self):
from pillar.api.utils.authentication import hash_auth_token
scst1 = '%s-1' % TEST_SUBCLIENT_TOKEN scst1 = '%s-1' % TEST_SUBCLIENT_TOKEN
scst2 = '%s-2' % TEST_SUBCLIENT_TOKEN scst2 = '%s-2' % TEST_SUBCLIENT_TOKEN
db_user1 = self._common_user_test(201, scst=scst1) db_user1 = self._common_user_test(201, scst=scst1)
@ -53,10 +51,8 @@ class BlenderIdSubclientTest(AbstractPillarTest):
# Now there should be two tokens. # Now there should be two tokens.
with self.app.test_request_context(): with self.app.test_request_context():
tokens = self.app.data.driver.db['tokens'] tokens = self.app.data.driver.db['tokens']
self.assertIsNotNone(tokens.find_one( self.assertIsNotNone(tokens.find_one({'user': db_user1['_id'], 'token': scst1}))
{'user': db_user1['_id'], 'token_hashed': hash_auth_token(scst1)})) self.assertIsNotNone(tokens.find_one({'user': db_user1['_id'], 'token': scst2}))
self.assertIsNotNone(tokens.find_one(
{'user': db_user1['_id'], 'token_hashed': hash_auth_token(scst2)}))
# There should still be only one auth element for blender-id in the user doc. # There should still be only one auth element for blender-id in the user doc.
self.assertEqual(1, len(db_user1['auth'])) self.assertEqual(1, len(db_user1['auth']))
@ -80,8 +76,6 @@ class BlenderIdSubclientTest(AbstractPillarTest):
def _common_user_test(self, expected_status_code, scst=TEST_SUBCLIENT_TOKEN, def _common_user_test(self, expected_status_code, scst=TEST_SUBCLIENT_TOKEN,
expected_full_name=TEST_FULL_NAME, expected_full_name=TEST_FULL_NAME,
mock_happy_blender_id=True): mock_happy_blender_id=True):
from pillar.api.utils.authentication import hash_auth_token
if mock_happy_blender_id: if mock_happy_blender_id:
self.mock_blenderid_validate_happy() self.mock_blenderid_validate_happy()
@ -110,7 +104,7 @@ class BlenderIdSubclientTest(AbstractPillarTest):
# Check that the token was succesfully stored. # Check that the token was succesfully stored.
tokens = self.app.data.driver.db['tokens'] tokens = self.app.data.driver.db['tokens']
db_token = tokens.find_one({'user': db_user['_id'], db_token = tokens.find_one({'user': db_user['_id'],
'token_hashed': hash_auth_token(scst)}) 'token': scst})
self.assertIsNotNone(db_token) self.assertIsNotNone(db_token)
return db_user return db_user

View File

@ -38,8 +38,6 @@ class LocalAuthTest(AbstractPillarTest):
self.assertEqual(200, resp.status_code, resp.data) self.assertEqual(200, resp.status_code, resp.data)
def test_login_expired_token(self): def test_login_expired_token(self):
from pillar.api.utils.authentication import hash_auth_token
user_id = self.create_test_user() user_id = self.create_test_user()
resp = self.client.post('/api/auth/make-token', resp = self.client.post('/api/auth/make-token',
@ -54,7 +52,7 @@ class LocalAuthTest(AbstractPillarTest):
tokens = self.app.data.driver.db['tokens'] tokens = self.app.data.driver.db['tokens']
exp = datetime.datetime.now(tz=tz_util.utc) - datetime.timedelta(1) exp = datetime.datetime.now(tz=tz_util.utc) - datetime.timedelta(1)
result = tokens.update_one({'token_hashed': hash_auth_token(token)}, result = tokens.update_one({'token': token},
{'$set': {'expire_time': exp}}) {'$set': {'expire_time': exp}})
self.assertEqual(1, result.modified_count) self.assertEqual(1, result.modified_count)

View File

@ -989,8 +989,6 @@ class IPRangeLoginRolesTest(AbstractIPRangeSingleOrgTest):
]}) ]})
def _test_api(self, headers: dict, env: dict): def _test_api(self, headers: dict, env: dict):
from pillar.api.utils.authentication import hash_auth_token
self.mock_blenderid_validate_happy() self.mock_blenderid_validate_happy()
# This should check the IP of the user agains the organization IP ranges and update the # This should check the IP of the user agains the organization IP ranges and update the
# user in the database. # user in the database.
@ -1003,7 +1001,7 @@ class IPRangeLoginRolesTest(AbstractIPRangeSingleOrgTest):
tokens_coll = self.app.db('tokens') tokens_coll = self.app.db('tokens')
token = tokens_coll.find_one({ token = tokens_coll.find_one({
'user': bson.ObjectId(me['_id']), 'user': bson.ObjectId(me['_id']),
'token_hashed': hash_auth_token('usertoken'), 'token': 'usertoken',
}) })
self.assertEqual(self.org_roles, set(token['org_roles'])) self.assertEqual(self.org_roles, set(token['org_roles']))
@ -1033,7 +1031,6 @@ class IPRangeLoginRolesTest(AbstractIPRangeSingleOrgTest):
self._test_api_remote_addr('192.168.3.254') self._test_api_remote_addr('192.168.3.254')
def _test_web_forwarded_for(self, ip_addr: str, ip_roles: typing.Set[str]): def _test_web_forwarded_for(self, ip_addr: str, ip_roles: typing.Set[str]):
from pillar.api.utils.authentication import hash_auth_token
from pillar import auth from pillar import auth
self.mock_blenderid_validate_happy() self.mock_blenderid_validate_happy()
@ -1053,7 +1050,7 @@ class IPRangeLoginRolesTest(AbstractIPRangeSingleOrgTest):
tokens_coll = self.app.db('tokens') tokens_coll = self.app.db('tokens')
token = tokens_coll.find_one({ token = tokens_coll.find_one({
'user': bson.ObjectId(my_id), 'user': bson.ObjectId(my_id),
'token_hashed': hash_auth_token('usertoken'), 'token': 'usertoken',
'expire_time': {'$gt': datetime.datetime.now(tz_util.utc)}, 'expire_time': {'$gt': datetime.datetime.now(tz_util.utc)},
}) })
self.assertEqual(ip_roles, set(token.get('org_roles', []))) self.assertEqual(ip_roles, set(token.get('org_roles', [])))