Home > flask, python > Flask (parte 5): password security

Flask (parte 5): password security

1 Luglio 2015

La sicurezza di un sito, passa per la autenticazione di un utente
(user authentication) e per la gestione della password collegata ad
esso.

E’ buona usanza non registrare nel database la password di un utente,
bensì l’HASH relativo ad essa.
Queste operazioni vengono gestite facilmente con Werkzeug:

nel caso non lo avessimo ancora installato

(venv) >pip install werkzeug

Werkzeug ci mette a disposizione semplicemente due funzioni:

generate_password_hash(password, method=pbkdf2:sha1, salt_length=8)
check_password_hash(hash, password)

il loro utilizzo è molto semplice, prima però di vedere come utilizzare
questo strumento, modifichiamo il model User:

from . import db
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    teams = db.relationship('Team', backref='user')
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)
    password_hash = db.Column(db.String(128))
    
    def __repr__(self):
        return "<User %r>" % self.username

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

Abbiamo aggiunto il campo password_hash nel model User, pertanto
dobbiamo migrare e aggiornare il database:

(venv) >python manage.py db migrate -m "password_hash field added to User"
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added column 'users.password_hash'
Generating >fantamanager\migrations\versions\ea1a33541f6_password_hash_field_added_to_user.py ... done

(venv) >python manage.py db upgrade
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.migration] Running upgrade 3f1bfb116a2 -> ea1a33541f6, password_hash field added to User

facciamo ora una prova da shell:

>>> u1 = User.query.get(1)
>>> u1.password = "rosso"
>>> u1.password_hash
'pbkdf2:sha1:1000$ZDq916DS$860f7130f1e822998518f99ef078b9be11613f15'
>>> u2 = User.query.get(2)
>>> u2.password = "verde"
>>> u2.password_hash
'pbkdf2:sha1:1000$ujESM9bp$dae6906b4eb6359cf5410c469f1ae82f9664b9fb'

Nel caso dessi ad uno User la stessa password di un altro:

>>> u3 = User.query.get(3)
>>> u3.password = "verde"
>>> u3.password_hash == u2.password_hash
False

Grazie alla property, se proviamo ad accedere in lettura all’attributo password,
si solleva un’eccezione:

>>> u3.password
...
AttributeError: password is not a readable attribute

Questo meccanismo va testato, pertanto nell dir tests della nostra applicazione,
scriviamo un test in modo da tutelarci da sorprese in caso di modifiche future

Creiamo il file tests/test_user_pw_hashing.py

import unittest
from app.models import User


class UserModelTestCase(unittest.TestCase):

    def setUp(self):
        self.user_1 = User(password='rosso')
        self.user_2 = User(password='rosso')      

    def test_password_setter(self):
        self.assertTrue(self.user_1.password_hash is not None)

    def test_no_password_getter(self):
        with self.assertRaises(AttributeError):
            self.user_1.password

    def test_password_verification(self):
        self.assertTrue(self.user_1.verify_password('rosso'))
        self.assertFalse(self.user_1.verify_password('verde'))

    def test_password_salts_are_random(self):
        self.assertTrue(self.user_1.password_hash != \
                        self.user_2.password_hash)

e modifichiamo il file manage.py per lanciare comodamente i test:

#!/usr/bin/env python
...
import unittest
...

@manager.command
def test():
    """Run the unit tests."""
    tests = unittest.TestLoader().discover('tests')
    unittest.TextTestRunner(verbosity=2).run(tests)
...

ora possiamo lanciare il comando “test”:

(venv) >python manage.py test
test_no_password_getter (test_user_pw_hashing.UserModelTestCase) ... ok
test_password_salts_are_random (test_user_pw_hashing.UserModelTestCase) ... ok
test_password_setter (test_user_pw_hashing.UserModelTestCase) ... ok
test_password_verification (test_user_pw_hashing.UserModelTestCase) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.086s

OK

articolo successivo:
Flask (parte 6): authentication

articoli precedenti:
Flask (parte 1): virtualenv
Flask (parte 2): struttura progetto complesso
Flask (parte 3): database
Flask (parte 4): views e templates

link utili:
“Flask Web Development” di Miguel Grinberg, fondamentale per calarsi nel mondo di Flask.
Altri link fondamentali:
Flask sito ufficiale
il blog dell’autore (Flask mega tutorial)

Categorie:flask, python Tag: ,
I commenti sono chiusi.