Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

Commit 7a01bbb

Browse files
committed
Handle connector OAuth denial callbacks
1 parent e95d4e5 commit 7a01bbb

2 files changed

Lines changed: 26 additions & 1 deletion

File tree

src/api/routes/connectors.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,9 @@ async def start_connector_oauth(
217217
@router.get("/{connector_id}/oauth/callback")
218218
async def connector_oauth_callback(
219219
connector_id: str,
220-
code: str = Query(..., min_length=1),
221220
state: str = Query(..., min_length=1),
221+
code: Optional[str] = Query(None, min_length=1),
222+
error: Optional[str] = Query(None, min_length=1),
222223
) -> dict:
223224
connector = _get_connector(connector_id)
224225
now = _now()
@@ -229,6 +230,11 @@ async def connector_oauth_callback(
229230
status_code=status.HTTP_400_BAD_REQUEST,
230231
detail="Invalid or expired connector authorization state",
231232
)
233+
if error or not code:
234+
raise HTTPException(
235+
status_code=status.HTTP_400_BAD_REQUEST,
236+
detail=f"Authorization denied: {error or 'no authorization code received'}",
237+
)
232238

233239
# Token exchange, encrypted credential storage, and source ingestion are intentionally
234240
# separate follow-up steps. Do not mark the connector as connected until those exist.

tests/api/test_connectors.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
from src.api.routes import connectors
88

99

10+
def setup_function() -> None:
11+
connectors._pending_states.clear()
12+
13+
1014
def _user() -> dict:
1115
return {"id": "user-1", "email": "user@example.com", "username": "user"}
1216

@@ -74,3 +78,18 @@ def test_callback_validates_state_without_marking_connected(monkeypatch) -> None
7478
disconnected = client.post("/api/connectors/notion/disconnect")
7579
assert disconnected.status_code == 200
7680
assert disconnected.json() == {"connector_id": "notion", "disconnected": False}
81+
82+
83+
def test_callback_handles_provider_denial_and_consumes_state(monkeypatch) -> None:
84+
monkeypatch.setenv("NOTION_CLIENT_ID", "notion-client")
85+
client = _client()
86+
87+
started = client.post("/api/connectors/notion/oauth/start")
88+
state = started.json()["state"]
89+
90+
callback = client.get(f"/api/connectors/notion/oauth/callback?error=access_denied&state={state}")
91+
92+
assert callback.status_code == 400
93+
assert "access_denied" in callback.json()["detail"]
94+
retry = client.get(f"/api/connectors/notion/oauth/callback?code=abc&state={state}")
95+
assert retry.status_code == 400

0 commit comments

Comments
 (0)