From e995ac27642aa40c9dde09b7c4d9337aa9b4f337 Mon Sep 17 00:00:00 2001 From: Paul Huliganga Date: Wed, 22 Apr 2026 22:09:15 -0400 Subject: [PATCH] Return auth callbacks to current page --- tests/test_api_auth.py | 39 +++++++++++++++++++++++++++++++++++++++ web/routers/auth.py | 20 +++++++++++++++----- web/static/js/api.js | 11 +++++++---- web/static/js/auth.js | 4 ++-- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/tests/test_api_auth.py b/tests/test_api_auth.py index a409573..5b23719 100644 --- a/tests/test_api_auth.py +++ b/tests/test_api_auth.py @@ -170,6 +170,26 @@ def test_adobe_callback_requires_matching_state(): assert "invalid oauth state" in resp.json()["error"] +@respx.mock +def test_adobe_callback_redirects_back_to_requested_page(): + respx.post(_ADOBE_TOKEN_URL).mock( + return_value=httpx.Response(200, json={ + "access_token": "adobe-test-token", + "refresh_token": "adobe-refresh", + }) + ) + + cookie = create_test_session({"adobe_oauth_state": "expected-state", "adobe_return_to": "#/help"}) + resp = client.get( + "/api/auth/adobe/callback?code=authcode123&state=expected-state", + cookies={"migrator_session": cookie}, + follow_redirects=False, + ) + + assert resp.status_code in (302, 307) + assert resp.headers["location"] == "#/help" + + def test_adobe_exchange_rejects_missing_code(): """POST /api/auth/adobe/exchange with no code in URL → 400.""" resp = client.post( @@ -245,6 +265,25 @@ def test_docusign_callback_stores_per_session_tokens(): assert status_resp.json()["docusign"] is True assert status_resp.json()["docusign_auth_mode"] == "session_oauth" assert status_resp.json()["docusign_account_selection_required"] is True + assert resp.headers["location"] == "#/templates" + + +def test_docusign_callback_redirects_back_to_requested_page(): + from unittest.mock import AsyncMock, patch + + cookie = create_test_session({"docusign_oauth_state": "expected-state", "docusign_return_to": "#/help"}) + with patch( + "docusign_auth.exchange_code_for_token", + return_value={"access_token": "access-123", "refresh_token": "refresh-123", "expires_in": 3600}, + ), patch("web.routers.auth.fetch_userinfo", new=AsyncMock(return_value=_userinfo_payload())): + resp = client.get( + "/api/auth/docusign/callback?code=authcode123&state=expected-state", + cookies={"migrator_session": cookie}, + follow_redirects=False, + ) + + assert resp.status_code in (302, 307) + assert resp.headers["location"] == "#/help" def test_docusign_sessions_are_isolated(): diff --git a/web/routers/auth.py b/web/routers/auth.py index 9df7549..d8cdf7a 100644 --- a/web/routers/auth.py +++ b/web/routers/auth.py @@ -39,6 +39,12 @@ def _adobe_redirect_uri() -> str: return settings.adobe_redirect_uri +def _sanitize_return_to(value: str | None) -> str: + if value and value.startswith("#/"): + return value + return "#/templates" + + def _build_adobe_authorization_url(state: str) -> str: return ( f"{_ADOBE_AUTH_URL}" @@ -156,11 +162,12 @@ def auth_status(request: Request): # --------------------------------------------------------------------------- @router.get("/adobe/url") -def adobe_auth_url(request: Request): +def adobe_auth_url(request: Request, return_to: str | None = None): session = get_session(request) state = secrets.token_urlsafe(24) session["adobe_oauth_state"] = state session["adobe_auth_mode"] = "authorization_pending" + session["adobe_return_to"] = _sanitize_return_to(return_to) response = JSONResponse({"url": _build_adobe_authorization_url(state)}) save_session(response, session) return response @@ -231,7 +238,7 @@ async def adobe_exchange(body: AdobeExchangeRequest, request: Request): @router.get("/adobe/connect") -async def adobe_connect(request: Request, force_oauth: bool = False): +async def adobe_connect(request: Request, force_oauth: bool = False, return_to: str | None = None): """ Obtain an Adobe Sign access token for this browser session. If session/env tokens are unavailable or force_oauth=true, return an @@ -299,6 +306,7 @@ async def adobe_connect(request: Request, force_oauth: bool = False): state = secrets.token_urlsafe(24) session["adobe_oauth_state"] = state session["adobe_auth_mode"] = "authorization_pending" + session["adobe_return_to"] = _sanitize_return_to(return_to) authorization_url = _build_adobe_authorization_url(state) log_event( request, @@ -359,7 +367,7 @@ async def adobe_callback(request: Request, code: str = "", state: str = ""): {"auth_mode": "session_oauth", "source": "browser_callback"}, ) - response = RedirectResponse("/#/settings") + response = RedirectResponse(session.pop("adobe_return_to", "#/templates")) save_session(response, session) return response @@ -392,7 +400,7 @@ def adobe_disconnect(request: Request): # --------------------------------------------------------------------------- @router.get("/docusign/connect") -async def docusign_connect(request: Request): +async def docusign_connect(request: Request, return_to: str | None = None): """ Obtain a DocuSign access token from the current browser session. If the session has not been authorized yet, return an authorization URL so @@ -423,6 +431,7 @@ async def docusign_connect(request: Request): state = secrets.token_urlsafe(24) session["docusign_oauth_state"] = state session["docusign_auth_mode"] = "authorization_pending" + session["docusign_return_to"] = _sanitize_return_to(return_to) authorization_url = build_authorization_url(state=state) log_event( request, @@ -480,6 +489,7 @@ def docusign_start(request: Request): state = secrets.token_urlsafe(24) session["docusign_oauth_state"] = state session["docusign_auth_mode"] = "authorization_pending" + session["docusign_return_to"] = "#/templates" authorization_url = build_authorization_url(state=state) log_event( request, @@ -528,7 +538,7 @@ async def docusign_callback(request: Request, code: str = "", state: str = ""): }, ) - response = RedirectResponse("/#/settings" if account_picker_required(session) else "/") + response = RedirectResponse(session.pop("docusign_return_to", "#/templates")) save_session(response, session) return response diff --git a/web/static/js/api.js b/web/static/js/api.js index bd774de..63b9035 100644 --- a/web/static/js/api.js +++ b/web/static/js/api.js @@ -26,8 +26,11 @@ export const api = { status() { return GET('/api/auth/status'); }, - connectAdobe(forceOauth = false) { - return GET(`/api/auth/adobe/connect${forceOauth ? '?force_oauth=true' : ''}`); + connectAdobe(forceOauth = false, returnTo = '#/templates') { + const params = new URLSearchParams(); + if (forceOauth) params.set('force_oauth', 'true'); + params.set('return_to', returnTo); + return GET(`/api/auth/adobe/connect?${params.toString()}`); }, adobeUrl() { return GET('/api/auth/adobe/url'); @@ -35,8 +38,8 @@ export const api = { exchangeAdobe(redirectUrl) { return POST('/api/auth/adobe/exchange', { redirect_url: redirectUrl }); }, - connectDocusign() { - return GET('/api/auth/docusign/connect'); + connectDocusign(returnTo = '#/templates') { + return GET(`/api/auth/docusign/connect?return_to=${encodeURIComponent(returnTo)}`); }, docusignAccounts() { return GET('/api/auth/docusign/accounts'); diff --git a/web/static/js/auth.js b/web/static/js/auth.js index b119f06..f96621e 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -143,7 +143,7 @@ async function connectAdobe(forceOauth = false) { closeAuthMenu(); setChipConnecting('chip-adobe'); try { - const data = await api.auth.connectAdobe(forceOauth); + const data = await api.auth.connectAdobe(forceOauth, window.location.hash || '#/templates'); if (data.connected) { setState('auth', { ...state.auth, adobe: true }); renderAuthChips(); @@ -165,7 +165,7 @@ async function connectDocusign() { closeAuthMenu(); setChipConnecting('chip-docusign'); try { - const data = await api.auth.connectDocusign(); + const data = await api.auth.connectDocusign(window.location.hash || '#/templates'); if (data.connected) { await refreshAuth(); if (!data.account_selection_required) {