From faa06adac5b0daccffebaca19b49c37db94b9e87d2ce846e11f5e106223b9217 Mon Sep 17 00:00:00 2001 From: Vladislav Date: Tue, 30 Jun 2026 19:13:50 -0400 Subject: [PATCH] =?UTF-8?q?Bot=20commit:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=84=D0=B0=D0=B9=D0=BB=20auth.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth.py | 320 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 auth.py diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..a1efc47 --- /dev/null +++ b/auth.py @@ -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