"""GitHub OAuth 授权码模式端点(SPA 友好的 JSON 流程)。 流程:前端调 authorize 拿 authorize_url → 浏览器跳转 GitHub → GitHub 回跳前端页(带 code+state)→ 前端调 callback 换平台 token。 """ import logging import secrets from urllib.parse import urlencode from chongming_worker import rpc_method from ..models import GithubAuthorizeGetResponse, GithubCallbackPostRequest, LoginPostResponse from ..providers import github as github_provider from .. import store logger = logging.getLogger("plugins.auth_oauth.github") _STATE_TTL = 300 # state 有效期(秒),一次性消费防 CSRF @rpc_method( subject="auth_oauth.github_authorize", method="GET", summary="GitHub 授权登录:生成授权跳转 URL(state 存缓存防 CSRF)", ) async def github_authorize() -> GithubAuthorizeGetResponse: from ..global_var import cache, plugin_config gh = plugin_config.get("github") or {} client_id = gh.get("client_id", "") if not client_id or client_id.startswith("your-"): return GithubAuthorizeGetResponse( result="fail", authorize_url="", state="GitHub OAuth 未配置(config/workers/auth_oauth/config.toml 的 [workers.config.github])", ) state = secrets.token_urlsafe(24) cache.set(f"auth_oauth:state:{state}", "1", expire=_STATE_TTL) params = urlencode({ "client_id": client_id, "redirect_uri": gh.get("redirect_uri", ""), "scope": gh.get("scope", "read:user user:email"), "state": state, }) return GithubAuthorizeGetResponse( result="success", authorize_url=f"https://github.com/login/oauth/authorize?{params}", state=state, ) @rpc_method( subject="auth_oauth.github_callback", method="POST", summary="GitHub 授权回调:校验 state、换取用户信息,find-or-create 后签发平台 token", ) async def github_callback(req: GithubCallbackPostRequest) -> LoginPostResponse: from ..global_var import cache, db, plugin_config if db is None or cache is None: return store.fail_response("插件未初始化") # state 一次性消费(防 CSRF / 重放) state_key = f"auth_oauth:state:{req.state}" if not cache.get(state_key): return store.fail_response("state 无效或已过期,请重新发起授权") cache.delete(state_key) gh = plugin_config.get("github") or {} try: access_token = await github_provider.exchange_code( gh.get("client_id", ""), gh.get("client_secret", ""), req.code, gh.get("redirect_uri", ""), ) profile = await github_provider.fetch_user(access_token) except RuntimeError as e: logger.warning("GitHub 回调失败: %s", e) return store.fail_response(str(e)) user, message = await store.find_or_create_user( db, "github", profile["uid"], profile ) if user is None: return store.fail_response(message) return store.issue_login_response(user)