DEV Community

Cover image for How to Implement User-Based eSignatures in ASP.NET Core PDF Viewer
Phinter Atieno for Syncfusion, Inc.

Posted on • Originally published at syncfusion.com on

How to Implement User-Based eSignatures in ASP.NET Core PDF Viewer

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.

Introduction

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.

Why User-based eSigning Is essential

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:

Granular permissions and control

  • Role-Based Assignments: Assign signature fields to specific users (employees, supervisors, managers, etc.)
  • Workflow Enforcement: Ensure documents follow proper approval hierarchies
  • Field-Level Security: Users can only access and modify their designated form fields

Enhanced security and compliance

  • User Authentication: Only authorized users can sign their assigned fields
  • Digital Integrity: Maintains document authenticity with tamper-evident signatures
  • Audit Trail: Complete tracking of who signed what and when
  • Legal Compliance: Meets requirements for ESIGN Act, eIDAS, and other regulations

Streamlined business processes

  • Automated Routing: Documents automatically flow between stakeholders
  • Reduced Errors: Prevents confusion about signing responsibilities
  • Faster Turnaround: Eliminates bottlenecks from unclear signing processes
  • Better User Experience: Clear visual indicators show each user’s responsibilities

Tools and setup: What you’ll need

To build this user-based eSign system, you’ll need the following components:

Core technology stack

  • NET Core (version 6.0 or later)
  • Syncfusion® PDF Viewer for ASP.NET Core package
  • Entity Framework Core (for user management) (optional)

Syncfusion® PDF Viewer: Your eSigning foundation

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:

  • Interactive Form Design: Drag-and-drop form field creation with real-time preview
  • Multi-Format Support: Works with PDFs containing existing form fields or blank documents
  • Advanced Tools: Built-in signature pad, text boxes, checkboxes, and custom form elements
  • Cross-Browser Compatibility: Consistent experience across modern browsers
  • Mobile Responsiveness: Touch-friendly interface for tablet and smartphone signing

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.

Additional requirements

  • Designer PDF Template: A base PDF document that will serve as your form template
  • User Authentication System: For managing user identities and permissions (optional – In this blog, we used the basic user management. You can integrate the authentication system based on the requirement)
  • Database: To store user details, form field assignments, and signing status (optional – In this blog, we used the application-level storage. You can integrate the database based on the requirement)

Implementation overview

Our user-based eSign system follows a five-phase workflow:

  1. User-Based Form Design: Assign form fields to specific users during the design phase
  2. Form Designer View: Interactive design interface for creating and configuring form fields
  3. Loading Designed Forms: Transform designed forms into signable documents
  4. User-Specific Signing: Allow users to sign only their assigned fields with validation
  5. Document Finalization: Flatten, secure, and distribute the completed document

Let’s dive into each phase with detailed implementation examples.

Phase 1: User-based form design

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:

User configuration structure

// 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: [] 
    }
];


Enter fullscreen mode Exit fullscreen mode

User selection interface

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>

Enter fullscreen mode Exit fullscreen mode

Color-coded field assignment

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;
}


Enter fullscreen mode Exit fullscreen mode

Phase 2: Form designer view – interactive field creation

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.

Document loading and mode initialization

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');
}


Enter fullscreen mode Exit fullscreen mode

Drag-and-drop field creation

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);
}


Enter fullscreen mode Exit fullscreen mode

Advanced field configuration

Beyond basic field creation, the system supports advanced configuration options:

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,
            }
        });
    }
}


Enter fullscreen mode Exit fullscreen mode

Reference designed PDF Form fields:

PDF form fields


PDF form fields

Phase 3: Loading designed forms in the viewer

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();
}


Enter fullscreen mode Exit fullscreen mode

User-specific field visibility

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' });
                    }
                });
            });
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Phase 4: User-specific signing process

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;
    }
}


Enter fullscreen mode Exit fullscreen mode

Real-time validation and feedback

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;
    }
}


Enter fullscreen mode Exit fullscreen mode

Reference screenshot after filling the form fields based on the user.

Form filling based on the user


Form filling based on the user

Phase 5: Document finalization and security

The final phase handles document completion, validation, flattening, and secure distribution.

Comprehensive validation before finalization

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);
        });
}


Enter fullscreen mode Exit fullscreen mode

Secure the document by flattening using the PDF library on the controller side

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}");
    }
}


Enter fullscreen mode Exit fullscreen mode

GitHub reference

For more details, refer to the GitHub demo.

Scalability recommendations

  • Database optimization: Use proper indexing for user and document queries
  • File storage: Consider cloud storage (Azure Blob, AWS S3) for large documents
  • Caching: Implement Redis for session management and temporary data
  • Load balancing: Use application load balancers for high-traffic scenarios
  • CDN integration: Serve static assets through a CDN for better performance

Conclusion

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:

  • Security: Granular user permissions and comprehensive audit trails
  • Usability: Intuitive interface with clear visual feedback
  • Compliance: Meets legal requirements for electronic signatures
  • Scalability: Designed to handle multiple users and complex workflows
  • Flexibility: Easily customizable for specific business requirements

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.

Additional Resources

Ready to build your own? Explore the new features of Essential Studio® from the license and downloads page or take advantage of 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!

Related Blogs

This article was originally published at Syncfusion.com.

Top comments (0)