DocuSign Integration

Dieses Skript zeigt, wie Sie eine Ausgabe automatisch über DocuSign versenden können. So kann eine Signatur angefragt werden.

Konfiguration

  • Word- oder PDF-Export

  • Signatur-Position angegeben durch versteckten Text /sn1/ (Weiß auf Weiß)

  • Das Skript wird AfterClosingDocument ausgeführt

  • Es werden einige Referenzen benötigt

    • aus Microsoft.NET 4 Installation in C:\Windows

      • System.ComponentModel.DataAnnotations

      • System.Net

      • System.Net.Http

    • aus cobra Module Ordner (z.B. C:\Program Files\cobra\CRMPRO\Programm\Module)

      • Microsoft.IdentityModel.Tokens

      • Microsoft.IdentityModel.Protocols

      • Microsoft.IdentityModel.Logging

      • Microsoft.IdentityModel.JsonWebTokens

      • System.IdentityModel.Tokens.Jwt

    • aus PRINT+PLUS Addin-Ordner

      • Newtonsoft.Json

    • zusätzlich aus nuget (.NET Framework 4 kompatibel)

Einrichtung bei DocuSign

Ideen

  • Anwendungsdaten werden in Konfigurations-JSON gespeichert (neben ADL)

    • ClientId: Integration-Key

    • AuthServer: "account-d.docusign.com" oder account.docusign.com Produktiv

    • ImpersonatedUserId: Nutzer-ID von dem die Dokumente aus versendet werden

    • PrivateKey: RSA-Private-Key

  • Signatur-Anfrage wird an "E-Mail ASP" versendet

  • Bestätigungen per CC an "E-Mail Untern" und Nutzer

Skript

// Version 5
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.CobraBase.Functions.Access.Ado;
	using Ruthardt.Common.Util;
	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 ExampleScript : IScriptAction
	{
		static readonly string DevCenterPage = "https://developers.docusign.com/platform/auth/consent";

		public void Execute(IPrintContext printContext, ICurrentContext currentContext, IChildContext childContext)
		{
			// Konfigurationsdatei neben ADL
			var curDb = CobraMain.UnitOfWorkProvider.CurrentAddressDataBase;
			var path = Path.Combine(Path.GetDirectoryName(curDb), Path.GetFileNameWithoutExtension(curDb) + "_DocuSign.json");
			if (!File.Exists(path))
			{
				var sampleConfig = new DocuSignSettings()
				{
					ClientId = "00000000-0000-0000-0000-000000000000", // Integration-Key
					AuthServer = "account-d.docusign.com", // in Production account.docusign.com
					ImpersonatedUserId = "00000000-0000-0000-0000-000000000000", // User-Id
					PrivateKey = "-----BEGIN RSA PRIVATE KEY-----\r\n[...]\r\n-----END RSA PRIVATE KEY-----\r\n"
				};
				var sampleJson = JsonConvert.SerializeObject(sampleConfig, Formatting.Indented);
				File.WriteAllText(path, sampleJson);

				printContext.WaitFormManager.ShowMessageBox("Konfiguration existiert nicht. Bitte Vorlage ausfüllen.", "Konfiguration existiert nicht.");
				printContext.IsExportCancelled = true;
				return;
			}
			var configJson = File.ReadAllText(path);
			var config = JsonConvert.DeserializeObject<DocuSignSettings>(configJson);

			Guid guid;
			if (!Guid.TryParse(config.ClientId, out guid))
			{
				printContext.WaitFormManager.ShowMessageBox("ClientId ungültig. Korrigieren Sie die Konfigurationsdatei.", "Konfiguration ungültig", MessageBoxIcon.Error);
				printContext.IsExportCancelled = true;
				return;
			}

			printContext.Logger.Info(config.ClientId);
			printContext.Logger.Info(config.AuthServer);
			printContext.Logger.Info(config.ImpersonatedUserId);
			printContext.Logger.Info(config.PrivateKey);

			#region Authentifizierung
			OAuth.OAuthToken accessToken = null;
			try
			{
				var privateKey = Encoding.ASCII.GetBytes(config.PrivateKey.Replace("\r\n", "\n"));
				accessToken = JwtAuth.AuthenticateWithJwt("ESignature", config.ClientId, config.ImpersonatedUserId, config.AuthServer, privateKey);
			}
			catch (ApiException apiExp)
			{
				// Consent for impersonation must be obtained to use JWT Grant
				if (apiExp.Message.Contains("consent_required"))
				{
					// build a URL to provide consent for this Integration Key and this userId
					string url = "https://" + config.AuthServer + "/oauth/auth?response_type=code^&scope=impersonation%20signature^&client_id=" + config.ClientId + "^&redirect_uri=" + DevCenterPage;

					string consentRequiredMessage = "Nutzerauthentifizierung wird benötigt - Starte Browser";

					printContext.WaitFormManager.ShowMessageBox(consentRequiredMessage);

					// Start new browser window for login and consent to this app by DocuSign user
					Process.Start(new ProcessStartInfo("cmd", "/c start " + url) { CreateNoWindow = false });

					printContext.Logger.Error("Unable to send envelope; Exiting. Please rerun the console app once consent was provided");
					printContext.WaitFormManager.ShowMessageBox("Ausgabe muss nach Bestätigung neu gestartet werden!", "Authentifizierung", MessageBoxIcon.Information);
				}
				else
				{
					printContext.WaitFormManager.ShowMessageBox("Authentifizierungs-Fehler: " + apiExp.ToString(), "Authentifizierung Fehlgeschlagen", MessageBoxIcon.Error);
					printContext.Logger.Error(apiExp, "Unbekannter Authentifizierungs-Fehler");
				}

				printContext.IsExportCancelled = true;
				return;
			}

			var docuSignClient = new DocuSignClient();
			docuSignClient.SetOAuthBasePath(config.AuthServer);
			var userInfo = docuSignClient.GetUserInfo(accessToken.access_token);
			var acct = userInfo.Accounts.FirstOrDefault();
			#endregion

			// Zugriff auf den aktuellen Datensatz
			var currentDatensatz = currentContext.Data;

			string signerEmail = currentDatensatz.GetStringValue("E-Mail Asp");
			string signerName = currentDatensatz.GetStringValue("Vorname");
			// Beispiel CC
			string ccEmail = currentDatensatz.GetStringValue("E-Mail Untern");
			string ccName = currentDatensatz.GetStringValue("Firma");

			var output = currentContext.DocumentFileName.ToString();

			printContext.Logger.Debug("Sending " + output);

			SigningViaEmail.SendEnvelopeViaEmail(
				"Bitte signieren Sie das Dokument",
				signerEmail,
				signerName,
				ccEmail,
				ccName,
				accessToken.access_token,
				acct.BaseUri + "/restapi",
				acct.AccountId,
				output,
				"sent");
		}
	}

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

			return RequestJWTUserToken(
				docuSignClient,
				clientId,
				impersonatedUserId,
				authServer,
				privateKeyBytes,
				1,
				scopes);
		}


		public static OAuth.OAuthToken RequestJWTUserToken(DocuSignClient client, string clientId, string userId, string oauthBasePath, byte[] privateKeyBytes, int expiresInHours, List<string> scopes = null)
		{
			string privateKey = Encoding.UTF8.GetString(privateKeyBytes);

			JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler
			{
				SetDefaultTimesOnTokenCreation = false
			};

			SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor()
			{
				Expires = DateTime.UtcNow.AddHours(expiresInHours),
				IssuedAt = DateTime.UtcNow,
			};

			scopes = scopes ?? new List<string> { OAuth.Scope_SIGNATURE };

			descriptor.Subject = new ClaimsIdentity();
			descriptor.Subject.AddClaim(new Claim("scope", String.Join(" ", scopes)));
			descriptor.Subject.AddClaim(new Claim("aud", oauthBasePath));
			descriptor.Subject.AddClaim(new Claim("iss", clientId));

			if (!string.IsNullOrEmpty(userId))
			{
				descriptor.Subject.AddClaim(new Claim("sub", userId));
			}
			else
			{
				throw new ApiException(400, "User Id not supplied or is invalid!");
			}

			if (!string.IsNullOrEmpty(privateKey))
			{
				var rsa = CreateRSAKeyFromPem(privateKey);
				RsaSecurityKey rsaKey = new RsaSecurityKey(rsa);
				descriptor.SigningCredentials = new SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256Signature);
			}
			else
			{
				throw new ApiException(400, "Private key not supplied or is invalid!");
			}

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

			var localVarFormParams = new Dictionary<String, String>();
			localVarFormParams.Add("grant_type", OAuth.Grant_Type_JWT);
			localVarFormParams.Add("assertion", jwtToken);

			DocuSignRequest request = client.PrepareOAuthRequest(oauthBasePath, "oauth/token", HttpMethod.Post, client.Configuration.DefaultHeader.ToList(), localVarFormParams.ToList());
			DocuSignResponse response = client.RestClient.SendRequest(request);

			if (response.StatusCode >= HttpStatusCode.OK && response.StatusCode < HttpStatusCode.BadRequest)
			{
				OAuth.OAuthToken tokenInfo = JsonConvert.DeserializeObject<OAuth.OAuthToken>(response.Content);
				if (!client.Configuration.DefaultHeader.ContainsKey("Authorization"))
				{
					client.Configuration.DefaultHeader.Add("Authorization", string.Format("{0} {1}", tokenInfo.token_type, tokenInfo.access_token));
				}
				else
				{
					client.Configuration.DefaultHeader["Authorization"] = string.Format("{0} {1}", tokenInfo.token_type, tokenInfo.access_token);
				}
				return tokenInfo;
			}
			else
			{
				throw new ApiException((int)response.StatusCode,
					  "Error while requesting server, received a non successful HTTP code with response Body: " + response.Content,
					   response.Content,
					   response);
			}
		}
		static RSA CreateRSAKeyFromPem(string key)
		{
			TextReader reader = new StringReader(key);
			PemReader pemReader = new PemReader(reader);

			object result = pemReader.ReadObject();

			RSA provider = RSA.Create();

			if (result.GetType() == typeof(AsymmetricCipherKeyPair))
			{
				AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair)result;
				var rsaParams = DotNetUtilities.ToRSAParameters((RsaPrivateCrtKeyParameters)keyPair.Private);
				provider.ImportParameters(rsaParams);
				return provider;
			}
			else if (result.GetType() == typeof(RsaKeyParameters))
			{
				RsaKeyParameters keyParameters = (RsaKeyParameters)result;
				var rsaParams = DotNetUtilities.ToRSAParameters(keyParameters);
				provider.ImportParameters(rsaParams);
				return provider;
			}

			throw new Exception("Unexpected PEM type");
		}
	}
	#endregion

	#region Signing
	static class SigningViaEmail
	{
		/// <summary>
		/// Creates an envelope that would include two documents and add a signer and cc recipients to be notified via email
		/// </summary>
		/// <param name="signerEmail">Email address for the signer</param>
		/// <param name="signerName">Full name of the signer</param>
		/// <param name="ccEmail">Email address for the cc recipient</param>
		/// <param name="ccName">Name of the cc recipient</param>
		/// <param name="accessToken">Access Token for API call (OAuth)</param>
		/// <param name="basePath">BasePath for API calls (URI)</param>
		/// <param name="accountId">The DocuSign Account ID (GUID or short version) for which the APIs call would be made</param>
		/// <param name="docPath">String of bytes representing the document</param>
		/// <param name="envStatus">Status to set the envelope to</param>
		/// <returns>EnvelopeId for the new envelope</returns>
		public static string SendEnvelopeViaEmail(string subject, string signerEmail, string signerName, string ccEmail, string ccName, string accessToken, string basePath, string accountId, string docPath, string envStatus)
		{
			//ds-snippet-start:eSign2Step3
			EnvelopeDefinition env = MakeEnvelope(subject, signerEmail, signerName, ccEmail, ccName, docPath, envStatus);
			var docuSignClient = new DocuSignClient(basePath);
			docuSignClient.Configuration.DefaultHeader.Add("Authorization", "Bearer " + accessToken);

			EnvelopesApi envelopesApi = new EnvelopesApi(docuSignClient);
			EnvelopeSummary results = envelopesApi.CreateEnvelope(accountId, env);
			return results.EnvelopeId;
			//ds-snippet-end:eSign2Step
		}

		public static EnvelopeDefinition MakeEnvelope(string subject, string signerEmail, string signerName, string ccEmail, string ccName, string docPath, string envStatus)
		{
			// Data for this method
			// signerEmail
			// signerName
			// ccEmail
			// ccName
			// Config.docPdf
			// RequestItemsService.Status -- the envelope status ('created' or 'sent')
			string docBytes = Convert.ToBase64String(System.IO.File.ReadAllBytes(docPath));

			//ds-snippet-start:eSign2Step2
			EnvelopeDefinition env = new EnvelopeDefinition();
			env.EmailSubject = subject;

			// Create document objects, one per document
			Document doc = new Document
			{
				DocumentBase64 = docBytes,
				Name = Path.GetFileNameWithoutExtension(docPath),
				FileExtension = Path.GetExtension(docPath),
				DocumentId = "1",
			};

			// The order in the docs array determines the order in the envelope
			env.Documents = new List<Document> { doc };

			// create a signer recipient to sign the document, identified by name and email
			// We're setting the parameters via the object creation
			Signer signer1 = new Signer
			{
				Email = signerEmail,
				Name = signerName,
				RecipientId = "1",
				RoutingOrder = "1",
			};

			// routingOrder (lower means earlier) determines the order of deliveries
			// to the recipients. Parallel routing order is supported by using the
			// same integer as the order for two or more recipients.

			// Create signHere fields (also known as tabs) on the documents,
			// We're using anchor (autoPlace) positioning
			SignHere signHere = new SignHere
			{
				AnchorString = "/sn1/",
				AnchorUnits = "pixels",
				AnchorYOffset = "10",
				AnchorXOffset = "20",
			};

			// Tabs are set per recipient / signer
			Tabs signer1Tabs = new Tabs
			{
				SignHereTabs = new List<SignHere> { signHere },
			};
			signer1.Tabs = signer1Tabs;

			// Add the recipients to the envelope object
			Recipients recipients = new Recipients
			{
				Signers = new List<Signer> { signer1 },
			};
			
			// create a cc recipient to receive a copy of the documents, identified by name and email
			// We're setting the parameters via setters
			if (!string.IsNullOrEmpty(ccEmail))
			{
				CarbonCopy cc1 = new CarbonCopy
				{
					Email = ccEmail,
					Name = ccName,
					RecipientId = "2",
					RoutingOrder = "2",
				};
				recipients.CarbonCopies = new List<CarbonCopy> { cc1 };
			}
			env.Recipients = recipients;

			// Request that the envelope be sent by setting |status| to "sent".
			// To request that the envelope be created as a draft, set to "created"
			env.Status = envStatus;

			return env;
			//ds-snippet-end:eSign2Step2
		}
	}
	#endregion
}

Last updated