fastapi-async-auth-kit 0.1.0__py3-none-any.whl

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.
@@ -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,17 @@
1
+ fastapi_async_auth_kit-0.1.0.dist-info/licenses/LICENSE,sha256=_NXHqoXfXIN9cX2fcEQZRjJnIeYAo-1zB8J7LsAihlg,1089
2
+ fastapi_auth_kit/__init__.py,sha256=frlKWesm2oGWoCerX1dxcnIrECX6ZybwpYeeHhv9H-A,228
3
+ fastapi_auth_kit/api/auth.py,sha256=sPsWnMWpnz-Ko6bT3iIvKd70l0skjzBVxZYge899NOI,986
4
+ fastapi_auth_kit/core/config.py,sha256=6sYXF4xW0lNplO2xW5SrnIGeyVekqChYgJRpK3cyqf4,157
5
+ fastapi_auth_kit/core/security.py,sha256=Qs6FHWKPhCSvKQZZeRNNLH7d32g0TAWMtN8FG6jwfEE,817
6
+ fastapi_auth_kit/core/setup.py,sha256=j7cSbyoIzsSu8nWKbMsbdxo9pQBst72rAMv78M3YI1o,451
7
+ fastapi_auth_kit/db/base.py,sha256=jjaKf8nclEwdX8IV7J_jKnB1MgqVSl9yK-KDjKFwOV4,279
8
+ fastapi_auth_kit/db/factory.py,sha256=DomyoHGZiasank4yRR1aRDsGmD2dFLL5vX4fE_qJIDU,295
9
+ fastapi_auth_kit/db/mongo_repo.py,sha256=jKmTXl0yVgLCxCoCqp6c1TxO0bt-KufPG810SrHMrHQ,911
10
+ fastapi_auth_kit/db/sqlalchemy_repo.py,sha256=hb2XQKY3smA5RVjNFBZMszotGqcsO57p325_xT6CmM8,2068
11
+ fastapi_auth_kit/dependencies/auth.py,sha256=4g9zFcSzJ43MHtYRUpC9h-IXsfrNVV6ILx6ECJuh80I,458
12
+ fastapi_auth_kit/schemas/auth.py,sha256=f4Hn3wlYuU1Q7tJozh5CbNy8KMvZol4Uno0b9HE_j0w,1340
13
+ fastapi_auth_kit/services/auth_service.py,sha256=NhkKHUfIJDGc3QYdSrosHnwEdTZXmp2jdO93B8s6QMY,3177
14
+ fastapi_async_auth_kit-0.1.0.dist-info/METADATA,sha256=jqxGzQcr7AFc5yxRYg2wM4OyA6zZ8SmPK5OXr123wY4,661
15
+ fastapi_async_auth_kit-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
+ fastapi_async_auth_kit-0.1.0.dist-info/top_level.txt,sha256=LCeW47pupGZVWyFMSeK48VYfdXS-5RF_FeNNX8-tN0A,17
17
+ fastapi_async_auth_kit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ fastapi_auth_kit
@@ -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"}