Changed module name from my_auth to myauth
Changed encryption algorithm to argon2 Added unit tests
This commit is contained in:
18
Makefile
Normal file
18
Makefile
Normal 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
138
README.md
@@ -22,7 +22,7 @@ with pluggable database backends.
|
|||||||
* Secure password reset (via secure token stored in DB).
|
* Secure password reset (via secure token stored in DB).
|
||||||
* Account activation / deactivation.
|
* Account activation / deactivation.
|
||||||
* **Security:**
|
* **Security:**
|
||||||
* Password hashing with `bcrypt` (configurable rounds).
|
* Password hashing with `argon2`.
|
||||||
* Strict password validation (uppercase, lowercase, digit, special character).
|
* Strict password validation (uppercase, lowercase, digit, special character).
|
||||||
* **Flexible Architecture:**
|
* **Flexible Architecture:**
|
||||||
* **Pluggable Backends:** Supports MongoDB, PostgreSQL, and SQLite out of the box.
|
* **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
|
```Python
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from my_auth import AuthService
|
from myauth import create_app_router_for_mongoDB
|
||||||
from my_auth.api import auth_router
|
|
||||||
from my_auth.persistence.mongodb import MongoUserRepository, MongoTokenRepository
|
|
||||||
|
|
||||||
# 1. Initialize FastAPI app
|
# 1. Initialize FastAPI app
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
# 2. Configure repositories for MongoDB
|
# 2. Configure repositories for MongoDB
|
||||||
# Make sure your connection string is correct
|
auth_router = create_app_router_for_mongoDB(mongodb_url="mongodb://localhost:27017",
|
||||||
user_repo = MongoUserRepository(connection_string="mongodb://localhost:27017/myappdb")
|
jwt_secret="THIS_NEEDS_TO_BE_CHANGED")
|
||||||
token_repo = MongoTokenRepository(connection_string="mongodb://localhost:27017/myappdb")
|
|
||||||
|
|
||||||
# 3. Configure the Authentication Service
|
# 3. Include the authentication routes
|
||||||
# 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
|
|
||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
|
|
||||||
|
|
||||||
@@ -113,36 +100,18 @@ This example configures myauth to use PostgreSQL as its backend.
|
|||||||
```Python
|
```Python
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from my_auth import AuthService
|
from myauth import create_app_router_for_postgreSQL
|
||||||
from my_auth.api import auth_router
|
|
||||||
from my_auth.persistence.postgresql import PostgreSQLUserRepository, PostgreSQLTokenRepository
|
|
||||||
|
|
||||||
# 1. Initialize FastAPI app
|
# 1. Initialize FastAPI app
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
# 2. Configure repositories for PostgreSQL
|
# 2. Configure repositories for MongoDB
|
||||||
# Update with your database credentials
|
auth_router = create_app_router_for_mongoDB(postgresql_url="mongodb://localhost:27017",
|
||||||
db_config = {
|
username="admin",
|
||||||
"host": "localhost",
|
password="password",
|
||||||
"port": 5432,
|
jwt_secret="THIS_NEEDS_TO_BE_CHANGED")
|
||||||
"database": "mydb",
|
|
||||||
"user": "postgres",
|
|
||||||
"password": "secret"
|
|
||||||
}
|
|
||||||
user_repo = PostgreSQLUserRepository(**db_config)
|
|
||||||
token_repo = PostgreSQLTokenRepository(**db_config)
|
|
||||||
|
|
||||||
# 3. Configure the Authentication Service
|
# 3. Include the authentication routes
|
||||||
# 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
|
|
||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
|
|
||||||
|
|
||||||
@@ -158,30 +127,15 @@ This example configures myauth to use SQLite, which is ideal for development or
|
|||||||
```Python
|
```Python
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from my_auth import AuthService
|
from myauth import create_app_router_for_sqlite
|
||||||
from my_auth.api import auth_router
|
|
||||||
from my_auth.persistence.sqlite import SQLiteUserRepository, SQLiteTokenRepository
|
|
||||||
|
|
||||||
# 1. Initialize FastAPI app
|
# 1. Initialize FastAPI app
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
# 2. Configure repositories for SQLite
|
# 2. Configure repositories for MongoDB
|
||||||
# This will create/use a file named 'auth.db' in the current directory
|
auth_router = create_app_router_for_sqlite(db_path="./UserDB", jwt_secret="THIS_NEEDS_TO_BE_CHANGED")
|
||||||
db_path = "./auth.db"
|
|
||||||
user_repo = SQLiteUserRepository(db_path=db_path)
|
|
||||||
token_repo = SQLiteTokenRepository(db_path=db_path)
|
|
||||||
|
|
||||||
# 3. Configure the Authentication Service
|
# 3. Include the authentication routes
|
||||||
# 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
|
|
||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
|
|
||||||
|
|
||||||
@@ -204,11 +158,14 @@ pip install "myauth[email]"
|
|||||||
|
|
||||||
```Python
|
```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(
|
email_service = SMTPEmailService(
|
||||||
host="smtp.gmail.com",
|
host="smtp.gmail.com",
|
||||||
port=587,
|
port=587,
|
||||||
@@ -217,15 +174,12 @@ email_service = SMTPEmailService(
|
|||||||
use_tls=True
|
use_tls=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Pass the email service to AuthService
|
# 3. Configure repositories for MongoDB
|
||||||
auth_service = AuthService(
|
auth_router = create_app_router_for_sqlite(db_path="./UserDB", jwt_secret="THIS_NEEDS_TO_BE_CHANGED",
|
||||||
user_repository=user_repo, # From Quick Start
|
email_service=email_service)
|
||||||
token_repository=token_repo, # From Quick Start
|
|
||||||
email_service=email_service, # Add this line
|
|
||||||
jwt_secret="YOUR_SUPER_LONG_AND_SECURE_JWT_SECRET_HERE"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ... (keep 'app.include_router(auth_router)')
|
# 4. Include the authentication routes
|
||||||
|
app.include_router(auth_router)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 2: Create a Custom Email Service
|
### 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
|
```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
|
# 1. Implement your custom email service
|
||||||
@@ -263,14 +220,12 @@ class CustomEmailService(EmailService):
|
|||||||
email_service = CustomEmailService(api_key="YOUR_API_KEY_HERE")
|
email_service = CustomEmailService(api_key="YOUR_API_KEY_HERE")
|
||||||
|
|
||||||
# 3. Pass your custom service to AuthService
|
# 3. Pass your custom service to AuthService
|
||||||
auth_service = AuthService(
|
auth_router = create_app_router_for_sqlite(db_path="./UserDB", jwt_secret="THIS_NEEDS_TO_BE_CHANGED",
|
||||||
user_repository=user_repo, # From Quick Start
|
email_service=email_service)
|
||||||
token_repository=token_repo, # From Quick Start
|
|
||||||
email_service=email_service, # Add this line
|
# 4. Include the authentication routes
|
||||||
jwt_secret="YOUR_SUPER_LONG_AND_SECURE_JWT_SECRET_HERE"
|
app.include_router(auth_router)
|
||||||
)
|
|
||||||
|
|
||||||
# ... (keep 'app.include_router(auth_router)')
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## API Endpoints Reference
|
## 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)**
|
* `EmailNotVerifiedError` → **403 Forbidden (on login attempt)**
|
||||||
* `AccountDisabledError` → **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)
|
## Appendix (Contributor & Development Details)
|
||||||
|
|
||||||
<details> <summary><b> Appendix A: Project Structure (src/my_auth)</b></summary>
|
<details> <summary><b> Appendix A: Project Structure (src/my_auth)</b></summary>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "myauth"
|
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."
|
description = "A reusable, modular authentication system for FastAPI applications with pluggable database backends."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
@@ -38,8 +38,9 @@ dependencies = [
|
|||||||
"fastapi",
|
"fastapi",
|
||||||
"pydantic",
|
"pydantic",
|
||||||
"pydantic-settings",
|
"pydantic-settings",
|
||||||
|
"pydantic[email]",
|
||||||
"python-jose[cryptography]",
|
"python-jose[cryptography]",
|
||||||
"passlib[bcrypt]",
|
"passlib[argon2]",
|
||||||
"python-multipart"
|
"python-multipart"
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -78,6 +79,9 @@ dev = [
|
|||||||
# Setuptools configuration
|
# Setuptools configuration
|
||||||
# This section tells the build system where to find your package code
|
# This section tells the build system where to find your package code
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
[tool.setuptools]
|
#[tool.setuptools]
|
||||||
package-dir = {"" = "src"}
|
#package-dir = {"myauth" = "src"}
|
||||||
packages = ["my_auth"]
|
#packages = ["my_auth"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-dir]
|
||||||
|
myauth = "src/myauth"
|
||||||
@@ -1,18 +1,33 @@
|
|||||||
annotated-types==0.7.0
|
annotated-types==0.7.0
|
||||||
anyio==4.11.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
|
certifi==2025.10.5
|
||||||
cffi==2.0.0
|
cffi==2.0.0
|
||||||
|
charset-normalizer==3.4.4
|
||||||
cryptography==46.0.3
|
cryptography==46.0.3
|
||||||
dnspython==2.8.0
|
dnspython==2.8.0
|
||||||
|
docutils==0.22.2
|
||||||
ecdsa==0.19.1
|
ecdsa==0.19.1
|
||||||
email-validator==2.3.0
|
email-validator==2.3.0
|
||||||
fastapi==0.119.0
|
fastapi==0.119.0
|
||||||
h11==0.16.0
|
h11==0.16.0
|
||||||
httpcore==1.0.9
|
httpcore==1.0.9
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
|
id==1.5.0
|
||||||
idna==3.11
|
idna==3.11
|
||||||
iniconfig==2.1.0
|
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
|
packaging==25.0
|
||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
pluggy==1.6.0
|
pluggy==1.6.0
|
||||||
@@ -22,13 +37,25 @@ pydantic==2.12.3
|
|||||||
pydantic-settings==2.11.0
|
pydantic-settings==2.11.0
|
||||||
pydantic_core==2.41.4
|
pydantic_core==2.41.4
|
||||||
Pygments==2.19.2
|
Pygments==2.19.2
|
||||||
|
pyproject_hooks==1.2.0
|
||||||
pytest==8.4.2
|
pytest==8.4.2
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
python-jose==3.5.0
|
python-jose==3.5.0
|
||||||
python-multipart==0.0.20
|
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
|
rsa==4.9.1
|
||||||
|
SecretStorage==3.4.0
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
starlette==0.48.0
|
starlette==0.48.0
|
||||||
|
twine==6.2.0
|
||||||
typing-inspection==0.4.2
|
typing-inspection==0.4.2
|
||||||
typing_extensions==4.15.0
|
typing_extensions==4.15.0
|
||||||
|
tzdata==2025.2
|
||||||
|
urllib3==2.5.0
|
||||||
|
|||||||
6
src/myauth/__init__.py
Normal file
6
src/myauth/__init__.py
Normal 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',
|
||||||
|
]
|
||||||
3
src/myauth/api/__init__.py
Normal file
3
src/myauth/api/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .routes import create_auth_router
|
||||||
|
|
||||||
|
__all__ = ["create_auth_router"]
|
||||||
5
src/myauth/core/__init__.py
Normal file
5
src/myauth/core/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .auth import AuthService
|
||||||
|
from .password import PasswordManager
|
||||||
|
from .token import TokenManager
|
||||||
|
|
||||||
|
__all__ = ["AuthService", "PasswordManager", "TokenManager"]
|
||||||
@@ -14,7 +14,7 @@ from .token import TokenManager
|
|||||||
from ..persistence.base import UserRepository, TokenRepository
|
from ..persistence.base import UserRepository, TokenRepository
|
||||||
from ..models.user import UserCreate, UserInDB, UserUpdate
|
from ..models.user import UserCreate, UserInDB, UserUpdate
|
||||||
from ..models.token import AccessTokenResponse, TokenData
|
from ..models.token import AccessTokenResponse, TokenData
|
||||||
from ..email.base import EmailService
|
from ..emailing.base import EmailService
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
InvalidCredentialsError,
|
InvalidCredentialsError,
|
||||||
UserNotFoundError,
|
UserNotFoundError,
|
||||||
@@ -12,31 +12,21 @@ class PasswordManager:
|
|||||||
"""
|
"""
|
||||||
Manager for password hashing and verification operations.
|
Manager for password hashing and verification operations.
|
||||||
|
|
||||||
This class uses bcrypt for secure password hashing with configurable
|
This class uses argon2 for secure password hashing
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rounds: int = 12):
|
def __init__(self):
|
||||||
"""
|
"""
|
||||||
Initialize the password manager.
|
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(
|
self._context = CryptContext(
|
||||||
schemes=["bcrypt"],
|
schemes=["argon2"],
|
||||||
deprecated="auto",
|
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:
|
def hash_password(self, password: str) -> str:
|
||||||
4
src/myauth/emailing/__init__.py
Normal file
4
src/myauth/emailing/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .base import EmailService
|
||||||
|
from .smtp import SMTPEmailService
|
||||||
|
|
||||||
|
__all__ = ["EmailService", "SMTPEmailService"]
|
||||||
78
src/myauth/factory.py
Normal file
78
src/myauth/factory.py
Normal 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)
|
||||||
@@ -7,7 +7,7 @@ reset operations including request and confirmation models.
|
|||||||
|
|
||||||
from pydantic import BaseModel, EmailStr, field_validator
|
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):
|
class EmailVerificationRequest(BaseModel):
|
||||||
@@ -10,7 +10,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from pydantic import BaseModel, EmailStr, Field, field_validator
|
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):
|
class UserBase(BaseModel):
|
||||||
6
src/myauth/persistence/__init__.py
Normal file
6
src/myauth/persistence/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from .sqlite import UserRepository, TokenRepository
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"UserRepository",
|
||||||
|
"TokenRepository",
|
||||||
|
]
|
||||||
@@ -12,9 +12,9 @@ import pytest
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from my_auth.api.routes import create_auth_router
|
from myauth.api.routes import create_auth_router
|
||||||
from my_auth.core.auth import AuthService
|
from myauth.core.auth import AuthService
|
||||||
from my_auth.exceptions import (
|
from myauth.exceptions import (
|
||||||
UserAlreadyExistsError,
|
UserAlreadyExistsError,
|
||||||
InvalidCredentialsError,
|
InvalidCredentialsError,
|
||||||
UserNotFoundError,
|
UserNotFoundError,
|
||||||
@@ -23,8 +23,8 @@ from my_auth.exceptions import (
|
|||||||
RevokedTokenError,
|
RevokedTokenError,
|
||||||
AccountDisabledError
|
AccountDisabledError
|
||||||
)
|
)
|
||||||
from my_auth.models.token import AccessTokenResponse
|
from myauth.models.token import AccessTokenResponse
|
||||||
from my_auth.models.user import UserInDB
|
from myauth.models.user import UserInDB
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from my_auth.core.password import PasswordManager
|
from myauth.core.password import PasswordManager
|
||||||
from my_auth.core.token import TokenManager
|
from myauth.core.token import TokenManager
|
||||||
from src.my_auth.core.auth import AuthService
|
from myauth.core.auth import AuthService
|
||||||
from src.my_auth.models.user import UserCreate, UserInDB
|
from myauth.models.user import UserCreate, UserInDB
|
||||||
from src.my_auth.persistence.sqlite import SQLiteUserRepository, SQLiteTokenRepository
|
from myauth.persistence.sqlite import SQLiteUserRepository, SQLiteTokenRepository
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# tests/core/test_auth_service.py
|
# tests/core/test_auth_service.py
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.my_auth.core.auth import AuthService
|
from myauth.core.auth import AuthService
|
||||||
from src.my_auth.exceptions import UserAlreadyExistsError, InvalidCredentialsError, InvalidTokenError, \
|
from myauth.exceptions import UserAlreadyExistsError, InvalidCredentialsError, InvalidTokenError, \
|
||||||
ExpiredTokenError, RevokedTokenError
|
ExpiredTokenError, RevokedTokenError
|
||||||
from src.my_auth.models.token import TokenData, TokenPayload
|
from myauth.models.token import TokenPayload
|
||||||
from src.my_auth.models.user import UserCreate, UserUpdate
|
from myauth.models.user import UserCreate
|
||||||
|
|
||||||
|
|
||||||
class TestAuthServiceRegisterLogin(object):
|
class TestAuthServiceRegisterLogin(object):
|
||||||
@@ -171,7 +171,6 @@ class TestAuthServiceTokenManagement(object):
|
|||||||
with pytest.raises(ExpiredTokenError):
|
with pytest.raises(ExpiredTokenError):
|
||||||
auth_service.get_current_user("expired_access_jwt")
|
auth_service.get_current_user("expired_access_jwt")
|
||||||
|
|
||||||
|
|
||||||
# class TestAuthServiceResetVerification(object):
|
# class TestAuthServiceResetVerification(object):
|
||||||
# """Tests for password reset and email verification flows."""
|
# """Tests for password reset and email verification flows."""
|
||||||
#
|
#
|
||||||
@@ -190,7 +189,7 @@ class TestAuthServiceTokenManagement(object):
|
|||||||
# # Restore hash mock
|
# # Restore hash mock
|
||||||
# pm.hash_password.return_value = original_hash
|
# 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):
|
# 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."""
|
# """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)
|
# updated_user = auth_service.user_repository.get_user_by_id(self.user.id)
|
||||||
# assert updated_user.hashed_password == "NEW_HASHED_PASSWORD_FOR_RESET"
|
# 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):
|
# def test_request_email_verification_success(self, mock_send_email: MagicMock, auth_service: AuthService):
|
||||||
# """Success: Requesting verification generates a token and sends an email."""
|
# """Success: Requesting verification generates a token and sends an email."""
|
||||||
#
|
#
|
||||||
|
|||||||
25
tests/core/test_password.py
Normal file
25
tests/core/test_password.py
Normal 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)
|
||||||
@@ -6,9 +6,9 @@ from unittest.mock import MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
|
|
||||||
from src.my_auth.core.token import TokenManager
|
from myauth.core.token import TokenManager
|
||||||
from src.my_auth.exceptions import InvalidTokenError, ExpiredTokenError
|
from myauth.exceptions import InvalidTokenError, ExpiredTokenError
|
||||||
from src.my_auth.models.user import UserInDB # Assuming you have a fixture for this
|
from myauth.models.user import UserInDB # Assuming you have a fixture for this
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -99,7 +99,7 @@ class TestTokenExpirationCalculations:
|
|||||||
"""Tests for token expiration date methods."""
|
"""Tests for token expiration date methods."""
|
||||||
|
|
||||||
# We patch datetime.now() to ensure stable calculations
|
# 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):
|
def test_get_refresh_token_expiration(self, mock_datetime, token_manager: TokenManager):
|
||||||
"""Should calculate refresh token expiration correctly."""
|
"""Should calculate refresh token expiration correctly."""
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ class TestTokenExpirationCalculations:
|
|||||||
|
|
||||||
assert actual_exp == expected_exp
|
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):
|
def test_get_password_reset_token_expiration(self, mock_datetime, token_manager: TokenManager):
|
||||||
"""Should calculate password reset token expiration correctly."""
|
"""Should calculate password reset token expiration correctly."""
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from uuid import uuid4
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from my_auth.models.token import TokenData
|
from myauth.models.token import TokenData
|
||||||
from my_auth.models.user import UserCreate
|
from myauth.models.user import UserCreate
|
||||||
from my_auth.persistence.sqlite import SQLiteUserRepository, SQLiteTokenRepository
|
from myauth.persistence.sqlite import SQLiteUserRepository, SQLiteTokenRepository
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from my_auth.models.token import TokenData
|
from myauth.models.token import TokenData
|
||||||
from my_auth.persistence.sqlite import SQLiteTokenRepository
|
from myauth.persistence.sqlite import SQLiteTokenRepository
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_save_and_retrieve_token(token_repository: SQLiteTokenRepository,
|
def test_i_can_save_and_retrieve_token(token_repository: SQLiteTokenRepository,
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import pytest
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from my_auth.persistence.sqlite import SQLiteUserRepository
|
from myauth.persistence.sqlite import SQLiteUserRepository
|
||||||
from my_auth.models.user import UserCreate, UserUpdate
|
from myauth.models.user import UserCreate, UserUpdate
|
||||||
from my_auth.exceptions import UserAlreadyExistsError, UserNotFoundError
|
from myauth.exceptions import UserAlreadyExistsError, UserNotFoundError
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_create_and_retrieve_user_by_email(user_repository: SQLiteUserRepository,
|
def test_i_can_create_and_retrieve_user_by_email(user_repository: SQLiteUserRepository,
|
||||||
|
|||||||
Reference in New Issue
Block a user