From 76edfe757792757842fce99d1d4afc4da5ad6811 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Fri, 8 May 2026 13:44:12 -0400 Subject: [PATCH] feat: add Gitea OAuth2 redirect and callback routes --- hbd/server/http.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/hbd/server/http.py b/hbd/server/http.py index 717a07d..2867c4b 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -16,6 +16,7 @@ from . import data from . import notify as notify_mod from . import settings as settings_mod from . import users as users_mod +from . import oauth as oauth_mod from . import ws as ws_mod logger = logging.getLogger(__name__) @@ -897,6 +898,47 @@ async def start( ) return web.Response(text=body, content_type="text/html") + async def oauth_gitea_redirect(request): + """GET /login/oauth/gitea — kick off the Gitea OAuth2 flow.""" + if not oauth_mod.is_enabled(config): + return web.Response(status=404, text="OAuth not configured") + state = oauth_mod.make_state() + redirect_uri = f"{request.url.origin()}/login/oauth/gitea/callback" + raise web.HTTPFound(oauth_mod.authorization_url(config, state, redirect_uri)) + + async def oauth_gitea_callback(request): + """GET /login/oauth/gitea/callback — handle Gitea's redirect back.""" + if not oauth_mod.is_enabled(config): + return web.Response(status=404, text="OAuth not configured") + code = request.rel_url.query.get("code", "") + state = request.rel_url.query.get("state", "") + if not code or not state: + return web.Response(status=400, text="Missing code or state") + if not oauth_mod.validate_state(state): + raise web.HTTPFound("/login?error=1") + redirect_uri = f"{request.url.origin()}/login/oauth/gitea/callback" + try: + token = await oauth_mod.exchange_code(config, code, redirect_uri) + profile = await oauth_mod.fetch_user(config, token) + except oauth_mod.OAuthError as exc: + logger.warning("OAuth error: %s", exc) + raise web.HTTPFound("/login?error=1") + user = users_mod.provision_oauth_user( + profile["login"], + profile["full_name"], + profile["avatar_url"], + ) + session_token = users_mod.create_session(user.username) + resp = web.HTTPFound("/") + resp.set_cookie( + SESSION_COOKIE, + session_token, + max_age=users_mod.SESSION_TTL, + httponly=True, + samesite="Lax", + ) + raise resp + app = web.Application() app.add_routes( [ @@ -908,6 +950,8 @@ async def start( web.get("/logout", web_logout), web.post("/api/0/auth/login", api_login), web.post("/api/0/auth/logout", api_logout), + web.get("/login/oauth/gitea", oauth_gitea_redirect), + web.get("/login/oauth/gitea/callback", oauth_gitea_callback), # Users web.get("/api/0/users", api_users), web.get("/api/0/users/me", api_user_self),