Changed module name from my_auth to myauth

Changed encryption algorithm to argon2
Added unit tests
This commit is contained in:
2025-10-19 23:16:32 +02:00
parent c5831ef5c0
commit ef2647e229
13 changed files with 136 additions and 159 deletions

138
README.md
View File

@@ -22,7 +22,7 @@ with pluggable database backends.
* Secure password reset (via secure token stored in DB).
* Account activation / deactivation.
* **Security:**
* Password hashing with `bcrypt` (configurable rounds).
* Password hashing with `argon2`.
* Strict password validation (uppercase, lowercase, digit, special character).
* **Flexible Architecture:**
* **Pluggable Backends:** Supports MongoDB, PostgreSQL, and SQLite out of the box.
@@ -75,29 +75,16 @@ This example configures myauth to use MongoDB as its backend.
```Python
from fastapi import FastAPI
from my_auth import AuthService
from my_auth.api import auth_router
from my_auth.persistence.mongodb import MongoUserRepository, MongoTokenRepository
from myauth import create_app_router_for_mongoDB
# 1. Initialize FastAPI app
app = FastAPI()
# 2. Configure repositories for MongoDB
# Make sure your connection string is correct
user_repo = MongoUserRepository(connection_string="mongodb://localhost:27017/myappdb")
token_repo = MongoTokenRepository(connection_string="mongodb://localhost:27017/myappdb")
auth_router = create_app_router_for_mongoDB(mongodb_url="mongodb://localhost:27017",
jwt_secret="THIS_NEEDS_TO_BE_CHANGED")
# 3. Configure the Authentication Service
# IMPORTANT: Change this to a long, random, secret string
auth_service = AuthService(
user_repository=user_repo,
token_repository=token_repo,
jwt_secret="YOUR_SUPER_LONG_AND_SECURE_JWT_SECRET_HERE"
# email_service will be added in the next step
)
# 4. Include the authentication routes
# Endpoints like /auth/login, /auth/register are now active
# 3. Include the authentication routes
app.include_router(auth_router)
@@ -113,36 +100,18 @@ This example configures myauth to use PostgreSQL as its backend.
```Python
from fastapi import FastAPI
from my_auth import AuthService
from my_auth.api import auth_router
from my_auth.persistence.postgresql import PostgreSQLUserRepository, PostgreSQLTokenRepository
from myauth import create_app_router_for_postgreSQL
# 1. Initialize FastAPI app
app = FastAPI()
# 2. Configure repositories for PostgreSQL
# Update with your database credentials
db_config = {
"host": "localhost",
"port": 5432,
"database": "mydb",
"user": "postgres",
"password": "secret"
}
user_repo = PostgreSQLUserRepository(**db_config)
token_repo = PostgreSQLTokenRepository(**db_config)
# 2. Configure repositories for MongoDB
auth_router = create_app_router_for_mongoDB(postgresql_url="mongodb://localhost:27017",
username="admin",
password="password",
jwt_secret="THIS_NEEDS_TO_BE_CHANGED")
# 3. Configure the Authentication Service
# IMPORTANT: Change this to a long, random, secret string
auth_service = AuthService(
user_repository=user_repo,
token_repository=token_repo,
jwt_secret="YOUR_SUPER_LONG_AND_SECURE_JWT_SECRET_HERE"
# email_service will be added in the next step
)
# 4. Include the authentication routes
# Endpoints like /auth/login, /auth/register are now active
# 3. Include the authentication routes
app.include_router(auth_router)
@@ -158,30 +127,15 @@ This example configures myauth to use SQLite, which is ideal for development or
```Python
from fastapi import FastAPI
from my_auth import AuthService
from my_auth.api import auth_router
from my_auth.persistence.sqlite import SQLiteUserRepository, SQLiteTokenRepository
from myauth import create_app_router_for_sqlite
# 1. Initialize FastAPI app
app = FastAPI()
# 2. Configure repositories for SQLite
# This will create/use a file named 'auth.db' in the current directory
db_path = "./auth.db"
user_repo = SQLiteUserRepository(db_path=db_path)
token_repo = SQLiteTokenRepository(db_path=db_path)
# 2. Configure repositories for MongoDB
auth_router = create_app_router_for_sqlite(db_path="./UserDB", jwt_secret="THIS_NEEDS_TO_BE_CHANGED")
# 3. Configure the Authentication Service
# IMPORTANT: Change this to a long, random, secret string
auth_service = AuthService(
user_repository=user_repo,
token_repository=token_repo,
jwt_secret="YOUR_SUPER_LONG_AND_SECURE_JWT_SECRET_HERE"
# email_service will be added in the next step
)
# 4. Include the authentication routes
# Endpoints like /auth/login, /auth/register are now active
# 3. Include the authentication routes
app.include_router(auth_router)
@@ -204,11 +158,14 @@ pip install "myauth[email]"
```Python
# ... (keep your app and repository config from the Quick Start)
from fastapi import FastAPI
from myauth.emailing.smtp import SMTPEmailService
from myauth import create_app_router_for_sqlite
from my_auth.email.smtp import SMTPEmailService
# 1. Initialize FastAPI app
app = FastAPI()
# 1. Configure the email service
# 2. Configure the email service
email_service = SMTPEmailService(
host="smtp.gmail.com",
port=587,
@@ -217,15 +174,12 @@ email_service = SMTPEmailService(
use_tls=True
)
# 2. Pass the email service to AuthService
auth_service = AuthService(
user_repository=user_repo, # From Quick Start
token_repository=token_repo, # From Quick Start
email_service=email_service, # Add this line
jwt_secret="YOUR_SUPER_LONG_AND_SECURE_JWT_SECRET_HERE"
)
# 3. Configure repositories for MongoDB
auth_router = create_app_router_for_sqlite(db_path="./UserDB", jwt_secret="THIS_NEEDS_TO_BE_CHANGED",
email_service=email_service)
# ... (keep 'app.include_router(auth_router)')
# 4. Include the authentication routes
app.include_router(auth_router)
```
### Option 2: Create a Custom Email Service
@@ -234,9 +188,12 @@ If you use a third-party service (like AWS SES, Mailgun) that requires an API, y
```Python
# ... (keep your app and repository config from the Quick Start)
from fastapi import FastAPI
from myauth.emailing.base import EmailService
from myauth import create_app_router_for_sqlite
from my_auth.email.base import EmailService
# 1. Initialize FastAPI app
app = FastAPI()
# 1. Implement your custom email service
@@ -263,14 +220,12 @@ class CustomEmailService(EmailService):
email_service = CustomEmailService(api_key="YOUR_API_KEY_HERE")
# 3. Pass your custom service to AuthService
auth_service = AuthService(
user_repository=user_repo, # From Quick Start
token_repository=token_repo, # From Quick Start
email_service=email_service, # Add this line
jwt_secret="YOUR_SUPER_LONG_AND_SECURE_JWT_SECRET_HERE"
)
auth_router = create_app_router_for_sqlite(db_path="./UserDB", jwt_secret="THIS_NEEDS_TO_BE_CHANGED",
email_service=email_service)
# 4. Include the authentication routes
app.include_router(auth_router)
# ... (keep 'app.include_router(auth_router)')
```
## API Endpoints Reference
@@ -302,25 +257,6 @@ The module uses custom exceptions that are automatically converted to the approp
* `EmailNotVerifiedError`**403 Forbidden (on login attempt)**
* `AccountDisabledError`**403 Forbidden (on login attempt)**
## Configuration Options
All options are passed during the `AuthService` initialization:
```Python
AuthService(
user_repository: UserRepository, # Required
token_repository: TokenRepository, # Required
jwt_secret: str, # Required
jwt_algorithm: str = "HS256", # Optional
access_token_expire_minutes: int = 30, # Optional
refresh_token_expire_days: int = 7, # Optional
password_reset_token_expire_minutes: int = 15, # Optional
password_hash_rounds: int = 12, # Optional (bcrypt cost)
email_service: EmailService = None # Optional
)
```
## Appendix (Contributor & Development Details)
<details> <summary><b> Appendix A: Project Structure (src/my_auth)</b></summary>

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "myauth"
version = "0.3.0"
version = "0.5.0"
description = "A reusable, modular authentication system for FastAPI applications with pluggable database backends."
readme = "README.md"
authors = [
@@ -38,8 +38,9 @@ dependencies = [
"fastapi",
"pydantic",
"pydantic-settings",
"pydantic[email]",
"python-jose[cryptography]",
"passlib[bcrypt]",
"passlib[argon2]",
"python-multipart"
]

View File

@@ -1,18 +1,33 @@
annotated-types==0.7.0
anyio==4.11.0
bcrypt==5.0.0
argon2-cffi==25.1.0
argon2-cffi-bindings==25.1.0
build==1.3.0
certifi==2025.10.5
cffi==2.0.0
charset-normalizer==3.4.4
cryptography==46.0.3
dnspython==2.8.0
docutils==0.22.2
ecdsa==0.19.1
email-validator==2.3.0
fastapi==0.119.0
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
id==1.5.0
idna==3.11
iniconfig==2.1.0
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.3.0
jeepney==0.9.0
keyring==25.6.0
markdown-it-py==4.0.0
mdurl==0.1.2
more-itertools==10.8.0
nh3==0.3.1
numpy==2.3.4
packaging==25.0
passlib==1.7.4
pluggy==1.6.0
@@ -22,13 +37,25 @@ pydantic==2.12.3
pydantic-settings==2.11.0
pydantic_core==2.41.4
Pygments==2.19.2
pyproject_hooks==1.2.0
pytest==8.4.2
python-dateutil==2.9.0.post0
python-dotenv==1.1.1
python-jose==3.5.0
python-multipart==0.0.20
pytz==2025.2
readme_renderer==44.0
requests==2.32.5
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.2.0
rsa==4.9.1
SecretStorage==3.4.0
six==1.17.0
sniffio==1.3.1
starlette==0.48.0
twine==6.2.0
typing-inspection==0.4.2
typing_extensions==4.15.0
tzdata==2025.2
urllib3==2.5.0

View File

@@ -12,31 +12,21 @@ class PasswordManager:
"""
Manager for password hashing and verification operations.
This class uses bcrypt for secure password hashing with configurable
cost factor (rounds). Higher rounds provide better security but slower
performance.
Attributes:
rounds: Bcrypt cost factor (default: 12). Higher values increase
security but also increase computation time.
This class uses argon2 for secure password hashing
"""
def __init__(self, rounds: int = 12):
def __init__(self):
"""
Initialize the password manager.
Args:
rounds: Bcrypt cost factor (4-31). Default is 12 which provides
a good balance between security and performance.
"""
if rounds < 4 or rounds > 31:
raise ValueError("Bcrypt rounds must be between 4 and 31")
self.rounds = rounds
self._context = CryptContext(
schemes=["bcrypt"],
schemes=["argon2"],
deprecated="auto",
bcrypt__rounds=self.rounds
# argon2__time_cost=3, # number of iterations (increases CPU time)
# argon2__memory_cost=65536, # memory usage in KiB (64 MiB)
# argon2__parallelism=2, # number of parallel threads
# argon2__salt_len=16 # length of the random salt in bytes
)
def hash_password(self, password: str) -> str:

View File

@@ -40,8 +40,8 @@ def create_sqlite_auth_service(db_path: str,
repositories and managers.
:rtype: AuthService
"""
from my_auth.persistence.sqlite import SQLiteUserRepository
from my_auth.persistence.sqlite import SQLiteTokenRepository
from .persistence.sqlite import SQLiteUserRepository
from .persistence.sqlite import SQLiteTokenRepository
user_repository = SQLiteUserRepository(db_path)
token_repository = SQLiteTokenRepository(db_path)
password_manager = password_manager or PasswordManager()
@@ -73,7 +73,6 @@ def create_app_router_for_sqlite(db_path: str,
communication and functionalities during authentication.
:return: An application router configured for handling authentication API routes.
"""
auth_service = create_sqlite_auth_service(
db_path, jwt_secret, email_service)
from my_auth.api.routes import create_auth_router
auth_service = create_sqlite_auth_service(db_path, jwt_secret, email_service)
from .api.routes import create_auth_router
return create_auth_router(auth_service)

View File

@@ -12,9 +12,9 @@ import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from my_auth.api.routes import create_auth_router
from my_auth.core.auth import AuthService
from my_auth.exceptions import (
from myauth.api.routes import create_auth_router
from myauth.core.auth import AuthService
from myauth.exceptions import (
UserAlreadyExistsError,
InvalidCredentialsError,
UserNotFoundError,
@@ -23,8 +23,8 @@ from my_auth.exceptions import (
RevokedTokenError,
AccountDisabledError
)
from my_auth.models.token import AccessTokenResponse
from my_auth.models.user import UserInDB
from myauth.models.token import AccessTokenResponse
from myauth.models.user import UserInDB
@pytest.fixture

View File

@@ -7,11 +7,11 @@ from unittest.mock import MagicMock
import pytest
from my_auth.core.password import PasswordManager
from my_auth.core.token import TokenManager
from src.my_auth.core.auth import AuthService
from src.my_auth.models.user import UserCreate, UserInDB
from src.my_auth.persistence.sqlite import SQLiteUserRepository, SQLiteTokenRepository
from myauth.core.password import PasswordManager
from myauth.core.token import TokenManager
from myauth.core.auth import AuthService
from myauth.models.user import UserCreate, UserInDB
from myauth.persistence.sqlite import SQLiteUserRepository, SQLiteTokenRepository
@pytest.fixture

View File

@@ -1,15 +1,15 @@
# tests/core/test_auth_service.py
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch
from datetime import datetime
from unittest.mock import patch
import pytest
from src.my_auth.core.auth import AuthService
from src.my_auth.exceptions import UserAlreadyExistsError, InvalidCredentialsError, InvalidTokenError, \
from myauth.core.auth import AuthService
from myauth.exceptions import UserAlreadyExistsError, InvalidCredentialsError, InvalidTokenError, \
ExpiredTokenError, RevokedTokenError
from src.my_auth.models.token import TokenData, TokenPayload
from src.my_auth.models.user import UserCreate, UserUpdate
from myauth.models.token import TokenPayload
from myauth.models.user import UserCreate
class TestAuthServiceRegisterLogin(object):
@@ -171,7 +171,6 @@ class TestAuthServiceTokenManagement(object):
with pytest.raises(ExpiredTokenError):
auth_service.get_current_user("expired_access_jwt")
# class TestAuthServiceResetVerification(object):
# """Tests for password reset and email verification flows."""
#
@@ -190,7 +189,7 @@ class TestAuthServiceTokenManagement(object):
# # Restore hash mock
# pm.hash_password.return_value = original_hash
#
# @patch('src.my_auth.core.email.send_email')
# @patch('myauth.core.email.send_email')
# def test_request_password_reset_success(self, mock_send_email: MagicMock, auth_service: AuthService):
# """Success: Requesting a password reset generates a token and sends an email."""
#
@@ -226,7 +225,7 @@ class TestAuthServiceTokenManagement(object):
# updated_user = auth_service.user_repository.get_user_by_id(self.user.id)
# assert updated_user.hashed_password == "NEW_HASHED_PASSWORD_FOR_RESET"
#
# @patch('src.my_auth.core.email.send_email')
# @patch('myauth.core.email.send_email')
# def test_request_email_verification_success(self, mock_send_email: MagicMock, auth_service: AuthService):
# """Success: Requesting verification generates a token and sends an email."""
#

View File

@@ -0,0 +1,25 @@
import pytest
from myauth.core import PasswordManager
@pytest.fixture()
def password_manager():
return PasswordManager()
def test_i_can_hash_password(password_manager):
hashed_password = password_manager.hash_password("password")
assert hashed_password is not None
assert hashed_password != "password"
def test_i_can_verify_password(password_manager):
password = "password"
hashed_password = password_manager.hash_password(password)
assert password_manager.verify_password(password, hashed_password)
def test_i_cannot_verify_invalid_password(password_manager):
password = "password"
hashed_password = password_manager.hash_password(password)
assert not password_manager.verify_password("invalid_password", hashed_password)

View File

@@ -6,9 +6,9 @@ from unittest.mock import MagicMock, patch
import pytest
from jose import jwt
from src.my_auth.core.token import TokenManager
from src.my_auth.exceptions import InvalidTokenError, ExpiredTokenError
from src.my_auth.models.user import UserInDB # Assuming you have a fixture for this
from myauth.core.token import TokenManager
from myauth.exceptions import InvalidTokenError, ExpiredTokenError
from myauth.models.user import UserInDB # Assuming you have a fixture for this
@pytest.fixture
@@ -99,7 +99,7 @@ class TestTokenExpirationCalculations:
"""Tests for token expiration date methods."""
# We patch datetime.now() to ensure stable calculations
@patch('src.my_auth.core.token.datetime')
@patch('myauth.core.token.datetime')
def test_get_refresh_token_expiration(self, mock_datetime, token_manager: TokenManager):
"""Should calculate refresh token expiration correctly."""
@@ -112,7 +112,7 @@ class TestTokenExpirationCalculations:
assert actual_exp == expected_exp
@patch('src.my_auth.core.token.datetime')
@patch('myauth.core.token.datetime')
def test_get_password_reset_token_expiration(self, mock_datetime, token_manager: TokenManager):
"""Should calculate password reset token expiration correctly."""

View File

@@ -1,12 +1,12 @@
from uuid import uuid4
from datetime import datetime, timedelta
from pathlib import Path
from uuid import uuid4
import pytest
from my_auth.models.token import TokenData
from my_auth.models.user import UserCreate
from my_auth.persistence.sqlite import SQLiteUserRepository, SQLiteTokenRepository
from myauth.models.token import TokenData
from myauth.models.user import UserCreate
from myauth.persistence.sqlite import SQLiteUserRepository, SQLiteTokenRepository
@pytest.fixture

View File

@@ -2,8 +2,8 @@
from datetime import datetime, timedelta
from my_auth.models.token import TokenData
from my_auth.persistence.sqlite import SQLiteTokenRepository
from myauth.models.token import TokenData
from myauth.persistence.sqlite import SQLiteTokenRepository
def test_i_can_save_and_retrieve_token(token_repository: SQLiteTokenRepository,

View File

@@ -4,9 +4,9 @@ import pytest
import json
from datetime import datetime
from my_auth.persistence.sqlite import SQLiteUserRepository
from my_auth.models.user import UserCreate, UserUpdate
from my_auth.exceptions import UserAlreadyExistsError, UserNotFoundError
from myauth.persistence.sqlite import SQLiteUserRepository
from myauth.models.user import UserCreate, UserUpdate
from myauth.exceptions import UserAlreadyExistsError, UserNotFoundError
def test_i_can_create_and_retrieve_user_by_email(user_repository: SQLiteUserRepository,