fastapi-async-auth-kit 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.
- fastapi_async_auth_kit-0.1.0/LICENSE +21 -0
- fastapi_async_auth_kit-0.1.0/PKG-INFO +21 -0
- fastapi_async_auth_kit-0.1.0/README.md +175 -0
- fastapi_async_auth_kit-0.1.0/fastapi_async_auth_kit.egg-info/PKG-INFO +21 -0
- fastapi_async_auth_kit-0.1.0/fastapi_async_auth_kit.egg-info/SOURCES.txt +21 -0
- fastapi_async_auth_kit-0.1.0/fastapi_async_auth_kit.egg-info/dependency_links.txt +1 -0
- fastapi_async_auth_kit-0.1.0/fastapi_async_auth_kit.egg-info/requires.txt +17 -0
- fastapi_async_auth_kit-0.1.0/fastapi_async_auth_kit.egg-info/top_level.txt +1 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/__init__.py +5 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/api/auth.py +34 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/core/config.py +6 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/core/security.py +33 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/core/setup.py +14 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/db/base.py +6 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/db/factory.py +10 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/db/mongo_repo.py +27 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/db/sqlalchemy_repo.py +61 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/dependencies/auth.py +14 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/schemas/auth.py +49 -0
- fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/services/auth_service.py +100 -0
- fastapi_async_auth_kit-0.1.0/pyproject.toml +28 -0
- fastapi_async_auth_kit-0.1.0/setup.cfg +4 -0
- fastapi_async_auth_kit-0.1.0/tests/test_auth.py +53 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kmistry1110
|
|
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,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-async-auth-kit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async FastAPI auth with JWT, refresh tokens, logout
|
|
5
|
+
Author-email: Keyurkumar <kmistry1110@gmail.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: fastapi
|
|
9
|
+
Requires-Dist: pydantic
|
|
10
|
+
Requires-Dist: python-jose
|
|
11
|
+
Requires-Dist: argon2-cffi
|
|
12
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
13
|
+
Provides-Extra: postgres
|
|
14
|
+
Requires-Dist: asyncpg; extra == "postgres"
|
|
15
|
+
Provides-Extra: mysql
|
|
16
|
+
Requires-Dist: aiomysql; extra == "mysql"
|
|
17
|
+
Provides-Extra: sqlite
|
|
18
|
+
Requires-Dist: aiosqlite; extra == "sqlite"
|
|
19
|
+
Provides-Extra: mongodb
|
|
20
|
+
Requires-Dist: motor; extra == "mongodb"
|
|
21
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# FastAPI Auth 🔐
|
|
2
|
+
|
|
3
|
+
Production-ready authentication system for FastAPI with async support, JWT, refresh tokens, and pluggable database backends.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Features
|
|
8
|
+
|
|
9
|
+
* ⚡ Fully async (no blocking I/O)
|
|
10
|
+
* 🔐 JWT authentication (access + refresh tokens)
|
|
11
|
+
* 🔁 Refresh token flow
|
|
12
|
+
* 🚪 Logout with token blacklist
|
|
13
|
+
* 🧱 Multi-database support:
|
|
14
|
+
|
|
15
|
+
* PostgreSQL (asyncpg)
|
|
16
|
+
* MySQL (aiomysql)
|
|
17
|
+
* SQLite (aiosqlite)
|
|
18
|
+
* MongoDB (motor)
|
|
19
|
+
* 🧠 Dependency-based auth (FastAPI native)
|
|
20
|
+
* 📦 Plug-and-play integration
|
|
21
|
+
* 🛡️ Password hashing with Argon2 (modern standard)
|
|
22
|
+
* 📄 Clean OpenAPI (Swagger) docs with Pydantic schemas
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 📦 Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install fastapi-auth[<db>]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### With database support
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install fastapi-auth[postgres]
|
|
36
|
+
pip install fastapi-auth[mysql]
|
|
37
|
+
pip install fastapi-auth[mongodb]
|
|
38
|
+
pip install fastapi-auth[sqlite]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## ⚙️ Quick Start
|
|
44
|
+
|
|
45
|
+
### Step 1: How to initiate auth on startup
|
|
46
|
+
```python
|
|
47
|
+
from fastapi import FastAPI
|
|
48
|
+
from fastapi_auth_ import init_auth, AuthConfig
|
|
49
|
+
|
|
50
|
+
app = FastAPI()
|
|
51
|
+
|
|
52
|
+
@app.on_event("startup")
|
|
53
|
+
async def startup():
|
|
54
|
+
await init_auth(
|
|
55
|
+
app,
|
|
56
|
+
AuthConfig(
|
|
57
|
+
secret_key="your-secret",
|
|
58
|
+
db_url="postgresql+asyncpg://user:pass@localhost/db",
|
|
59
|
+
db_type="postgres"
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Step 2: How to validate token for all your FastAPIs
|
|
65
|
+
```python
|
|
66
|
+
from fastapi import APIRouter, Depends
|
|
67
|
+
from fastapi_auth.dependencies.auth import get_current_user
|
|
68
|
+
router = APIRouter()
|
|
69
|
+
|
|
70
|
+
@router.get("/user")
|
|
71
|
+
async def me(user=Depends(get_current_user)):
|
|
72
|
+
return user
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 🔐 Available Endpoints
|
|
79
|
+
|
|
80
|
+
| Endpoint | Description |
|
|
81
|
+
| ------------------- | ----------------------- |
|
|
82
|
+
| POST /auth/register | Register new user |
|
|
83
|
+
| POST /auth/login | Login and get tokens |
|
|
84
|
+
| POST /auth/refresh | Refresh access token |
|
|
85
|
+
| POST /auth/logout | Logout and revoke token |
|
|
86
|
+
| GET /auth/me | Get current user |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 🧪 Example
|
|
91
|
+
|
|
92
|
+
### Login
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
POST /auth/login
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
"username": "john",
|
|
99
|
+
"password": "Strong@123"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Response
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"access": "jwt_token",
|
|
108
|
+
"refresh": "refresh_token"
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 🧠 Architecture
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
FastAPI App
|
|
118
|
+
↓
|
|
119
|
+
Auth Service
|
|
120
|
+
↓
|
|
121
|
+
Repository Layer
|
|
122
|
+
↓
|
|
123
|
+
Database (Async)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 🛡️ Security
|
|
129
|
+
|
|
130
|
+
* Argon2 password hashing
|
|
131
|
+
* JWT token expiration
|
|
132
|
+
* Refresh token blacklist
|
|
133
|
+
* No sensitive data exposure
|
|
134
|
+
* Clean error handling
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 🧩 Extensibility
|
|
139
|
+
|
|
140
|
+
* Add RBAC (roles & permissions)
|
|
141
|
+
* Plug custom user models
|
|
142
|
+
* Add OAuth providers (Google, GitHub)
|
|
143
|
+
* Integrate Redis for token storage
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 🛠 Tech Stack
|
|
148
|
+
|
|
149
|
+
* FastAPI
|
|
150
|
+
* SQLAlchemy (async)
|
|
151
|
+
* Pydantic
|
|
152
|
+
* python-jose (JWT)
|
|
153
|
+
* Argon2 (password hashing)
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 📌 Roadmap
|
|
158
|
+
|
|
159
|
+
* [ ] RBAC support
|
|
160
|
+
* [ ] Redis token blacklist
|
|
161
|
+
* [ ] OAuth integration
|
|
162
|
+
* [ ] Rate limiting
|
|
163
|
+
* [ ] Email verification
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 🤝 Contributing
|
|
168
|
+
|
|
169
|
+
Pull requests are welcome. For major changes, open an issue first.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 📄 License
|
|
174
|
+
|
|
175
|
+
MIT License
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-async-auth-kit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async FastAPI auth with JWT, refresh tokens, logout
|
|
5
|
+
Author-email: Keyurkumar <kmistry1110@gmail.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: fastapi
|
|
9
|
+
Requires-Dist: pydantic
|
|
10
|
+
Requires-Dist: python-jose
|
|
11
|
+
Requires-Dist: argon2-cffi
|
|
12
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
13
|
+
Provides-Extra: postgres
|
|
14
|
+
Requires-Dist: asyncpg; extra == "postgres"
|
|
15
|
+
Provides-Extra: mysql
|
|
16
|
+
Requires-Dist: aiomysql; extra == "mysql"
|
|
17
|
+
Provides-Extra: sqlite
|
|
18
|
+
Requires-Dist: aiosqlite; extra == "sqlite"
|
|
19
|
+
Provides-Extra: mongodb
|
|
20
|
+
Requires-Dist: motor; extra == "mongodb"
|
|
21
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
fastapi_async_auth_kit.egg-info/PKG-INFO
|
|
5
|
+
fastapi_async_auth_kit.egg-info/SOURCES.txt
|
|
6
|
+
fastapi_async_auth_kit.egg-info/dependency_links.txt
|
|
7
|
+
fastapi_async_auth_kit.egg-info/requires.txt
|
|
8
|
+
fastapi_async_auth_kit.egg-info/top_level.txt
|
|
9
|
+
fastapi_auth_kit/__init__.py
|
|
10
|
+
fastapi_auth_kit/api/auth.py
|
|
11
|
+
fastapi_auth_kit/core/config.py
|
|
12
|
+
fastapi_auth_kit/core/security.py
|
|
13
|
+
fastapi_auth_kit/core/setup.py
|
|
14
|
+
fastapi_auth_kit/db/base.py
|
|
15
|
+
fastapi_auth_kit/db/factory.py
|
|
16
|
+
fastapi_auth_kit/db/mongo_repo.py
|
|
17
|
+
fastapi_auth_kit/db/sqlalchemy_repo.py
|
|
18
|
+
fastapi_auth_kit/dependencies/auth.py
|
|
19
|
+
fastapi_auth_kit/schemas/auth.py
|
|
20
|
+
fastapi_auth_kit/services/auth_service.py
|
|
21
|
+
tests/test_auth.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastapi_auth_kit
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request, Depends
|
|
2
|
+
from fastapi_auth_kit.schemas.auth import (
|
|
3
|
+
RegisterRequest,
|
|
4
|
+
LoginRequest,
|
|
5
|
+
RefreshRequest,
|
|
6
|
+
TokenResponse
|
|
7
|
+
)
|
|
8
|
+
from fastapi_auth_kit.dependencies.auth import get_current_user
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/register")
|
|
14
|
+
async def register(req: Request, data: RegisterRequest):
|
|
15
|
+
return await req.app.state.auth.register(data.username, data.password)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.post("/login", response_model=TokenResponse)
|
|
19
|
+
async def login(req: Request, data: LoginRequest):
|
|
20
|
+
return await req.app.state.auth.login(data.username, data.password)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.post("/refresh")
|
|
24
|
+
async def refresh(req: Request, data: RefreshRequest):
|
|
25
|
+
return await req.app.state.auth.refresh(data.refresh)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@router.post("/logout")
|
|
29
|
+
async def logout(req: Request, data: RefreshRequest):
|
|
30
|
+
return await req.app.state.auth.logout(data.refresh)
|
|
31
|
+
|
|
32
|
+
@router.get("/me")
|
|
33
|
+
async def me(user=Depends(get_current_user)):
|
|
34
|
+
return user
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from passlib.context import CryptContext
|
|
2
|
+
from jose import jwt
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
pwd = CryptContext(schemes=["argon2"], deprecated="auto")
|
|
6
|
+
|
|
7
|
+
ALGO = "HS256"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def hash_password(password: str) -> str:
|
|
11
|
+
return pwd.hash(password)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def verify_password(password: str, hashed: str) -> bool:
|
|
15
|
+
valid = pwd.verify(password, hashed)
|
|
16
|
+
|
|
17
|
+
if valid and pwd.needs_update(hashed):
|
|
18
|
+
new_hash = pwd.hash(password)
|
|
19
|
+
|
|
20
|
+
return valid
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_token(data, secret, minutes, ttype):
|
|
24
|
+
payload = data.copy()
|
|
25
|
+
payload.update({
|
|
26
|
+
"exp": datetime.utcnow() + timedelta(minutes=minutes),
|
|
27
|
+
"type": ttype
|
|
28
|
+
})
|
|
29
|
+
return jwt.encode(payload, secret, algorithm=ALGO)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def decode_token(token, secret):
|
|
33
|
+
return jwt.decode(token, secret, algorithms=[ALGO])
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from fastapi_auth_kit.db.factory import get_repo
|
|
2
|
+
from fastapi_auth_kit.services.auth_service import AuthService
|
|
3
|
+
from fastapi_auth_kit.api.auth import router
|
|
4
|
+
from fastapi_auth_kit.dependencies import auth
|
|
5
|
+
|
|
6
|
+
async def init_auth(app, config):
|
|
7
|
+
repo = await get_repo(config)
|
|
8
|
+
|
|
9
|
+
service = AuthService(repo, config.secret_key)
|
|
10
|
+
|
|
11
|
+
auth.SECRET = config.secret_key
|
|
12
|
+
|
|
13
|
+
app.state.auth = service
|
|
14
|
+
app.include_router(router, prefix="/auth")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from fastapi_auth_kit.db.sqlalchemy_repo import SQLRepo
|
|
2
|
+
from fastapi_auth_kit.db.mongo_repo import MongoRepo
|
|
3
|
+
|
|
4
|
+
async def get_repo(config):
|
|
5
|
+
if config.db_type == "mongodb":
|
|
6
|
+
return MongoRepo(config.db_url)
|
|
7
|
+
|
|
8
|
+
repo = SQLRepo(config.db_url)
|
|
9
|
+
await repo.init()
|
|
10
|
+
return repo
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
|
2
|
+
|
|
3
|
+
class MongoRepo:
|
|
4
|
+
def __init__(self, url):
|
|
5
|
+
client = AsyncIOMotorClient(url)
|
|
6
|
+
db = client["auth"]
|
|
7
|
+
self.users = db["users"]
|
|
8
|
+
self.tokens = db["tokens"]
|
|
9
|
+
|
|
10
|
+
async def get_user(self, username):
|
|
11
|
+
return await self.users.find_one({"username": username})
|
|
12
|
+
|
|
13
|
+
async def create_user(self, u, p):
|
|
14
|
+
await self.users.insert_one({"username": u, "password": p})
|
|
15
|
+
|
|
16
|
+
async def save_refresh_token(self, token):
|
|
17
|
+
await self.tokens.insert_one({"token": token, "blacklisted": False})
|
|
18
|
+
|
|
19
|
+
async def is_token_blacklisted(self, token):
|
|
20
|
+
t = await self.tokens.find_one({"token": token})
|
|
21
|
+
return t and t["blacklisted"]
|
|
22
|
+
|
|
23
|
+
async def blacklist_token(self, token):
|
|
24
|
+
await self.tokens.update_one(
|
|
25
|
+
{"token": token},
|
|
26
|
+
{"$set": {"blacklisted": True}}
|
|
27
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
2
|
+
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
3
|
+
from sqlalchemy import Column, String, Integer, select
|
|
4
|
+
|
|
5
|
+
Base = declarative_base()
|
|
6
|
+
|
|
7
|
+
class User(Base):
|
|
8
|
+
__tablename__ = "users"
|
|
9
|
+
id = Column(Integer, primary_key=True)
|
|
10
|
+
username = Column(String, unique=True)
|
|
11
|
+
password = Column(String)
|
|
12
|
+
|
|
13
|
+
class Token(Base):
|
|
14
|
+
__tablename__ = "tokens"
|
|
15
|
+
token = Column(String, primary_key=True)
|
|
16
|
+
blacklisted = Column(Integer, default=0)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SQLRepo:
|
|
20
|
+
def __init__(self, url):
|
|
21
|
+
self.engine = create_async_engine(url, future=True)
|
|
22
|
+
self.Session = sessionmaker(
|
|
23
|
+
self.engine,
|
|
24
|
+
class_=AsyncSession,
|
|
25
|
+
expire_on_commit=False
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
async def init(self):
|
|
29
|
+
async with self.engine.begin() as conn:
|
|
30
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
31
|
+
|
|
32
|
+
async def get_user(self, username):
|
|
33
|
+
async with self.Session() as db:
|
|
34
|
+
res = await db.execute(select(User).where(User.username == username))
|
|
35
|
+
return res.scalar_one_or_none()
|
|
36
|
+
|
|
37
|
+
async def create_user(self, u, p):
|
|
38
|
+
async with self.Session() as db:
|
|
39
|
+
user = User(username=u, password=p)
|
|
40
|
+
db.add(user)
|
|
41
|
+
await db.commit()
|
|
42
|
+
return user
|
|
43
|
+
|
|
44
|
+
async def save_refresh_token(self, token):
|
|
45
|
+
async with self.Session() as db:
|
|
46
|
+
db.add(Token(token=token))
|
|
47
|
+
await db.commit()
|
|
48
|
+
|
|
49
|
+
async def is_token_blacklisted(self, token):
|
|
50
|
+
async with self.Session() as db:
|
|
51
|
+
res = await db.execute(select(Token).where(Token.token == token))
|
|
52
|
+
t = res.scalar_one_or_none()
|
|
53
|
+
return t and t.blacklisted == 1
|
|
54
|
+
|
|
55
|
+
async def blacklist_token(self, token):
|
|
56
|
+
async with self.Session() as db:
|
|
57
|
+
res = await db.execute(select(Token).where(Token.token == token))
|
|
58
|
+
t = res.scalar_one_or_none()
|
|
59
|
+
if t:
|
|
60
|
+
t.blacklisted = 1
|
|
61
|
+
await db.commit()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from fastapi import Depends, HTTPException
|
|
2
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
3
|
+
from fastapi_auth_kit.core.security import decode_token
|
|
4
|
+
|
|
5
|
+
security = HTTPBearer()
|
|
6
|
+
|
|
7
|
+
async def get_current_user(
|
|
8
|
+
credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
9
|
+
):
|
|
10
|
+
try:
|
|
11
|
+
token = credentials.credentials
|
|
12
|
+
return decode_token(token, SECRET)
|
|
13
|
+
except:
|
|
14
|
+
raise HTTPException(401, "Invalid token")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, field_validator
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
USERNAME_REGEX = r"^[a-zA-Z][a-zA-Z0-9_]{2,29}$"
|
|
6
|
+
PASSWORD_REGEX = r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RegisterRequest(BaseModel):
|
|
10
|
+
username: str = Field(
|
|
11
|
+
...,
|
|
12
|
+
description="3-30 chars, letters/numbers/_ only, must start with letter"
|
|
13
|
+
)
|
|
14
|
+
password: str = Field(
|
|
15
|
+
...,
|
|
16
|
+
description="Min 8 chars, include uppercase, lowercase, number, special char"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@field_validator("username")
|
|
20
|
+
@classmethod
|
|
21
|
+
def validate_username(cls, v):
|
|
22
|
+
if not re.match(USERNAME_REGEX, v):
|
|
23
|
+
raise ValueError(
|
|
24
|
+
"Username must be 3-30 chars, start with letter, and contain only letters, numbers, underscores"
|
|
25
|
+
)
|
|
26
|
+
return v
|
|
27
|
+
|
|
28
|
+
@field_validator("password")
|
|
29
|
+
@classmethod
|
|
30
|
+
def validate_password(cls, v):
|
|
31
|
+
if not re.match(PASSWORD_REGEX, v):
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"Password must be at least 8 characters long and include uppercase, lowercase, number, and special character"
|
|
34
|
+
)
|
|
35
|
+
return v
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LoginRequest(BaseModel):
|
|
39
|
+
username: str
|
|
40
|
+
password: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RefreshRequest(BaseModel):
|
|
44
|
+
refresh: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TokenResponse(BaseModel):
|
|
48
|
+
access: str
|
|
49
|
+
refresh: str
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from fastapi import HTTPException, status
|
|
2
|
+
from fastapi_auth_kit.core.security import *
|
|
3
|
+
|
|
4
|
+
class AuthService:
|
|
5
|
+
def __init__(self, repo, secret):
|
|
6
|
+
self.repo = repo
|
|
7
|
+
self.secret = secret
|
|
8
|
+
|
|
9
|
+
async def register(self, username: str, password: str):
|
|
10
|
+
existing_user = await self.repo.get_user(username)
|
|
11
|
+
|
|
12
|
+
if existing_user:
|
|
13
|
+
raise HTTPException(
|
|
14
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
15
|
+
detail="User already exists"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
user = await self.repo.create_user(
|
|
19
|
+
username,
|
|
20
|
+
hash_password(password)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
"message": "User registered successfully",
|
|
25
|
+
"username": username
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async def login(self, username: str, password: str):
|
|
29
|
+
user = await self.repo.get_user(username)
|
|
30
|
+
|
|
31
|
+
if not user or not verify_password(password, user.password):
|
|
32
|
+
raise HTTPException(
|
|
33
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
34
|
+
detail="Invalid username or password"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
access = create_token({"sub": username}, self.secret, 15, "access")
|
|
38
|
+
refresh = create_token({"sub": username}, self.secret, 60*24, "refresh")
|
|
39
|
+
|
|
40
|
+
await self.repo.save_refresh_token(refresh)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"access": access,
|
|
44
|
+
"refresh": refresh
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async def refresh(self, token: str):
|
|
48
|
+
try:
|
|
49
|
+
data = decode_token(token, self.secret)
|
|
50
|
+
except Exception:
|
|
51
|
+
raise HTTPException(
|
|
52
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
53
|
+
detail="Invalid or expired refresh token"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if data.get("type") != "refresh":
|
|
57
|
+
raise HTTPException(
|
|
58
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
59
|
+
detail="Invalid token type (expected refresh token)"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if await self.repo.is_token_blacklisted(token):
|
|
63
|
+
raise HTTPException(
|
|
64
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
65
|
+
detail="Token has been revoked (logout)"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"access": create_token(
|
|
70
|
+
{"sub": data["sub"]},
|
|
71
|
+
self.secret,
|
|
72
|
+
15,
|
|
73
|
+
"access"
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async def logout(self, token: str):
|
|
78
|
+
try:
|
|
79
|
+
data = decode_token(token, self.secret)
|
|
80
|
+
except Exception:
|
|
81
|
+
raise HTTPException(
|
|
82
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
83
|
+
detail="Invalid or expired refresh token"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if data.get("type") != "refresh":
|
|
87
|
+
raise HTTPException(
|
|
88
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
89
|
+
detail="Invalid token type"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if await self.repo.is_token_blacklisted(token):
|
|
93
|
+
raise HTTPException(
|
|
94
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
95
|
+
detail="Token already logged out"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
await self.repo.blacklist_token(token)
|
|
99
|
+
|
|
100
|
+
return {"message": "Logged out successfully"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastapi-async-auth-kit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Async FastAPI auth with JWT, refresh tokens, logout"
|
|
5
|
+
authors = [{ name="Keyurkumar", email="kmistry1110@gmail.com" }]
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
"fastapi",
|
|
10
|
+
"pydantic",
|
|
11
|
+
"python-jose",
|
|
12
|
+
"argon2-cffi",
|
|
13
|
+
"sqlalchemy>=2.0"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
postgres = ["asyncpg"]
|
|
18
|
+
mysql = ["aiomysql"]
|
|
19
|
+
sqlite = ["aiosqlite"]
|
|
20
|
+
mongodb = ["motor"]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["setuptools", "wheel"]
|
|
24
|
+
build-backend = "setuptools.build_meta"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
include = ["fastapi_auth_kit*"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from httpx import AsyncClient
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
from fastapi_auth_kit import init_auth, AuthConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
async def test_auth_flow():
|
|
9
|
+
app = FastAPI()
|
|
10
|
+
|
|
11
|
+
@app.on_event("startup")
|
|
12
|
+
async def setup():
|
|
13
|
+
await init_auth(
|
|
14
|
+
app,
|
|
15
|
+
AuthConfig(
|
|
16
|
+
secret_key="test",
|
|
17
|
+
db_url="sqlite+aiosqlite:///:memory:",
|
|
18
|
+
db_type="sqlite"
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async with AsyncClient(app=app, base_url="http://test") as ac:
|
|
23
|
+
|
|
24
|
+
# register
|
|
25
|
+
r = await ac.post("/auth/register", json={
|
|
26
|
+
"username": "test",
|
|
27
|
+
"password": "123"
|
|
28
|
+
})
|
|
29
|
+
assert r.status_code == 200
|
|
30
|
+
|
|
31
|
+
# login
|
|
32
|
+
r = await ac.post("/auth/login", json={
|
|
33
|
+
"username": "test",
|
|
34
|
+
"password": "123"
|
|
35
|
+
})
|
|
36
|
+
data = r.json()
|
|
37
|
+
|
|
38
|
+
assert "access" in data
|
|
39
|
+
assert "refresh" in data
|
|
40
|
+
|
|
41
|
+
refresh = data["refresh"]
|
|
42
|
+
|
|
43
|
+
# refresh token
|
|
44
|
+
r = await ac.post("/auth/refresh", json={"refresh": refresh})
|
|
45
|
+
assert r.status_code == 200
|
|
46
|
+
|
|
47
|
+
# logout
|
|
48
|
+
r = await ac.post("/auth/logout", json={"refresh": refresh})
|
|
49
|
+
assert r.status_code == 200
|
|
50
|
+
|
|
51
|
+
# refresh again (should fail)
|
|
52
|
+
r = await ac.post("/auth/refresh", json={"refresh": refresh})
|
|
53
|
+
assert r.status_code != 200
|