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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/python-auth-toolkit.svg)](https://pypi.org/project/python-auth-toolkit/)
32
+ [![Python versions](https://img.shields.io/pypi/pyversions/python-auth-toolkit.svg)](https://pypi.org/project/python-auth-toolkit/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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