fix: DocuSign connects via JWT grant from .env, no browser sign-in
Replaces the DocuSign OAuth redirect flow with a direct JWT grant call using credentials already in .env (same as the CLI). Clicking "Connect DocuSign" now calls GET /api/auth/docusign/connect which calls get_access_token() and stores the result in the session cookie. No email sign-in required. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1383586d91
commit
aa88ba363d
|
|
@ -70,19 +70,16 @@ def test_adobe_exchange_rejects_missing_code():
|
|||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_docusign_callback_stores_token():
|
||||
"""Successful DocuSign OAuth callback → session has docusign_access_token."""
|
||||
from web.config import settings
|
||||
respx.post(f"https://{settings.docusign_auth_server}/oauth/token").mock(
|
||||
return_value=httpx.Response(200, json={
|
||||
"access_token": "ds-test-token",
|
||||
"refresh_token": "ds-refresh",
|
||||
})
|
||||
)
|
||||
def test_docusign_connect_stores_token():
|
||||
"""GET /api/auth/docusign/connect uses JWT grant from .env → session connected."""
|
||||
from unittest.mock import patch
|
||||
import web.routers.auth as auth_module
|
||||
|
||||
resp = client.get("/api/auth/docusign/callback?code=dscode123", follow_redirects=False)
|
||||
assert resp.status_code in (302, 307)
|
||||
with patch("docusign_auth.get_access_token", return_value="ds-jwt-token"):
|
||||
resp = client.get("/api/auth/docusign/connect")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["connected"] is True
|
||||
|
||||
session_cookie = resp.cookies.get("migrator_session")
|
||||
assert session_cookie is not None
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ DocuSign uses a standard redirect callback handled directly by this server.
|
|||
Tokens are stored in a signed session cookie.
|
||||
"""
|
||||
|
||||
import base64
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
import httpx
|
||||
|
|
@ -128,52 +127,30 @@ def adobe_disconnect(request: Request):
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DocuSign — standard redirect callback
|
||||
# DocuSign — JWT grant using existing .env credentials
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@router.get("/docusign/start")
|
||||
def docusign_start():
|
||||
"""Redirect the browser to the DocuSign OAuth authorization page."""
|
||||
params = (
|
||||
f"?response_type=code"
|
||||
f"&scope=signature"
|
||||
f"&client_id={settings.docusign_client_id}"
|
||||
f"&redirect_uri={settings.docusign_redirect_uri}"
|
||||
)
|
||||
auth_url = f"https://{settings.docusign_auth_server}/oauth/auth" + params
|
||||
return RedirectResponse(auth_url)
|
||||
@router.get("/docusign/connect")
|
||||
def docusign_connect(request: Request):
|
||||
"""
|
||||
Obtain a DocuSign access token via JWT grant using the credentials already
|
||||
in .env (DOCUSIGN_CLIENT_ID, DOCUSIGN_USER_ID, DOCUSIGN_PRIVATE_KEY_PATH).
|
||||
No browser sign-in needed — consent was already granted via the CLI setup.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
||||
from docusign_auth import get_access_token
|
||||
|
||||
try:
|
||||
token = get_access_token()
|
||||
except RuntimeError as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@router.get("/docusign/callback")
|
||||
async def docusign_callback(request: Request, code: str = ""):
|
||||
"""Exchange DocuSign authorization code for access token."""
|
||||
if not code:
|
||||
return JSONResponse({"error": "missing code"}, status_code=400)
|
||||
|
||||
credentials = base64.b64encode(
|
||||
f"{settings.docusign_client_id}:{settings.docusign_client_secret}".encode()
|
||||
).decode()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
f"https://{settings.docusign_auth_server}/oauth/token",
|
||||
headers={"Authorization": f"Basic {credentials}"},
|
||||
data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": settings.docusign_redirect_uri,
|
||||
},
|
||||
)
|
||||
|
||||
if not resp.is_success:
|
||||
return JSONResponse({"error": "token exchange failed", "detail": resp.text}, status_code=502)
|
||||
|
||||
token_data = resp.json()
|
||||
session = get_session(request)
|
||||
session["docusign_access_token"] = token_data.get("access_token")
|
||||
session["docusign_refresh_token"] = token_data.get("refresh_token")
|
||||
session["docusign_access_token"] = token
|
||||
|
||||
response = RedirectResponse("/")
|
||||
response = JSONResponse({"connected": True})
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
||||
|
|
@ -182,7 +159,6 @@ async def docusign_callback(request: Request, code: str = ""):
|
|||
def docusign_disconnect(request: Request):
|
||||
session = get_session(request)
|
||||
session.pop("docusign_access_token", None)
|
||||
session.pop("docusign_refresh_token", None)
|
||||
response = JSONResponse({"disconnected": "docusign"})
|
||||
save_session(response, session)
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -38,13 +38,13 @@ function renderAuthBar() {
|
|||
? () => disconnectPlatform('adobe')
|
||||
: () => startAdobeAuth();
|
||||
|
||||
// DocuSign: standard redirect
|
||||
// DocuSign: JWT grant from .env — no browser sign-in needed
|
||||
const dsEl = $('badge-docusign');
|
||||
dsEl.textContent = authState.docusign ? '✓ DocuSign' : 'Connect DocuSign';
|
||||
dsEl.className = 'auth-badge' + (authState.docusign ? ' connected' : '');
|
||||
dsEl.onclick = authState.docusign
|
||||
? () => disconnectPlatform('docusign')
|
||||
: () => { window.location.href = '/api/auth/docusign/start'; };
|
||||
: () => connectDocusign();
|
||||
}
|
||||
|
||||
async function disconnectPlatform(platform) {
|
||||
|
|
@ -54,6 +54,21 @@ async function disconnectPlatform(platform) {
|
|||
await refreshTemplates();
|
||||
}
|
||||
|
||||
async function connectDocusign() {
|
||||
const dsEl = $('badge-docusign');
|
||||
dsEl.textContent = 'Connecting…';
|
||||
const resp = await fetch('/api/auth/docusign/connect');
|
||||
const data = await resp.json();
|
||||
if (data.connected) {
|
||||
authState.docusign = true;
|
||||
renderAuthBar();
|
||||
await refreshTemplates();
|
||||
} else {
|
||||
dsEl.textContent = 'Connect DocuSign';
|
||||
setStatus('DocuSign error: ' + (data.error || 'unknown'));
|
||||
}
|
||||
}
|
||||
|
||||
// Adobe Sign uses the same manual-paste flow as the CLI:
|
||||
// 1. Open auth URL in new tab
|
||||
// 2. User authorizes → lands on failed https://localhost:8080/callback page
|
||||
|
|
|
|||
Loading…
Reference in New Issue