No longer hash auth tokens + store the token scopes
This partially reverts commit c57aefd48b
.
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:
@@ -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',
|
||||||
|
@@ -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:
|
||||||
|
@@ -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:
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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', [])))
|
||||||
|
Reference in New Issue
Block a user