DS Server

Custom Signing Endpoints

Topic

When a user signs a document in the Document Viewer, the signed document can be sent on to an endpoint of your choice, where your own code stores it, applies a certificate or starts a workflow. A plugin can provide that endpoint inside DS Server. Since DS Server 5.0, the request to the endpoint carries the current OAuth access token, so the plugin can accept signed documents only from clients with a valid token.

What follows covers how the signed document reaches the endpoint, a plugin that receives and stores it, and the Document Viewer settings that point to it. The plugin project is set up as in Creating a Plugin.

How a Custom Signing Endpoint Works

The Document Viewer shows a signature bar when its signature settings enable it. The user signs the signature boxes of the document, and the signed document then takes this route:

Browser                     DS Server                                    Plugin in DS Server

Document Viewer  --signed-->  Document Viewer controller
                              |
                              +--HTTP POST, SignatureData as JSON,------>  POST /plugin/signatures/sign
                                 with the current access token (5.0)       stores or processes the document
  • The redirect URL. The property RedirectUrlAfterSignature of the signature settings names the endpoint. The Document Viewer controller sends an HTTP POST request to this URL with a SignatureData object as its body.
  • The access token. Since version 5.0, the Document Viewer controller adds the access token of the current viewer to this request. An endpoint that uses the access token validation of DS Server then accepts the request, while other callers without a valid token are rejected.
  • The address. DS Server sends the request itself, not the browser. Use an address under which DS Server reaches its own plugin, such as its public address or http://localhost:5000 on the server.

The Signature Data

The JSON body holds the result of the signing process. The properties an endpoint typically reads, named as in the SignatureData class that Text Control's signature plugin sample deserializes:

PropertyContent
SignedDocument.DocumentThe signed document, encoded as Base64
SignedDocument.SignerName, SignedDocument.SignerInitialsThe name and initials of the signer
SignedDocument.NumPagesThe number of pages of the signed document
UniqueIdA unique string rendered on the document, which can serve as the envelope ID when you store it
DocumentNameThe name of the document
SignatureBoxesThe signature boxes, each with its Name and whether it is Signed
FormFieldsThe completed form fields, each with its Name and Value

The model binding of ASP.NET Core matches the JSON names to the properties of a class without regard to case.

Create the Signing Plugin

The plugin below receives the signed document, converts it to PDF with the IDocumentProcessingService of DS Server and saves the file in a folder of the plugin. It consists of four files.

SigningSettings.cs holds the folder for the signed documents:

namespace SigningPlugin;

public class SigningSettings
{
    public string OutputFolder { get; set; } = "";
}

SignatureRequest.cs maps the part of the signature data the endpoint reads:

namespace SigningPlugin;

// The parts of the signature data that the endpoint reads
public class SignatureRequest
{
    public string? UniqueId { get; set; }
    public SignedDocumentData? SignedDocument { get; set; }
}

public class SignedDocumentData
{
    public string? Document { get; set; }
    public string? SignerName { get; set; }
}

SigningPlugin.cs reads the folder from the configuration and creates it when the plugin starts:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TXTextControl.DocumentServices.Plugin.Abstractions;

namespace SigningPlugin;

public class SigningPlugin : IPlugin
{
    public string Name => "SigningPlugin";
    public string Description => "Receives signed documents from the Document Viewer at /plugin/signatures/sign.";
    public string Version => "1.0.0";

    public void ConfigureServices(IServiceCollection services, PluginContext context)
    {
        // A relative folder is resolved against the plugin's own folder
        string pluginFolder = Path.GetDirectoryName(typeof(SigningPlugin).Assembly.Location)!;
        string outputFolder = context.Configuration["SigningPlugin:OutputFolder"] ?? "SignedDocuments";

        services.AddSingleton(new SigningSettings
        {
            OutputFolder = Path.Combine(pluginFolder, outputFolder)
        });
    }

    public void OnStart(IServiceProvider services, PluginContext context)
    {
        SigningSettings settings = services.GetRequiredService<SigningSettings>();
        Directory.CreateDirectory(settings.OutputFolder);

        services.GetService<ILogger<SigningPlugin>>()?
            .LogInformation("SigningPlugin stores signed documents in {Folder}", settings.OutputFolder);
    }
}

SignaturesController.cs is the endpoint:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using TXTextControl.DocumentServices.DocumentProcessing.Abstractions;
using TXTextControl.DocumentServices.DocumentProcessing.Models;
using TXTextControl.DocumentServices.Plugin.Abstractions;

namespace SigningPlugin;

[ApiController]
[Route("plugin/signatures")]
[Authorize(AuthenticationSchemes = PluginContext.AuthenticationScheme)]
public class SignaturesController : ControllerBase
{
    private readonly IDocumentProcessingService m_processing;
    private readonly SigningSettings m_settings;
    private readonly ILogger<SignaturesController> m_logger;

    public SignaturesController(IDocumentProcessingService processing,
        SigningSettings settings, ILogger<SignaturesController> logger)
    {
        m_processing = processing;
        m_settings = settings;
        m_logger = logger;
    }

    [HttpPost("sign")]
    public async Task<IActionResult> Sign([FromBody] SignatureRequest request)
    {
        if (string.IsNullOrEmpty(request.SignedDocument?.Document))
        {
            return BadRequest("The request contains no signed document.");
        }

        // The signed document arrives encoded as Base64
        byte[] document = Convert.FromBase64String(request.SignedDocument.Document);

        byte[] pdf = await m_processing.ConvertAsync(
            document, ReturnFormat.PDF, HttpContext.RequestAborted);

        string fileName = $"{Guid.NewGuid():N}.pdf";
        await System.IO.File.WriteAllBytesAsync(
            Path.Combine(m_settings.OutputFolder, fileName), pdf, HttpContext.RequestAborted);

        m_logger.LogInformation("Stored the document signed by {Signer} as {File}",
            request.SignedDocument.SignerName, fileName);

        return Ok(fileName);
    }
}

The code works as follows:

  • SigningPlugin implements only the members it needs: ConfigureServices registers the settings and OnStart creates the folder. The folder is SignedDocuments in the plugin's subfolder unless the key SigningPlugin:OutputFolder of appsettings.json names another one.
  • [Authorize(AuthenticationSchemes = PluginContext.AuthenticationScheme)] makes DS Server check the access token of every request to the controller, as described below.
  • Sign decodes signedDocument.document and converts it with ConvertAsync into ReturnFormat.PDF. The document arrives in the internal format of TX Text Control, which ConvertAsync accepts like any other format.
  • The file gets a new GUID as its name, so no value from the request becomes part of a path. The endpoint returns the file name.

Build the project, copy SigningPlugin.dll into Plugins/SigningPlugin/ of DS Server and restart DS Server, as described in Build and Deploy the Plugin.

Protect the Endpoint

The endpoints of a plugin are not covered by the OAuth protection of the built-in services: without an Authorize attribute, DS Server passes every request to them, with or without a token. A signing endpoint receives documents, so protect it with the authentication scheme of DS Server, TXTextControl, as the controller above does.

With OAuth enabled in the admin portal, DS Server then answers a request without a valid access token with 401 Unauthorized and writes the reason to its log:

[Information] TXTextControl was not authenticated. Failure message: OAuth authorization: No authorization header, access token query parameter or access token cookie found.
[Information] AuthenticationScheme: TXTextControl was challenged.

The request of the Document Viewer controller carries the access token of the viewer since version 5.0, so it passes the check. With OAuth disabled, the scheme accepts every request, as the built-in services do. Enable OAuth describes the switch, and Creating a Plugin shows the results of the check in a table.

Point the Document Viewer to the Endpoint

Set RedirectUrlAfterSignature in the signature settings of the Document Viewer. In an ASP.NET Core view with the package TXTextControl.DocumentServices.DocumentViewer:

@using TXTextControl.DocumentServices.DocumentViewer

@await Html.TXTextControl().DocumentViewer(settings => {
    settings.BasePath = "https://ds.example.com";
    settings.OAuthSettings.ClientId = "<client ID>";
    settings.OAuthSettings.ClientSecret = "<client secret>";
    settings.OAuthSettings.RedirectUri = "https://your.app/textcontrol/documentviewer/auth/callback";
    settings.DocumentData = Model.DocumentData;
    settings.SignatureSettings = new SignatureSettings {
        ShowSignatureBar = true,
        OwnerName = "Paul Paulsen",
        SignerName = "Tim Typer",
        SignerInitials = "TT",
        RedirectUrlAfterSignature = "https://ds.example.com/plugin/signatures/sign",
        SignatureBoxes = new SignatureBox[] {
            new SignatureBox("txsign") { SigningRequired = true }
        }
    };
}).RenderAsync()
  • BasePath and OAuthSettings connect the viewer to DS Server, as described in Client-Side Libraries: ASP.NET Core.
  • DocumentData is the Base64-encoded document with a signature box. SignatureBox("txsign") names the frame of the document that serves as the box.
  • RedirectUrlAfterSignature is the address of the plugin endpoint on DS Server.

The Angular, React and JavaScript viewers take the same setting as redirectUrlAfterSignature in their SignatureSettings object: see Angular and React.

Test the Endpoint Without the Viewer

You can call the endpoint with cURL to check the plugin and the token validation before you connect the viewer. Request an access token with the client credentials of a security profile, as described in OAuth Grant Types, and send a body with a Base64-encoded document:

curl -X POST http://localhost:5000/plugin/signatures/sign \
  -H "Authorization: Bearer <access token>" \
  -H "Content-Type: application/json" \
  -d '{ "uniqueId": "abc", "signedDocument": { "document": "<Base64-encoded document>", "signerName": "Tim Typer" } }'

The endpoint answers with the name of the stored file, and the log shows the entry of the controller:

[Information] Stored the document signed by Tim Typer as 94a1d9c9da63402b87cccda1388f909c.pdf

Without the Authorization header, the endpoint answers 401 Unauthorized when OAuth is enabled.

Apply a Certificate

The plugin above stores the document as the Document Viewer signed it. To add a digital signature with your organization's certificate, Text Control's open source TXTextControl.DSServer.SignaturePlugin loads the signed document with TX Text Control .NET Server and signs each signature box with a .pfx certificate whose path and password it reads from appsettings.json. It also serves pages under /signature-plugin, linked from the Plugins page of the admin portal, that list the signed documents. The sample was written for DS Server 4.1: it targets .NET 8 and references version 4.1.0 of the plugin package and TXTextControl.TextControl.Core.SDK 33.0.3, and its signing controller has the Authorize attribute commented out. The blog article Extending DS Server with Custom Digital Signature APIs walks through its code.

Next Steps