Return auth callbacks to current page

This commit is contained in:
Paul Huliganga 2026-04-22 22:09:15 -04:00
parent e1ae1c91af
commit e995ac2764
4 changed files with 63 additions and 11 deletions

View File

@ -170,6 +170,26 @@ def test_adobe_callback_requires_matching_state():
assert "invalid oauth state" in resp.json()["error"] 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(): def test_adobe_exchange_rejects_missing_code():
"""POST /api/auth/adobe/exchange with no code in URL → 400.""" """POST /api/auth/adobe/exchange with no code in URL → 400."""
resp = client.post( 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"] is True
assert status_resp.json()["docusign_auth_mode"] == "session_oauth" assert status_resp.json()["docusign_auth_mode"] == "session_oauth"
assert status_resp.json()["docusign_account_selection_required"] is True 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(): def test_docusign_sessions_are_isolated():

View File

@ -39,6 +39,12 @@ def _adobe_redirect_uri() -> str:
return settings.adobe_redirect_uri 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: def _build_adobe_authorization_url(state: str) -> str:
return ( return (
f"{_ADOBE_AUTH_URL}" f"{_ADOBE_AUTH_URL}"
@ -156,11 +162,12 @@ def auth_status(request: Request):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@router.get("/adobe/url") @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) session = get_session(request)
state = secrets.token_urlsafe(24) state = secrets.token_urlsafe(24)
session["adobe_oauth_state"] = state session["adobe_oauth_state"] = state
session["adobe_auth_mode"] = "authorization_pending" session["adobe_auth_mode"] = "authorization_pending"
session["adobe_return_to"] = _sanitize_return_to(return_to)
response = JSONResponse({"url": _build_adobe_authorization_url(state)}) response = JSONResponse({"url": _build_adobe_authorization_url(state)})
save_session(response, session) save_session(response, session)
return response return response
@ -231,7 +238,7 @@ async def adobe_exchange(body: AdobeExchangeRequest, request: Request):
@router.get("/adobe/connect") @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. Obtain an Adobe Sign access token for this browser session.
If session/env tokens are unavailable or force_oauth=true, return an 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) state = secrets.token_urlsafe(24)
session["adobe_oauth_state"] = state session["adobe_oauth_state"] = state
session["adobe_auth_mode"] = "authorization_pending" session["adobe_auth_mode"] = "authorization_pending"
session["adobe_return_to"] = _sanitize_return_to(return_to)
authorization_url = _build_adobe_authorization_url(state) authorization_url = _build_adobe_authorization_url(state)
log_event( log_event(
request, request,
@ -359,7 +367,7 @@ async def adobe_callback(request: Request, code: str = "", state: str = ""):
{"auth_mode": "session_oauth", "source": "browser_callback"}, {"auth_mode": "session_oauth", "source": "browser_callback"},
) )
response = RedirectResponse("/#/settings") response = RedirectResponse(session.pop("adobe_return_to", "#/templates"))
save_session(response, session) save_session(response, session)
return response return response
@ -392,7 +400,7 @@ def adobe_disconnect(request: Request):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@router.get("/docusign/connect") @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. Obtain a DocuSign access token from the current browser session.
If the session has not been authorized yet, return an authorization URL so 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) state = secrets.token_urlsafe(24)
session["docusign_oauth_state"] = state session["docusign_oauth_state"] = state
session["docusign_auth_mode"] = "authorization_pending" session["docusign_auth_mode"] = "authorization_pending"
session["docusign_return_to"] = _sanitize_return_to(return_to)
authorization_url = build_authorization_url(state=state) authorization_url = build_authorization_url(state=state)
log_event( log_event(
request, request,
@ -480,6 +489,7 @@ def docusign_start(request: Request):
state = secrets.token_urlsafe(24) state = secrets.token_urlsafe(24)
session["docusign_oauth_state"] = state session["docusign_oauth_state"] = state
session["docusign_auth_mode"] = "authorization_pending" session["docusign_auth_mode"] = "authorization_pending"
session["docusign_return_to"] = "#/templates"
authorization_url = build_authorization_url(state=state) authorization_url = build_authorization_url(state=state)
log_event( log_event(
request, 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) save_session(response, session)
return response return response

View File

@ -26,8 +26,11 @@ export const api = {
status() { status() {
return GET('/api/auth/status'); return GET('/api/auth/status');
}, },
connectAdobe(forceOauth = false) { connectAdobe(forceOauth = false, returnTo = '#/templates') {
return GET(`/api/auth/adobe/connect${forceOauth ? '?force_oauth=true' : ''}`); 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() { adobeUrl() {
return GET('/api/auth/adobe/url'); return GET('/api/auth/adobe/url');
@ -35,8 +38,8 @@ export const api = {
exchangeAdobe(redirectUrl) { exchangeAdobe(redirectUrl) {
return POST('/api/auth/adobe/exchange', { redirect_url: redirectUrl }); return POST('/api/auth/adobe/exchange', { redirect_url: redirectUrl });
}, },
connectDocusign() { connectDocusign(returnTo = '#/templates') {
return GET('/api/auth/docusign/connect'); return GET(`/api/auth/docusign/connect?return_to=${encodeURIComponent(returnTo)}`);
}, },
docusignAccounts() { docusignAccounts() {
return GET('/api/auth/docusign/accounts'); return GET('/api/auth/docusign/accounts');

View File

@ -143,7 +143,7 @@ async function connectAdobe(forceOauth = false) {
closeAuthMenu(); closeAuthMenu();
setChipConnecting('chip-adobe'); setChipConnecting('chip-adobe');
try { try {
const data = await api.auth.connectAdobe(forceOauth); const data = await api.auth.connectAdobe(forceOauth, window.location.hash || '#/templates');
if (data.connected) { if (data.connected) {
setState('auth', { ...state.auth, adobe: true }); setState('auth', { ...state.auth, adobe: true });
renderAuthChips(); renderAuthChips();
@ -165,7 +165,7 @@ async function connectDocusign() {
closeAuthMenu(); closeAuthMenu();
setChipConnecting('chip-docusign'); setChipConnecting('chip-docusign');
try { try {
const data = await api.auth.connectDocusign(); const data = await api.auth.connectDocusign(window.location.hash || '#/templates');
if (data.connected) { if (data.connected) {
await refreshAuth(); await refreshAuth();
if (!data.account_selection_required) { if (!data.account_selection_required) {