Bot commit: Добавлен файл auth.py

This commit is contained in:
Vladislav 2026-06-30 19:13:50 -04:00
parent f00d974af6
commit faa06adac5

320
auth.py Normal file
View File

@ -0,0 +1,320 @@
from fastapi import HTTPException, Request, Response, Security
from fastapi.security import SecurityScopes
import jwt
from typing import List, Any, Dict, Optional
from datetime import timedelta
from urllib.parse import urlencode
from bson import ObjectId
import httpx
from db import *
from utils import *
from config import *
import logging
import secrets
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
bearer_scheme = HTTPBearer(auto_error=False)
def get_token(
creds: HTTPAuthorizationCredentials = Security(bearer_scheme),
):
if not creds or creds.scheme.lower() != "bearer":
raise HTTPException(status_code=401, detail="Unauthorized")
if creds.credentials != ATOKEN:
raise HTTPException(status_code=401, detail="Unauthorized")
return creds.credentials
def is_browser_request(request: Request) -> bool:
accept = request.headers.get("accept", "")
return "text/html" in accept
async def token_create(user_id: str, url: str = '/profile'):
raw_token = secrets.token_urlsafe(48)
token_hash = hash_token(raw_token)
created_at = datetime.now(timezone.utc)
await users_token_collection.insert_one({
'token_hash': token_hash,
'url': url,
'user_id': user_id,
'created_at': created_at
})
return raw_token
def get_bearer_token(request: Request) -> Optional[str]:
auth = request.headers.get("Authorization")
if auth and auth.lower().startswith("bearer "):
parts = auth.split(" ", 1)
if len(parts) == 2:
return parts[1].strip()
return None
async def get_access_token(request: Request, response: Response) -> Optional[str]:
token = request.cookies.get(ACCESS_COOKIE)
if token:
t = token.strip()
if t:
return t
bearer = get_bearer_token(request)
if bearer:
return bearer
return None
def get_refresh_token(request: Request) -> Optional[str]:
token = request.cookies.get(REFRESH_COOKIE)
if token:
t = token.strip()
if t:
return t
return None
async def validate_refresh_and_rotate(refresh_token: str, response: Response, request: Request) -> Optional[Dict[str, str]]:
try:
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
except jwt.InvalidTokenError:
return None
if payload.get("type") != "refresh":
return None
user_id_str = payload.get("sub")
if not user_id_str:
return None
refresh_hash = hash_token(refresh_token)
stored = await redis_client.get(f"refresh:{refresh_hash}")
if not stored or stored != user_id_str:
return None
await redis_client.delete(f"refresh:{refresh_hash}")
user = await users_collection.find_one({"_id": ObjectId(user_id_str)},
{"hashed_password": 0})
if not user:
return None
scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", []))
client_id = payload.get("client_id")
client_ip = payload.get("ip")
access_exp = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES)
refresh_exp = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
new_access = create_jwt_token(
{"sub": user_id_str, "scopes": scopes, "client_id": client_id, "ip": client_ip},
access_exp
)
new_refresh = create_jwt_token(
{"sub": user_id_str, "type": "refresh", "client_id": client_id, "ip": client_ip},
refresh_exp
)
new_refresh_hash = hash_token(new_refresh)
await redis_client.setex(f"access:{new_access}", int(access_exp.total_seconds()), user_id_str)
await redis_client.setex(f"refresh:{new_refresh_hash}", int(refresh_exp.total_seconds()), user_id_str)
session_key = f"user_sessions:{user_id_str}"
await redis_client.sadd(session_key, f"access:{new_access}")
await redis_client.sadd(session_key, f"refresh:{new_refresh_hash}")
response.set_cookie(
key="access_token",
value=new_access,
httponly=True,
secure=PROD,
samesite="Strict",
max_age=int(access_exp.total_seconds())
)
response.set_cookie(
key="refresh_token",
value=new_refresh_hash,
httponly=True,
secure=PROD,
samesite="Strict",
max_age=int(refresh_exp.total_seconds())
)
user['scopes'] = scopes
user['id'] = user_id_str
user = bson_clear(user)
return await _build_user(user, request)
async def get_current_user(
request: Request,
response: Response,
security_scopes: SecurityScopes
) -> Dict[str, Any]:
token_old = request.cookies.get('token') or request.cookies.get('v2token')
access = await get_access_token(request, response)
if token_old and not access:
try:
headers = {'Authorization': f"Bearer {token_old}"}
async with httpx.AsyncClient(http2=True, verify=ctx) as client:
res = await client.get(f'{FULL_PATH_DOMAIN}/api/v1/auth/me', headers=headers)
email = res.json()['data']['email']
user = await users_collection.find_one({
"$or": [
{"email": email},
{"email": email.lower()}
]
})
except Exception as e:
logging.error(e)
raise HTTPException(status_code=307, headers={"Location": '/login'}, detail="User not found")
user_id_str = str(user["_id"])
scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", []))
for scope in security_scopes.scopes:
if scope not in scopes:
raise HTTPException(status_code=403, detail="Not enough permissions")
client_ip = request.client.host
access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES)
refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
access_token = create_jwt_token(
{"sub": user_id_str, "scopes": scopes, "client_id": 'old_token', "ip": client_ip},
access_exp_delta
)
refresh_token = create_jwt_token(
{"sub": user_id_str, "type": "refresh", "client_id": 'old_token', "ip": client_ip},
refresh_exp_delta
)
await redis_client.setex(f"access:{access_token}", int(access_exp_delta.total_seconds()), user_id_str)
await redis_client.setex(f"refresh:{hash_token(refresh_token)}", int(refresh_exp_delta.total_seconds()), user_id_str)
session_key = f"user_sessions:{user_id_str}"
await redis_client.sadd(session_key, f"access:{access_token}")
await redis_client.sadd(session_key, f"refresh:{hash_token(refresh_token)}")
response.set_cookie(
key="access_token",
value=access_token,
httponly=True,
secure=PROD,
samesite="Strict",
max_age=int(access_exp_delta.total_seconds())
)
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=PROD,
samesite="Strict",
max_age=int(refresh_exp_delta.total_seconds())
)
if user:
user['scopes'] = scopes
user['id'] = user_id_str
user = bson_clear(user)
return await _build_user(user, request)
check, scopes = await validate_access_token(access, request, security_scopes)
if access and check:
return await get_user_details(access, scopes, response, request)
refresh = get_refresh_token(request)
if refresh:
rotated = await validate_refresh_and_rotate(refresh, response, request)
if rotated:
return rotated
if is_browser_request(request):
next_url = str(request.url)
login_url = "/login?" + urlencode({"next": next_url})
raise HTTPException(status_code=307, headers={"Location": login_url}, detail="Redirecting to login")
raise HTTPException(status_code=401, detail="Not authenticated")
async def validate_access_token(access: str, request: Request, security_scopes: SecurityScopes) -> bool:
key = f"access:{access}"
user_id_str = await redis_client.get(key)
if user_id_str:
try:
payload = jwt.decode(access, SECRET_KEY, algorithms=[ALGORITHM])
if payload.get("sub") != user_id_str:
raise HTTPException(status_code=401, detail="Token mismatch")
# Validate scopes
token_scopes: List[str] = payload.get("scopes", [])
if request.cookies.get("admin_access_token"):
token_scopes.append('back')
for scope in security_scopes.scopes:
if scope not in token_scopes:
raise HTTPException(status_code=403, detail="Not enough permissions")
return True, token_scopes
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return False, []
return False, []
async def get_user_details(access: str, scopes, response: Response, request: Request,) -> Dict[str, Any]:
key = f"access:{access}"
user_id_str = await redis_client.get(key)
if user_id_str:
user = await users_collection.find_one(
{"_id": ObjectId(user_id_str)},
{"hashed_password": 0}
)
if user:
user['scopes'] = scopes
user['id'] = user_id_str
user = bson_clear(user)
return await _build_user(user, request)
raise HTTPException(status_code=307, headers={"Location": '/login'}, detail="User not found")
async def _build_user(user, request: Request):
await notify_collection.update_one({'link':str(request.url).replace(str(request.base_url),'/')},{'$set':{'read':False}})
notify = []
if (read_id:=request.cookies.get('read')):
print(read_id)
async for doc in notify_collection.find({'user_id':user['id']}).limit(100):
notify.append(doc)
user['notify'] = notify
return user
async def get_user(
request: Request,
response: Response,
) -> Optional[Dict[str, Any]]:
token = await get_access_token(request, response)
if not token:
return None
key = f"access:{token}"
user_id_str = await redis_client.get(key)
if not user_id_str:
return None
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
if payload.get("sub") != user_id_str:
return None
user = await users_collection.find_one({"_id": ObjectId(user_id_str)}, {"hashed_password": 0})
if not user:
return None
scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", []))
if request.cookies.get("admin_access_token"):
scopes.append('back')
user['scopes'] = scopes
user['id'] = user_id_str
user = bson_clear(user)
return await _build_user(user, request)
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None