diff --git a/env-gateway-int-forgejo.env b/env-gateway-int-forgejo.env index 9f2f2f37..49eb8675 100644 --- a/env-gateway-int-forgejo.env +++ b/env-gateway-int-forgejo.env @@ -48,10 +48,10 @@ Service_GOOGLE_DATA_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.ap Service_GOOGLE_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnFBa1kyUTUwNXNGaHRNaGxxbF9sdWJ3Q0xLYU5yOHB4Yk8zMDZvQ29yaEhWOE5JMENXRk5jb2ZBdzRKQ2ZTTld6ZlIxemhOYzN1VE10TjBDRWZEMXlLVWRNYjZ0VG5RZ3I3NWt0SEJzMzdsUmRzcVNmbktRNHZqTUF6a2EyUkVUSFJnZFE= Service_GOOGLE_DATA_REDIRECT_URI = https://api-int.poweron.swiss/api/google/auth/connect/callback -# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly. +# ClickUp OAuth — same app as gateway-int; add https://api-int.poweron.swiss as second redirect in ClickUp (root URL, no path). Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4 Service_CLICKUP_CLIENT_SECRET = CZECD706WLSX6UV13YI4ACNW50ADZHHXDAJALHE0YE030QFSI6Y9HP4Y61JT7CF0 -Service_CLICKUP_OAUTH_REDIRECT_URI = https://api-int.poweron.swiss/api/clickup/auth/connect/callback +Service_CLICKUP_OAUTH_REDIRECT_URI = https://api-int.poweron.swiss # Infomaniak: no OAuth client. Users paste a Personal Access Token (kdrive + mail) per UI. diff --git a/env-gateway-int.env b/env-gateway-int.env index 7301ecc9..16619558 100644 --- a/env-gateway-int.env +++ b/env-gateway-int.env @@ -48,10 +48,11 @@ Service_GOOGLE_DATA_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.ap Service_GOOGLE_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnFBa1kyV1FRVjF0c0d3d0dyWU1TdW9HdXVkdHdsVWZKYTJjbGZPRDhMRjA2M0FkaUZIVmhIUmFKNjg2ekFodHd6NG80VTI3TC1icW1LZ01jWVZuQ1pKRm5nMW5UREJEaGp2Wl9oRDRCSmZVT0JpTnkwXzgwY0pkV29yczQ5akF2d1ZGcVY= Service_GOOGLE_DATA_REDIRECT_URI = https://gateway-int.poweron.swiss/api/google/auth/connect/callback -# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly. +# ClickUp OAuth — redirect URL must match ClickUp app exactly (often API root only). +# OAuth lands on /?code=&state=; gateway forwards to /api/clickup/auth/connect/callback (routeAdmin root). Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4 Service_CLICKUP_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnB5dkd5SE1uVURMNVE3NkM4cHBKa2R2TjBnLWdpSXI5dHpKWGExZVFiUF95TFNnZ1NwLWFLdmh6eWFZTHVHYTBzU2FGRUpLYkVyM1NvZjZkWDZHN21qUER5ZVNOaGpCc3NrUGd3VnFTclF3OW1nUlVuWXQ1UVhDLVpyb1BwRExOeFpDeVhtbEhDVnd4TVdpbzNBNk5QQWFPdjdza0xBWGxFY1E3WFpCSUlNa1l4RDlBPQ== -Service_CLICKUP_OAUTH_REDIRECT_URI = https://gateway-int.poweron.swiss/api/clickup/auth/connect/callback +Service_CLICKUP_OAUTH_REDIRECT_URI = https://gateway-int.poweron.swiss # Infomaniak: no OAuth client. Users paste a Personal Access Token (kdrive + mail) per UI. diff --git a/modules/auth/oauthConnectTicket.py b/modules/auth/oauthConnectTicket.py index f54187cb..82547cf1 100644 --- a/modules/auth/oauthConnectTicket.py +++ b/modules/auth/oauthConnectTicket.py @@ -29,6 +29,25 @@ _msg = apiRouteContext("oauthConnectTicket") _CONNECT_TICKET_TTL_SEC = 600 +# OAuth providers sometimes redirect to the API root if the app redirect URL omits the path. +OAUTH_FLOW_CALLBACK_PATHS: Dict[str, str] = { + "clickup_connect": "/api/clickup/auth/connect/callback", + "msft_connect": "/api/msft/auth/connect/callback", + "google_connect": "/api/google/auth/connect/callback", +} + + +def oauth_callback_redirect_path(state: str) -> str | None: + """Map connect-ticket JWT (ClickUp ``state`` param) to the correct callback route.""" + try: + data = jose_jwt.decode(state, SECRET_KEY, algorithms=[ALGORITHM]) + except JWTError: + return None + flow = data.get("flow") + if not isinstance(flow, str): + return None + return OAUTH_FLOW_CALLBACK_PATHS.get(flow) + def issue_connect_ticket(flow: str, connection_id: str, user_id: str) -> str: """Issue a short-lived JWT for starting a data-connection OAuth popup.""" diff --git a/modules/routes/routeAdmin.py b/modules/routes/routeAdmin.py index 0f671f0a..3add5663 100644 --- a/modules/routes/routeAdmin.py +++ b/modules/routes/routeAdmin.py @@ -1,7 +1,7 @@ # Copyright (c) 2025 Patrick Motsch # All rights reserved. from fastapi import APIRouter, Response, Depends, Request, Body -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, RedirectResponse from fastapi.staticfiles import StaticFiles import os import logging @@ -11,6 +11,7 @@ from fastapi import HTTPException, status from modules.shared.configuration import APP_CONFIG from modules.auth import limiter, getCurrentUser +from modules.auth.oauthConnectTicket import oauth_callback_redirect_path from modules.datamodels.datamodelUam import User from modules.interfaces.interfaceDbApp import getRootInterface from modules.shared.i18nRegistry import apiRouteContext @@ -35,8 +36,15 @@ router.mount( @router.get("/") @limiter.limit("30/minute") -def root(request: Request) -> Dict[str, str]: - """API status endpoint""" +def root(request: Request): + """API status endpoint; forwards OAuth callbacks that land on ``/`` by mistake.""" + code = request.query_params.get("code") + state = request.query_params.get("state") + if code and state: + callback_path = oauth_callback_redirect_path(state) + if callback_path: + return RedirectResponse(url=f"{callback_path}?{request.url.query}", status_code=302) + # Validate required configuration values allowedOrigins = APP_CONFIG.get("APP_ALLOWED_ORIGINS") if not allowedOrigins: diff --git a/modules/routes/routeSecurityClickup.py b/modules/routes/routeSecurityClickup.py index 935509bc..e7f7a442 100644 --- a/modules/routes/routeSecurityClickup.py +++ b/modules/routes/routeSecurityClickup.py @@ -257,6 +257,8 @@ async def auth_connect_callback( except Exception as _cbErr: logger.warning("connection.established callback failed for %s: %s", connection.id, _cbErr) + allowed = (APP_CONFIG.get("APP_ALLOWED_ORIGINS") or "").split(",")[0].strip() + post_target = allowed if allowed else "*" return HTMLResponse( content=f""" @@ -273,7 +275,7 @@ async def auth_connect_callback( lastChecked: {getUtcTimestamp()}, expiresAt: {expires_at} }} - }}, '*'); + }}, {json.dumps(post_target)}); setTimeout(() => window.close(), 1000); }} else {{ window.close();