CoolFace
Apppublic

Rhinox13/chatapi

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
auth.py162 linesDownload Raw Back to core
1from __future__ import annotations
2
3import base64
4import binascii
5import hashlib
6import hmac
7import struct
8import time
9from functools import wraps
10from typing import Any, Callable
11
12from flask import jsonify, request, session
13
14
15class AuthContext:
16    def __init__(self, store: Any | None = None, user_store: Any | None = None):
17        self.store = store
18        self.user_store = user_store
19
20    def current_user(self) -> dict[str, str] | None:
21        user_id = str(session.get("user_id", "") or "").strip()
22        username = str(session.get("username", "") or "").strip()
23        role = str(session.get("role", "") or "").strip()
24        if not user_id or not username:
25            return None
26        if self.user_store is None:
27            return {"id": user_id, "username": username, "role": role}
28
29        db_user = self.user_store.get_user(user_id)
30        if db_user is None:
31            session.clear()
32            return None
33
34        if username != db_user.username or role != db_user.role:
35            session["username"] = db_user.username
36            session["role"] = db_user.role
37
38        return {"id": db_user.id, "username": db_user.username, "role": db_user.role}
39
40    def request_api_key(self) -> str:
41        return str(
42            request.headers.get("Authorization", "").removeprefix("Bearer ")
43            or request.headers.get("X-API-Key", "")
44            or ""
45        ).strip()
46
47    def resolve_owner_from_api_key(self) -> str | None:
48        api_key = self.request_api_key()
49        if not api_key or self.user_store is None:
50            return None
51        return self.user_store.resolve_api_key_owner(api_key)
52
53    def request_api_key_name(self) -> str:
54        api_key = self.request_api_key()
55        if not api_key or self.user_store is None:
56            return ""
57        return self.user_store.resolve_api_key_name(api_key) or ""
58
59    def is_request_authorized_by_api_key(self) -> bool:
60        return self.resolve_owner_from_api_key() is not None
61
62    def owner_id(self) -> str:
63        user = self.current_user()
64        if user:
65            return user["id"]
66        owner = self.resolve_owner_from_api_key()
67        if owner:
68            return owner
69        return "anonymous"
70
71    def is_admin(self) -> bool:
72        user = self.current_user()
73        return user is not None and user.get("role") == "admin"
74
75    def require_auth(self, view: Callable[..., Any]):
76        @wraps(view)
77        def wrapped(*args, **kwargs):
78            if self.current_user() is None and not self.is_request_authorized_by_api_key():
79                return jsonify({"error": "unauthorized"}), 401
80            return view(*args, **kwargs)
81        return wrapped
82
83    def require_session_auth(self, view: Callable[..., Any]):
84        @wraps(view)
85        def wrapped(*args, **kwargs):
86            if self.current_user() is None:
87                return jsonify({"error": "unauthorized"}), 401
88            return view(*args, **kwargs)
89
90        return wrapped
91
92    def require_admin(self, view: Callable[..., Any]):
93        @wraps(view)
94        def wrapped(*args, **kwargs):
95            if not self.is_admin():
96                return jsonify({"error": "forbidden"}), 403
97            return view(*args, **kwargs)
98        return wrapped
99
100    def request_headers_snapshot(self) -> dict[str, str]:
101        return {
102            "user_agent": str(request.headers.get("User-Agent", "")).strip(),
103            "content_type": str(request.headers.get("Content-Type", "")).strip(),
104            "origin": str(request.headers.get("Origin", "")).strip(),
105            "referer": str(request.headers.get("Referer", "")).strip(),
106        }
107
108
109def _normalize_totp_secret(secret: str) -> bytes:
110    normalized = "".join(secret.split()).upper()
111    if not normalized:
112        return b""
113    padding = "=" * ((8 - len(normalized) % 8) % 8)
114    try:
115        return base64.b32decode(f"{normalized}{padding}", casefold=True)
116    except (binascii.Error, ValueError):
117        return secret.encode("utf-8")
118
119
120def _totp_code(secret: bytes, counter: int, digits: int = 6) -> str:
121    digest = hmac.new(secret, struct.pack(">Q", counter), hashlib.sha1).digest()
122    offset = digest[-1] & 0x0F
123    binary = struct.unpack(">I", digest[offset : offset + 4])[0] & 0x7FFFFFFF
124    return str(binary % (10**digits)).zfill(digits)
125
126
127def generate_totp_secret() -> str:
128    raw = base64.b32encode(hashlib.sha256(time.time_ns().to_bytes(16, "big")).digest()[:20]).decode()
129    return raw.rstrip("=")
130
131
132def build_totp_uri(secret: str, username: str, issuer: str = "ChatAPI") -> str:
133    import urllib.parse
134    label = urllib.parse.quote(f"{issuer}:{username}")
135    issuer_encoded = urllib.parse.quote(issuer)
136    return f"otpauth://totp/{label}?secret={secret}&issuer={issuer_encoded}&algorithm=SHA1&digits=6&period=30"
137
138
139def verify_totp_code(
140    secret: str,
141    code: str,
142    *,
143    for_time: int | None = None,
144    step: int = 30,
145    window: int = 1,
146    digits: int = 6,
147) -> bool:
148    secret_bytes = _normalize_totp_secret(secret)
149    if not secret_bytes:
150        return False
151
152    candidate = "".join(str(code).split())
153    if not candidate.isdigit() or len(candidate) != digits:
154        return False
155
156    now = int(time.time() if for_time is None else for_time)
157    counter = now // step
158    for drift in range(-window, window + 1):
159        if hmac.compare_digest(_totp_code(secret_bytes, counter + drift, digits=digits), candidate):
160            return True
161    return False
162