SHARED_SIGNING.md
The Shared Signing feature enables collaborative document signing workflows where a document owner can request signatures from multiple participants. Each participant receives a secure token to access the document, submit their digital signature (with optional wet signature overlay), and track the signing progress.
Key Capabilities:
workflow_sessions
workflow_participants
NOTIFIED status is reserved for a future email notification feature; no current code path sets itshareToken (UUID) for token-based access — no separate FileShare record is createdaccessRole controls what actions the participant can perform. COMMENTER (and EDITOR) allow submitting a signature; VIEWER does not. After signing/declining, effective role is automatically downgraded to VIEWERuser_server_certificates
stored_files
workflow_session_id to link files to signing sessionsfile_purpose enum (SIGNING_ORIGINAL, SIGNING_SIGNED, etc.)file_shares
workflow_participant_id column is deprecated; participant access is self-contained in WorkflowParticipant.shareTokenWorkflowSessionService (816 lines)
Key responsibilities:
UnifiedAccessControlService
UserServerCertificateService
SigningSessionController (Owner-facing + Authenticated participant endpoints)
POST /api/v1/security/cert-sign/sessions - Create signing sessionGET /api/v1/security/cert-sign/sessions - List user's sessionsGET /api/v1/security/cert-sign/sessions/{id} - Get session detailsGET /api/v1/security/cert-sign/sessions/{id}/pdf - Download original PDFPOST /api/v1/security/cert-sign/sessions/{id}/finalize - Finalize and apply signaturesGET /api/v1/security/cert-sign/sessions/{id}/signed-pdf - Download signed PDFDELETE /api/v1/security/cert-sign/sessions/{id} - Delete sessionPOST /api/v1/security/cert-sign/sessions/{id}/participants - Add participantsDELETE /api/v1/security/cert-sign/sessions/{id}/participants/{participantId} - Remove participantGET /api/v1/security/cert-sign/sign-requests - List sign requests for authenticated userGET /api/v1/security/cert-sign/sign-requests/{id} - Get sign request detailsGET /api/v1/security/cert-sign/sign-requests/{id}/document - Download document for signingPOST /api/v1/security/cert-sign/sign-requests/{id}/sign - Sign document (authenticated)POST /api/v1/security/cert-sign/sign-requests/{id}/decline - Decline sign request (authenticated)WorkflowParticipantController (Participant-facing, token-based)
GET /api/v1/workflow/participant/session?token={token} - View session detailsGET /api/v1/workflow/participant/details?token={token} - Get participant detailsGET /api/v1/workflow/participant/document?token={token} - Download PDFPOST /api/v1/workflow/participant/submit-signature - Submit signaturePOST /api/v1/workflow/participant/decline?token={token} - Decline to signOwner creates session → Participants receive tokens →
Participants access via token (or authenticated) → Participants submit signatures →
Owner finalizes → System applies signatures → [Optional: append summary page] → Signed PDF generated
SignPopout Component
ActiveSessionsPanel
CompletedSessionsPanel
SignRequestWorkbenchView
SessionDetailWorkbenchView
FileContext Integration
ToolWorkflowContext
workflowService.ts
useWorkflowSession.ts
useParticipantSession.ts
Owner → Uploads PDF → Selects participants → Creates session
↓
System creates:
- WorkflowSession record
- WorkflowParticipant records (one per participant, each with a unique shareToken)
↓
Participants receive token (via email or share link)
API Call:
POST /api/v1/security/cert-sign/sessions
Content-Type: multipart/form-data
file: document.pdf
workflowType: SIGNING
documentName: "contract.pdf" # Optional display name
participantUserIds: [1, 2, 3] # Registered user IDs
participantEmails: ["[email protected]"] # External/unregistered users
participants: [...] # Detailed participant configs (optional)
message: "Please sign this contract"
dueDate: "2025-12-31"
ownerEmail: "[email protected]" # Optional, for notifications
workflowMetadata: '{"showSignature": false, "showLogo": false, "includeSummaryPage": true}'
Session-level workflowMetadata fields:
| Field | Type | Description |
|---|---|---|
showSignature | boolean | Show visible digital signature block on PDF |
pageNumber | integer | Page to place digital signature on |
showLogo | boolean | Show logo in digital signature block |
includeSummaryPage | boolean | Append a signature summary page before digital signing |
Response:
{
"sessionId": "uuid",
"documentName": "contract.pdf",
"participants": [
{
"userId": 1,
"email": "[email protected]",
"shareToken": "token1",
"status": "PENDING"
}
],
"participantCount": 3,
"signedCount": 0
}
Participant → Clicks token link → Views session details
↓
Status changes: PENDING/NOTIFIED → VIEWED
↓
Participant downloads PDF to review
Access URL (unauthenticated):
https://app.example.com/sign?token={participant_token}
Authenticated participants can also use:
GET /api/v1/security/cert-sign/sign-requests
GET /api/v1/security/cert-sign/sign-requests/{sessionId}
GET /api/v1/security/cert-sign/sign-requests/{sessionId}/document
Automatic Status Update:
Participant → Selects certificate type → Uploads certificate (if needed)
→ Draws/uploads wet signatures (optional, multiple supported)
→ Submits signature
↓
System stores:
- Certificate data (P12/JKS keystore as base64)
- Certificate password
- Wet signatures metadata (JSON array: base64 image + coordinates per signature)
↓
Status changes: VIEWED → SIGNED
Access role: EDITOR → VIEWER (automatic downgrade)
API Call (token-based, unauthenticated):
POST /api/v1/workflow/participant/submit-signature
Content-Type: multipart/form-data
participantToken: {token}
certType: P12 | JKS | SERVER | USER_CERT
p12File: certificate.p12 (if certType=P12)
jksFile: keystore.jks (if certType=JKS)
password: cert_password
showSignature: false
pageNumber: 1
location: "New York"
reason: "I approve this contract"
showLogo: false
wetSignaturesData: '[{"page":0,"x":100,"y":200,"width":150,"height":50,"type":"IMAGE","data":"base64..."}]'
API Call (authenticated users):
POST /api/v1/security/cert-sign/sign-requests/{sessionId}/sign
Content-Type: multipart/form-data
certType: SERVER | USER_CERT | UPLOAD | PEM | PKCS12 | PFX | JKS
p12File: certificate.p12 (if applicable)
password: cert_password
reason: "I approve this contract"
location: "New York"
wetSignaturesData: '[...]'
Metadata Storage (JSONB):
{
"certificateSubmission": {
"certType": "P12",
"password": "cert_password",
"p12Keystore": "base64_encoded_keystore",
"showSignature": false,
"pageNumber": 1,
"location": "New York",
"reason": "I approve this contract",
"showLogo": false
},
"wetSignatures": [
{
"type": "IMAGE",
"data": "base64_image",
"page": 0,
"x": 100,
"y": 200,
"width": 150,
"height": 50
}
]
}
Note: Multiple wet signatures are supported per participant (array).
Owner → Views session list → Sees "2/5 signatures"
→ Clicks session → Views participant status
↓
Participant list shows:
- [email protected]: SIGNED ✓
- [email protected]: SIGNED ✓
- [email protected]: VIEWED (pending)
- [email protected]: PENDING
- [email protected]: DECLINED ✗
↓
Auto-refresh every 15 seconds
Badge Colors:
Owner → Clicks "Finalize" → System processes signatures
↓
Processing steps:
1. Apply wet signatures to PDF (visual overlays)
1.5. Append signature summary page (if includeSummaryPage=true)
2. Apply digital certificates in participant order
- Visual signature block suppressed when summary page is enabled
3. Store signed PDF
4. Clear wet signature metadata (GDPR compliance)
↓
Owner downloads signed PDF
Finalization Process:
Apply Wet Signatures First
for (WetSignature sig : wetSignatures) {
PDPage page = document.getPage(sig.getPage());
byte[] imageBytes = Base64.decode(sig.getData());
// Convert Y from top-left (UI) to bottom-left (PDF) coordinate system
float pdfY = page.getMediaBox().getHeight() - sig.getY() - sig.getHeight();
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "signature");
contentStream.drawImage(image, sig.getX(), pdfY, sig.getWidth(), sig.getHeight());
}
Append Summary Page (optional, before digital signing)
If includeSummaryPage=true, a new A4 page is appended showing:
This step occurs before digital certificate signing so signatures are not invalidated.
When a summary page is added, the visual digital signature block (showSignature) is suppressed — wet signatures (hand-drawn overlays) are unaffected.
Apply Digital Certificates (in participant order)
for (Participant p : participants) {
if (p.status == SIGNED) {
KeyStore keystore = buildKeystore(p.certificate);
// Reason: participant override > owner default > "Document Signing"
// Location: participant-provided only (no default)
CertSignController.sign(pdfBytes, keystore, password, settings);
}
}
Store and Cleanup
StoredFile signedFile = storeFile(signedPdfBytes, SIGNING_SIGNED);
session.setProcessedFile(signedFile);
session.setFinalized(true);
// GDPR: Clear sensitive metadata after finalization
for (Participant p : participants) {
p.metadata.remove("wetSignatures"); // Clears wet signature image data
p.metadata.remove("certificateSubmission"); // Clears keystore bytes + password
}
API Call:
POST /api/v1/security/cert-sign/sessions/{sessionId}/finalize
Authorization: Bearer {owner_token}
Response: Binary PDF file with Content-Disposition header
Problem: JSONB columns were storing JSON strings instead of JSON objects, requiring double-parsing.
Solution: Created JsonMapConverter JPA AttributeConverter:
@Convert(converter = JsonMapConverter.class)
@Column(name = "participant_metadata", columnDefinition = "jsonb")
private Map<String, Object> participantMetadata;
Benefits:
Implementation:
WorkflowSessionResponse includes participantCount and signedCountWorkflowMapper calculates counts when converting to DTONo Authentication Required for Participants:
Authenticated Participant Access:
/api/v1/security/cert-sign/sign-requestsAutomatic Role Downgrade:
Unified with File Sharing:
StorageProvider (Database or Local)P12/PKCS12/PFX: User uploads PKCS#12 file + password JKS: User uploads Java KeyStore + password PEM/UPLOAD: User uploads PEM certificate + private key SERVER: Uses organization's server certificate (no upload needed) USER_CERT: Uses user's auto-generated personal certificate (one-click)
Note: UPLOAD, PEM, PKCS12, PFX are available on the authenticated (sign-requests) path. The token-based path uses P12, JKS, SERVER, USER_CERT.
application.properties:
# Database (H2 or PostgreSQL)
spring.jpa.hibernate.ddl-auto=update
# Security
DOCKER_ENABLE_SECURITY=true
# Storage Provider (DATABASE or LOCAL)
storage.provider=LOCAL
storage.maxFileSize=100GB
Quick Access Bar:
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/security/cert-sign/sessions | Create session |
| GET | /api/v1/security/cert-sign/sessions | List sessions |
| GET | /api/v1/security/cert-sign/sessions/{id} | Get details |
| POST | /api/v1/security/cert-sign/sessions/{id}/finalize | Finalize session |
| GET | /api/v1/security/cert-sign/sessions/{id}/pdf | Download original |
| GET | /api/v1/security/cert-sign/sessions/{id}/signed-pdf | Download signed |
| DELETE | /api/v1/security/cert-sign/sessions/{id} | Delete session |
| POST | /api/v1/security/cert-sign/sessions/{id}/participants | Add participants |
| DELETE | /api/v1/security/cert-sign/sessions/{id}/participants/{pid} | Remove participant |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/security/cert-sign/sign-requests | List sign requests |
| GET | /api/v1/security/cert-sign/sign-requests/{id} | Get sign request details |
| GET | /api/v1/security/cert-sign/sign-requests/{id}/document | Download document |
| POST | /api/v1/security/cert-sign/sign-requests/{id}/sign | Sign document |
| POST | /api/v1/security/cert-sign/sign-requests/{id}/decline | Decline signing |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/workflow/participant/session?token={token} | View session |
| GET | /api/v1/workflow/participant/details?token={token} | Get participant details |
| GET | /api/v1/workflow/participant/document?token={token} | Download PDF |
| POST | /api/v1/workflow/participant/submit-signature | Submit signature |
| POST | /api/v1/workflow/participant/decline?token={token} | Decline signing |
"Token invalid" error:
Signature not appearing on PDF:
"Awaiting signatures" not updating:
Wet signatures not visible after finalization:
wetSignaturesData was sent as valid JSON arrayincludeSummaryPage setting-- Check session status
SELECT session_id, status, finalized,
(SELECT COUNT(*) FROM workflow_participants WHERE workflow_session_id = ws.id) as participant_count,
(SELECT COUNT(*) FROM workflow_participants WHERE workflow_session_id = ws.id AND status = 'SIGNED') as signed_count
FROM workflow_sessions ws;
-- Check participant tokens
SELECT email, status, share_token, expires_at
FROM workflow_participants
WHERE workflow_session_id = (SELECT id FROM workflow_sessions WHERE session_id = '{session_id}');
-- Check metadata storage
SELECT email,
participant_metadata->'certificateSubmission'->>'certType' as cert_type,
jsonb_array_length(participant_metadata->'wetSignatures') as wet_sig_count
FROM workflow_participants;
The Shared Signing feature provides a complete collaborative signing workflow with:
The architecture leverages existing file sharing infrastructure while adding workflow-specific features, ensuring consistency and maintainability across the application.