From e5c4d3c94923a4507f7f28fcd5a7210770858e8a Mon Sep 17 00:00:00 2001 From: root Date: Fri, 3 Jul 2026 08:26:48 +0000 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=85=D0=BE=D0=B4=20=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20VK=20ID=20(OAuth=202.1=20+=20PKCE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /api/auth/vk/{start,callback}: PKCE (code_verifier в cookie), обмен на id.vk.com/oauth2/auth + user_info, аккаунт vk: + cookie-сессия - Кнопка VK в меню входа, i18n Co-Authored-By: Claude Opus 4.8 --- app/main.py | 79 +++++++++++++++++++++++++++++++++++++++++++ app/static/index.html | 11 ++++++ 2 files changed, 90 insertions(+) diff --git a/app/main.py b/app/main.py index 0a22658..61714b7 100644 --- a/app/main.py +++ b/app/main.py @@ -62,6 +62,10 @@ YANDEX_CLIENT_SECRET = os.getenv("YANDEX_CLIENT_SECRET", "") YANDEX_REDIRECT = f"{os.getenv('SITE_BASE_URL', 'https://pdf2scan.online')}/api/auth/yandex/callback" SESSION_COOKIE = "p2s_session" SESSION_DAYS = 30 +# ── OAuth: VK ID (OAuth 2.1 / PKCE) ─────────────────────────────────────────── +VK_CLIENT_ID = os.getenv("VK_CLIENT_ID", "") +VK_CLIENT_SECRET = os.getenv("VK_CLIENT_SECRET", "") +VK_REDIRECT = f"{os.getenv('SITE_BASE_URL', 'https://pdf2scan.online')}/api/auth/vk/callback" # Чанковая загрузка (туннель схлопывает крупные передачи — грузим кусками ≤6 МБ) UPLOAD_DIR = f"{BOT_TEMP}uploads/" # временные .part-файлы @@ -1902,6 +1906,81 @@ async def auth_logout(request: Request): return resp +# ── OAuth: VK ID (OAuth 2.1 + PKCE) ─────────────────────────────────────────── +def _pkce_pair(): + verifier = secrets.token_urlsafe(48) # 43–128 символов + challenge = _b64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode() + return verifier, challenge + + +def _vk_token_exchange(code: str, verifier: str, device_id: str) -> dict: + data = _urlparse.urlencode({ + "grant_type": "authorization_code", "code": code, "code_verifier": verifier, + "client_id": VK_CLIENT_ID, "device_id": device_id, "redirect_uri": VK_REDIRECT, + }).encode() + req = _urlreq.Request("https://id.vk.com/oauth2/auth", data=data, method="POST") + req.add_header("Content-Type", "application/x-www-form-urlencoded") + with _urlreq.urlopen(req, timeout=20) as resp: + return _pjson.loads(resp.read().decode()) + + +def _vk_userinfo(access_token: str) -> dict: + data = _urlparse.urlencode({"access_token": access_token, "client_id": VK_CLIENT_ID}).encode() + req = _urlreq.Request("https://id.vk.com/oauth2/user_info", data=data, method="POST") + req.add_header("Content-Type", "application/x-www-form-urlencoded") + with _urlreq.urlopen(req, timeout=20) as resp: + return _pjson.loads(resp.read().decode()) + + +@app.get("/api/auth/vk/start") +async def vk_start(): + if not VK_CLIENT_ID: + raise HTTPException(status_code=503, detail="Вход через VK не настроен.") + state = secrets.token_hex(16) + verifier, challenge = _pkce_pair() + url = ("https://id.vk.com/authorize?response_type=code" + f"&client_id={VK_CLIENT_ID}&redirect_uri={_urlparse.quote(VK_REDIRECT, safe='')}" + f"&state={state}&code_challenge={challenge}&code_challenge_method=S256&scope=email") + resp = RedirectResponse(url) + resp.set_cookie("p2s_vk_state", state, max_age=600, httponly=True, secure=True, samesite="lax", path="/") + resp.set_cookie("p2s_vk_verifier", verifier, max_age=600, httponly=True, secure=True, samesite="lax", path="/") + return resp + + +@app.get("/api/auth/vk/callback") +async def vk_callback(request: Request, code: str = "", state: str = "", device_id: str = ""): + if not code: + return RedirectResponse(f"{SITE_BASE_URL}/?login=error") + if state and request.cookies.get("p2s_vk_state") != state: + return RedirectResponse(f"{SITE_BASE_URL}/?login=error") + verifier = request.cookies.get("p2s_vk_verifier") or "" + try: + tok = await asyncio.to_thread(_vk_token_exchange, code, verifier, device_id) + access = tok.get("access_token") + vk_uid = str(tok.get("user_id") or "") + email = tok.get("email") or "" + info = await asyncio.to_thread(_vk_userinfo, access) if access else {} + except Exception as e: + print(f"vk oauth error: {e}") + return RedirectResponse(f"{SITE_BASE_URL}/?login=error") + user = (info.get("user") or {}) if isinstance(info, dict) else {} + if not vk_uid: + vk_uid = str(user.get("user_id") or "") + if not vk_uid: + return RedirectResponse(f"{SITE_BASE_URL}/?login=error") + email = email or user.get("email") or "" + name = ((user.get("first_name") or "") + " " + (user.get("last_name") or "")).strip() or "VK" + account_id = f"vk:{vk_uid}" + _db_ensure_account(account_id) + token = _db_create_session(account_id, "vk", name, email) + resp = RedirectResponse(f"{SITE_BASE_URL}/?login=vk") + resp.set_cookie(SESSION_COOKIE, token, max_age=SESSION_DAYS * 86400, + httponly=True, secure=True, samesite="lax", path="/") + resp.delete_cookie("p2s_vk_state", path="/") + resp.delete_cookie("p2s_vk_verifier", path="/") + return resp + + _API_DOCS_HTML = """ diff --git a/app/static/index.html b/app/static/index.html index c082678..ce75f48 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -861,6 +861,10 @@ Я Через Яндекс +