Skip to content

Commit e007cc5

Browse files
authored
refactor(auth): use Python stdlib instead of deprecated passlib (#614)
refactor(auth): use Python stdlib instead of deprecated passlib test(auth): add test to ensure passlib compatibility
1 parent f2af78c commit e007cc5

File tree

5 files changed

+37
-14
lines changed

5 files changed

+37
-14
lines changed

client/cypress/support/commands.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ Cypress.Commands.add('login' as any, () => {
66
77
cy.get('[data-cy="login-form-password"] input').focus().type('admin');
88
cy.get('[data-cy="login-form-submit"]').click();
9+
cy.url().should('not.eq', 'http://localhost:4200/login');
910
});

server/Pipfile

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ name = "pypi"
77
pillow = "*"
88
flask = "*"
99
flask-jwt-extended = "*"
10-
passlib = "*"
1110
flask-sqlalchemy = "*"
1211
python-dateutil = "*"
1312
flask-cors = "*"

server/Pipfile.lock

-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/src/models/user.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import secrets
2+
from base64 import b64decode, b64encode
3+
from hashlib import pbkdf2_hmac
14
from typing import Self
25

3-
from passlib.hash import pbkdf2_sha256
46
from sqlalchemy.dialects.postgresql import UUID
57

68
from extensions import db
@@ -43,11 +45,33 @@ class User(HasSlug, IsSearchable, BaseEntity):
4345

4446
@staticmethod
4547
def generate_hash(password):
46-
return pbkdf2_sha256.hash(password)
48+
# recommended iteration count by OWASP cheat sheet
49+
# https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
50+
iterations = 600_000
51+
salt = secrets.token_bytes(16)
52+
hash_value = pbkdf2_hmac("sha256", password.encode(), salt, iterations)
53+
54+
salt_encoded = b64encode(salt, altchars=b"./").decode().rstrip("=")
55+
hash_encoded = b64encode(hash_value, altchars=b"./").decode().rstrip("=")
56+
57+
return f"$pbkdf2-sha256${iterations}${salt_encoded}${hash_encoded}"
4758

4859
@staticmethod
4960
def verify_hash(password, password_hash):
50-
return pbkdf2_sha256.verify(password, password_hash)
61+
if not password_hash.startswith("$pbkdf2-sha256$") or password_hash.count("$") != 4:
62+
raise ValueError("Invalid password hash")
63+
64+
details = password_hash[15:].split("$") # Remove known prefix
65+
iterations = int(details[0])
66+
67+
# base64 padding is required
68+
# this workaround is probably CPython specific, but simplifies the code
69+
salt = b64decode(details[1] + "===", altchars=b"./")
70+
known_hash = b64decode(details[2] + "===", altchars=b"./")
71+
72+
new_hash = pbkdf2_hmac("sha256", password.encode(), salt, iterations)
73+
74+
return secrets.compare_digest(new_hash, known_hash)
5175

5276
@classmethod
5377
def find_by_reset_password_hash(cls, password_hash) -> Self | None:

server/tests/test_auth_resources.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from models.enums.map_marker_type_enum import MapMarkerType
99
from models.file import File
1010
from models.user import User
11-
from tests.conftest import admin_token
1211

1312

1413
def test_successful_login(client):
@@ -203,7 +202,6 @@ def test_revoked_access_token_behaviour(client, member_token):
203202

204203

205204
def test_revoked_refresh_token_behaviour(client, admin_refresh_token):
206-
refresh_token = ""
207205
rv = client.post("/api/logout/refresh", token=admin_refresh_token)
208206
assert rv.status_code == 200
209207
res = rv.json
@@ -305,3 +303,12 @@ def test_permission_levels(client, user_token, member_token, moderator_token):
305303

306304
rv = client.post("/api/crags", token=moderator_token, json=crag_data)
307305
assert rv.status_code == 201
306+
307+
308+
def test_passlib_compatibility():
309+
# passlib.hash.pbkdf2_sha256.hash("abc")
310+
hash1 = "$pbkdf2-sha256$29000$2HtPiVGKEcKYU2pt7R1jTA$wXMP6Wpr6FdM2Pnb.bMG0nVBmBmaX6WzPm2g0.GVHIU"
311+
# passlib.hash.pbkdf2_sha256.hash("⚡🏜️🦥")
312+
hash2 = "$pbkdf2-sha256$29000$bo3xvtc6pzTm/F9L6V2LMQ$9voplTQmhlXiJakG38j/e5QMOGZmfA5xbQtE2Xf5XKE"
313+
assert User.verify_hash("abc", hash1), "Hashing compatilibity error"
314+
assert User.verify_hash("⚡🏜️🦥", hash2), "Unicode hashing compatibility error"

0 commit comments

Comments
 (0)