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.
Files changed (23) hide show
  1. fastapi_async_auth_kit-0.1.0/LICENSE +21 -0
  2. fastapi_async_auth_kit-0.1.0/PKG-INFO +21 -0
  3. fastapi_async_auth_kit-0.1.0/README.md +175 -0
  4. fastapi_async_auth_kit-0.1.0/fastapi_async_auth_kit.egg-info/PKG-INFO +21 -0
  5. fastapi_async_auth_kit-0.1.0/fastapi_async_auth_kit.egg-info/SOURCES.txt +21 -0
  6. fastapi_async_auth_kit-0.1.0/fastapi_async_auth_kit.egg-info/dependency_links.txt +1 -0
  7. fastapi_async_auth_kit-0.1.0/fastapi_async_auth_kit.egg-info/requires.txt +17 -0
  8. fastapi_async_auth_kit-0.1.0/fastapi_async_auth_kit.egg-info/top_level.txt +1 -0
  9. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/__init__.py +5 -0
  10. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/api/auth.py +34 -0
  11. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/core/config.py +6 -0
  12. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/core/security.py +33 -0
  13. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/core/setup.py +14 -0
  14. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/db/base.py +6 -0
  15. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/db/factory.py +10 -0
  16. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/db/mongo_repo.py +27 -0
  17. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/db/sqlalchemy_repo.py +61 -0
  18. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/dependencies/auth.py +14 -0
  19. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/schemas/auth.py +49 -0
  20. fastapi_async_auth_kit-0.1.0/fastapi_auth_kit/services/auth_service.py +100 -0
  21. fastapi_async_auth_kit-0.1.0/pyproject.toml +28 -0
  22. fastapi_async_auth_kit-0.1.0/setup.cfg +4 -0
  23. 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,17 @@
1
+ fastapi
2
+ pydantic
3
+ python-jose
4
+ argon2-cffi
5
+ sqlalchemy>=2.0
6
+
7
+ [mongodb]
8
+ motor
9
+
10
+ [mysql]
11
+ aiomysql
12
+
13
+ [postgres]
14
+ asyncpg
15
+
16
+ [sqlite]
17
+ aiosqlite
@@ -0,0 +1,5 @@
1
+ from fastapi_auth_kit.core.setup import init_auth
2
+ from fastapi_auth_kit.core.config import AuthConfig
3
+ from fastapi_auth_kit.dependencies.auth import get_current_user
4
+
5
+ __all__ = ["init_auth", "AuthConfig", "get_current_user"]
@@ -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,6 @@
1
+ from pydantic import BaseModel
2
+
3
+ class AuthConfig(BaseModel):
4
+ secret_key: str
5
+ db_url: str
6
+ db_type: str # postgres | mysql | sqlite | mongodb
@@ -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,6 @@
1
+ class UserRepository:
2
+ async def get_user(self, username): ...
3
+ async def create_user(self, username, password): ...
4
+ async def save_refresh_token(self, token): ...
5
+ async def is_token_blacklisted(self, token): ...
6
+ async def blacklist_token(self, token): ...
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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