This open-source DS Server signature plugin reference implementation allows you to extend DS Server with your own digital signing logic and certificate handling without modifying the core. The plugin shows how the new architecture allows you to add fully integrated API endpoints and custom UI components. Why a Signature Plugin? DS Server supports digital signatures by default through the DocumentViewer component, which allows users to sign documents directly in the browser. However, many organizations need to route signed documents through custom signing logic to apply corporate certificates, integrate with internal PKI systems, or trigger compliance workflows, for instance. This plugin does exactly that. It adds a custom /signatures/sign API endpoint to the DS Server. This endpoint receives signed documents from the DocumentViewer and passes them to your signing process. How It Works When a document is signed in the web viewer, the signed document data is posted to the signature-plugin/signatures/sign endpoint. The plugin uses TX Text Control .NET Server for ASP.NET to load the document and access the signature fields. Then, it applies a digital signature using a .pfx certificate file. The certificate configuration is flexible. Both the path and the password can be adjusted in the DS Server app.settings file. "SignaturePlugin": { "CertificatePath": "certificate.pfx", "CertificatePassword": "password" } This makes it easy to integrate different certificates for each environment, such as testing, staging, or production. The Plugin Structure The plugin implements the IPlugin interface and registers its own services, routes, and controllers. Below is a brief overview of the main class: using DSSignaturePlugin.Services; using Microsoft.AspNetCore.Builder; sing Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using TXTextControl.DocumentServices.Plugin.Abstractions; public class SignaturePlugin : IPlugin { public string Name => "Text Control DS Server Signature Plugin"; public string Description => "Adds a /signature-plugin/signatures/sign endpoint."; public string Version => "1.0.0"; /// <summary> /// The base path of the plugin's web interface. This is used to create a /// link in the DS Server plugin overview. /// </summary> public string UIBasePath => "/signature-plugin"; public void ConfigureServices(IServiceCollection services, PluginContext context) { var certificateName = context.Configuration["SignaturePlugin:CertificatePath"] ?? "certificate.pfx"; var certificatePassword = context.Configuration["SignaturePlugin:CertificatePassword"] ?? "password"; services.AddSingleton(new CertificateSettings { CertificatePath = certificateName, CertificatePassword = certificatePassword }); var asm = typeof(SignaturePlugin).Assembly; services.AddControllersWithViews() .AddApplicationPart(asm); } public void ConfigureMiddleware(WebApplication app, PluginContext context) { var state = app.Services.GetService<CertificateSettings>() ?? throw new InvalidOperationException("SignatureSettings service not registered."); var group = app.MapGroup(UIBasePath); group.MapControllerRoute( name: "signatureplugin-mvc", pattern: "{controller=SignatureUi}/{action=Index}/{id?}") .WithMetadata(new Microsoft.AspNetCore.Routing.SuppressLinkGenerationMetadata()); } public void OnStart(IServiceProvider services, PluginContext context) { // This method is called when the plugin is started. You can use this to // initialize resources or perform startup logic. Implementing OnStart is optional. var logger = services.GetService<ILogger<SignaturePlugin>>(); var settings = services.GetService<CertificateSettings>(); logger?.LogInformation("{Name} (v{Version}) started. Certificate: {Certificate}", Name, Version, settings?.CertificatePath); } public void OnStop() { // Cleanup logic if needed. This is also optional. } } In ConfigureServices, the plugin: Reads the certificate configuration and registers it as a singleton service. Adds the plugin's MVC controllers and views to DS Server via AddApplicationPart. In ConfigureMiddleware, it maps a new route group under /signature-plugin, which hosts both API and UI routes. The Signing Controller The SignController is at the heart of the plugin. The SignController defines the custom web API that receives signed documents from the DocumentViewer and applies server-side digital signatures. using DSSignaturePlugin.Services; using Microsoft.AspNetCore.Mvc; using System.Security.Cryptography.X509Certificates; using System.Text.Json; using TXTextControl; namespace DSSignaturePlugin.Controllers { [ApiController] //[Authorize(AuthenticationSchemes = "TXTextControl")] [Route("signature-plugin/signatures/[controller]")] public class SignController : ControllerBase { private readonly CertificateSettings m_settings; public SignController(CertificateSettings settings) { m_settings = settings; } // Testing endpoint to verify that the controller is reachable [HttpGet] public string Get() { return "Hello from SignController!"; } [HttpPost] public string Post([FromBody] object data) { if (data is null) throw new ArgumentException("Request body is required."); // robust manual deserialization SignatureData signatureData; try { if (data is JsonElement je) signatureData = je.Deserialize<SignatureData>()!; else signatureData = JsonSerializer.Deserialize<SignatureData>(data.ToString() ?? string.Empty)!; if (signatureData is null) throw new JsonException("Failed to deserialize SignatureData."); } catch (JsonException ex) { throw new ArgumentException($"Invalid JSON: {ex.Message}"); } if (string.IsNullOrWhiteSpace(signatureData?.SignedDocument?.Document)) throw new ArgumentException("Missing signed document data."); byte[] docBytes; try { docBytes = Convert.FromBase64String(signatureData.SignedDocument.Document); } catch (FormatException) { throw new ArgumentException("Signed document is not valid Base64."); } var assemblyPath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); // prepare paths var certPath = Path.Combine(assemblyPath!, "Certificates", m_settings.CertificatePath); var docsDir = Path.Combine(assemblyPath!, "Documents"); Directory.CreateDirectory(docsDir); using var cert = new X509Certificate2(certPath, m_settings.CertificatePassword, X509KeyStorageFlags.Exportable); using var tx = new TXTextControl.ServerTextControl(); tx.Create(); // load the internal format tx.Load(docBytes, TXTextControl.BinaryStreamType.InternalUnicodeFormat); // signature fields (null/empty-safe) var signatureFields = (signatureData.SignatureBoxes ?? Enumerable.Empty<SignatureBox>()) .Where(b => !string.IsNullOrWhiteSpace(b?.Name)) .Select(b => new DigitalSignature(cert, null, b!.Name)) .ToArray(); var saveSettings = new TXTextControl.SaveSettings { CreatorApplication = "Your Application", SignatureFields = signatureFields }; var uniqueFileName = $"{Guid.NewGuid():N}.pdf"; var outputPath = Path.Combine(docsDir, uniqueFileName); tx.Save(outputPath, TXTextControl.StreamType.AdobePDF, saveSettings); return uniqueFileName; } } } When the DocumentViewer submits a signed document, it is sent as Base64-encoded data to the POST /signature-plugin/signatures/sign endpoint. The controller deserializes the incoming JSON into a SignatureData object and extracts the document data. TX Text Control .NET Server for ASP.NET opens the signed document and finds all defined signature fields. Then, it applies a digital signature to each field. The controller saves the newly signed PDF document to a local folder and returns the file name or another custom response. This creates a clear distinction between the client-side signature input and the server-side signing process. This ensures that the final document is securely signed with your organization's certificate. The application that uses the DocumentViewer simply specifies the API endpoint to which the signed document should be sent for processing using the RedirectAfterSignature property. @Html.TXTextControl().DocumentViewer(settings => { settings.BasePath = "http://localhost"; settings.OAuthSettings.ClientId = "dsserver.nBAcn5eU21mPkpPHz4XrNQnMPLWpkOeT"; settings.OAuthSettings.ClientSecret = "HWwOaZ29NT7EgLFFqzSevV1s8pj760mu"; settings.Dock = DocumentViewerSettings.DockStyle.Fill; settings.DocumentData = base64String; settings.SignatureSettings = new SignatureSettings() { ShowSignatureBar = true, OwnerName = "Paul Paulsen", SignerName = "Tim Typer", SignerInitials = "TT", RedirectUrlAfterSignature = "http://localhost/signature-plugin/signatures/sign", SignatureBoxes = new SignatureBox[] { new SignatureBox("txsign") { SigningRequired = true, Style = SignatureBox.SignatureBoxStyle.Signature } }}; }).Render() UI Integration in DS Server Portal In addition to the backend logic, this plugin adds custom UI controllers and Razor views to the DS Server web portal. These allow administrators and users to list and view signed documents directly in the DS Server interface. The plugin appears as a fully integrated part of the portal by registering its own routes, controllers, and views. This demonstrates the flexibility of the DS Server plug-in model for extending both API and UI functionality. Try It Yourself The signature plugin is an excellent starting point for developing your own DS Server extensions, including custom signing workflows, automated post-processing, and document analytics. With the DS Server Plugin API, you can seamlessly add endpoints, services, and UI elements that integrate seamlessly with DS Server, while keeping your logic cleanly separated.