ironauth 0.1.0__tar.gz

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.
Files changed (33) hide show
  1. ironauth-0.1.0/.gitignore +52 -0
  2. ironauth-0.1.0/PKG-INFO +25 -0
  3. ironauth-0.1.0/README.md +0 -0
  4. ironauth-0.1.0/docs/api-references.md +54 -0
  5. ironauth-0.1.0/docs/configuration.md +65 -0
  6. ironauth-0.1.0/docs/getting-started.md +71 -0
  7. ironauth-0.1.0/docs/guides/oauth.md +46 -0
  8. ironauth-0.1.0/docs/guides/two-factor.md +56 -0
  9. ironauth-0.1.0/docs/index.md +41 -0
  10. ironauth-0.1.0/ironauth/__init__.py +0 -0
  11. ironauth-0.1.0/ironauth/adapters/__init__.py +0 -0
  12. ironauth-0.1.0/ironauth/adapters/database/__init__.py +0 -0
  13. ironauth-0.1.0/ironauth/adapters/database/sqlalchemy.py +91 -0
  14. ironauth-0.1.0/ironauth/adapters/frameworks/__init__.py +0 -0
  15. ironauth-0.1.0/ironauth/adapters/frameworks/fastapi.py +184 -0
  16. ironauth-0.1.0/ironauth/core/__init__.py +0 -0
  17. ironauth-0.1.0/ironauth/core/auth.py +71 -0
  18. ironauth-0.1.0/ironauth/core/config.py +35 -0
  19. ironauth-0.1.0/ironauth/core/password.py +41 -0
  20. ironauth-0.1.0/ironauth/core/session.py +98 -0
  21. ironauth-0.1.0/ironauth/models/__init__.py +0 -0
  22. ironauth-0.1.0/ironauth/models/user.py +47 -0
  23. ironauth-0.1.0/ironauth/plugins/__init__.py +0 -0
  24. ironauth-0.1.0/ironauth/plugins/oauth.py +142 -0
  25. ironauth-0.1.0/ironauth/plugins/two_factor.py +93 -0
  26. ironauth-0.1.0/main.py +6 -0
  27. ironauth-0.1.0/mkdocs.yml +64 -0
  28. ironauth-0.1.0/pyproject.toml +53 -0
  29. ironauth-0.1.0/tests/conftest.py +51 -0
  30. ironauth-0.1.0/tests/test_database.py +64 -0
  31. ironauth-0.1.0/tests/test_password.py +53 -0
  32. ironauth-0.1.0/tests/test_routes.py +98 -0
  33. ironauth-0.1.0/tests/test_session.py +41 -0
@@ -0,0 +1,52 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ eggs/
12
+ parts/
13
+ var/
14
+ sdist/
15
+ develop-eggs/
16
+ .installed.cfg
17
+ lib/
18
+ lib64/
19
+
20
+ # uv
21
+ .venv/
22
+ .python-version
23
+ uv.lock
24
+
25
+ # Tests
26
+ .pytest_cache/
27
+ .coverage
28
+ htmlcov/
29
+ .tox/
30
+
31
+ # MkDocs
32
+ site/
33
+
34
+ # Env
35
+ .env
36
+ .env.local
37
+ .env.*.local
38
+
39
+ # IDE
40
+ .vscode/
41
+ .idea/
42
+ *.swp
43
+ *.swo
44
+
45
+ # OS
46
+ .DS_Store
47
+ Thumbs.db
48
+
49
+ # Secrets
50
+ *.pem
51
+ *.key
52
+ .pypirc
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: ironauth
3
+ Version: 0.1.0
4
+ Summary: Framework-agnostic authentication library for Python
5
+ Project-URL: Homepage, https://github.com/sileyekounou-1/ironauth
6
+ Project-URL: Repository, https://github.com/sileyekounou-1/ironauth
7
+ Author-email: sileyekounou-1 <kounousileye@gmail.com>
8
+ License: MIT
9
+ Keywords: auth,authentication,fastapi,jwt,oauth
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: FastAPI
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Security
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: fastapi>=0.136.1
19
+ Requires-Dist: httpx>=0.28.1
20
+ Requires-Dist: passlib[argon2]>=1.7.4
21
+ Requires-Dist: pydantic[email]>=2.13.4
22
+ Requires-Dist: pyjwt>=2.12.1
23
+ Requires-Dist: pyotp>=2.9.0
24
+ Requires-Dist: qrcode[pil]>=8.2
25
+ Requires-Dist: sqlalchemy>=2.0.49
File without changes
@@ -0,0 +1,54 @@
1
+ # API Reference
2
+
3
+ ## ironauth
4
+
5
+ ```python
6
+ ironauth(
7
+ database: SQLAlchemyAdapter,
8
+ config: dict,
9
+ plugins: list = [],
10
+ adapter: dict | None = None,
11
+ )
12
+ ```
13
+
14
+ | Parameter | Type | Description |
15
+ | ---------- | ------------------- | ---------------------------------------------------------- |
16
+ | `database` | `SQLAlchemyAdapter` | Database adapter |
17
+ | `config` | `dict` | Configuration dict (see [Configuration](configuration.md)) |
18
+ | `plugins` | `list` | List of plugins |
19
+ | `adapter` | `dict` | Framework adapter |
20
+
21
+ ### Methods
22
+
23
+ | Method | Description |
24
+ | ---------------------------------- | -------------------------- |
25
+ | `await auth.init()` | Initialize database tables |
26
+ | `auth.router` | FastAPI router to mount |
27
+ | `auth.current_user(required=True)` | FastAPI dependency |
28
+
29
+ ## Routes
30
+
31
+ ### Email / Password
32
+
33
+ | Method | Route | Body | Description |
34
+ | ------ | ---------------- | ------------------- | --------------- |
35
+ | `POST` | `/auth/register` | `{email, password}` | Register |
36
+ | `POST` | `/auth/login` | `{email, password}` | Login |
37
+ | `POST` | `/auth/logout` | — | Logout |
38
+ | `POST` | `/auth/refresh` | — | Refresh session |
39
+
40
+ ### OAuth
41
+
42
+ | Method | Route | Description |
43
+ | ------ | --------------------------------- | ---------------- |
44
+ | `GET` | `/auth/oauth/{provider}` | Start OAuth flow |
45
+ | `GET` | `/auth/oauth/{provider}/callback` | OAuth callback |
46
+
47
+ ### 2FA
48
+
49
+ | Method | Route | Body | Description |
50
+ | ------ | -------------------- | ----------------- | ---------------- |
51
+ | `POST` | `/auth/2fa/enable` | — | Generate QR code |
52
+ | `POST` | `/auth/2fa/confirm` | `{code}` | Activate 2FA |
53
+ | `POST` | `/auth/2fa/validate` | `{user_id, code}` | Validate code |
54
+ | `POST` | `/auth/2fa/disable` | `{code}` | Disable 2FA |
@@ -0,0 +1,65 @@
1
+ # Configuration
2
+
3
+ ## Full reference
4
+
5
+ ```python
6
+ auth = ironauth(
7
+ database=sqlalchemy_adapter("postgresql+asyncpg://..."),
8
+ adapter=fastapi_adapter(),
9
+ plugins=[...],
10
+ config={
11
+ # Required
12
+ "secret_key": "your-secret-key",
13
+
14
+ # Token settings (optional)
15
+ "token": {
16
+ "access_token_expiry": 900, # 15 minutes
17
+ "refresh_token_expiry": 604800, # 7 days
18
+ "algorithm": "HS256",
19
+ },
20
+
21
+ # Cookie settings (optional)
22
+ "cookie": {
23
+ "http_only": True,
24
+ "secure": True,
25
+ "same_site": "lax",
26
+ "access_token_name": "af_access_token",
27
+ "refresh_token_name": "af_refresh_token",
28
+ },
29
+
30
+ # OAuth (required if using oauth plugin)
31
+ "oauth": {
32
+ "google": {
33
+ "client_id": "...",
34
+ "client_secret": "...",
35
+ "redirect_uri": "http://localhost:8000/auth/oauth/google/callback",
36
+ },
37
+ "github": {
38
+ "client_id": "...",
39
+ "client_secret": "...",
40
+ "redirect_uri": "http://localhost:8000/auth/oauth/github/callback",
41
+ },
42
+ },
43
+ },
44
+ )
45
+ ```
46
+
47
+ ## Security defaults
48
+
49
+ ironauth ships with secure defaults out of the box.
50
+
51
+ | Setting | Default | Why |
52
+ | ---------------------- | ------- | -------------------------------------- |
53
+ | Password hashing | Argon2 | Winner of Password Hashing Competition |
54
+ | Cookie `HttpOnly` | `True` | Prevents XSS token theft |
55
+ | Cookie `Secure` | `True` | HTTPS only |
56
+ | Cookie `SameSite` | `lax` | CSRF protection |
57
+ | Access token expiry | 15 min | Limits exposure window |
58
+ | Refresh token rotation | Always | Detects token theft |
59
+
60
+ !!! warning "Secret key"
61
+ Always use a long random secret key in production.
62
+
63
+ ```bash
64
+ python -c "import secrets; print(secrets.token_urlsafe(64))"
65
+ ```
@@ -0,0 +1,71 @@
1
+ # Getting Started
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ pip install ironauth
7
+
8
+ # With uv
9
+ uv add ironauth
10
+ ```
11
+
12
+ ## Minimal setup
13
+
14
+ ### 1. Create your ironauth instance
15
+
16
+ ```python
17
+ # auth.py
18
+ from ironauth import ironauth
19
+ from ironauth.adapters.database.sqlalchemy import sqlalchemy_adapter
20
+ from ironauth.adapters.frameworks.fastapi import fastapi_adapter
21
+
22
+ auth = ironauth(
23
+ database=sqlalchemy_adapter("sqlite+aiosqlite:///./db.sqlite3"),
24
+ adapter=fastapi_adapter(),
25
+ config={"secret_key": "change-me-in-production"},
26
+ )
27
+ ```
28
+
29
+ ### 2. Mount on your FastAPI app
30
+
31
+ ```python
32
+ # main.py
33
+ from fastapi import FastAPI, Depends
34
+ from auth import auth
35
+
36
+ app = FastAPI()
37
+
38
+ @app.on_event("startup")
39
+ async def startup():
40
+ await auth.init() # Creates tables automatically
41
+
42
+ app.include_router(auth.router)
43
+ ```
44
+
45
+ ### 3. Protect a route
46
+
47
+ ```python
48
+ @app.get("/profile")
49
+ async def profile(user=Depends(auth.current_user())):
50
+ return {"email": user.email}
51
+ ```
52
+
53
+ That's it. You now have the following routes available:
54
+
55
+ | Method | Route | Description |
56
+ | ------ | ---------------- | ------------------------------ |
57
+ | `POST` | `/auth/register` | Register with email + password |
58
+ | `POST` | `/auth/login` | Login |
59
+ | `POST` | `/auth/logout` | Logout |
60
+ | `POST` | `/auth/refresh` | Refresh session |
61
+
62
+ ## Optional routes
63
+
64
+ ```python
65
+ # Unprotected route (user may or may not be logged in)
66
+ @app.get("/public")
67
+ async def public(user=Depends(auth.current_user(required=False))):
68
+ if user:
69
+ return {"message": f"Hello {user.email}"}
70
+ return {"message": "Hello stranger"}
71
+ ```
@@ -0,0 +1,46 @@
1
+ # OAuth
2
+
3
+ ironauth supports OAuth2 with Google and GitHub out of the box.
4
+
5
+ ## Setup
6
+
7
+ ### 1. Get your credentials
8
+
9
+ === "Google" 1. Go to [Google Cloud Console](https://console.cloud.google.com) 2. Create a project → APIs & Services → Credentials 3. Create OAuth 2.0 Client ID 4. Add `http://localhost:8000/auth/oauth/google/callback` to authorized redirect URIs
10
+
11
+ === "GitHub" 1. Go to GitHub → Settings → Developer settings → OAuth Apps 2. Create a new OAuth App 3. Set callback URL to `http://localhost:8000/auth/oauth/github/callback`
12
+
13
+ ### 2. Configure ironauth
14
+
15
+ ```python
16
+ from ironauth.plugins.oauth import oauth
17
+
18
+ auth = ironauth(
19
+ ...
20
+ plugins=[oauth(providers=["google", "github"])],
21
+ config={
22
+ "secret_key": "...",
23
+ "oauth": {
24
+ "google": {
25
+ "client_id": "YOUR_CLIENT_ID",
26
+ "client_secret": "YOUR_CLIENT_SECRET",
27
+ "redirect_uri": "http://localhost:8000/auth/oauth/google/callback",
28
+ },
29
+ },
30
+ },
31
+ )
32
+ ```
33
+
34
+ ### 3. Redirect your users
35
+
36
+ ```python
37
+ # Frontend: redirect to this URL to start OAuth flow
38
+ GET /auth/oauth/google
39
+ GET /auth/oauth/github
40
+ ```
41
+
42
+ ironauth handles the callback automatically and sets session cookies.
43
+
44
+ ## Account linking
45
+
46
+ If a user signs in with OAuth using an email that already exists in your database, ironauth automatically links the OAuth account to the existing user — no duplicate accounts.
@@ -0,0 +1,56 @@
1
+ # Two-Factor Authentication (TOTP)
2
+
3
+ ironauth supports TOTP-based 2FA compatible with Google Authenticator, Authy, and any TOTP app.
4
+
5
+ ## Setup
6
+
7
+ ```python
8
+ from ironauth.plugins.two_factor import two_factor
9
+
10
+ auth = ironauth(
11
+ ...
12
+ plugins=[two_factor()],
13
+ )
14
+ ```
15
+
16
+ ## Flow
17
+
18
+ ### Enable 2FA
19
+
20
+ ```python
21
+ # 1. Request setup — returns QR code + secret
22
+ POST /auth/2fa/enable
23
+ # Response:
24
+ {
25
+ "secret": "BASE32SECRET",
26
+ "qr_code": "<base64 PNG>"
27
+ }
28
+ ```
29
+
30
+ Display the QR code to your user so they can scan it with their TOTP app.
31
+
32
+ ```python
33
+ # 2. Confirm with first code
34
+ POST /auth/2fa/confirm
35
+ { "code": "123456" }
36
+ ```
37
+
38
+ ### Validate at login
39
+
40
+ ```python
41
+ POST /auth/2fa/validate
42
+ {
43
+ "user_id": "...",
44
+ "code": "123456"
45
+ }
46
+ ```
47
+
48
+ ### Disable 2FA
49
+
50
+ ```python
51
+ POST /auth/2fa/disable
52
+ { "code": "123456" }
53
+ ```
54
+
55
+ !!! info "TOTP window"
56
+ ironauth accepts codes valid within a 30-second window before and after the current time to account for clock drift.
@@ -0,0 +1,41 @@
1
+ # ironauth
2
+
3
+ **ironauth** is a framework-agnostic authentication library for Python — simple to use, secure by default, and extensible via plugins.
4
+
5
+ ## Why ironauth?
6
+
7
+ The Python ecosystem has authentication solutions, but none that feel like a single cohesive tool across frameworks. ironauth changes that.
8
+
9
+ - **Framework-agnostic** — FastAPI today, Django and Flask coming soon
10
+ - **Secure by default** — Argon2 hashing, HTTP-only cookies, JWT rotation
11
+ - **Plugin system** — OAuth, 2FA, and more
12
+ - **Fully async** — built for modern Python
13
+
14
+ ## Quick example
15
+
16
+ ```python
17
+ from ironauth import ironauth
18
+ from ironauth.adapters.database import sqlalchemy_adapter
19
+ from ironauth.adapters.frameworks import fastapi_adapter
20
+ from ironauth.plugins import oauth, two_factor
21
+
22
+ auth = ironauth(
23
+ database=sqlalchemy_adapter("postgresql+asyncpg://user:pass@localhost/db"),
24
+ adapter=fastapi_adapter(),
25
+ plugins=[
26
+ oauth(providers=["google", "github"]),
27
+ two_factor(),
28
+ ],
29
+ config={"secret_key": "your-secret-key"},
30
+ )
31
+
32
+ app.include_router(auth.router)
33
+ ```
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install ironauth
39
+ ```
40
+
41
+ [Get started →](getting-started.md)
File without changes
File without changes
File without changes
@@ -0,0 +1,91 @@
1
+ from typing import Any, Optional
2
+
3
+ from ironauth.models.user import Base, OAuthAccount, User
4
+ from sqlalchemy import delete, select, update
5
+ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
6
+
7
+
8
+ class SQLAlchemyAdapter:
9
+ def __init__(self, database_url: str):
10
+ self.engine = create_async_engine(database_url, echo=False)
11
+ self.session_factory = async_sessionmaker(self.engine, expire_on_commit=False)
12
+
13
+ async def init(self):
14
+ async with self.engine.begin() as conn:
15
+ await conn.run_sync(Base.metadata.create_all)
16
+
17
+ # --- User ---
18
+
19
+ async def create_user(
20
+ self, email: str, hashed_password: Optional[str] = None
21
+ ) -> User:
22
+ async with self.session_factory() as session:
23
+ user = User(email=email, hashed_password=hashed_password)
24
+ session.add(user)
25
+ await session.commit()
26
+ await session.refresh(user)
27
+ return user
28
+
29
+ async def get_user_by_email(self, email: str) -> Optional[User]:
30
+ async with self.session_factory() as session:
31
+ result = await session.execute(select(User).where(User.email == email))
32
+ return result.scalar_one_or_none()
33
+
34
+ async def get_user_by_id(self, user_id: str) -> Optional[User]:
35
+ async with self.session_factory() as session:
36
+ result = await session.execute(select(User).where(User.id == user_id))
37
+ return result.scalar_one_or_none()
38
+
39
+ async def update_user(self, user_id: str, **kwargs: Any) -> Optional[User]:
40
+ async with self.session_factory() as session:
41
+ await session.execute(
42
+ update(User).where(User.id == user_id).values(**kwargs)
43
+ )
44
+ await session.commit()
45
+ return await self.get_user_by_id(user_id)
46
+
47
+ async def delete_user(self, user_id: str) -> None:
48
+ async with self.session_factory() as session:
49
+ await session.execute(delete(User).where(User.id == user_id))
50
+ await session.commit()
51
+
52
+ # --- OAuth ---
53
+
54
+ async def create_oauth_account(
55
+ self,
56
+ user_id: str,
57
+ provider: str,
58
+ provider_user_id: str,
59
+ access_token: str,
60
+ refresh_token: Optional[str] = None,
61
+ expires_at=None,
62
+ ) -> OAuthAccount:
63
+ async with self.session_factory() as session:
64
+ account = OAuthAccount(
65
+ user_id=user_id,
66
+ provider=provider,
67
+ provider_user_id=provider_user_id,
68
+ access_token=access_token,
69
+ refresh_token=refresh_token,
70
+ expires_at=expires_at,
71
+ )
72
+ session.add(account)
73
+ await session.commit()
74
+ await session.refresh(account)
75
+ return account
76
+
77
+ async def get_oauth_account(
78
+ self, provider: str, provider_user_id: str
79
+ ) -> Optional[OAuthAccount]:
80
+ async with self.session_factory() as session:
81
+ result = await session.execute(
82
+ select(OAuthAccount).where(
83
+ OAuthAccount.provider == provider,
84
+ OAuthAccount.provider_user_id == provider_user_id,
85
+ )
86
+ )
87
+ return result.scalar_one_or_none()
88
+
89
+
90
+ def sqlalchemy_adapter(database_url: str) -> SQLAlchemyAdapter:
91
+ return SQLAlchemyAdapter(database_url)