authwarden 0.7.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.
- authwarden-0.7.0/.github/workflows/publish.yml +55 -0
- authwarden-0.7.0/.gitignore +12 -0
- authwarden-0.7.0/LICENSE +21 -0
- authwarden-0.7.0/PKG-INFO +431 -0
- authwarden-0.7.0/README.md +380 -0
- authwarden-0.7.0/authwarden/__init__.py +43 -0
- authwarden-0.7.0/authwarden/authentication/__init__.py +0 -0
- authwarden-0.7.0/authwarden/authentication/encryption.py +47 -0
- authwarden-0.7.0/authwarden/authentication/jwt.py +345 -0
- authwarden-0.7.0/authwarden/authentication/oauth.py +516 -0
- authwarden-0.7.0/authwarden/authentication/oauth_state.py +94 -0
- authwarden-0.7.0/authwarden/authentication/password.py +133 -0
- authwarden-0.7.0/authwarden/core/__init__.py +0 -0
- authwarden-0.7.0/authwarden/core/config.py +143 -0
- authwarden-0.7.0/authwarden/core/context.py +44 -0
- authwarden-0.7.0/authwarden/core/manager.py +207 -0
- authwarden-0.7.0/authwarden/dependencies/__init__.py +0 -0
- authwarden-0.7.0/authwarden/dependencies/current_user.py +85 -0
- authwarden-0.7.0/authwarden/dependencies/permissions.py +81 -0
- authwarden-0.7.0/authwarden/email/__init__.py +0 -0
- authwarden-0.7.0/authwarden/email/base.py +77 -0
- authwarden-0.7.0/authwarden/email/console.py +53 -0
- authwarden-0.7.0/authwarden/email/mailgun.py +97 -0
- authwarden-0.7.0/authwarden/email/sendgrid.py +96 -0
- authwarden-0.7.0/authwarden/email/smtp.py +103 -0
- authwarden-0.7.0/authwarden/email/templates.py +139 -0
- authwarden-0.7.0/authwarden/exceptions.py +241 -0
- authwarden-0.7.0/authwarden/flows/__init__.py +0 -0
- authwarden-0.7.0/authwarden/flows/change_password.py +49 -0
- authwarden-0.7.0/authwarden/flows/forgot_password.py +63 -0
- authwarden-0.7.0/authwarden/flows/login.py +134 -0
- authwarden-0.7.0/authwarden/flows/logout.py +32 -0
- authwarden-0.7.0/authwarden/flows/oauth_accounts.py +24 -0
- authwarden-0.7.0/authwarden/flows/oauth_authorize.py +53 -0
- authwarden-0.7.0/authwarden/flows/oauth_callback.py +147 -0
- authwarden-0.7.0/authwarden/flows/oauth_connect.py +85 -0
- authwarden-0.7.0/authwarden/flows/oauth_disconnect.py +35 -0
- authwarden-0.7.0/authwarden/flows/refresh.py +36 -0
- authwarden-0.7.0/authwarden/flows/register.py +85 -0
- authwarden-0.7.0/authwarden/flows/resend_verification.py +58 -0
- authwarden-0.7.0/authwarden/flows/reset_password.py +55 -0
- authwarden-0.7.0/authwarden/flows/reset_password_otp.py +73 -0
- authwarden-0.7.0/authwarden/flows/set_password.py +38 -0
- authwarden-0.7.0/authwarden/flows/verify_email.py +50 -0
- authwarden-0.7.0/authwarden/flows/verify_otp.py +85 -0
- authwarden-0.7.0/authwarden/mfa/__init__.py +0 -0
- authwarden-0.7.0/authwarden/mfa/backup_codes.py +37 -0
- authwarden-0.7.0/authwarden/mfa/totp.py +127 -0
- authwarden-0.7.0/authwarden/models/__init__.py +0 -0
- authwarden-0.7.0/authwarden/models/requests.py +119 -0
- authwarden-0.7.0/authwarden/models/token.py +49 -0
- authwarden-0.7.0/authwarden/models/user.py +184 -0
- authwarden-0.7.0/authwarden/notifications/__init__.py +0 -0
- authwarden-0.7.0/authwarden/notifications/service.py +165 -0
- authwarden-0.7.0/authwarden/permissions/__init__.py +0 -0
- authwarden-0.7.0/authwarden/permissions/policies.py +46 -0
- authwarden-0.7.0/authwarden/permissions/roles.py +75 -0
- authwarden-0.7.0/authwarden/py.typed +0 -0
- authwarden-0.7.0/authwarden/routers/__init__.py +0 -0
- authwarden-0.7.0/authwarden/routers/_errors.py +33 -0
- authwarden-0.7.0/authwarden/routers/auth.py +201 -0
- authwarden-0.7.0/authwarden/routers/mfa.py +65 -0
- authwarden-0.7.0/authwarden/routers/oauth.py +129 -0
- authwarden-0.7.0/authwarden/session/__init__.py +0 -0
- authwarden-0.7.0/authwarden/session/base.py +98 -0
- authwarden-0.7.0/authwarden/session/memory.py +116 -0
- authwarden-0.7.0/authwarden/session/redis.py +149 -0
- authwarden-0.7.0/authwarden/sms/__init__.py +0 -0
- authwarden-0.7.0/authwarden/sms/base.py +42 -0
- authwarden-0.7.0/authwarden/sms/console.py +20 -0
- authwarden-0.7.0/authwarden/sms/sns.py +47 -0
- authwarden-0.7.0/authwarden/sms/templates.py +36 -0
- authwarden-0.7.0/authwarden/sms/twilio.py +55 -0
- authwarden-0.7.0/authwarden/storage/__init__.py +0 -0
- authwarden-0.7.0/authwarden/storage/base.py +162 -0
- authwarden-0.7.0/authwarden/storage/memory.py +208 -0
- authwarden-0.7.0/authwarden/utils.py +125 -0
- authwarden-0.7.0/pyproject.toml +72 -0
- authwarden-0.7.0/pytest.ini +3 -0
- authwarden-0.7.0/tests/__init__.py +0 -0
- authwarden-0.7.0/tests/conftest.py +7 -0
- authwarden-0.7.0/tests/test_phase1.py +575 -0
- authwarden-0.7.0/tests/test_phase2.py +488 -0
- authwarden-0.7.0/tests/test_phase3.py +706 -0
- authwarden-0.7.0/tests/test_phase4.py +633 -0
- authwarden-0.7.0/tests/test_phase5.py +780 -0
- authwarden-0.7.0/tests/test_phase6.py +328 -0
- authwarden-0.7.0/tests/test_phase7.py +809 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Fires when a GitHub Release is published — NOT on every phase tag.
|
|
4
|
+
# This decouples "phase complete" tagging from "publish to PyPI",
|
|
5
|
+
# so only intentional releases (triggered by clicking "Publish release")
|
|
6
|
+
# ever reach PyPI.
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build distribution
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
|
|
23
|
+
- name: Install build tool
|
|
24
|
+
run: python -m pip install --upgrade build
|
|
25
|
+
|
|
26
|
+
- name: Build sdist and wheel
|
|
27
|
+
run: python -m build
|
|
28
|
+
|
|
29
|
+
- name: Upload build artifacts
|
|
30
|
+
uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
|
|
35
|
+
publish:
|
|
36
|
+
name: Publish on PyPI
|
|
37
|
+
needs: build
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
# This environment name must match exactly what is configured as the
|
|
40
|
+
# trusted publisher on PyPI (Settings → Publishing on the project page).
|
|
41
|
+
environment:
|
|
42
|
+
name: pypi
|
|
43
|
+
url: https://pypi.org/p/authwarden
|
|
44
|
+
permissions:
|
|
45
|
+
# Required for PyPI trusted publishing (OIDC) — no API token needed.
|
|
46
|
+
id-token: write
|
|
47
|
+
steps:
|
|
48
|
+
- name: Download build artifacts
|
|
49
|
+
uses: actions/download-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
53
|
+
|
|
54
|
+
- name: Publish to PyPI
|
|
55
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
authwarden-0.7.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adeniran John
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authwarden
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: A production-grade, pluggable authentication library for FastAPI — JWT, OAuth2 (8 providers), MFA, RBAC, and full flow flexibility (email/SMS, link/OTP, multi-identifier login).
|
|
5
|
+
Project-URL: Homepage, https://github.com/timihack/authwarden
|
|
6
|
+
Project-URL: Repository, https://github.com/timihack/authwarden
|
|
7
|
+
Project-URL: Issues, https://github.com/timihack/authwarden/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/timihack/authwarden/releases
|
|
9
|
+
Author: John Adeniran (timihack)
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: authentication,fastapi,jwt,mfa,oauth2,rbac,security
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Session
|
|
22
|
+
Classifier: Topic :: Security :: Cryptography
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: aiosmtplib>=3.0.0
|
|
26
|
+
Requires-Dist: authlib>=1.3.0
|
|
27
|
+
Requires-Dist: cryptography>=42.0.0
|
|
28
|
+
Requires-Dist: fastapi>=0.110.0
|
|
29
|
+
Requires-Dist: itsdangerous>=2.1.0
|
|
30
|
+
Requires-Dist: pwdlib[argon2,bcrypt]>=0.2.0
|
|
31
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
32
|
+
Requires-Dist: pydantic>=2.0.0
|
|
33
|
+
Requires-Dist: pydantic[email]>=2.0.0
|
|
34
|
+
Requires-Dist: pyjwt>=2.8.0
|
|
35
|
+
Requires-Dist: pyotp>=2.9.0
|
|
36
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
37
|
+
Provides-Extra: all
|
|
38
|
+
Requires-Dist: boto3>=1.34.0; extra == 'all'
|
|
39
|
+
Requires-Dist: redis>=5.0.0; extra == 'all'
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: boto3>=1.34.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
43
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
44
|
+
Requires-Dist: redis>=5.0.0; extra == 'dev'
|
|
45
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
46
|
+
Provides-Extra: redis
|
|
47
|
+
Requires-Dist: redis>=5.0.0; extra == 'redis'
|
|
48
|
+
Provides-Extra: sns
|
|
49
|
+
Requires-Dist: boto3>=1.34.0; extra == 'sns'
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
# authwarden
|
|
53
|
+
|
|
54
|
+
Production-grade FastAPI authentication and authorization library. Wraps proven cryptographic libraries — never rolls its own.
|
|
55
|
+
|
|
56
|
+
[](https://python.org)
|
|
57
|
+
[](https://fastapi.tiangolo.com)
|
|
58
|
+
[](LICENSE)
|
|
59
|
+
[](https://pypi.org/project/authwarden/)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- **JWT authentication** — access + refresh tokens, rotation, per-token revocation via `jti`
|
|
66
|
+
- **Password hashing** — argon2 (default) or bcrypt via `pwdlib`, auto-rehash on login
|
|
67
|
+
- **Full auth flows** — register, verify email, login, logout, forgot/reset/change password
|
|
68
|
+
- **MFA** — TOTP setup/confirm/disable (pyotp), hashed backup codes
|
|
69
|
+
- **OAuth 2.0 / Social login** — Google, GitHub, Facebook, Apple, Twitter/X, Microsoft, LinkedIn, Discord
|
|
70
|
+
- **RBAC** — role hierarchy, scope guards, `Depends()` factories
|
|
71
|
+
- **Session management** — pluggable backends (in-memory, Redis)
|
|
72
|
+
- **Email** — SMTP + console backends, HTML + plain-text templates for every flow
|
|
73
|
+
- **Plug-and-play router** — mount all endpoints with one line
|
|
74
|
+
- **Storage-agnostic** — implement `AbstractUserStore` for any ORM or database
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install authwarden
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
With Redis session support:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install authwarden[redis]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Quickstart
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from fastapi import FastAPI, Depends
|
|
96
|
+
from authwarden import AuthWarden, WardenConfig
|
|
97
|
+
from authwarden.storage.memory import MemoryUserStore
|
|
98
|
+
|
|
99
|
+
app = FastAPI()
|
|
100
|
+
|
|
101
|
+
warden = AuthWarden(
|
|
102
|
+
config=WardenConfig(secret_key="your-secret-key"),
|
|
103
|
+
user_store=MemoryUserStore(),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Mount all auth endpoints under /auth
|
|
107
|
+
app.include_router(warden.router, prefix="/auth", tags=["auth"])
|
|
108
|
+
|
|
109
|
+
# Protect your own routes
|
|
110
|
+
@app.get("/profile")
|
|
111
|
+
async def profile(user=Depends(warden.current_user)):
|
|
112
|
+
return user
|
|
113
|
+
|
|
114
|
+
@app.delete("/admin/users/{user_id}")
|
|
115
|
+
async def delete_user(user=Depends(warden.require_roles("admin"))):
|
|
116
|
+
...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This mounts:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
POST /auth/register
|
|
123
|
+
POST /auth/verify-email
|
|
124
|
+
POST /auth/resend-verification
|
|
125
|
+
POST /auth/login
|
|
126
|
+
POST /auth/logout
|
|
127
|
+
POST /auth/refresh
|
|
128
|
+
POST /auth/forgot-password
|
|
129
|
+
POST /auth/reset-password
|
|
130
|
+
POST /auth/change-password
|
|
131
|
+
POST /auth/mfa/setup
|
|
132
|
+
POST /auth/mfa/confirm
|
|
133
|
+
POST /auth/mfa/disable
|
|
134
|
+
GET /auth/oauth/{provider}/authorize
|
|
135
|
+
POST /auth/oauth/{provider}/callback
|
|
136
|
+
POST /auth/oauth/{provider}/connect
|
|
137
|
+
DEL /auth/oauth/{provider}/disconnect
|
|
138
|
+
GET /auth/oauth/accounts
|
|
139
|
+
POST /auth/set-password
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from authwarden import WardenConfig
|
|
148
|
+
|
|
149
|
+
config = WardenConfig(
|
|
150
|
+
# Required
|
|
151
|
+
secret_key="a-long-random-secret",
|
|
152
|
+
|
|
153
|
+
# JWT
|
|
154
|
+
algorithm="HS256",
|
|
155
|
+
access_token_ttl=900, # 15 minutes
|
|
156
|
+
refresh_token_ttl=604800, # 7 days
|
|
157
|
+
enable_refresh_rotation=True,
|
|
158
|
+
|
|
159
|
+
# Passwords
|
|
160
|
+
password_hasher="argon2", # or "bcrypt"
|
|
161
|
+
min_password_length=8,
|
|
162
|
+
require_password_uppercase=False,
|
|
163
|
+
require_password_digit=False,
|
|
164
|
+
require_password_special=False,
|
|
165
|
+
|
|
166
|
+
# Email
|
|
167
|
+
email_backend="smtp", # or "console" for dev
|
|
168
|
+
smtp_host="smtp.example.com",
|
|
169
|
+
smtp_port=587,
|
|
170
|
+
smtp_username="user@example.com",
|
|
171
|
+
smtp_password="...",
|
|
172
|
+
emails_from_address="noreply@example.com",
|
|
173
|
+
|
|
174
|
+
# Registration
|
|
175
|
+
require_email_verification=True,
|
|
176
|
+
allow_registration=True,
|
|
177
|
+
|
|
178
|
+
# MFA
|
|
179
|
+
enable_mfa=True,
|
|
180
|
+
mfa_issuer_name="MyApp",
|
|
181
|
+
|
|
182
|
+
# Session (optional)
|
|
183
|
+
session_backend="redis",
|
|
184
|
+
redis_url="redis://localhost:6379",
|
|
185
|
+
|
|
186
|
+
# Frontend URLs (for email links)
|
|
187
|
+
frontend_base_url="https://myapp.com",
|
|
188
|
+
verify_email_path="/auth/verify-email",
|
|
189
|
+
reset_password_path="/auth/reset-password",
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
All settings can also be loaded from environment variables (via `pydantic-settings`):
|
|
194
|
+
|
|
195
|
+
```env
|
|
196
|
+
SECRET_KEY=your-secret-key
|
|
197
|
+
EMAIL_BACKEND=smtp
|
|
198
|
+
SMTP_HOST=smtp.example.com
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Custom User Store
|
|
204
|
+
|
|
205
|
+
Implement `AbstractUserStore` to connect any database:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from authwarden.storage.base import AbstractUserStore
|
|
209
|
+
from authwarden.models.user import UserInDB
|
|
210
|
+
|
|
211
|
+
class SQLAlchemyUserStore(AbstractUserStore):
|
|
212
|
+
async def get_by_id(self, user_id: str) -> UserInDB | None:
|
|
213
|
+
...
|
|
214
|
+
|
|
215
|
+
async def get_by_email(self, email: str) -> UserInDB | None:
|
|
216
|
+
...
|
|
217
|
+
|
|
218
|
+
async def create(self, user: UserInDB) -> UserInDB:
|
|
219
|
+
...
|
|
220
|
+
|
|
221
|
+
async def update(self, user: UserInDB) -> UserInDB:
|
|
222
|
+
...
|
|
223
|
+
|
|
224
|
+
async def delete(self, user_id: str) -> None:
|
|
225
|
+
...
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## OAuth / Social Login
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from authwarden.authentication.oauth import OAuthProviderConfig
|
|
234
|
+
|
|
235
|
+
warden = AuthWarden(
|
|
236
|
+
config=WardenConfig(
|
|
237
|
+
secret_key="...",
|
|
238
|
+
oauth_providers={
|
|
239
|
+
"google": OAuthProviderConfig(
|
|
240
|
+
client_id="...",
|
|
241
|
+
client_secret="...",
|
|
242
|
+
redirect_uri="https://myapp.com/auth/oauth/google/callback",
|
|
243
|
+
),
|
|
244
|
+
"github": OAuthProviderConfig(
|
|
245
|
+
client_id="...",
|
|
246
|
+
client_secret="...",
|
|
247
|
+
redirect_uri="https://myapp.com/auth/oauth/github/callback",
|
|
248
|
+
),
|
|
249
|
+
}
|
|
250
|
+
),
|
|
251
|
+
user_store=MyUserStore(),
|
|
252
|
+
)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Apple Sign In** requires additional fields:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
config = WardenConfig(
|
|
259
|
+
...
|
|
260
|
+
apple_team_id="TEAM123",
|
|
261
|
+
apple_key_id="KEY123",
|
|
262
|
+
apple_private_key_pem="-----BEGIN PRIVATE KEY-----\n...",
|
|
263
|
+
oauth_providers={
|
|
264
|
+
"apple": OAuthProviderConfig(
|
|
265
|
+
client_id="com.myapp.service",
|
|
266
|
+
client_secret="", # auto-generated from private key
|
|
267
|
+
redirect_uri="https://myapp.com/auth/oauth/apple/callback",
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## MFA (TOTP)
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
# Enable globally
|
|
279
|
+
config = WardenConfig(secret_key="...", enable_mfa=True)
|
|
280
|
+
|
|
281
|
+
# Endpoints are automatically mounted:
|
|
282
|
+
# POST /auth/mfa/setup → returns { secret, qr_uri, backup_codes }
|
|
283
|
+
# POST /auth/mfa/confirm → activates MFA after verifying first code
|
|
284
|
+
# POST /auth/mfa/disable → requires password + TOTP or backup code
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Login with MFA:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
POST /auth/login
|
|
291
|
+
{
|
|
292
|
+
"email": "user@example.com",
|
|
293
|
+
"password": "hunter2",
|
|
294
|
+
"totp_code": "123456"
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## RBAC
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
from fastapi import Depends
|
|
304
|
+
|
|
305
|
+
# Require a single role
|
|
306
|
+
@app.get("/admin")
|
|
307
|
+
async def admin(user=Depends(warden.require_roles("admin"))):
|
|
308
|
+
...
|
|
309
|
+
|
|
310
|
+
# Require any of multiple roles
|
|
311
|
+
@app.get("/reports")
|
|
312
|
+
async def reports(user=Depends(warden.require_roles("admin", "analyst"))):
|
|
313
|
+
...
|
|
314
|
+
|
|
315
|
+
# Require a scope
|
|
316
|
+
@app.post("/items")
|
|
317
|
+
async def create_item(user=Depends(warden.require_scopes("items:write"))):
|
|
318
|
+
...
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Email Templates
|
|
324
|
+
|
|
325
|
+
Override any template by subclassing `EmailTemplates`:
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
from authwarden.email.templates import EmailTemplates
|
|
329
|
+
|
|
330
|
+
class MyTemplates(EmailTemplates):
|
|
331
|
+
def verify_email(self, user, link: str) -> tuple[str, str, str]:
|
|
332
|
+
# returns (subject, plain_text, html)
|
|
333
|
+
return (
|
|
334
|
+
"Verify your account",
|
|
335
|
+
f"Click here: {link}",
|
|
336
|
+
f"<a href='{link}'>Verify your account</a>",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
warden = AuthWarden(config=..., user_store=..., email_templates=MyTemplates())
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Auth Flows Reference
|
|
345
|
+
|
|
346
|
+
| Flow | Endpoint | Auth required |
|
|
347
|
+
|---|---|---|
|
|
348
|
+
| Register | `POST /auth/register` | No |
|
|
349
|
+
| Verify email | `POST /auth/verify-email` | No |
|
|
350
|
+
| Resend verification | `POST /auth/resend-verification` | No |
|
|
351
|
+
| Login | `POST /auth/login` | No |
|
|
352
|
+
| Logout | `POST /auth/logout` | Bearer token |
|
|
353
|
+
| Refresh token | `POST /auth/refresh` | No |
|
|
354
|
+
| Forgot password | `POST /auth/forgot-password` | No |
|
|
355
|
+
| Reset password | `POST /auth/reset-password` | No |
|
|
356
|
+
| Change password | `POST /auth/change-password` | Bearer token |
|
|
357
|
+
| MFA setup | `POST /auth/mfa/setup` | Bearer token |
|
|
358
|
+
| MFA confirm | `POST /auth/mfa/confirm` | Bearer token |
|
|
359
|
+
| MFA disable | `POST /auth/mfa/disable` | Bearer token |
|
|
360
|
+
| OAuth authorize | `GET /auth/oauth/{provider}/authorize` | No |
|
|
361
|
+
| OAuth callback | `POST /auth/oauth/{provider}/callback` | No |
|
|
362
|
+
| Connect provider | `POST /auth/oauth/{provider}/connect` | Bearer token |
|
|
363
|
+
| Disconnect provider | `DELETE /auth/oauth/{provider}/disconnect` | Bearer token |
|
|
364
|
+
| List linked accounts | `GET /auth/oauth/accounts` | Bearer token |
|
|
365
|
+
| Set password (OAuth) | `POST /auth/set-password` | Bearer token |
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Security Notes
|
|
370
|
+
|
|
371
|
+
- Passwords hashed with **argon2** by default (bcrypt available)
|
|
372
|
+
- JWT tokens include a `jti` (UUID) claim — enables per-token revocation
|
|
373
|
+
- Password reset and email verification tokens stored as **hashes only**
|
|
374
|
+
- Forgot password and resend verification always return `200` (anti-enumeration)
|
|
375
|
+
- Constant-time comparison used for all token lookups (`hmac.compare_digest`)
|
|
376
|
+
- PKCE (S256) used for all OAuth flows
|
|
377
|
+
- Refresh token rotation enabled by default — old `jti` blacklisted on use
|
|
378
|
+
- MFA backup codes stored as **argon2 hashes**, single-use
|
|
379
|
+
- OAuth provider tokens encrypted at rest (Fernet)
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Development
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
git clone https://github.com/yourusername/authwarden.git
|
|
387
|
+
cd authwarden
|
|
388
|
+
python -m venv .venv && source .venv/bin/activate
|
|
389
|
+
pip install -e ".[dev]"
|
|
390
|
+
pytest
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Running Tests
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
# All tests
|
|
399
|
+
pytest
|
|
400
|
+
|
|
401
|
+
# With coverage
|
|
402
|
+
pytest --cov=authwarden --cov-report=term-missing
|
|
403
|
+
|
|
404
|
+
# Specific suite
|
|
405
|
+
pytest tests/test_oauth.py -v
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Project Structure
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
authwarden/
|
|
414
|
+
├── core/ # WardenConfig, AuthWarden facade, request context
|
|
415
|
+
├── authentication/ # JWT, password hashing, OAuth provider base
|
|
416
|
+
├── flows/ # One module per auth flow
|
|
417
|
+
├── mfa/ # TOTP + backup codes
|
|
418
|
+
├── permissions/ # Roles + scope guards
|
|
419
|
+
├── session/ # Memory + Redis session backends
|
|
420
|
+
├── dependencies/ # FastAPI Depends() factories
|
|
421
|
+
├── routers/ # Plug-and-play FastAPI routers
|
|
422
|
+
├── email/ # SMTP, console backends + templates
|
|
423
|
+
├── models/ # Pydantic v2 models
|
|
424
|
+
└── storage/ # AbstractUserStore + MemoryUserStore
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## License
|
|
430
|
+
|
|
431
|
+
MIT — see [LICENSE](LICENSE).
|