Bot commit: Добавлен файл auth.py
This commit is contained in:
parent
f00d974af6
commit
faa06adac5
320
auth.py
Normal file
320
auth.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user