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:17:38 +02:00
parent 7634631b90
commit 0138ac247a
37 changed files with 261 additions and 160 deletions

18
Makefile Normal file
View File

@@ -0,0 +1,18 @@
# Makefile for cleaning packaging files and directories
.PHONY: clean-package clean-build clean
# Clean distribution artifacts (dist/ and *.egg-info)
clean-package:
rm -rf dist
rm -rf *.egg-info
rm -rf src/*.egg-info
# Clean all Python build artifacts (dist, egg-info, pyc, and cache files)
clean-build: clean-package
find . -name "__pycache__" -type d -exec rm -rf {} +
find . -name "*.pyc" -exec rm -f {} +
find . -name "*.pyo" -exec rm -f {} +
# Alias to clean everything
clean: clean-build

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.2.0" # Start with an initial version
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"
]
@@ -78,6 +79,9 @@ dev = [
# Setuptools configuration
# This section tells the build system where to find your package code
# -------------------------------------------------------------------
[tool.setuptools]
package-dir = {"" = "src"}
packages = ["my_auth"]
#[tool.setuptools]
#package-dir = {"myauth" = "src"}
#packages = ["my_auth"]
[tool.setuptools.package-dir]
myauth = "src/myauth"

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

6
src/myauth/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
from .factory import create_sqlite_auth_service, create_app_router_for_sqlite
__all__ = [
'create_sqlite_auth_service',
'create_app_router_for_sqlite',
]

View File

@@ -0,0 +1,3 @@
from .routes import create_auth_router
__all__ = ["create_auth_router"]

View File

@@ -0,0 +1,5 @@
from .auth import AuthService
from .password import PasswordManager
from .token import TokenManager
__all__ = ["AuthService", "PasswordManager", "TokenManager"]

View File

@@ -14,7 +14,7 @@ from .token import TokenManager
from ..persistence.base import UserRepository, TokenRepository
from ..models.user import UserCreate, UserInDB, UserUpdate
from ..models.token import AccessTokenResponse, TokenData
from ..email.base import EmailService
from ..emailing.base import EmailService
from ..exceptions import (
InvalidCredentialsError,
UserNotFoundError,

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

@@ -0,0 +1,4 @@
from .base import EmailService
from .smtp import SMTPEmailService
__all__ = ["EmailService", "SMTPEmailService"]

78
src/myauth/factory.py Normal file
View File

@@ -0,0 +1,78 @@
from typing import Optional
from .core import AuthService
from .core.password import PasswordManager
from .core.token import TokenManager
from .emailing.base import EmailService
def create_sqlite_auth_service(db_path: str,
jwt_secret: str,
email_service: Optional[EmailService],
password_manager: PasswordManager = None,
token_manager: TokenManager = None,
):
"""
Creates and configures an authentication service using SQLite as the underlying
data store.
This function is responsible for setting up the necessary repositories
and managers required for the AuthService. It uses SQLite repositories
for user and token data and initializes the required managers for password
and token processing, as well as an optional email service.
:param db_path: Path to the SQLite database file used to persist authentication
data.
:type db_path: str
:param jwt_secret: Secret key used to encode and decode JSON Web Tokens (JWTs).
:type jwt_secret: str
:param email_service: Optional email service instance for managing email-based
communication.
:type email_service: Optional[EmailService]
:param password_manager: Optional custom PasswordManager instance responsible for
password-related operations. Defaults to a new instance.
:type password_manager: PasswordManager
:param token_manager: Optional custom TokenManager instance responsible for token
generation and verification. Defaults to a new instance
configured with `jwt_secret`.
:type token_manager: TokenManager
:return: A fully configured instance of AuthService with the specified
repositories and managers.
:rtype: AuthService
"""
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()
token_manager = token_manager or TokenManager(jwt_secret)
auth_service = AuthService(
user_repository=user_repository,
token_repository=token_repository,
password_manager=password_manager,
token_manager=token_manager,
email_service=email_service)
return auth_service
def create_app_router_for_sqlite(db_path: str,
jwt_secret: str,
email_service: Optional[EmailService] = None):
"""
Creates an application router designed to use with an SQLite database backend.
This function initializes an authentication service using the provided database
path and JWT secret, optionally integrating an email service for email-based
authentication functionalities. The authentication service is then used to
create an application router for handling authentication-related API routes.
:param db_path: Path to the SQLite database file.
:param jwt_secret: A secret string used for signing and verifying JWT tokens.
:param email_service: An optional email service instance for managing email-related
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 .api.routes import create_auth_router
return create_auth_router(auth_service)

View File

@@ -7,7 +7,7 @@ reset operations including request and confirmation models.
from pydantic import BaseModel, EmailStr, field_validator
from my_auth.models.validators import validate_password_strength
from ..models.validators import validate_password_strength
class EmailVerificationRequest(BaseModel):

View File

@@ -10,7 +10,7 @@ from typing import Optional
from pydantic import BaseModel, EmailStr, Field, field_validator
from my_auth.models.validators import validate_password_strength, validate_username_not_empty
from ..models.validators import validate_password_strength, validate_username_not_empty
class UserBase(BaseModel):

View File

@@ -0,0 +1,6 @@
from .sqlite import UserRepository, TokenRepository
__all__ = [
"UserRepository",
"TokenRepository",
]

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,