Direkt zum Hauptinhalt

DocuSign Integration

Dieses Skript zeigt, wie ein generiertes Dokument automatisch über DocuSign zur digitalen Signatur versendet werden kann.

 



Anwendungsfall

Nach der Erstellung eines Vertrags, Angebots oder anderen unterschriftspflichtigen Dokuments soll dieses automatisch über DocuSign an den Empfänger zur elektronischen Signatur versendet werden. Optional erhält eine weitere Person (z.B. der zuständige Mitarbeiter) eine Kopie.

 



Konfiguration in PRINT+PLUS

  • Exporter-Typ: Word- oder PDF-Export
  • Event: AfterClosingDocument
  • Signatur-Position in der Vorlage: Über versteckten Text /sn1/ (weiße Schrift auf weißem Hintergrund) wird die Position der Unterschrift markiert

 


Einrichtung bei DocuSign

  1. Anwendung anlegen: DocuSign Developer Platform
  2. Integration-Key speichern: Apps and Keys
  3. RSA-Private-Key erzeugen: RSA Key Pair
  4. Nutzer-ID ermitteln: Users
  5. Signatur-Platzhalter Doku: DocuSign Support

Für den Produktivbetrieb ersetzen Sie alle URLs ohne "demo" (z.B. account.docusign.com statt account-d.docusign.com).

 



Erforderliche Referenzen

Aus der .NET-Installation (C:\Windows\...):

  • System.ComponentModel.DataAnnotations
  • System.Net
  • System.Net.Http

Aus dem cobra-Module-Ordner (C:\Program Files\cobra\CRMPRO\Programm\Module\):

  • Microsoft.IdentityModel.Tokens
  • Microsoft.IdentityModel.Protocols
  • Microsoft.IdentityModel.Logging
  • Microsoft.IdentityModel.JsonWebTokens
  • System.IdentityModel.Tokens.Jwt

Aus dem PRINT+PLUS Addin-Ordner:

  • Newtonsoft.Json

Zusätzlich aus NuGet (.NET Framework 4.8 kompatibel):

 



Konfigurationsdatei

Das Skript erwartet eine JSON-Konfigurationsdatei neben der cobra-ADL-Datei (z. B. MeineDatenbank_DocuSign.json). Beim ersten Start wird automatisch eine Vorlage erstellt:

{
  "ClientId": "00000000-0000-0000-0000-000000000000",
  "AuthServer": "account-d.docusign.com",
  "ImpersonatedUserId": "00000000-0000-0000-0000-000000000000",
  "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\r\n[...]\r\n-----END RSA PRIVATE KEY-----\r\n"
}
Feld Beschreibung
ClientId Integration-Key aus der DocuSign-App-Registrierung
AuthServer account-d.docusign.com (Demo) oder account.docusign.com (Produktiv)
ImpersonatedUserId Nutzer-ID, in deren Namen Dokumente versendet werden
PrivateKey RSA-Private-Key (inkl. Header/Footer)

 



Vollständiges Skript

// Version 6
namespace Ruthardt.PrintPlus.Skripting
{
	using Cobra.Common;
	using DocuSign.eSign.Api;
	using DocuSign.eSign.Client;
	using DocuSign.eSign.Client.Auth;
	using DocuSign.eSign.Model;
	using Microsoft.IdentityModel.Tokens;
	using Newtonsoft.Json;
	using Org.BouncyCastle.Crypto;
	using Org.BouncyCastle.Crypto.Parameters;
	using Org.BouncyCastle.OpenSsl;
	using Org.BouncyCastle.Security;
	using Ruthardt.PrintPlus.Model.Interfaces;
	using System;
	using System.Collections.Generic;
	using System.Diagnostics;
	using System.IdentityModel.Tokens.Jwt;
	using System.IO;
	using System.Linq;
	using System.Net;
	using System.Net.Http;
	using System.Security.Claims;
	using System.Security.Cryptography;
	using System.Text;
	using System.Windows.Forms;

	class DocuSignSettings
	{
		public string ClientId { get; set; }
		public string AuthServer { get; set; }
		public string ImpersonatedUserId { get; set; }
		public string PrivateKey { get; set; }
	}

	public class DocuSignVersand : IScriptAction
	{
		private static readonly string DevCenterPage = "https://developers.docusign.com/platform/auth/consent";

		public void Execute(IPrintContext printContext, ICurrentContext currentContext, IChildContext childContext)
		{
			// Konfigurationsdatei neben ADL laden
			var curDb = CobraMain.UnitOfWorkProvider.CurrentAddressDataBase;
			var configPath = Path.Combine(
				Path.GetDirectoryName(curDb),
				Path.GetFileNameWithoutExtension(curDb) + "_DocuSign.json");

			if (!File.Exists(configPath))
			{
				// Vorlage erstellen
				var sampleConfig = new DocuSignSettings
				{
					ClientId = "00000000-0000-0000-0000-000000000000",
					AuthServer = "account-d.docusign.com",
					ImpersonatedUserId = "00000000-0000-0000-0000-000000000000",
					PrivateKey = "-----BEGIN RSA PRIVATE KEY-----\r\n[...]\r\n-----END RSA PRIVATE KEY-----\r\n"
				};
				File.WriteAllText(configPath, JsonConvert.SerializeObject(sampleConfig, Formatting.Indented));
				printContext.WaitFormManager.ShowMessageBox(
					"Konfiguration existiert nicht. Bitte Vorlage ausfüllen.",
					"Konfiguration existiert nicht.");
				printContext.IsExportCancelled = true;
				return;
			}

			var config = JsonConvert.DeserializeObject<DocuSignSettings>(File.ReadAllText(configPath));

			// Authentifizierung via JWT
			OAuth.OAuthToken accessToken;
			try
			{
				var privateKeyBytes = Encoding.ASCII.GetBytes(config.PrivateKey.Replace("\r\n", "\n"));
				accessToken = JwtAuth.AuthenticateWithJwt(
					config.ClientId, config.ImpersonatedUserId, config.AuthServer, privateKeyBytes);
			}
			catch (ApiException apiEx) when (apiEx.Message.Contains("consent_required"))
			{
				// Consent benötigt – Browser öffnen
				var url = $"https://{config.AuthServer}/oauth/auth?response_type=code&scope=impersonation%20signature&client_id={config.ClientId}&redirect_uri={DevCenterPage}";
				Process.Start(new ProcessStartInfo("cmd", "/c start " + url) { CreateNoWindow = true });

				printContext.WaitFormManager.ShowMessageBox(
					"Ausgabe muss nach Bestätigung neu gestartet werden!",
					"Authentifizierung", MessageBoxIcon.Information);
				printContext.IsExportCancelled = true;
				return;
			}

			// Account-Infos abrufen
			var docuSignClient = new DocuSignClient();
			docuSignClient.SetOAuthBasePath(config.AuthServer);
			var userInfo = docuSignClient.GetUserInfo(accessToken.access_token);
			var acct = userInfo.Accounts.First();

			// Empfänger aus dem Datensatz
			var datensatz = currentContext.Data;
			var signerEmail = datensatz.GetStringValue("E-Mail Asp");
			var signerName = datensatz.GetStringValue("Vorname");
			var ccEmail = datensatz.GetStringValue("E-Mail Untern");
			var ccName = datensatz.GetStringValue("Firma");

			// Dokument an DocuSign senden
			SigningViaEmail.SendEnvelopeViaEmail(
				"Bitte signieren Sie das Dokument",
				signerEmail, signerName,
				ccEmail, ccName,
				accessToken.access_token,
				acct.BaseUri + "/restapi",
				acct.AccountId,
				currentContext.DocumentFileName.FullName,
				"sent");

			printContext.Logger.Info($"DocuSign-Envelope an '{signerEmail}' gesendet.");
		}
	}

	// --- JWT-Authentifizierung ---
	static class JwtAuth
	{
		public static OAuth.OAuthToken AuthenticateWithJwt(
			string clientId, string userId, string authServer, byte[] privateKeyBytes)
		{
			var docuSignClient = new DocuSignClient();
			var scopes = new List<string> { "signature", "impersonation" };

			string privateKey = Encoding.UTF8.GetString(privateKeyBytes);

			var handler = new JwtSecurityTokenHandler { SetDefaultTimesOnTokenCreation = false };
			var descriptor = new SecurityTokenDescriptor
			{
				Expires = DateTime.UtcNow.AddHours(1),
				IssuedAt = DateTime.UtcNow,
				Subject = new ClaimsIdentity(new[]
				{
					new Claim("scope", string.Join(" ", scopes)),
					new Claim("aud", authServer),
					new Claim("iss", clientId),
					new Claim("sub", userId)
				}),
				SigningCredentials = new SigningCredentials(
					new RsaSecurityKey(CreateRSAKeyFromPem(privateKey)),
					SecurityAlgorithms.RsaSha256Signature)
			};

			var jwtToken = handler.WriteToken(handler.CreateToken(descriptor));

			var formParams = new Dictionary<string, string>
			{
				{ "grant_type", OAuth.Grant_Type_JWT },
				{ "assertion", jwtToken }
			};

			var request = docuSignClient.PrepareOAuthRequest(
				authServer, "oauth/token", HttpMethod.Post,
				docuSignClient.Configuration.DefaultHeader.ToList(),
				formParams.Select(kv => new KeyValuePair<string, string>(kv.Key, kv.Value)).ToList());

			var response = docuSignClient.RestClient.SendRequest(request);

			if (response.StatusCode >= HttpStatusCode.OK && response.StatusCode < HttpStatusCode.BadRequest)
			{
				return JsonConvert.DeserializeObject<OAuth.OAuthToken>(response.Content);
			}

			throw new ApiException((int)response.StatusCode,
				"Fehler bei der DocuSign-Authentifizierung: " + response.Content);
		}

		private static RSA CreateRSAKeyFromPem(string pem)
		{
			var reader = new PemReader(new StringReader(pem));
			var result = reader.ReadObject();
			var provider = RSA.Create();

			if (result is AsymmetricCipherKeyPair keyPair)
			{
				provider.ImportParameters(
					DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)keyPair.Private));
			}
			else if (result is RsaKeyParameters keyParams)
			{
				provider.ImportParameters(DotNetUtilities.ToRSAParameters(keyParams));
			}
			else
			{
				throw new InvalidOperationException("Unerwarteter PEM-Typ.");
			}

			return provider;
		}
	}

	// --- Envelope erstellen und senden ---
	static class SigningViaEmail
	{
		public static string SendEnvelopeViaEmail(
			string subject, string signerEmail, string signerName,
			string ccEmail, string ccName,
			string accessToken, string basePath, string accountId,
			string docPath, string envStatus)
		{
			var env = new EnvelopeDefinition
			{
				EmailSubject = subject,
				Status = envStatus,
				Documents = new List<Document>
				{
					new Document
					{
						DocumentBase64 = Convert.ToBase64String(File.ReadAllBytes(docPath)),
						Name = Path.GetFileNameWithoutExtension(docPath),
						FileExtension = Path.GetExtension(docPath),
						DocumentId = "1"
					}
				}
			};

			// Signer mit Signatur-Platzhalter
			var signer = new Signer
			{
				Email = signerEmail,
				Name = signerName,
				RecipientId = "1",
				RoutingOrder = "1",
				Tabs = new Tabs
				{
					SignHereTabs = new List<SignHere>
					{
						new SignHere
						{
							AnchorString = "/sn1/",
							AnchorUnits = "pixels",
							AnchorYOffset = "10",
							AnchorXOffset = "20"
						}
					}
				}
			};

			var recipients = new Recipients
			{
				Signers = new List<Signer> { signer }
			};

			// CC-Empfänger (optional)
			if (!string.IsNullOrWhiteSpace(ccEmail))
			{
				recipients.CarbonCopies = new List<CarbonCopy>
				{
					new CarbonCopy
					{
						Email = ccEmail,
						Name = ccName,
						RecipientId = "2",
						RoutingOrder = "2"
					}
				};
			}

			env.Recipients = recipients;

			var client = new DocuSignClient(basePath);
			client.Configuration.DefaultHeader.Add("Authorization", "Bearer " + accessToken);
			var envelopesApi = new EnvelopesApi(client);
			var result = envelopesApi.CreateEnvelope(accountId, env);

			return result.EnvelopeId;
		}
	}
}

 



Ablauf im Überblick

  1. Konfiguration laden: Die DocuSign-Zugangsdaten werden aus einer JSON-Datei neben der cobra-ADL gelesen.
  2. JWT-Authentifizierung: Per RSA-Private-Key wird ein JWT-Token erstellt und gegen die DocuSign-API eingetauscht.
  3. Consent: Beim allerersten Aufruf muss der Benutzer die App autorisieren (Browser öffnet sich automatisch).
  4. Dokument hochladen: Das generierte Dokument wird Base64-kodiert an DocuSign übermittelt.
  5. Signatur-Position: Der Text /sn1/ in der Vorlage (weiß auf weiß, für den Empfänger unsichtbar) definiert, wo die Unterschrift platziert wird.
  6. Versand: Die Signatur-Anfrage wird per E-Mail an den Empfänger gesendet.

 



Hinweise

Produktiv vs. Demo: Für den Produktivbetrieb verwendet der AuthServer account.docusign.com (ohne -d). Alle Admin-URLs ohne „demo".

Signatur-Platzhalter: Der Anker-Text /sn1/ muss in der Vorlage vorhanden sein (als versteckter oder weißer Text). DocuSign platziert das Unterschriftenfeld genau dort.

Die Nutzerautorisierung ist einmalig. Nach Bestätigung im Browser funktioniert das Skript ohne weitere Interaktion.