DocuSign Connect Webhook with .Net Core 3 - c#

I am creating a webhook in a .Net Core 3 Web API for DocuSign Connect to invoke and provide me status updates + signed documents from envelopes my app has created. The C# example at https://www.docusign.com/blog/dsdev-adding-webhooks-application was very helpful in getting me almost to my goal. The code from the example is:
[HttpPost("api/[controller]/ConnectWebHook")]
public void ConnectWebHook(HttpRequestMessage request)
{
XmlDocument xmldoc = new XmlDocument();
xmldoc.Load(request.Content.ReadAsStreamAsync().Result);
var mgr = new XmlNamespaceManager(xmldoc.NameTable);
mgr.AddNamespace("a", "http://www.docusign.net/API/3.0");
XmlNode envelopeStatus = xmldoc.SelectSingleNode("//a:EnvelopeStatus", mgr);
XmlNode envelopeId = envelopeStatus.SelectSingleNode("//a:EnvelopeID", mgr);
XmlNode status = envelopeStatus.SelectSingleNode("./a:Status", mgr);
var targetFileDirectory = #"\\my-network-share\";
if (envelopeId != null)
{
System.IO.File.WriteAllText($"{targetFileDirectory}{envelopeId.InnerText}_{status.InnerText}_.xml", xmldoc.OuterXml);
}
if (status.InnerText == "Completed")
{
// Loop through the DocumentPDFs element, storing each document.
XmlNode docs = xmldoc.SelectSingleNode("//a:DocumentPDFs", mgr);
foreach (XmlNode doc in docs.ChildNodes)
{
string documentName = doc.ChildNodes[0].InnerText; // pdf.SelectSingleNode("//a:Name", mgr).InnerText;
string documentId = doc.ChildNodes[2].InnerText; // pdf.SelectSingleNode("//a:DocumentID", mgr).InnerText;
string byteStr = doc.ChildNodes[1].InnerText; // pdf.SelectSingleNode("//a:PDFBytes", mgr).InnerText;
System.IO.File.WriteAllText($"{targetFileDirectory}{envelopeId.InnerText}_{documentId}_{documentName}", byteStr);
}
}
}
For testing purposes, my Web API is allowing all origins and exposed to the outside world via NGROK, and I can hit other test endpoints (both GET and POST), but for some reason this webhook is not being hit by Connect when there is a notification-worthy event on my envelope.
I can see in the DocuSign Admin portal logs that Connect invoked my webhook but got The remote server returned an error: (415) Unsupported Media Type.. This led me to add the [FromBody] attribute to my method signature like so but I still get the same error when my webhook is invoked by Connect.
[HttpPost("api/[controller]/ConnectWebHook")]
public void ConnectWebHook([FromBody] HttpRequestMessage request)
{
// ... rest of the method was unchanged, removed for brevity
}
I have never used HttpRequestMessage before but it looks straightforward enough. I noticed in the DocuSign Admin portal logs that the data that Connect tried to send to the webhook is just XML. I could try to change the webhook's signature to look for an XmlDocument instead of an HttpRequestMessage but I am not sure what, if anything, I will be missing out on.
Has anyone else integrated with Connect via a webhook recently? And were you able to make the HttpRequestMessage work for you?
Added on 10/18/2019:
DocuSign mentions that the content type is XML. Here is what the content looks like:
<DocuSignEnvelopeInformation
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.docusign.net/API/3.0">
<EnvelopeStatus>...</EnvelopeStatus>
<DocumentPDFs>...</DocumentPDFs>
</DocuSignEnvelopeInformation>
I have added AddXmlSerializerFormatters() to the ConfigureServices method in Startup.cs. This being .Net Core 3, I had to set it up like services.AddControllers().AddXmlSerializerFormatters() instead of services.AddMVC().AddXmlSerializerFormatters() per https://learn.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.0&tabs=visual-studio.
With that change, I have now tried using [FromForm] like so and my webhook IS being hit, but the request input parameter is essentially empty ... request.Content = null:
[HttpPost("api/[controller]/ConnectWebHook")]
public void ConnectWebHook([FromForm] HttpRequestMessage request)
{
// ... rest of the method was unchanged, removed for brevity
}
Since the request is being sent from DocuSign Connect, I have no control over the headers/format/content. As far as I can tell, they are not submitting an XML object, not a form, so [FromForm] is probably not the way to go.

That linked example is not for .net core. HttpRequestMessage is no longer a first class citizen in asp.net-core framework and will treated as a normal model.
Just extract the content directly from the Request's body and the rest should be able to remain the same as in the example.
[HttpPost("api/[controller]/ConnectWebHook")]
public IActionResult ConnectWebHook() {
Stream stream = Request.Body;
XmlDocument xmldoc = new XmlDocument();
xmldoc.Load(stream);
var mgr = new XmlNamespaceManager(xmldoc.NameTable);
mgr.AddNamespace("a", "http://www.docusign.net/API/3.0");
XmlNode envelopeStatus = xmldoc.SelectSingleNode("//a:EnvelopeStatus", mgr);
XmlNode envelopeId = envelopeStatus.SelectSingleNode("//a:EnvelopeID", mgr);
XmlNode status = envelopeStatus.SelectSingleNode("./a:Status", mgr);
var targetFileDirectory = #"\\my-network-share\";
if (envelopeId != null) {
System.IO.File.WriteAllText($"{targetFileDirectory}{envelopeId.InnerText}_{status.InnerText}_.xml", xmldoc.OuterXml);
}
if (status.InnerText == "Completed") {
// Loop through the DocumentPDFs element, storing each document.
XmlNode docs = xmldoc.SelectSingleNode("//a:DocumentPDFs", mgr);
foreach (XmlNode doc in docs.ChildNodes) {
string documentName = doc.ChildNodes[0].InnerText; // pdf.SelectSingleNode("//a:Name", mgr).InnerText;
string documentId = doc.ChildNodes[2].InnerText; // pdf.SelectSingleNode("//a:DocumentID", mgr).InnerText;
string byteStr = doc.ChildNodes[1].InnerText; // pdf.SelectSingleNode("//a:PDFBytes", mgr).InnerText;
System.IO.File.WriteAllText($"{targetFileDirectory}{envelopeId.InnerText}_{documentId}_{documentName}", byteStr);
}
}
return Ok();
}

Related

Void an email previously sent using DocuSign C# API

As the title says, I am using the DocuSign C# API and I am trying to void an envelope that has been sent, then send an updated one in its place.
So an example:
I send out a DocuSign envelope to a customer
They open it to see that I have made a mistake (or want items adding etc)
They ask for a new quote
I trigger a resend (via the API using my back end database using status flags)
It is now being resent, but the previous one is still "active" and doesn't automatically void.
So how would I go about voiding this using the C# API?
Thanks for the help, if you need any more information just ask and I will get back to you.
EDIT - here is the current code that I have currently
try
{
envelopeGenerator = new EnvelopeDefinitionGenerator(quoteID, account_id);
envDef = envelopeGenerator.GetDefinition();
if (item.EQuoteStatus == 4)
{
envelope = envelopesApi.GetEnvelope(account_id, envelopeId);
envelope.Status = "voided";
envelope.VoidedReason = "This envelope was voided";
envelope.PurgeState = null;
updateSummary = envelopesApi.Update(account_id, envelopeSummary.EnvelopeId, envelope);
envelopeInfo = JObject.FromObject(envDef);
jsonString = JsonConvert.SerializeObject(envelopeInfo);
//throw new Exception(); // DEBUG LINE, COMMENT OUT - TO TEST IF EXCEPTION CODE WORKS.
envelopesApi = new EnvelopesApi(apiClient.Configuration);
envelopeSummary = envelopesApi.CreateEnvelope(account_id, envDef);
Console.WriteLine($"Quote {envelopeCounter} of {quotes.Count}. Envelope Summary: {envelopeSummary.Status}");
Logger.Log.Info($"Quote {envelopeCounter} of {quotes.Count}. Envelope Summary: {envelopeSummary.Status}");
successFlag = true;
}
When using the UpdateEnvelope() method to void an envelope, don't use the previous envelope's object as the envelope definition parameter. Instead, create a new, empty Envelope object and use that.
It also looks like you're referencing envelope in the first part, and then getting the envelope ID from envelopeSummary.EnvelopeId. You'll want to confirm you have the correct ID for the envelope you're attempting to void.
Try this:
Envelope nullEnvelope = new Envelope();
nullEnvelope.Status = "voided";
nullEnvelope.VoidedReason = "This envelope was voided";
updateSummary = envelopesApi.Update(account_id, envelopeId, nullEnvelope);
If you have only one signer scenario and that signer has complained that something is wrong on the document, then I would suggest to use Update document flow that would save your envelope count and some saving. You can update Document(s) in an envelope if no recipient in the workflow has taken any action by following any of the below endpoint,
Update
Document
Update Document
List

Add subscriber to Mailchimp through API 3.0

I'm attempting to integrate version 3 of Mailchimp's API to add a subscriber to one of my mailing lists. The code below is what I have thus far, and my intention is to call it in my contact form method when the user fills out their email to subscribe. In theory it should get this email stored in emailAddress and POST it to MailChimp, but theorys are not practical. Below is my current method of POST data:
private string InsertIntoMailChimpGeneralList(string emailAddress)
{
var apiKey = ConfigurationManager.AppSettings["MailChimpAPIKeyGeneral"];
var dataCenter = ConfigurationManager.AppSettings["MailChimpDataCenterIDGeneral"];
var listId = ConfigurationManager.AppSettings["mailChimpListIDGeneral"];
var email_address = emailAddress;
var status = "subscribed";
using (var wc = new System.Net.WebClient())
{
// Data to be posted to add email address to list
var data = new { email_address, status };
// Serialize to JSON using Json.Net
var json = JsonConvert.SerializeObject(data);
// Base URL to MailChimp API
string apiUrl = "https://" + dataCenter + ".api.mailchimp.com/3.0/";
// Construct URL to API endpoint being used
var url = string.Concat(apiUrl, "lists/", listId, "/members?");
// Set content type
wc.Headers.Add("Content-Type", "application/json");
// Generate authorization header
string credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(":" + apiKey));
// Set authorization header
wc.Headers[HttpRequestHeader.Authorization] = string.Format("Basic {0}", credentials);
// Post and get JSON response
string sendUrl = wc.UploadString(url, json);
return sendUrl;
}
}
In my contact form I have a check where I want the email address added in the contact form (emailAddress is the variable used here) to post that data to the list, when the form is submitted:
if (ConfigurationManager.AppSettings["UseMailChimpIntegration"].ToString().ToLower().Trim() == "true")
{
InsertIntoMailChimpGeneralList(emailAddress);
}
I feel I've implemented this wrong. I was able to get it working on v2 but upgrading to v3 has left me clueless at this point. My contact form runs fine and stored my values in my local database, but does not POST that data through to mailchimp.
I've triple checked my API/datacenter values, and would appreciate some assistance.

Twilio TwiML app XML error

I have a TwiML app with this code in the Connect action of the CallController. This code is taken straight from the Twilio demos.
[HttpPost]
public virtual ActionResult Connect(string phoneNumber, string called)
{
var response = new VoiceResponse();
var dial = new Dial(callerId: "+6138595????");
if (phoneNumber != null)
{
dial.Number(phoneNumber);
}
else
{
dial.Client("support_agent");
}
response.Dial(dial);
return TwiML(response);
}
When this is called it raises the error "Data at the root level is invalid. Line 1, position 1."
The XML this generates is
<?xml version="1.0" encoding="utf-8"?>
<Response>
<Dial callerId="+6138595????">
<Client>support_agent</Client>
</Dial>
</Response>
Twilio Evangelist here.
A quick question - is this happening every time the method is invoked, or only when specific inputs are provided? Needing to build the string manually is of course not desired. So I would like to get to the bottom of what triggered this result.
I have found I can fix it by replacing
return TwiML(response);
with
return new TwiMLResult(response.ToString(), new UTF8Encoding());
Appears to be some kind of encoding issue using the first method.
I had the same problem we solved it in our WebApi by skipping the Twilio sdk and generating the xml by ourselves.
I hope this will work for you too:
[HttpPost]
public virtual HttpResponseMessage Connect(string phoneNumber, string called)
{
string twiml = $"<?xml version=\"1.0\" encoding=\"utf-8\"?><Response><Dial callerId=\"{phoneNumber}\"><Client>support_agent</Client></Dial></Response>";
var xmlResponse = new HttpResponseMessage();
xmlResponse.Content = new StringContent(twiml, Encoding.UTF8, "text/xml");
return xmlResponse;
}
Please notice that there are no end of lines - "\n", \r", etc.

Signature failed core validation error

I have designed a WCF.net client that sends a SOAP request to the vendor. To meet vendor WS security requirements , I have to create a custom SOAP header and send request with the custom header to the web service on the vendor side. So i created a custom header by implementing a new class derieved from MessageHeader (see below)
public class SignOnlyMessageHeader : MessageHeader
{
private const string PREFIX_CP = "wsse";
public string m_Username { get; set; }
public string m_Envelope { get; set; }
public SignOnlyMessageHeader(string Username, string Envelope)
{
m_Username = Username;
m_Envelope = Envelope;
}
public override string Name
{
get { return "wsse:Security"; }
}
public override string Namespace
{
get { return null; }
}
public override bool MustUnderstand
{
get
{
return false;
}
}
protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
base.OnWriteStartHeader(writer, messageVersion);
writer.WriteXmlnsAttribute(PREFIX_CP, "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
}
protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
{
writer.WriteStartElement(PREFIX_CP, "UsernameToken", null);
writer.WriteAttributeString("wsu:Id", "UsernameToken-20");
writer.WriteXmlnsAttribute("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
writer.WriteElementString(PREFIX_CP, "Username", null, m_Username);
writer.WriteEndElement();
SignXmlFile(writer);
}
public void SignXmlFile(XmlDictionaryWriter writer)
{
string certificatePath = "C:\\Users\\22428-cert.p12";
System.Security.Cryptography.X509Certificates.X509Certificate2 cert = new X509Certificate2(certificatePath, "changeit");
// Create a new XML document.
XmlDocument doc = new XmlDocument();
// Format the document to ignore white spaces.
doc.PreserveWhitespace = false;
doc.LoadXml(m_Envelope);
// Create a SignedXml object.
SignedXml signedXml = new SignedXml(doc);
// Add the key to the SignedXml document.
//signedXml.SigningKey = Key;
signedXml.SigningKey = cert.PrivateKey;
// Create a new KeyInfo object.
KeyInfo keyInfo = new KeyInfo();
keyInfo.Id = "";
// Load the certificate into a KeyInfoX509Data object
// and add it to the KeyInfo object.
KeyInfoX509Data keyInfoData = new KeyInfoX509Data();
keyInfoData.AddCertificate(cert);
keyInfo.AddClause(keyInfoData);
// Add the KeyInfo object to the SignedXml object.
signedXml.KeyInfo = keyInfo;
signedXml.SignedInfo.CanonicalizationMethod = "http://www.w3.org/2001/10/xml-exc-c14n#";
// Create a reference to be signed.
Reference reference = new Reference();
reference.Uri = "";
// Add an enveloped transformation to the reference.
XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform();
reference.AddTransform(env);
reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256";
// Add the reference to the SignedXml object.
signedXml.AddReference(reference);
signedXml.Signature.Id = "";
// Compute the signature.
signedXml.ComputeSignature();
// Get the XML representation of the signature and save
// it to an XmlElement object.
XmlElement xmlDigitalSignature = signedXml.GetXml();
// Check the signature and return the result.
if (!signedXml.CheckSignature(new X509Certificate2(certificatePath, "changeit"), true))
{
Console.WriteLine("invalid signature");
}
xmlDigitalSignature.WriteTo(writer);
}
So after creating the custom header class, I overrode the IClientMessageInspector.BeforeSendRequest method to intercept the outgoing request and add my custom header to the soap request. See code below,
object IClientMessageInspector.BeforeSendRequest(ref System.ServiceModel.Channels.Message request, IClientChannel channel)
{
request.Headers.RemoveAt(0);
SignOnlyMessageHeader header = new SignOnlyMessageHeader("x509user", env);
request.Headers.Add(header);
return null;
}
The end result is I am intercepting the SOAP request and correctly replacing the current header with the custom header. Before the request is sent out , I checked the updated the SOAP request (placed a breakpoint) , the structure matches EXACTLY what the vendor requested. But I receive an error after the request is processed at vendor side. It only says "Signature failed core validation". I think I am correctly signing the entire envelop in the "SignXmlFile" method. I even checked the validity within the method (if (!signedXml.CheckSignature(new X509Certificate2(certificatePath, "changeit"), true))), the statement returns a false which indicates signature is valid.
What am I doing wrong ?
Well I tried and tried, something with the way I am intercepting the header and after i inject the header with the Signature ..the validation is failing. As a work around, I striped out the entire header from my .net client. I am routing my request with just the soap baod to a XML gateway, we configured the gateway to intercept the request and add the necessary header init and forward the request to the external vendor. It worked.

Android -- How to access data in an ASP.NET database via app?

I have a Windows web server already set up with a website (unlimited application pools) and I want to be able to access a database on that server via the Android app I'm developing. How can I do this? Can someone point me to a tutorial or give code example of how this cross-platform (Android/Java to ASP.NET/C#) communication can be done?
(I'm trying to create a leader board or global scoreboard for my Android game on my server.)
Thanks.
Your app should expose a webservice.
There is no native support for .net soap based webservices. But you can use the ksoap android port:
http://code.google.com/p/ksoap2-android/
which allows an android app to consume a .net asmx webservice.
However the deserialisation of complex on the client side involves lot of code writing for every object you want so pass to the client.
I tried it for a project and there were some problems I ran into (either I could get result back to the client but the parameters i passed where always null or the other way - I could pass arguments but the result was null).
Here is an example I posted for getting an int: How to call a .NET Webservice from Android using KSOAP2?
However, from my current knowlege I would suggest using a .asmx webservice that returns a json string and use a java json serialiser to parse the output. The advantages:
Write less code
Faster, since mobile devices don't always have good internet connections and the xml overhead from soap is bigger than json.
Quickstart:
Create a new asmx Webservice in your .net webapp.
Include a reference to System.Web.
Decorate your webservice class with [ScriptService] and your method with [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
[ScriptService]
public class WebService1 : System.Web.Services.WebService
{
[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public string HelloAndroid()
{
return "Hello Android";
}
}
(I think you have to add a reference to System.Web.Extension.dll which is available since .net 3.5).
Your webservice will still return XML (so you can use it with a soap client) unless you make a HTTPPost request with content-type "application/json".
use this code to contact the webservice from android:
private JSONObject sendJsonRequest(string host, int port,
String uri, JSONObject param)
throws ClientProtocolException, IOException, JSONException
{
HttpClient httpClient = new DefaultHttpClient();
HttpHost httpHost = new HttpHost(host, port);
HttpPost httpPost = new HttpPost(uri);
httpPost.addHeader("Content-Type", "application/json; charset=utf-8");
if (param != null)
{
HttpEntity bodyEntity = new StringEntity(param.toString(), "utf8");
httpPost.setEntity(bodyEntity);
}
HttpResponse response = httpClient.execute(httpHost, httpPost);
HttpEntity entity = response.getEntity();
String result = null;
if (entity != null) {
InputStream instream = entity.getContent();
BufferedReader reader = new BufferedReader(
new InputStreamReader(instream));
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null)
sb.append(line + "\n");
result = sb.toString();
instream.close();
}
httpPost.abort();
return result != null ? new JSONObject(result) : null;
}
if your webservice methods looks like this:
[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public User GetUser(string name, int age)
{
return new User { Name = name, Age = age; }
}
You can call it this way from android:
public void getUser() {
// if you put a json object to the server
// the properties are automagically mapped to the methods' input parameters
JSONObject param = new JSONObject();
param.put("name", "John Doe");
param.put("age", 47);
JSONObject result = sendJsonRequest("server", 80,
"http://server:80/service1.asmx/GetUser", param);
if (result != null) {
JSONObject user = new JSONObject(result.getString("d"));
// .net webservices always return the result
// wrapped in a parameter named "d"
system.out.println(user.getString("name"));
system.out.println(user.getInt("age").toString());
}
}
Handling server exceptions on the client side:
Add this class to your project:
import org.json.JSONException;
import org.json.JSONObject;
public class JSONExceptionHelper {
private static final String KEY_MESSAGE = "Message";
private static final String KEY_EXCEPTIONTYPE = "ExceptionType";
private static final String KEY_STACKTRACE = "StackTrace";
public static boolean isException(JSONObject json) {
return json == null
? false
: json.has(KEY_MESSAGE) &&
json.has(KEY_EXCEPTIONTYPE) &&
json.has(KEY_STACKTRACE);
}
public static void ThrowJsonException(JSONObject json) throws JSONException {
String message = json.getString(KEY_MESSAGE);
String exceptiontype = json.getString(KEY_EXCEPTIONTYPE);
String stacktrace = json.getString(KEY_STACKTRACE);
StringBuilder sb = new StringBuilder();
sb.append(exceptiontype);
sb.append(": ");
sb.append(message);
sb.append(System.getProperty("line.separator"));
sb.append(stacktrace);
throw new JSONException(sb.toString());
}
}
Now replace the return statement from the sendJSONRequest with:
JSONObject json = result != null ? new JSONObject(result) : null
if (JSONExceptionHelper.isException(json))
JSONExceptionHelper.ThrowJsonException(json);
return json;
Please note: The exception is passed to the client only if connection comes from localhost.
Otherwise you get an http error 500 (or 501? I can't remember). You have to configure your IIS to send error 500 to the client.
Try it out and create a webservice that always throws an exception.
Sounds like a job for Web Services.
Start by creating a Web Service on the Windows web server, you can do this with ASP.NET (or maybe this might be more current).
On the Java side you can call the webservice and use the results that you get back. I think this question may help you get started on this side.
In case you have trouble writing web methods which return array of objects, you may want to refer here:
ksoap android web-service tutorial
Hope it helps.

Categories