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)
DocuSign.eSign (https://www.nuget.org/packages/DocuSign.eSign.dll) Version 6.6.0 nehmen!
BouncyCastle.Cryptography (https://www.nuget.org/packages/BouncyCastle.Cryptography/2.2.1)
Einrichtung bei DocuSign
Anwendung anlegen https://developers.docusign.com/platform/configure-app/
Integration-Key Speichern https://admindemo.docusign.com/apps-and-keys
RSA-Private-Key Speichern https://developers.docusign.com/platform/configure-app/#rsa-key-pair
Nutzer-ID Speichern https://admindemo.docusign.com/users
Platzhalter für die Unterschrift: https://support.docusign.com/s/document-item?language=de&bundleId=iqc1594156517921&topicId=rvg1644270913133.html&_LANG=dede
(Produktiv ohne "demo" in der URL")
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