python-auth-toolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_auth_toolkit/__init__.py +19 -0
- python_auth_toolkit/exceptions.py +27 -0
- python_auth_toolkit/password_hasher.py +201 -0
- python_auth_toolkit/password_validator.py +356 -0
- python_auth_toolkit/py.typed +1 -0
- python_auth_toolkit-0.1.0.dist-info/METADATA +155 -0
- python_auth_toolkit-0.1.0.dist-info/RECORD +10 -0
- python_auth_toolkit-0.1.0.dist-info/WHEEL +5 -0
- python_auth_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_auth_toolkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
python-auth-toolkit: A secure and configurable password and authentication toolkit.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from python_auth_toolkit.password_hasher import PasswordHasher
|
|
6
|
+
from python_auth_toolkit.password_validator import PasswordValidator
|
|
7
|
+
from python_auth_toolkit.exceptions import (
|
|
8
|
+
AuthToolkitError,
|
|
9
|
+
UnsupportedAlgorithmError,
|
|
10
|
+
HashingError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PasswordHasher",
|
|
15
|
+
"PasswordValidator",
|
|
16
|
+
"AuthToolkitError",
|
|
17
|
+
"UnsupportedAlgorithmError",
|
|
18
|
+
"HashingError",
|
|
19
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for python-auth-toolkit.
|
|
3
|
+
|
|
4
|
+
All exceptions inherit from AuthToolkitError for easy catch-all handling.
|
|
5
|
+
"""
|
|
6
|
+
class AuthToolkitError(Exception):
|
|
7
|
+
"""Base exception for all python-auth-toolkit errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str = "An authentication toolkit error occurred."):
|
|
10
|
+
self.message = message
|
|
11
|
+
super().__init__(self.message)
|
|
12
|
+
|
|
13
|
+
class UnsupportedAlgorithmError(AuthToolkitError):
|
|
14
|
+
"""Raised when an unsupported hashing algorithm is requested."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, algorithm: str = ""):
|
|
17
|
+
self.algorithm = algorithm
|
|
18
|
+
message = (
|
|
19
|
+
f"Unsupported algorithm: '{algorithm}'. Supported algorithms: 'bcrypt', 'argon2'."
|
|
20
|
+
)
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
|
|
23
|
+
class HashingError(AuthToolkitError):
|
|
24
|
+
"""Raised when password hashing or verification fails unexpectedly."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, message: str = "An error occurred during password hashing."):
|
|
27
|
+
super().__init__(message)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import argon2
|
|
4
|
+
import bcrypt
|
|
5
|
+
|
|
6
|
+
from python_auth_toolkit.exceptions import ( UnsupportedAlgorithmError, HashingError )
|
|
7
|
+
# Supported algorithms
|
|
8
|
+
_SUPPORTED_ALGORITHMS = {"bcrypt", "argon2"}
|
|
9
|
+
|
|
10
|
+
# Default cost parameters
|
|
11
|
+
_DEFAULT_BCRYPT_ROUNDS = 12
|
|
12
|
+
_DEFAULT_ARGON2_TIME_COST = 3
|
|
13
|
+
_DEFAULT_ARGON2_MEMORY_COST = 65536 # 64 MiB
|
|
14
|
+
_DEFAULT_ARGON2_PARALLELISM = 4
|
|
15
|
+
|
|
16
|
+
class PasswordHasher:
|
|
17
|
+
"""
|
|
18
|
+
Secure password hasher support Bcrypt and argon2.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
hasher = PasswordHasher() #defaults to bycrpt, 12 rounds
|
|
22
|
+
hashed = hasher.hash("my_secure_password")
|
|
23
|
+
|
|
24
|
+
if hasher.varify("my_secure_password", hashed):
|
|
25
|
+
print("Password Matches!!!")
|
|
26
|
+
|
|
27
|
+
# chech if hash needs upgrading (e.g: after changing rounds)
|
|
28
|
+
if hasher.needs_rehash(hashed):
|
|
29
|
+
new_hash = hasher.hash("my_secure_password")
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
algorithm: Hashing algorithm to use. One of 'bcrypt' or 'argon2'.
|
|
33
|
+
rounds: Cost factor for bcrypt (4-31). Ignored for argon2.
|
|
34
|
+
time_cost: Time cost for argon2. Ignored for bcrypt.
|
|
35
|
+
memory_cost: Memory cost in KiB for argon2. Ignored for bcrypt.
|
|
36
|
+
parallelism: Parallelism factor for argon2. Ignored for bcrypt.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
algorithm: str = 'bcrypt',
|
|
42
|
+
rounds: int = _DEFAULT_BCRYPT_ROUNDS,
|
|
43
|
+
time_cost: int = _DEFAULT_ARGON2_TIME_COST,
|
|
44
|
+
memory_cost: int = _DEFAULT_ARGON2_MEMORY_COST,
|
|
45
|
+
parallelism: int = _DEFAULT_ARGON2_PARALLELISM,
|
|
46
|
+
) -> None:
|
|
47
|
+
algorithm = algorithm.lower().strip()
|
|
48
|
+
if algorithm not in _SUPPORTED_ALGORITHMS:
|
|
49
|
+
raise UnsupportedAlgorithmError(algorithm)
|
|
50
|
+
|
|
51
|
+
self.algorithm = algorithm
|
|
52
|
+
self.rounds = rounds
|
|
53
|
+
self.time_cost = time_cost
|
|
54
|
+
self.memory_cost = memory_cost
|
|
55
|
+
self.parallelism = parallelism
|
|
56
|
+
|
|
57
|
+
def hash(self, password: str) -> str:
|
|
58
|
+
"""Hash a plaintext password with automatic salt generation.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
password: The plain text password to hash
|
|
62
|
+
|
|
63
|
+
Returs:
|
|
64
|
+
The hashed password string (includes embedded salt and parameters)
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
HashingError: If hashing fails unexpectedly.
|
|
68
|
+
ValueError: If password is empty.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
if not password:
|
|
72
|
+
raise ValueError("Password cannot be empty.")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
if self.algorithm == 'bcrypt':
|
|
76
|
+
return self._hash_bcrypt(password)
|
|
77
|
+
else:
|
|
78
|
+
return self._hash_argon2(password)
|
|
79
|
+
except (ValueError, TypeError) as e:
|
|
80
|
+
raise HashingError(f"Failed to hash password: {e}") from e
|
|
81
|
+
|
|
82
|
+
def verify(self, password: str, hashed: str) -> bool:
|
|
83
|
+
"""Verify plain text password against a stored hash.
|
|
84
|
+
Uses timing-safe comparision internally (provided by bcrypt/agron2 libs)
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
password: The plain text password to verify.
|
|
88
|
+
hashed: The stored hash string is to verify against.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if password matches the hash, else false.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
HashingError: If verification fails due to currupted hash.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
if not password or not hashed:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Auto detect algorithm from hash format
|
|
102
|
+
if hashed.startswith("$argon2"):
|
|
103
|
+
return self._verify_argon2(password, hashed)
|
|
104
|
+
elif hashed.startswith("$2b$") or hashed.startswith("$2a$"):
|
|
105
|
+
return self._verify_bcrypt(password, hashed)
|
|
106
|
+
else:
|
|
107
|
+
raise HashingError(
|
|
108
|
+
"Unrecognized hash format. Hash must start with '$2b$', '$2a$', or '$argon2'."
|
|
109
|
+
)
|
|
110
|
+
except HashingError:
|
|
111
|
+
raise
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise HashingError(f"Failed to verify password: {e}") from e
|
|
114
|
+
|
|
115
|
+
def needs_rehash(self, hashed: str) -> bool:
|
|
116
|
+
"""Check if a hash needs to be regenerated due to changed parameters.
|
|
117
|
+
|
|
118
|
+
This is useful when you upgrade your cost parameters — existing hashes
|
|
119
|
+
still verify correctly, but new hashes should use the updated params.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
hashed: The stored hash string to check.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
True if the hash should be regenerated with current parameters.
|
|
126
|
+
"""
|
|
127
|
+
if not hashed:
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
if hashed.startswith("$argon2"):
|
|
132
|
+
return self._needs_rehash_argon2(hashed)
|
|
133
|
+
elif hashed.startswith("$2b$") or hashed.startswith("$2a$"):
|
|
134
|
+
return self._needs_rehash_bcrypt(hashed)
|
|
135
|
+
else:
|
|
136
|
+
# Unknown format → definitely needs rehash
|
|
137
|
+
return True
|
|
138
|
+
except Exception:
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
# Internal: bcrypt
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def _hash_bcrypt(self, password: str) -> str:
|
|
146
|
+
"""Hash with bcrypt. Salt is auto-generated."""
|
|
147
|
+
salt = bcrypt.gensalt(rounds=self.rounds)
|
|
148
|
+
hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
|
|
149
|
+
return hashed.decode("utf-8")
|
|
150
|
+
|
|
151
|
+
def _verify_bcrypt(self, password: str, hashed: str) -> bool:
|
|
152
|
+
"""Verify with bcrypt (timing-safe internally)."""
|
|
153
|
+
return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
|
|
154
|
+
|
|
155
|
+
def _needs_rehash_bcrypt(self, hashed: str) -> bool:
|
|
156
|
+
"""Check if bcrypt hash uses different rounds than configured."""
|
|
157
|
+
# bcrypt hash format: $2b$<rounds>$<salt+hash>
|
|
158
|
+
match = re.match(r"\$2[ab]\$(\d+)\$", hashed)
|
|
159
|
+
if not match:
|
|
160
|
+
return True
|
|
161
|
+
stored_rounds = int(match.group(1))
|
|
162
|
+
return stored_rounds != self.rounds
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def _argon2_hasher(self) -> argon2.PasswordHasher:
|
|
166
|
+
"""Lazily initialize the argon2 hasher instance."""
|
|
167
|
+
if not hasattr(self, "_argon2_hasher_instance"):
|
|
168
|
+
self._argon2_hasher_instance = argon2.PasswordHasher(
|
|
169
|
+
time_cost=self.time_cost,
|
|
170
|
+
memory_cost=self.memory_cost,
|
|
171
|
+
parallelism=self.parallelism,
|
|
172
|
+
type=argon2.Type.ID,
|
|
173
|
+
)
|
|
174
|
+
return self._argon2_hasher_instance
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# Internal: argon2
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def _hash_argon2(self, password: str) -> str:
|
|
181
|
+
"""Hash with argon2id. Salt is auto-generated."""
|
|
182
|
+
return self._argon2_hasher.hash(password)
|
|
183
|
+
|
|
184
|
+
def _verify_argon2(self, password: str, hashed: str) -> bool:
|
|
185
|
+
"""Verify with argon2 (timing-safe internally)."""
|
|
186
|
+
try:
|
|
187
|
+
return self._argon2_hasher.verify(hashed, password)
|
|
188
|
+
except argon2.exceptions.VerifyMismatchError:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def _needs_rehash_argon2(self, hashed: str) -> bool:
|
|
192
|
+
"""Check if argon2 hash uses different parameters than configured."""
|
|
193
|
+
return self._argon2_hasher.check_needs_rehash(hashed)
|
|
194
|
+
|
|
195
|
+
def __repr__(self) -> str:
|
|
196
|
+
if self.algorithm == "bcrypt":
|
|
197
|
+
return f"PasswordHasher(algorithm='bcrypt', rounds={self.rounds})"
|
|
198
|
+
return (
|
|
199
|
+
f"PasswordHasher(algorithm='argon2', time_cost={self.time_cost}, "
|
|
200
|
+
f"memory_cost={self.memory_cost}, parallelism={self.parallelism})"
|
|
201
|
+
)
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
# Some Common passwords to block if block_common_passwords is True
|
|
5
|
+
COMMON_PASSWORDS: frozenset[str] = frozenset(
|
|
6
|
+
{
|
|
7
|
+
"123456",
|
|
8
|
+
"password",
|
|
9
|
+
"12345678",
|
|
10
|
+
"qwerty",
|
|
11
|
+
"123456789",
|
|
12
|
+
"12345",
|
|
13
|
+
"1234",
|
|
14
|
+
"111111",
|
|
15
|
+
"1234567",
|
|
16
|
+
"dragon",
|
|
17
|
+
"123123",
|
|
18
|
+
"baseball",
|
|
19
|
+
"abc123",
|
|
20
|
+
"football",
|
|
21
|
+
"monkey",
|
|
22
|
+
"letmein",
|
|
23
|
+
"shadow",
|
|
24
|
+
"master",
|
|
25
|
+
"666666",
|
|
26
|
+
"qwertyuiop",
|
|
27
|
+
"123321",
|
|
28
|
+
"mustang",
|
|
29
|
+
"1234567890",
|
|
30
|
+
"michael",
|
|
31
|
+
"654321",
|
|
32
|
+
"superman",
|
|
33
|
+
"1qaz2wsx",
|
|
34
|
+
"7777777",
|
|
35
|
+
"121212",
|
|
36
|
+
"000000",
|
|
37
|
+
"qazwsx",
|
|
38
|
+
"123qwe",
|
|
39
|
+
"killer",
|
|
40
|
+
"trustno1",
|
|
41
|
+
"jordan",
|
|
42
|
+
"jennifer",
|
|
43
|
+
"zxcvbnm",
|
|
44
|
+
"asdfgh",
|
|
45
|
+
"hunter",
|
|
46
|
+
"buster",
|
|
47
|
+
"soccer",
|
|
48
|
+
"harley",
|
|
49
|
+
"batman",
|
|
50
|
+
"andrew",
|
|
51
|
+
"tigger",
|
|
52
|
+
"sunshine",
|
|
53
|
+
"iloveyou",
|
|
54
|
+
"2000",
|
|
55
|
+
"charlie",
|
|
56
|
+
"robert",
|
|
57
|
+
"thomas",
|
|
58
|
+
"hockey",
|
|
59
|
+
"ranger",
|
|
60
|
+
"daniel",
|
|
61
|
+
"starwars",
|
|
62
|
+
"klaster",
|
|
63
|
+
"112233",
|
|
64
|
+
"george",
|
|
65
|
+
"computer",
|
|
66
|
+
"michelle",
|
|
67
|
+
"jessica",
|
|
68
|
+
"pepper",
|
|
69
|
+
"1111",
|
|
70
|
+
"zxcvbn",
|
|
71
|
+
"555555",
|
|
72
|
+
"11111111",
|
|
73
|
+
"131313",
|
|
74
|
+
"freedom",
|
|
75
|
+
"777777",
|
|
76
|
+
"pass",
|
|
77
|
+
"maggie",
|
|
78
|
+
"159753",
|
|
79
|
+
"aaaaaa",
|
|
80
|
+
"ginger",
|
|
81
|
+
"princess",
|
|
82
|
+
"joshua",
|
|
83
|
+
"cheese",
|
|
84
|
+
"amanda",
|
|
85
|
+
"summer",
|
|
86
|
+
"love",
|
|
87
|
+
"ashley",
|
|
88
|
+
"nicole",
|
|
89
|
+
"chelsea",
|
|
90
|
+
"biteme",
|
|
91
|
+
"matthew",
|
|
92
|
+
"access",
|
|
93
|
+
"yankees",
|
|
94
|
+
"987654321",
|
|
95
|
+
"dallas",
|
|
96
|
+
"austin",
|
|
97
|
+
"thunder",
|
|
98
|
+
"taylor",
|
|
99
|
+
"matrix",
|
|
100
|
+
"mobilemail",
|
|
101
|
+
"william",
|
|
102
|
+
"corvette",
|
|
103
|
+
"hello",
|
|
104
|
+
"martin",
|
|
105
|
+
"heather",
|
|
106
|
+
"secret",
|
|
107
|
+
"merlin",
|
|
108
|
+
"diamond",
|
|
109
|
+
"1234qwer",
|
|
110
|
+
"gfhjkm",
|
|
111
|
+
"hammer",
|
|
112
|
+
"silver",
|
|
113
|
+
"222222",
|
|
114
|
+
"88888888",
|
|
115
|
+
"anthony",
|
|
116
|
+
"justin",
|
|
117
|
+
"test",
|
|
118
|
+
"bailey",
|
|
119
|
+
"q1w2e3r4t5",
|
|
120
|
+
"patrick",
|
|
121
|
+
"internet",
|
|
122
|
+
"scooter",
|
|
123
|
+
"orange",
|
|
124
|
+
"11111",
|
|
125
|
+
"golfer",
|
|
126
|
+
"cookie",
|
|
127
|
+
"richard",
|
|
128
|
+
"samantha",
|
|
129
|
+
"bigdog",
|
|
130
|
+
"guitar",
|
|
131
|
+
"jackson",
|
|
132
|
+
"whatever",
|
|
133
|
+
"mickey",
|
|
134
|
+
"chicken",
|
|
135
|
+
"sparky",
|
|
136
|
+
"snoopy",
|
|
137
|
+
"maverick",
|
|
138
|
+
"phoenix",
|
|
139
|
+
"camaro",
|
|
140
|
+
"peanut",
|
|
141
|
+
"morgan",
|
|
142
|
+
"welcome",
|
|
143
|
+
"falcon",
|
|
144
|
+
"cowboy",
|
|
145
|
+
"ferrari",
|
|
146
|
+
"samsung",
|
|
147
|
+
"andrea",
|
|
148
|
+
"smokey",
|
|
149
|
+
"steelers",
|
|
150
|
+
"joseph",
|
|
151
|
+
"mercedes",
|
|
152
|
+
"dakota",
|
|
153
|
+
"arsenal",
|
|
154
|
+
"eagles",
|
|
155
|
+
"melissa",
|
|
156
|
+
"boomer",
|
|
157
|
+
"booboo",
|
|
158
|
+
"spider",
|
|
159
|
+
"nascar",
|
|
160
|
+
"monster",
|
|
161
|
+
"tigers",
|
|
162
|
+
"yellow",
|
|
163
|
+
"xxxxxx",
|
|
164
|
+
"123123123",
|
|
165
|
+
"gateway",
|
|
166
|
+
"marina",
|
|
167
|
+
"diablo",
|
|
168
|
+
"bulldog",
|
|
169
|
+
"qwer1234",
|
|
170
|
+
"compaq",
|
|
171
|
+
"purple",
|
|
172
|
+
"hardcore",
|
|
173
|
+
"banana",
|
|
174
|
+
"junior",
|
|
175
|
+
"hannah",
|
|
176
|
+
"123654",
|
|
177
|
+
"lazarus",
|
|
178
|
+
"nicholas",
|
|
179
|
+
"swimming",
|
|
180
|
+
"trustme",
|
|
181
|
+
"dolphin",
|
|
182
|
+
"wildcats",
|
|
183
|
+
"adrian",
|
|
184
|
+
"alexis",
|
|
185
|
+
"password1",
|
|
186
|
+
"password123",
|
|
187
|
+
"admin",
|
|
188
|
+
"admin123",
|
|
189
|
+
"root",
|
|
190
|
+
"toor",
|
|
191
|
+
"pass123",
|
|
192
|
+
"changeme",
|
|
193
|
+
"welcome1",
|
|
194
|
+
"passw0rd",
|
|
195
|
+
"p@ssw0rd",
|
|
196
|
+
"p@ssword",
|
|
197
|
+
"qwerty123",
|
|
198
|
+
"letmein1",
|
|
199
|
+
"abc1234",
|
|
200
|
+
"welcome123",
|
|
201
|
+
"password1234",
|
|
202
|
+
"admin1234",
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class ValidationResult:
|
|
208
|
+
"""Result of a password validation check.
|
|
209
|
+
|
|
210
|
+
Attributes:
|
|
211
|
+
is_valid: Whether the password passed all validation rules.
|
|
212
|
+
errors: List of human-readable error messages for failed rules.
|
|
213
|
+
strength_score: Password strength from 0 (very weak) to 100 (very strong).
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
is_valid: bool
|
|
217
|
+
errors: list[str] = field(default_factory=list)
|
|
218
|
+
strength_score: int = 0
|
|
219
|
+
|
|
220
|
+
class PasswordValidator:
|
|
221
|
+
"""
|
|
222
|
+
Configurable password strength validator.
|
|
223
|
+
Validates password against a set of configurable rules and provides an entropy score.
|
|
224
|
+
|
|
225
|
+
Example:
|
|
226
|
+
validator = PasswordValidator(
|
|
227
|
+
min_length=8,
|
|
228
|
+
required_uppercase=1,
|
|
229
|
+
required_lowercase=1,
|
|
230
|
+
required_digits=1,
|
|
231
|
+
required_special=1)
|
|
232
|
+
result = validator.validate("P@ssw0rd")
|
|
233
|
+
|
|
234
|
+
if result.is_valid:
|
|
235
|
+
print("Password is valid with entropy:", result.entropy)
|
|
236
|
+
else:
|
|
237
|
+
print("Password is invalid. Errors:", result.errors)
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def __init__(
|
|
241
|
+
self,
|
|
242
|
+
min_length: int = 8,
|
|
243
|
+
max_length: int = 128,
|
|
244
|
+
required_uppercase: bool = True,
|
|
245
|
+
required_lowercase: bool = True,
|
|
246
|
+
required_digits: bool = True,
|
|
247
|
+
required_special: bool = True,
|
|
248
|
+
block_common_passwords: bool = True,
|
|
249
|
+
) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Initializes the PasswordValidator with configurable rules.
|
|
252
|
+
Args:
|
|
253
|
+
min_length (int): Minimum length of the password.
|
|
254
|
+
max_length (int): Maximum length of the password.
|
|
255
|
+
required_uppercase (bool): Whether at least one uppercase letter is required.
|
|
256
|
+
required_lowercase (bool): Whether at least one lowercase letter is required.
|
|
257
|
+
required_digits (bool): Whether at least one digit is required.
|
|
258
|
+
required_special (bool): Whether at least one special character is required.
|
|
259
|
+
block_common_passwords (bool): Whether to block common passwords.
|
|
260
|
+
"""
|
|
261
|
+
if min_length < 1:
|
|
262
|
+
raise ValueError("Minimum length must be at least 1.")
|
|
263
|
+
if max_length < min_length:
|
|
264
|
+
raise ValueError("Maximum length must be greater than or equal to minimum length.")
|
|
265
|
+
|
|
266
|
+
self.min_length = min_length
|
|
267
|
+
self.max_length = max_length
|
|
268
|
+
self.required_uppercase = required_uppercase
|
|
269
|
+
self.required_lowercase = required_lowercase
|
|
270
|
+
self.required_digits = required_digits
|
|
271
|
+
self.required_special = required_special
|
|
272
|
+
self.block_common_passwords = block_common_passwords
|
|
273
|
+
|
|
274
|
+
def validate(self, password: str) -> ValidationResult:
|
|
275
|
+
"""
|
|
276
|
+
Validate a password against all configurable rules.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
password (str): The password to validate.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
ValidationResult: The result of the validation, including validity, errors, and strength score.
|
|
283
|
+
"""
|
|
284
|
+
errors: list[str] = []
|
|
285
|
+
|
|
286
|
+
if len(password) < self.min_length:
|
|
287
|
+
errors.append(f"Password must be at least {self.min_length} characters long."
|
|
288
|
+
f" Current length: {len(password)}.")
|
|
289
|
+
if len(password) > self.max_length:
|
|
290
|
+
errors.append(f"Password must be no more than {self.max_length} characters long."
|
|
291
|
+
f" Current length: {len(password)}.")
|
|
292
|
+
|
|
293
|
+
# Check for uppercase letters
|
|
294
|
+
if self.required_uppercase and not re.search(r"[A-Z]", password):
|
|
295
|
+
errors.append("Password must contain at least one uppercase letter.")
|
|
296
|
+
|
|
297
|
+
# Check for lowercase letters
|
|
298
|
+
if self.required_lowercase and not re.search(r"[a-z]", password):
|
|
299
|
+
errors.append("Password must contain at least one lowercase letter.")
|
|
300
|
+
|
|
301
|
+
# Check for digits
|
|
302
|
+
if self.required_digits and not re.search(r"\d", password):
|
|
303
|
+
errors.append("Password must contain at least one digit.")
|
|
304
|
+
|
|
305
|
+
# Check for special characters
|
|
306
|
+
if self.required_special and not re.search(r"[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>\/?`~]", password):
|
|
307
|
+
errors.append("Password must contain at least one special character.")
|
|
308
|
+
|
|
309
|
+
# Check against common passwords
|
|
310
|
+
if self.block_common_passwords and password in COMMON_PASSWORDS:
|
|
311
|
+
errors.append("Password is too common and easily guessable.")
|
|
312
|
+
|
|
313
|
+
# Compute strength score
|
|
314
|
+
strength_score = self._calculate_strength(password)
|
|
315
|
+
|
|
316
|
+
return ValidationResult(
|
|
317
|
+
is_valid=len(errors) == 0,
|
|
318
|
+
errors=errors,
|
|
319
|
+
strength_score=strength_score
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def _calculate_strength(self, password: str) -> int:
|
|
323
|
+
"""
|
|
324
|
+
Calculate a password strength score based on its characteristics.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
password (str): The password to evaluate.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
int: Strength score from 0 (very weak) to 100 (very strong).
|
|
331
|
+
"""
|
|
332
|
+
score = 0
|
|
333
|
+
|
|
334
|
+
if not password:
|
|
335
|
+
return 0
|
|
336
|
+
|
|
337
|
+
# Length contributes up to 40 points
|
|
338
|
+
if len(password) >= self.min_length:
|
|
339
|
+
score += min(40, (len(password) - self.min_length) * 4)
|
|
340
|
+
|
|
341
|
+
# Character variety contributes up to 30 points
|
|
342
|
+
if re.search(r"[A-Z]", password):
|
|
343
|
+
score += 10
|
|
344
|
+
if re.search(r"[a-z]", password):
|
|
345
|
+
score += 10
|
|
346
|
+
if re.search(r"\d", password):
|
|
347
|
+
score += 10
|
|
348
|
+
if re.search(r"[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>\/?`~]", password):
|
|
349
|
+
score += 10
|
|
350
|
+
|
|
351
|
+
# Deduct points for common passwords
|
|
352
|
+
if self.block_common_passwords and password in COMMON_PASSWORDS:
|
|
353
|
+
score -= 20
|
|
354
|
+
|
|
355
|
+
return max(0, min(100, score))
|
|
356
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-auth-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: All-in-one Python authentication toolkit: password validation, password hashing, email validation.
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: bcrypt>=4.0
|
|
20
|
+
Requires-Dist: argon2-cffi>=21.3
|
|
21
|
+
Requires-Dist: email-validator>=2.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# python-auth-toolkit
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/python-auth-toolkit/)
|
|
32
|
+
[](https://pypi.org/project/python-auth-toolkit/)
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
|
|
35
|
+
An all-in-one, highly secure, and configurable Python authentication toolkit. It provides robust password strength validation, secure password hashing (Bcrypt & Argon2), and custom validation error feedback.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Password Validator**: Fully customizable rules (minimum/maximum length, uppercase/lowercase requirements, digit requirements, and special character requirements).
|
|
42
|
+
- **Common Passwords Protection**: Built-in protection to block the most common easily guessable passwords (e.g., "123456", "password", "qwerty").
|
|
43
|
+
- **Password Hasher**: Secure password hashing supporting **Bcrypt** and **Argon2id** with timing-safe verification.
|
|
44
|
+
- **Upgrades & Re-hashing**: Easy checking for out-of-date password hashes (e.g., when cost factors are increased) to allow seamless re-hashing.
|
|
45
|
+
- **Type Safety**: Full PEP 561 type annotation support.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
Install using `pip`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install python-auth-toolkit
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
### 1. Password Hashing
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from python_auth_toolkit import PasswordHasher
|
|
65
|
+
|
|
66
|
+
# Initialize with default settings (Bcrypt with 12 rounds)
|
|
67
|
+
hasher = PasswordHasher()
|
|
68
|
+
|
|
69
|
+
# Hash a password
|
|
70
|
+
hashed = hasher.hash("my_secure_password")
|
|
71
|
+
print(hashed) # Output: $2b$12$... (embedded salt and rounds)
|
|
72
|
+
|
|
73
|
+
# Verify a password
|
|
74
|
+
is_match = hasher.verify("my_secure_password", hashed)
|
|
75
|
+
print(is_match) # True
|
|
76
|
+
|
|
77
|
+
# Check if a hash needs to be regenerated (e.g., after increasing security defaults)
|
|
78
|
+
upgraded_hasher = PasswordHasher(rounds=14)
|
|
79
|
+
if upgraded_hasher.needs_rehash(hashed):
|
|
80
|
+
new_hash = upgraded_hasher.hash("my_secure_password")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Using **Argon2**:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from python_auth_toolkit import PasswordHasher
|
|
87
|
+
|
|
88
|
+
# Initialize with Argon2id
|
|
89
|
+
hasher = PasswordHasher(algorithm="argon2", time_cost=3, memory_cost=65536)
|
|
90
|
+
|
|
91
|
+
# Hash and verify
|
|
92
|
+
hashed = hasher.hash("another_password")
|
|
93
|
+
is_match = hasher.verify("another_password", hashed)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 2. Password Strength Validation
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from python_auth_toolkit import PasswordValidator
|
|
100
|
+
|
|
101
|
+
# Initialize validator with custom rules
|
|
102
|
+
validator = PasswordValidator(
|
|
103
|
+
min_length=10,
|
|
104
|
+
required_uppercase=True,
|
|
105
|
+
required_lowercase=True,
|
|
106
|
+
required_digits=True,
|
|
107
|
+
required_special=True,
|
|
108
|
+
block_common_passwords=True
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Validate a password
|
|
112
|
+
result = validator.validate("P@ssw0rd123")
|
|
113
|
+
|
|
114
|
+
if result.is_valid:
|
|
115
|
+
print(f"Valid! Strength Score: {result.strength_score}/100")
|
|
116
|
+
else:
|
|
117
|
+
print("Invalid Password. Errors:")
|
|
118
|
+
for error in result.errors:
|
|
119
|
+
print(f" - {error}")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Configuration Options
|
|
125
|
+
|
|
126
|
+
### `PasswordValidator` parameters:
|
|
127
|
+
- `min_length` (int, default: 8): Minimum length required.
|
|
128
|
+
- `max_length` (int, default: 128): Maximum length allowed.
|
|
129
|
+
- `required_uppercase` (bool, default: True): Require at least one uppercase letter.
|
|
130
|
+
- `required_lowercase` (bool, default: True): Require at least one lowercase letter.
|
|
131
|
+
- `required_digits` (bool, default: True): Require at least one digit.
|
|
132
|
+
- `required_special` (bool, default: True): Require at least one special character.
|
|
133
|
+
- `block_common_passwords` (bool, default: True): Reject passwords present in the common passwords list.
|
|
134
|
+
|
|
135
|
+
### `PasswordHasher` parameters:
|
|
136
|
+
- `algorithm` (str, default: 'bcrypt'): Choose between `'bcrypt'` or `'argon2'`.
|
|
137
|
+
- `rounds` (int, default: 12): Cost factor rounds for Bcrypt.
|
|
138
|
+
- `time_cost` (int, default: 3): Time cost for Argon2.
|
|
139
|
+
- `memory_cost` (int, default: 65536): Memory cost in KiB for Argon2.
|
|
140
|
+
- `parallelism` (int, default: 4): Parallelism factor threads for Argon2.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Requirements
|
|
145
|
+
|
|
146
|
+
- Python >= 3.9
|
|
147
|
+
- bcrypt >= 4.0
|
|
148
|
+
- argon2-cffi >= 21.3
|
|
149
|
+
- email-validator >= 2.0
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
python_auth_toolkit/__init__.py,sha256=P0yxDmQ8N1AL3GFKfXNTYsbFf1tZeln5rIown2hcNMA,482
|
|
2
|
+
python_auth_toolkit/exceptions.py,sha256=9v-33wCXmX1ks4Cp24o_Y84jdfIQsYVcUAbyCGmEJGc,997
|
|
3
|
+
python_auth_toolkit/password_hasher.py,sha256=dHd7IuOHStwqvH9D1mG9qMQDyYL78N1OTncQB31vG_o,7630
|
|
4
|
+
python_auth_toolkit/password_validator.py,sha256=dJaSUyGn6u6mmkBChRBkLjZgSxX7-QU6aYREaXQrpfA,9919
|
|
5
|
+
python_auth_toolkit/py.typed,sha256=bWew9mHgMy8LqMu7RuqQXFXLBxh2CRx0dUbSx-3wE48,27
|
|
6
|
+
python_auth_toolkit-0.1.0.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
|
|
7
|
+
python_auth_toolkit-0.1.0.dist-info/METADATA,sha256=Lb6RnVL_nBnlximsWK5wVcH1UYSPiFvQ0VZwijge904,5291
|
|
8
|
+
python_auth_toolkit-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
python_auth_toolkit-0.1.0.dist-info/top_level.txt,sha256=_dmUHQy6a111OC6iVloDJsQxVDm2NdXDGFAzhcQynGA,20
|
|
10
|
+
python_auth_toolkit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
python_auth_toolkit
|