OAuth real browser login: GET callback, userinfo, API key issuance

Add GET /oauth/{provider}/callback (code+state from query, client_id
and redirect_uri recovered from the state record) alongside the
existing POST callback. Callback now fetches real userinfo
(GitHub api.github.com/user, Google openidconnect userinfo) and
issues a usable local API key on success: new users get the
create_user key, existing users rotate (old key dead, audit
oauth_login + api_key_rotated). Key plaintext appears only once
on the success page/response; failures are 400 HTML/JSON without
secrets, and a missing client_secret fails before any network call.
State remains one-time, 10 minutes, provider/redirect_uri matched.
Tests: 10 new OAuth real-login tests pass; host 27, cloud 249
(4 skipped are the pre-existing real-Postgres tests).
Real end-to-end still needs a registered OAuth App and a public
HTTPS callback entry (server still binds localhost only).
This commit is contained in:
host committed 2026-10-26 03:35:40 +08:00
1 parent fbb00b8b6f
commit 0ed301b010
5 files changed
+637 -69

No files matched your search

+20
View File
@@ -84,6 +84,26 @@ class AuthStore:
self.conn.commit()
return user_id, api_key_plain
def rotate_api_key(self, user_id):
"""轮换用户的 API key,返回新明文 key(只返回这一次)。
旧 key 的哈希即被替换,轮换后旧 key 立即失效。
用户不存在时抛 ValueError(调用方不得用它凭空建用户)。
"""
row = self.conn.execute(
"SELECT id FROM users WHERE id = ?", (user_id,)
).fetchone()
if row is None:
raise ValueError("unknown user: %r" % (user_id,))
api_key_plain = secrets.token_urlsafe(32)
api_key_hash = hashlib.sha256(api_key_plain.encode()).hexdigest()
self.conn.execute(
"UPDATE users SET api_key_hash = ? WHERE id = ?",
(api_key_hash, user_id),
)
self.conn.commit()
return api_key_plain
def authenticate(self, api_key):
"""用 API key 认证,成功返回 user_id,失败返回 None。"""
if not api_key: