[sources/native] automatically update passwords using deprecated hashes on login
Closes #2465904
--- a/server/sources/native.py Fri Sep 14 17:42:24 2012 +0200
+++ b/server/sources/native.py Mon Sep 10 15:17:10 2012 +0200
@@ -61,7 +61,7 @@
from cubicweb.schema import VIRTUAL_RTYPES
from cubicweb.cwconfig import CubicWebNoAppConfiguration
from cubicweb.server import hook
-from cubicweb.server.utils import crypt_password, eschema_eid
+from cubicweb.server.utils import crypt_password, eschema_eid, verify_and_update
from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
from cubicweb.server.rqlannotation import set_qdata
from cubicweb.server.hook import CleanupDeletedEidsCacheOp
@@ -1629,7 +1629,22 @@
# get eid from login and (crypted) password
rset = self.source.syntax_tree_search(session, self._auth_rqlst, args)
try:
- return rset[0][0]
+ user = rset[0][0]
+ # If the stored hash uses a deprecated scheme (e.g. DES or MD5 used
+ # before 3.14.7), update with a fresh one
+ if pwd.getvalue():
+ verify, newhash = verify_and_update(password, pwd.getvalue())
+ if not verify: # should not happen, but...
+ raise AuthenticationError('bad password')
+ if newhash:
+ session.system_sql("UPDATE %s SET %s=%%(newhash)s WHERE %s=%%(login)s" % (
+ SQL_PREFIX + 'CWUser',
+ SQL_PREFIX + 'upassword',
+ SQL_PREFIX + 'login'),
+ {'newhash': self.source._binary(newhash),
+ 'login': login})
+ session.commit(free_cnxset=False)
+ return user
except IndexError:
raise AuthenticationError('bad password')
--- a/server/test/unittest_security.py Fri Sep 14 17:42:24 2012 +0200
+++ b/server/test/unittest_security.py Mon Sep 10 15:17:10 2012 +0200
@@ -25,9 +25,10 @@
from rql import RQLException
from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb import Unauthorized, ValidationError, QueryError
+from cubicweb import Unauthorized, ValidationError, QueryError, Binary
from cubicweb.schema import ERQLExpression
from cubicweb.server.querier import check_read_access
+from cubicweb.server.utils import _CRYPTO_CTX
class BaseSecurityTC(CubicWebTC):
@@ -35,7 +36,8 @@
def setup_database(self):
super(BaseSecurityTC, self).setup_database()
self.create_user(self.request(), 'iaminusersgrouponly')
-
+ hash = _CRYPTO_CTX.encrypt('oldpassword', scheme='des_crypt')
+ self.create_user(self.request(), 'oldpassword', password=Binary(hash))
class LowLevelSecurityFunctionTC(BaseSecurityTC):
@@ -60,6 +62,18 @@
self.assertRaises(Unauthorized,
cu.execute, 'Any X,P WHERE X is CWUser, X upassword P')
+ def test_update_password(self):
+ """Ensure that if a user's password is stored with a deprecated hash, it will be updated on next login"""
+ oldhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])
+ with self.login('oldpassword') as cu:
+ pass
+ newhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])
+ self.assertNotEqual(oldhash, newhash)
+ self.assertTrue(newhash.startswith('$6$'))
+ with self.login('oldpassword') as cu:
+ pass
+ self.assertEqual(newhash, str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0]))
+
class SecurityRewritingTC(BaseSecurityTC):
def hijack_source_execute(self):
--- a/server/utils.py Fri Sep 14 17:42:24 2012 +0200
+++ b/server/utils.py Mon Sep 10 15:17:10 2012 +0200
@@ -52,7 +52,9 @@
return md5crypt(secret, self.salt.encode('ascii')).decode('utf-8')
_calc_checksum = calc_checksum
-_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'])
+_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'],
+ deprecated=['cubicwebmd5crypt', 'des_crypt'])
+verify_and_update = _CRYPTO_CTX.verify_and_update
def crypt_password(passwd, salt=None):
"""return the encrypted password using the given salt or a generated one