[sources/native] automatically update passwords using deprecated hashes on login stable
authorJulien Cristau <julien.cristau@logilab.fr>
Mon, 10 Sep 2012 15:17:10 +0200
branchstable
changeset 8546 3d2038d6f20d
parent 8545 eb7a171cec72
child 8547 f23ac525ddd1
[sources/native] automatically update passwords using deprecated hashes on login Closes #2465904
server/sources/native.py
server/test/unittest_security.py
server/utils.py
--- 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