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
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
@respx.mock
|
def test_docusign_connect_stores_token():
|
||||||
def test_docusign_callback_stores_token():
|
"""GET /api/auth/docusign/connect uses JWT grant from .env → session connected."""
|
||||||
"""Successful DocuSign OAuth callback → session has docusign_access_token."""
|
from unittest.mock import patch
|
||||||
from web.config import settings
|
import web.routers.auth as auth_module
|
||||||
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",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
resp = client.get("/api/auth/docusign/callback?code=dscode123", follow_redirects=False)
|
with patch("docusign_auth.get_access_token", return_value="ds-jwt-token"):
|
||||||
assert resp.status_code in (302, 307)
|
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")
|
session_cookie = resp.cookies.get("migrator_session")
|
||||||
assert session_cookie is not None
|
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.
|
Tokens are stored in a signed session cookie.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
import httpx
|
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")
|
@router.get("/docusign/connect")
|
||||||
def docusign_start():
|
def docusign_connect(request: Request):
|
||||||
"""Redirect the browser to the DocuSign OAuth authorization page."""
|
"""
|
||||||
params = (
|
Obtain a DocuSign access token via JWT grant using the credentials already
|
||||||
f"?response_type=code"
|
in .env (DOCUSIGN_CLIENT_ID, DOCUSIGN_USER_ID, DOCUSIGN_PRIVATE_KEY_PATH).
|
||||||
f"&scope=signature"
|
No browser sign-in needed — consent was already granted via the CLI setup.
|
||||||
f"&client_id={settings.docusign_client_id}"
|
"""
|
||||||
f"&redirect_uri={settings.docusign_redirect_uri}"
|
import sys
|
||||||
)
|
import os
|
||||||
auth_url = f"https://{settings.docusign_auth_server}/oauth/auth" + params
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
||||||
return RedirectResponse(auth_url)
|
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 = get_session(request)
|
||||||
session["docusign_access_token"] = token_data.get("access_token")
|
session["docusign_access_token"] = token
|
||||||
session["docusign_refresh_token"] = token_data.get("refresh_token")
|
|
||||||
|
|
||||||
response = RedirectResponse("/")
|
response = JSONResponse({"connected": True})
|
||||||
save_session(response, session)
|
save_session(response, session)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
@ -182,7 +159,6 @@ async def docusign_callback(request: Request, code: str = ""):
|
||||||
def docusign_disconnect(request: Request):
|
def docusign_disconnect(request: Request):
|
||||||
session = get_session(request)
|
session = get_session(request)
|
||||||
session.pop("docusign_access_token", None)
|
session.pop("docusign_access_token", None)
|
||||||
session.pop("docusign_refresh_token", None)
|
|
||||||
response = JSONResponse({"disconnected": "docusign"})
|
response = JSONResponse({"disconnected": "docusign"})
|
||||||
save_session(response, session)
|
save_session(response, session)
|
||||||
return response
|
return response
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,13 @@ function renderAuthBar() {
|
||||||
? () => disconnectPlatform('adobe')
|
? () => disconnectPlatform('adobe')
|
||||||
: () => startAdobeAuth();
|
: () => startAdobeAuth();
|
||||||
|
|
||||||
// DocuSign: standard redirect
|
// DocuSign: JWT grant from .env — no browser sign-in needed
|
||||||
const dsEl = $('badge-docusign');
|
const dsEl = $('badge-docusign');
|
||||||
dsEl.textContent = authState.docusign ? '✓ DocuSign' : 'Connect DocuSign';
|
dsEl.textContent = authState.docusign ? '✓ DocuSign' : 'Connect DocuSign';
|
||||||
dsEl.className = 'auth-badge' + (authState.docusign ? ' connected' : '');
|
dsEl.className = 'auth-badge' + (authState.docusign ? ' connected' : '');
|
||||||
dsEl.onclick = authState.docusign
|
dsEl.onclick = authState.docusign
|
||||||
? () => disconnectPlatform('docusign')
|
? () => disconnectPlatform('docusign')
|
||||||
: () => { window.location.href = '/api/auth/docusign/start'; };
|
: () => connectDocusign();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disconnectPlatform(platform) {
|
async function disconnectPlatform(platform) {
|
||||||
|
|
@ -54,6 +54,21 @@ async function disconnectPlatform(platform) {
|
||||||
await refreshTemplates();
|
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:
|
// Adobe Sign uses the same manual-paste flow as the CLI:
|
||||||
// 1. Open auth URL in new tab
|
// 1. Open auth URL in new tab
|
||||||
// 2. User authorizes → lands on failed https://localhost:8080/callback page
|
// 2. User authorizes → lands on failed https://localhost:8080/callback page
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue