TL;DR: Developers often struggle with assigning eSignature permissions in multi-user PDF workflows. This guide shows how to implement user-based eSignatures using Syncfusion® ASP.NET Core PDF Viewer, enabling secure, role-specific signing with audit trails and form field control.
Managing eSignatures in multi-user environments can be tricky. Developers need a secure, scalable way to assign signing permissions without compromising document integrity. Electronic signatures (eSignatures) have become essential for streamlining document workflows and ensuring secure, compliant approval processes. While basic eSign functionality is valuable, implementing user-based eSigning elevates document management to an enterprise level by providing granular control over who can sign what, when, and where.
In this guide, we’ll walk through how to implement user-based eSignatures using Syncfusion® ASP.NET Core PDF Viewer, solving a common pain point in enterprise document workflows. Unlike generic eSign solutions, our implementation assigns specific form fields to designated users, ensures proper validation, and maintains a secure audit trail throughout the signing process.
Traditional eSign solutions often fall short in complex business scenarios where multiple stakeholders need to sign different sections of the same document. User-based eSigning addresses these challenges by providing:
To build this user-based eSign system, you’ll need the following components:
The Syncfusion® ASP.NET Core PDF Viewer is an enterprise-grade component that serves as the backbone of our user-based eSigning solution. Here’s what makes it perfect for this implementation:
Key Capabilities:
The PDF Viewer’s flexibility allows developers to create sophisticated workflows where each user sees only their designated form fields, complete with color-coding and validation rules.
Our user-based eSign system follows a five-phase workflow:
Let’s dive into each phase with detailed implementation examples.
The foundation of our system is the user management structure that defines who can sign what. Here’s how we set up user-based field assignments.
// Define users with unique identifiers and properties
var userDetails = [
{
Name: 'Andrew Fuller',
Eimg: 'profile2',
Id: "ff0000", // Unique color identifier for visual distinction
Mail: "andrew@mycompany.com",
Role: "Manager",
fieldIds: [] // Will store assigned form field references
},
{
Name: 'Anne Dodsworth',
Eimg: 'profile1',
Id: "00ff00",
Mail: "anne@mycompany.com",
Role: "Employee",
fieldIds: []
}
];
The system provides an intuitive dropdown interface for selecting users during form design.
<!-- User Selection Dropdown -->
<ejs-dropdownlist id="e-pv-e-sign-employees" dataSource="@userDetails" index="0" popupHeight="200px" select="userChange">
<e-dropdownlist-fields text="Name" value="Id"></e-dropdownlist-fields>
</ejs-dropdownlist>
Each user is assigned a unique color that visually distinguishes their form fields.
/* User-specific color coding */.andrew-fields {
background-color: rgba(255, 0, 0, 0.2); /* Red for Andrew */ border: 2px solid #ff0000;
}
.anne-fields {
background-color: rgba(0, 255, 0, 0.2); /* Green for Anne */ border: 2px solid #00ff00;
}
The Form Designer View provides a comprehensive interface for creating and assigning form fields. This phase combines drag-and-drop functionality with user assignment logic.
function documentLoad() {
pdfViewer = document.getElementById('pdfviewer').ej2_instances[0];
if (showFinishSigningButton) {
// Switch to signing mode
pdfViewer.designerMode = false;
updateUserFormField();
pdfViewer.updateViewerContainer();
}
else {
// Enable form design mode
pdfViewer.designerMode = true;
initializeFormFieldPalette();
}
}
function initializeFormFieldPalette() {
// Initialize draggable form fields
initializeDraggable(document.getElementById('signature-btn'), 'SignatureField');
initializeDraggable(document.getElementById('textbox-btn'), 'Textbox');
initializeDraggable(document.getElementById('password-btn'), 'Password');
initializeDraggable(document.getElementById('checkbox-btn'), 'CheckBox');
initializeDraggable(document.getElementById('radio-btn'), 'RadioButton');
initializeDraggable(document.getElementById('dropdown-btn'), 'DropDown');
initializeDraggable(document.getElementById('list-btn'), 'ListBox');
initializeDraggable(document.getElementById('initial-btn'), 'InitialField');
}
The system supports intuitive drag-and-drop field creation with automatic user assignment.
function initializeDraggable(element, fieldType) {
// Enable drag functionality
element.draggable = true;
element.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('fieldType', fieldType);
e.dataTransfer.setData('currentUser', getCurrentUser());
});
}
// Handle field drop events
pdfViewer.addEventListener('formFieldAdd', addFormField);
function addFormField(args) {
var currentUser = getCurrentUser();
var userColor = getUserColor(currentUser);
// Assign user-specific metadata to the form field
if (currentUser === 'andrew@mycompany.com') {
pdfViewer.formDesigner.updateFormField(
pdfViewer.retrieveFormFields()[pdfViewer.formFieldCollections.length - 1],
{
customData: { author: 'andrew' },
backgroundColor: userColor,
isReadOnly: false
}
);
}
else if (currentUser === 'anne@mycompany.com') {
pdfViewer.formDesigner.updateFormField(
pdfViewer.retrieveFormFields()[pdfViewer.formFieldCollections.length - 1],
{
customData: { author: 'anne' },
backgroundColor: userColor,
isReadOnly: false
}
);
}
// Add field to user's collection
var currentUserDetails = userDetails.filter(user => user.Mail === currentUser)[0];
var currentFormField = pdfViewer.formFieldCollections.filter(field => field.id === args.field.id)[0];
currentUserDetails.fieldIds.push(currentFormField);
// Visual feedback
showFieldAssignmentNotification(currentUser, args.field.fieldType);
}
Beyond basic field creation, the system supports advanced configuration options, as shown below.
function configureAdvancedFieldProperties(fieldId, userConfig) {
var field = pdfViewer.formFieldCollections.find(f => f.id === fieldId);
if (field) {
pdfViewer.formDesigner.updateFormField(field, {
// User-specific styling
backgroundColor: userConfig.backgroundColor,
borderColor: userConfig.borderColor,
fontColor: userConfig.fontColor,
// Permissions
isReadOnly: false, // Will be set during signing phase
customData: { //Set more user-specific details as JSON data
author: userConfig.author,
}
});
}
}
Reference designed PDF Form fields.
Once form design is complete, the system transitions from design mode to signing mode. This phase involves saving the designed form and reloading it with user-specific permission.
function signFile() {
// Save the designed form as a blob
pdfViewer.saveAsBlob().then(function (value) {
data = value;
var reader = new FileReader();
reader.readAsDataURL(data);
reader.onload = () => {
base64data = reader.result;
// Reload the form in signing mode
pdfViewer.load(base64data, null);
pdfViewer.width = "100%";
pdfViewer.updateViewerContainer();
// Configure UI for signing mode
transitionToSigningMode();
};
});
}
function transitionToSigningMode() {
// Update UI state
showSignButton = false;
showFinishSigningButton = true;
// Hide design tools
document.getElementById("sidebarObj").style.display = "none";
document.getElementById("pdf-div").style.width = "100%";
document.getElementById("pdf-div").style.marginLeft = "0";
// Update toolbar for signing operations
updateToolbar();
}
When transitioning to signing mode, the system configures field visibility based on user permissions.
function updateUserFormField() {
// Get all form fields from the PDF viewer
const formFieldCollections = pdfViewer.formFieldCollections;
// Separate fields based on assigned user (author)
const otherFormFieldDetails = formFieldCollections.filter(f => f.customData.author === 'anne');
const currentFormFieldDetails = formFieldCollections.filter(f => f.customData.author === 'andrew');
// If the current user is Andrew
if (currentUser === 'andrew@mycompany.com') {
otherFormFieldDetails.forEach(field => {
const isFilled = field.value !== "";
const fieldElement = document.getElementById(field.id + '_content_html_element');
const currentField = formFieldCollections.find(f => f.id === field.id);
if (isFilled) {
// Mark Anne's completed fields as finished and read-only
pdfViewer.formDesigner.updateFormField(field, { backgroundColor: finishedBackground });
pdfViewer.formDesignerModule.updateFormField(field, { isReadOnly: true });
// Mark Andrew's fields as read-only with his background color
currentFormFieldDetails.forEach(currentField => {
pdfViewer.formDesigner.updateFormField(currentField, { backgroundColor: andrewBackground });
pdfViewer.formDesignerModule.updateFormField(currentField, { isReadOnly: true });
});
}
else {
// If Anne's fields are not filled, highlight Andrew's fields
currentFormFieldDetails.forEach(currentField => {
pdfViewer.formDesigner.updateFormField(currentField, { backgroundColor: andrewBackground });
});
}
// Hide Anne's unfilled fields from Andrew's view
if (fieldElement && currentField) {
const shouldHide = currentField.type !== 'DropDown'
? !currentField.value
: currentField.value.length !== 0;
if (shouldHide) {
pdfViewer.formDesignerModule.updateFormField(currentField, { visibility: 'hidden' });
}
}
});
}
else {
// For Anne or other users, validate Andrew's fields first
validation(currentFormFieldDetails);
if (!state) {
// If validation fails, lock Andrew's fields and enable Anne's
currentFormFieldDetails.forEach(field => {
pdfViewer.formDesigner.updateFormField(field, { backgroundColor: finishedBackground });
pdfViewer.formDesignerModule.updateFormField(field, { isReadOnly: true });
otherFormFieldDetails.forEach(otherField => {
pdfViewer.formDesigner.updateFormField(otherField, { backgroundColor: anneBackground });
pdfViewer.formDesignerModule.updateFormField(otherField, { isReadOnly: false });
// Make Anne's fields visible
const otherUserField = document.getElementById(otherField.id + '_content_html_element');
if (otherUserField) {
pdfViewer.formDesignerModule.updateFormField(otherField, { visibility: 'visible' });
}
});
});
}
}
}
This is the core phase where users interact with their assigned form fields. The system enforces strict validation and provides clear visual feedback.
function userChange(args) {
// Update the current user based on the selected item's email
currentUser = args.itemData.Mail;
// Get the user image element
const userImage = document.getElementById('user-img');
// Set border color based on selected user
if (currentUser === 'andrew@mycompany.com') {
userImage.style.borderColor = 'red';
}
else {
userImage.style.borderColor = 'green';
}
// Update form fields based on the selected user
updateUserFormField();
// If change is prevented (e.g., due to validation), revert the border color and cancel the selection
if (preventChange) {
userImage.style.borderColor = 'red';
// Optional: could be based on previous user
args.cancel = true;
}
}
This function validates all required form fields, including checkboxes, radio buttons, dropdowns, and text inputs, before allowing users to proceed. It provides immediate feedback by highlighting missing fields and displaying a clear error message dialog.
function validation(forms) {
let errorMessage = "Required Field(s): ";
let allFieldsValid = true;
let radioGroupName = "";
let radioSelected = false;
// Iterate through each form field to validate
forms.forEach(form => {
let fieldNameToAdd = "";
if (form.isRequired) {
switch (form.type.toString()) {
case "Checkbox":
// If the checkbox is required but not checked
if (!form.isChecked) {
fieldNameToAdd = form.name;
allFieldsValid = false;
}
break;
case "RadioButton":
// Track if any radio button in the group is selected
if (!radioSelected) {
radioGroupName = form.name;
if (form.isSelected) radioSelected = true;
}
break;
case "DropdownList":
// If dropdown is required but no value selected
if (!form.value || form.value.length === 0) {
fieldNameToAdd = form.name;
allFieldsValid = false;
}
break;
default:
// For text fields or others, check if value is empty
if (!form.value || (typeof form.newValue === 'string' && form.newValue === "")) {
fieldNameToAdd = form.name;
allFieldsValid = false;
}
break;
}
// Append field name to error message if validation failed
if (fieldNameToAdd) {
errorMessage += errorMessage === "Required Field(s): " ? fieldNameToAdd : `, ${fieldNameToAdd}`;
}
}
});
// If no radio button was selected in a required group
if (!radioSelected && radioGroupName) {
errorMessage += errorMessage === "Required Field(s): " ? radioGroupName : `, ${radioGroupName}`;
allFieldsValid = false;
}
// Show error dialog if any field is invalid
if (!allFieldsValid) {
state = true;
preventChange = true;
dialogObj.content = errorMessage;
dialogObj.show();
}
else {
state = false;
preventChange = false;
}
}
Reference screenshot after filling the form fields based on the user.
The final phase handles document completion, validation, flattening, and secure distribution.
This method finalizes the signing process by marking all form fields as completed and sending the document to the server for flattening. It then downloads the secured PDF and prevents further edits, ensuring the document is ready for distribution or archival.
function finishSigning() {
// Step 1: Mark all form fields as completed with a background color
for (const formField of pdfViewer.formFieldCollections) {
pdfViewer?.formDesignerModule.updateFormField(formField, {
backgroundColor: finishedBackground
});
}
// Step 2: Define the API endpoint for flattening the document
const url = '/api/Home/FlattenDownload';
// Step 3: Save the current PDF as a Blob and convert it to Base64
pdfViewer.saveAsBlob()
.then(blob => convertBlobToBase64(blob))
.then(base64String => {
// Step 4: Prepare and send the POST request with the Base64 string
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
const requestData = JSON.stringify({ base64String });
xhr.onload = () => {
if (xhr.status === 200) {
// Step 5: Extract the Base64 PDF from the response
const responseBase64 = xhr.responseText.split('base64,')[1];
if (responseBase64) {
// Step 6: Convert Base64 to Blob and create a download link
const blob = createBlobFromBase64(responseBase64, 'application/pdf');
const blobUrl = URL.createObjectURL(blob);
// Step 7: Trigger the download and reload the PDF in read-only mode
downloadDocument(blobUrl);
pdfViewer.load(xhr.responseText, null);
// Step 8: Disable further signing actions
document.getElementById('e-pv-e-sign-finishSigning').disabled = true;
document.getElementById('e-pv-e-sign-employees').ej2_instances[0].enabled = false;
}
else {
console.error('Invalid base64 response.');
}
}
else {
console.error('Download failed:', xhr.statusText);
}
};
xhr.onerror = () => {
console.error('An error occurred during the download:', xhr.statusText);
};
xhr.send(requestData);
})
.catch(error => {
console.error('Error saving Blob:', error);
});
}
This API endpoint receives a base64-encoded PDF, flattens form fields and annotations to make them non-editable, and returns the updated document. It ensures the secure finalization of signed documents before download or archival.
public IActionResult FlattenDownload([FromBody] Dictionary<string, string> jsonObject)
{
try
{
string documentBase = "";
// Step 1: Extract base64 string from the request payload
if (jsonObject != null && jsonObject.ContainsKey("base64String"))
{
documentBase = jsonObject["base64String"];
}
// Step 2: Remove data URI prefix if present
string base64String = documentBase.Contains("data:application/pdf;base64,")
? documentBase.Split(new[] { "data:application/pdf;base64," }, StringSplitOptions.None)[1]
: documentBase;
// Step 3: Convert base64 string to byte array
byte[] byteArray = Convert.FromBase64String(base64String);
// Step 4: Load the PDF document from a byte array
PdfLoadedDocument loadedDocument = new PdfLoadedDocument(byteArray);
// Step 5: Flatten form fields and annotations if present
if (loadedDocument.Form != null)
{
loadedDocument.FlattenAnnotations(); // Optional: flatten annotations
loadedDocument.Form.Flatten = true; // Make form fields non-editable
}
// Step 6: Save the updated PDF to a memory stream
using (MemoryStream stream = new MemoryStream())
{
loadedDocument.Save(stream);
stream.Position = 0;
loadedDocument.Close(true);
// Step 7: Convert the stream back to base64
string updatedDocumentBase = Convert.ToBase64String(stream.ToArray());
string responseBase64 = "data:application/pdf;base64," + updatedDocumentBase;
// Step 8: Return the flattened PDF as a base64 string
return Content(responseBase64);
}
}
catch (Exception ex)
{
// Return error message if processing fails
return BadRequest($"Error processing PDF: {ex.Message}");
}
}
For more details, refer to the GitHub demo.
Q1: How scalable is this approach for large organizations with hundreds of users and documents?
This approach is highly scalable. By leveraging ASP.NET Core’s robust backend capabilities and Syncfusion® efficient PDF Viewer, the system can handle concurrent user sessions and large volumes of documents. Scalability can be further enhanced by integrating load balancers, distributed caching (e.g., Redis), and cloud-based storage solutions like Azure Blob Storage or Amazon S3 for handling user and form fields details.
Q2: Can this system be integrated with external identity providers like Azure AD or Okta for user authentication?
Yes, the system can be integrated with external identity providers such as Azure Active Directory, Okta, Auth0, or any OpenID Connect-compliant provider. ASP.NET Core supports these integrations out of the box, allowing secure and centralized user authentication and role management in the application level.
Q3: Can the PDF Viewer handle dynamic form field generation based on user roles fetched from a database?
Yes, dynamic form field generation is possible. You can fetch user roles from a database and programmatically add or modify form fields with unique ID in the PDF using Syncfusion’s PDF Viewer form fields update/add API. This allows role-based customization of the signing experience.
Q4: What happens if a user partially fills their fields and exits?
To handle partial completions, you can implement autosave or draft functionality. This involves saving the current state of the PDF (including filled fields) to a database or storage service. When the user returns, the system can reload the saved state, allowing them to continue from where they left off in the application level.
Q5: Is there support for multilingual form fields and UI for international users?
Absolutely. Syncfusion® components support localization and globalization. You can provide multilingual UI labels and form field placeholders by using resource files or localization libraries in ASP.NET Core, ensuring a seamless experience for international users based on the application requirement.
Thank you for reading! Implementing user-based eSignatures doesn’t have to be complex. With Syncfusion’s ASP.NET Core PDF Viewer, you can build secure, role-driven workflows that scale. This comprehensive user-based eSign PDF form provides a robust foundation for enterprise document workflows. The implementation ensures:
The modular architecture allows organizations to extend the system with additional features, such as integration with existing HR systems, advanced approval workflows, or industry-specific compliance requirements.
By implementing this solution, organizations can significantly streamline their document approval processes while maintaining the highest standards of security and legal compliance. The user-based approach ensures that the right people sign the right fields at the right time, eliminating confusion and reducing processing time.
Whether you’re building a simple approval system or a complex multi-stakeholder document workflow, this implementation provides the foundation and flexibility to meet your organization’s unique requirements.
Ready to build your own? Explore the new features of Essential Studio® from the license and downloads page or try our 30-day free trial to transform your data narratives. If you’ve any questions or need support, contact us through our support forum, support portal, or feedback portal. Happy coding!