DEV Community

Cover image for How to Secure Multi-User PDF Signing in a React PDF Viewer
Phinter Atieno for Syncfusion, Inc.

Posted on • Originally published at syncfusion.com on

How to Secure Multi-User PDF Signing in a React PDF Viewer

TL;DR: Struggling to build a secure multi‑party document‑signing workflow? You can enable user‑based e‑signatures in a React PDF Viewer by authenticating recipients, assigning signature fields, validating roles, and locking down PDF workflows to create a compliance‑ready signing experience.

Building a reliable e‑signature workflow is rarely straightforward. In today’s remote‑first environment, businesses expect fast, secure, and legally compliant signing tools that eliminate printing and manual processes. Developers, however, face challenges, especially when supporting multiple signers, enforcing signing order, validating fields, and securing completed documents.

This guide walks you through implementing a structured, recipient‑aware e‑signature workflow using Syncfusion® React PDF Viewer. Throughout this tutorial, you’ll learn how to:

  • Authenticate recipients before enabling signing.
  • Assign signature fields dynamically with clear visual cues.
  • Validate user inputs in real time for accuracy and compliance.
  • Finalize and lock documents securely through field flattening.

By the end, you’ll have an interactive signing workflow that transforms static PDFs into smart, dynamic documents suitable for modern digital processes.

Setting up a React project with Syncfusion PDF Viewer

Before you can build the signing workflow, you’ll need to set up your React environment and configure the Syncfusion PDF Viewer. The steps below guide you through initializing your project, installing dependencies, importing styles, and enabling the Form Designer.

Step 1: Initialize your React project

Start by creating a new React application using Create React App command:

npx create-react-app esigningagreement
cd esigningagreement
Enter fullscreen mode Exit fullscreen mode

This scaffolds a clean React workspace to host your PDF signing workflow.

Step 2: Install Syncfusion React PDF Viewer

Next, install the required Syncfusion React PDF Viewer package:

npm install @syncfusion/ej2-react-pdfviewer
Enter fullscreen mode Exit fullscreen mode

This package provides the components and APIs needed to load, edit, and design interactive PDFs in React.

Step 3: Import required styles

To ensure consistent styling across viewer components, import the required CSS files into index.css:

@import '../node_modules/@syncfusion/ej2-base/styles/material.css';  
@import '../node_modules/@syncfusion/ej2-buttons/styles/material.css';
@import '../node_modules/@syncfusion/ej2-dropdowns/styles/material.css';  
@import '../node_modules/@syncfusion/ej2-inputs/styles/material.css';  
@import '../node_modules/@syncfusion/ej2-navigations/styles/material.css';
@import '../node_modules/@syncfusion/ej2-popups/styles/material.css';
@import '../node_modules/@syncfusion/ej2-splitbuttons/styles/material.css';
@import "../node_modules/@syncfusion/ej2-pdfviewer/styles/material.css";
Enter fullscreen mode Exit fullscreen mode

These style sheets ensure that every toolbar element, dialog, and form field renders with the correct theme and layout.

Step 4: Load and configure the PDF Viewer with form designer

With the setup complete and the PDF Viewer installed, you’re ready to load a PDF document and enable the Form Designer tools. These tools allow you to create and edit interactive form fields directly within the viewer, making them ideal for building secure and flexible e‑signature workflows.

To proceed, navigate to the index.tsx TypeScript file and add the following code to load the document and configure the PDF Viewer with form designer capabilities.

{/* PDF Viewer with form designer and other tools enabled */}
<PdfViewerComponent
    id="pdfViewer"
    ref={pdfViewer}
    resourceUrl="https://cdn.syncfusion.com/ej2/31.2.10/dist/ej2-pdfviewer-lib"
    style={{ height: "calc(100vh - 70px)"}}
    enableToolbar={true}
    documentPath={window.location.origin + "/assets/SoftwareLicenseAgreement.pdf"}
    formFieldAdd={formFieldAdd}
    isFormDesignerToolbarVisible={true}
    documentLoad={documentLoaded}
    showNotificationDialog={false}
    formFieldPropertiesChange={fieldChange}
    enableDownload = {false}>

    {/* Inject required PDF viewer services and features */}
    <Inject services={[
                    Toolbar, Magnification, Navigation, Annotation, LinkAnnotation, BookmarkView, ThumbnailView, Print, TextSelection, TextSearch, FormFields, FormDesigner
    ]} />
</PdfViewerComponent>
Enter fullscreen mode Exit fullscreen mode

Creating and editing interactive form fields inside the React PDF Viewer


Creating and editing interactive form fields inside the React PDF Viewer

This configuration enables the Form Designer toolbar and supporting services, allowing you to create, assign, and manage interactive form fields directly inside the React PDF Viewer.

Recipient authentication: The first step to secure the signing flow

Before you design or assign any form fields, you need to ensure that only authorized recipients can sign the document. This is where the Recipient Authentication Setup page becomes essential. It forms the foundation of a secure, user‑based e‑signature workflow by allowing you to define, manage, and validate every signer before moving into document preparation.

Getting this step right prevents misassigned fields, accidental signer mix-ups, and workflow disruptions, creating a smoother experience for both developers and recipients.

Let’s walk through the setup:

Step 1: Create a recipient form

Begin by creating a new TypeScript JSX file named RecipientSetup.tsx. This component enables users to add, edit, and remove recipients before the PDF Viewer is loaded.

The form collects essential signer information such as Name and Email, and uses modern UI components to keep the interface clean and intuitive. The page starts with one recipient by default, and the + Add Recipient button lets users dynamically add more, making it flexible enough for everything from simple two‑party signatures to complex, multi‑signer agreements.

Once recipients are confirmed, users can move on to adding handwritten signatures and mapping fields to the proper signers.

{/* Form for entering recipient details */}
<form id="userForm" ref={formRef} onSubmit={formSubmit}>
    {recipients.map((recipient, idx) => (
        <div
            key={idx}
            style={{
                display: 'flex',
                justifyContent: 'center',
                marginBottom: '40px'
            }}>
            <div
                style={{
                    display: 'flex',
                    gap: '30px',
                    alignItems: 'center',
                    width: '80%'
                }}>

                {/* Recipient Name Field */}
                <div style={{ flex: 1 }}>
                    <label
                        style={{
                            display: 'block',
                            marginBottom: '5px',
                            fontWeight: 500,
                        }}>
                        Recipient Name
                    </label>
                    <TextBoxComponent
                        placeholder="Enter recipient name"
                        floatLabelType="Auto"
                        cssClass="e-outline"
                        value={recipient.userName}
                        name={`name_${idx}`}
                        input={(e) => recipientChange(idx, 'userName', e.value)}/>
                </div>

                {/* Recipient Email Field */}
                <div style={{ flex: 1 }}>
                    <label
                        style={{
                            display: 'block',
                            marginBottom: '5px',
                            fontWeight: 500,
                        }}>
                        Recipient Email
                    </label>
                    <TextBoxComponent
                        placeholder="Enter recipient email"
                        floatLabelType="Auto"
                        cssClass="e-outline"
                        value={recipient.userEmail}
                        name={`email_${idx}`}
                        input={(e) => recipientChange(idx, 'userEmail', e.value)/>
                </div>

                {/* Remove recipient button */}
                <ButtonComponent
                    cssClass="e-flat e-danger"
                    iconCss="e-icons e-trash"
                    style={{ marginTop: '22px', height: '40px', width: '40px' }}
                    onClick={() => removeRecipient(idx)}/>
            </div>
        </div>
    ))}

    {/* Submit button */}
    <div
        style={{
            display: 'flex',
            justifyContent: 'center',
            marginTop: '40px'
        }}>
        <ButtonComponent
            ref={formBtnRef}
            isPrimary={true}
            cssClass="e-success"
            type="submit"
            style={{ width: '40%', height: '40px' }}>
            Continue to Form Creation
        </ButtonComponent>
    </div>
</form>
Enter fullscreen mode Exit fullscreen mode

Step: 2 Real-time input validation

Accurate signer information is essential for a secure workflow. Missing names, invalid emails, or malformed inputs can break the signing process later. Using FormValidator, you can validate fields dynamically as recipients are added or removed.

Validation enforces:

  • Required fields for both name and email.
  • A minimum name length for clarity and professionalism.
  • A valid email format to prevent delivery issues and maintain trust.

The best part? Validation rules automatically update whenever the recipient list changes, ensuring a seamless user experience.

useEffect(() => {
        if(formRef.current) {
            const dynamicRules: Record<string, any> = {};

            recipients.forEach((_, idx) => {
                dynamicRules[`name_${idx}`] = {
                    required: [true, '* Name is required'],
                    minLength: [3, '* Name must be at least 3 characters']
                };
                dynamicRules[`email_${idx}`] = {
                    required: [true, '* Email is required'],
                    email: [true, '* Please enter a valid email']
                };
            });

            if (validatorRef.current) {

                // Update existing validator rules without destroying them
                validatorRef.current.rules = dynamicRules;
            } else {

                // Create new validator only if it doesn't exist
                validatorRef.current = new FormValidator(formRef.current, {
                    rules: dynamicRules
                });
            }
        }
    }, [recipients]);
Enter fullscreen mode Exit fullscreen mode

Step 3: Navigation and data persistence

Once all inputs pass validation, the recipient list is stored safely in localStorage. This allows the PDF Viewer to access signer information when generating and assigning form fields.

After the details are saved, the user can click Continue to Form Creation to move directly into the PDF Viewer interface, where the interactive document design begins.

// Handles form submission, validates input, stores data, and navigates to the PDF viewer
    const formSubmit = (event: React.FormEvent) => {       
        event.preventDefault();
        if (validatorRef.current && validatorRef.current.validate()) {
            const validRecipients = recipients.filter(
                r => r.userName.trim() !== '' && r.userEmail.trim() !== ''
            );
            localStorage.setItem('recipients', JSON.stringify(validRecipients));
            navigate('/pdf-viewer');
        }
    };
Enter fullscreen mode Exit fullscreen mode

Form creation in React PDF Viewer


Form creation in React PDF Viewer

Interactive PDF designer: Add and assign signature fields easily

With authentication complete, the next step is converting a static PDF into a structured, signable form where multiple recipients can fill and sign fields in the correct order.

Step 1: Import recipients from local storage

Begin by loading the recipient information saved during the authentication step. Retrieve the stored data from localStorage, convert it into the internal UserDetails structure, and merge it with any existing user list, automatically filtering out duplicates.

This step ensures:

  • A secure, structured workflow for multi‑party agreements.
  • Accurate, recipient‑specific field assignment (especially for signatures).
  • Reliable mapping between each signer and their form fields.

Once the data is loaded, you can use the Form Designer Toolbar to add fields such as Signature, Initials, or Text, and assign each field to the correct recipient.

// Load recipients from localStorage, convert them to userDetails format, and merge without duplicates
useEffect(() => {
  const storedRecipients = localStorage.getItem('recipients');

  try {
    let fieldId = 0;
    const newUsers: UserDetails[] = [];

    if (storedRecipients) {
      const recipients = JSON.parse(storedRecipients);

      recipients.forEach((r: any) => {
        newUsers.push({
          Name: r.userName,
          Mail: r.userEmail,
          fieldIds: [fieldId.toString()]
        });
        fieldId++;
      });
    }

    setUserDetails(prev => {
      const existingEmails = new Set(prev.map(u => u.Mail));
      const filteredNewUsers = newUsers.filter(
        u => !existingEmails.has(u.Mail)
      );
      return [...filteredNewUsers, ...prev];
    });
  } catch (error) {
    console.error("Error parsing user info or recipients:", error);
  }
}, []);
``
Enter fullscreen mode Exit fullscreen mode

Step 2: Map fields to recipients

After loading recipients, the next step is assigning each form field to the correct signer. Whether it’s a signature box, initials, or a text input, proper field mapping is essential for a secure and organized signing experience.

This step provides important benefits:

  • Clear and accurate field ownership for each participant.
  • Protection of sensitive data through controlled field access.
  • Prevention of unauthorized edits, ensuring workflow integrity.

Correctly mapped fields mean each signer interacts only with their assigned inputs, maintaining clarity, compliance, and trust throughout the signing process.

// Renders the dialog content with user selection and submit button
const getRecipientDialogContent = () => {
    return (
        <div className="dialog-content">
            <p style={{ fontWeight: 'bold', fontSize: '16px' }}>
                    Select the User:
            </p>
            <div className="radio-options" style={{ display: 'flex', gap: '50px', marginTop: '30px', }}>
                {userDetails.map((user, index) => (
                    <div className="radio-item" key={index}>
                        <RadioButtonComponent
                            ref={radioBtnRef}
                            label={user.Name}
                            name="userType"
                            value={user.Name}
                            checked={selectedUser === user.Name}
                            change={userChange}/>
                    </div>
                ))}
            </div>
            <div className="button-container" style={{ marginTop: '25px', textAlign: 'center' }}>
                <ButtonComponent className="e-btn e-primary" style={{ width: '80%', borderRadius: '5px' }} onClick={userSelectionSubmit}>Submit</ButtonComponent>
            </div>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

Your dialog for selecting a recipient looks like this:

Mapping fields to recipients


Mapping fields to recipients

Step 3: Enhance field visibility for recipients

To make the signing experience intuitive, especially in multi‑signer workflows, style each assigned field using a unique background color. This is done via the customData property and provides immediate visual clarity for each signer.

This enhancement offers:

  • A clearly guided signing process.
  • Reduced confusion when multiple recipients are involved.
  • Strong linkage between recipients and their assigned fields.

By applying distinct colors, signers can easily identify the fields they need to complete, keeping the workflow organized and user‑friendly.

// Handles the Submit button click after user selection
const userSelectionSubmit = () => {
    setSelectedUser(radioBtnRef.current?.getSelectedValue() || '');

    setStatus({ hideDialog: false });
    dialogClose();

    // Get the last added form field
    const formFields = pdfViewer.current?.retrieveFormFields();
    const lastField = formFields?.[formFields.length - 1];

    // Find the selected user's details
    const user = userDetails.find(u => u.Name === selectedUser);

    if (lastField && user) {
        let color = userColors[user.Mail];
        if (!color) {
            color = getRandomLightColor();
            setUserColors(prev => ({ ...prev, [user.Mail]: color }));
        }

        // Check if the field is a signature field (adjust property as needed)
        const isSignatureField = lastField.type === 'SignatureField' || lastField.name?.toLowerCase().includes('sign');

        // Use 40% opacity for signature fields, solid for others
        userColour.current = isSignatureField ? hexToRgba(color, 0.4) : color;

        pdfViewer.current?.formDesigner.updateFormField(
                lastField,
                {
            customData: {
                name: user.Name,
                email: user.Mail
            },
                name: `${user.Name || ""} ${lastField.name || ""}`.trim(),
                backgroundColor: userColour.current,
                isRequired: true,
                borderColor: '#303030'
            } as any
        );
     }
};
Enter fullscreen mode Exit fullscreen mode

Enhancing field visibility using the customData property


Enhancing field visibility using the customData property

When the Form Designer Toolbar is open, the viewer is in design mode, allowing you to place and configure fields. Closing the toolbar switches the viewer back into interactive mode, where recipients can begin filling and signing their assigned fields.

Enforce signing order and validation

To maintain a structured, compliant, and error‑free signing experience, it’s important to enforce the correct signing order and validate all required fields before allowing recipients to proceed. This ensures no signer is skipped and helps preserve the integrity of the document throughout the workflow.

Step:1 Validate signing context

When the Form Designer toolbar is closed, the PDF Viewer automatically switches to interactive mode. At this point, the application should determine which signer is active and display only the fields assigned to that recipient. This keeps the interface clean and prevents users from interacting with fields that don’t belong to them.

This approach ensures:

  • Each signer sees only their assigned fields.
  • The signing flow remains intuitive and distraction‑free.
  • Errors are minimized in multi‑recipient workflows.

By validating context and controlling visibility, you maintain a secure and compliant e‑signature flow from signer to signer.

// Initializes event listener for closing the form designer and shows the user dropdown
    const documentLoaded = () => {
        document.getElementById("pdfViewer_formdesigner_closeContainer")?.addEventListener("click", async function () {
            setShowDropdown(true);
            if (pdfViewer.current)
                pdfViewer.current.designerMode = false;
            showOnlyCurrentUserFields();
        })

    }
Enter fullscreen mode Exit fullscreen mode

Step:2 Enforce sequential signing

Next, enforce a strict signing order, ensuring each recipient completes their required fields before the workflow advances.

When transitioning to the next signer:

  • Validate all required fields assigned to the current signer.
  • Prevent progression if any mandatory fields are incomplete.
  • Display a helpful message prompting the signer to fill missing inputs.

This prevents incomplete signatures, maintains document consistency, and guarantees a reliable, structured signing experience, especially important when adding legally binding e‑signatures to PDFs.

Enforcing sequential signing using the React PDF Viewer


Enforcing sequential signing using the React PDF Viewer

Finalizing the document

Once all recipients have completed their assigned fields, the document must be finalized securely. Finalization includes enabling the finishing action and converting the signed PDF into a non‑editable file.

This phase involves two key steps.

Step:1 Activate the finish signing button

The Finish Signing button stays disabled until all mandatory fields are completed. By tracking field changes through the formFieldPropertiesChange event, the app can automatically detect when the signer has fulfilled all requirements.

After validation succeeds:

  • Enable the Finish Signing button.
  • Set enableDownload to true so users can download or submit the completed document.
  • Optionally allow the signer to add a handwritten signature for a more authentic experience.

Activating the finish signing button in the React PDF Viewer


Activating the finish signing button in the React PDF Viewer

Step:2 Secure the document

The final step is to flatten all form fields. Flattening converts interactive elements into static content, making the PDF non‑editable and tamper‑proof.

This is essential because:

  • It prevents further modification of completed signatures.
  • Ensures document integrity.
  • Meets legal and compliance standards for digital signing.

The code below handles saving, flattening, downloading, and securing the viewer UI.

// Handles the final signing process; saves the PDF, sends it to the server, downloads the signed version, and disables UI controls
    const finishSigning = () => {

        // Show the spinner when signing starts
        const spinnerTarget = document.getElementById('container');
        if (spinnerTarget) showSpinner(spinnerTarget);
        const url = "https://document.syncfusion.com/web-services/pdf-viewer/api/pdfviewer/FlattenDownload";
        pdfViewer.current?.saveAsBlob().then((blob) => {
            return convertBlobToBase64(blob);
        }).then((base64String) => {
            const httpResponse = new XMLHttpRequest();
            httpResponse.open('POST', url, true);
            httpResponse.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
            const requestData = JSON.stringify({ base64String });
            httpResponse.onload = () => {

                // Always hide spinner at the end (success or error)
                if (spinnerTarget) hideSpinner(spinnerTarget);
                if (httpResponse.status === 200) {
                    const responseBase64 = httpResponse.responseText.split('base64,')[1];
                    if (responseBase64) {
                        const blob = createBlobFromBase64(responseBase64, 'application/pdf');
                        const blobUrl = URL.createObjectURL(blob);
                        downloadDocument(blobUrl);
                        pdfViewer.current?.load(httpResponse.responseText, "");
                        if (btnElement.current)
                            btnElement.current.disabled = true;
                        if (userMenu.current)
                            userMenu.current.enabled = false;
                    } else {
                        console.error('Invalid base64 response.');
                    }
                } else {
                    console.error('Download failed:', httpResponse.statusText);
                }
            };
            httpResponse.onerror = () => {
                if (spinnerTarget) hideSpinner(spinnerTarget);
                console.error('An error occurred during the download:', httpResponse.statusText);
            };
            httpResponse.send(requestData);
        }).catch((error) => {
            if (spinnerTarget) hideSpinner(spinnerTarget);
            console.error('Error saving Blob:', error);
        });
    }
Enter fullscreen mode Exit fullscreen mode

Flattening the form fields using the React PDF Viewer


Flattening the form fields using the React PDF Viewer

Note: Refer to the official documentation for more on flattening form fields to prevent edits and ensure signature security.

GitHub reference

To explore the full implementation, you can access the complete project in the GitHub demo.

Conclusion

Thanks for reading! In this guide, you learned that creating a secure and intuitive digital signing experience goes beyond simply adding signature fields. It requires designing a structured, user-aware workflow that enforces signing order, validates input, and ensures compliance, all while keeping the process smooth and user-friendly.

With Syncfusion’s React PDF Viewer, developers gain full control over every stage of the user-based e-signature process. By leveraging advanced features like dynamic field assignment, real-time validation, conditional UI behavior, and secure document finalization. This approach strengthens security and integrates seamlessly into modern React applications, making digital signing workflows efficient and reliable.

Dive deeper with the React PDF Viewer User guide or try it live in the React PDF Viewer demo. If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.

You can also contact us through our support forum, support portal, or feedback portal for queries. We are always happy to assist you!

Related Blogs

This article was originally published at Syncfusion.com.

Top comments (0)