DocuSign Embedded RequestRecipientToken - 500 internal Server Error - vb.net

-Issue Resolved
On Live accounts that Use Embedded Signing, The Account Manager will need to either disable In Session or apply the accounts X.509 Certificate.
There is no way to bypass without the DocuSign Account Managers/Customer Support making updates to non forward facing settings.
-Using SOAP API in a VB.NET application.
I have recently moved our application to live after endless testing on the staging environment. Everything is working as expected except when I get too opening the recipient signature page.
When I make the RequestRecipientToken call I receive the error "One or both of Username and Password are invalid."
The API log give me 00_Internal Server Error_RequestRecipientToken.txt
the log doesn't really give me any info just shows that call.
I know this all works on staging, and I have tried to have all my account settings the same on both environments.
After looking all over I saw that X509 Certificate was recommended, so I added this last line in my DSAPI
-update code:
Protected Overrides Function GetWebRequest(uri As Uri) As WebRequest
IntegratorKey = SettingsHelper.sIntegrationKey
Password = SettingsHelper.sAPIPassword
Username = SettingsHelper.sAPIUserName
Dim r As System.Net.HttpWebRequest = MyBase.GetWebRequest(uri)
r.Headers.Add("X-DocuSign-Authentication", "<DocuSignCredentials><Username>" & Username & "</Username><Password>" & Password & "</Password><IntegratorKey>" & IntegratorKey & "</IntegratorKey></DocuSignCredentials>")
Dim store As X509Store = New X509Store(StoreName.My, StoreLocation.CurrentUser)
store.Open(OpenFlags.ReadOnly)
Dim certs As X509Certificate2Collection = store.Certificates.Find(X509FindType.FindByIssuerDistinguishedName, <{OUR VALUE}>, False)
Dim cert As New X509Certificate2
If certs.Count > 0 Then
cert = certs(0)
Dim securityToken = New X509SecurityToken(cert)
r.ClientCertificates.Add(securityToken.Certificate)
End If
store.Close()
Return r
End Function
This did not help.
Any help would be appreciated.

Related

Replace SSL Certificate on Remote IIS 10 server from VB.NET

I'm attempting to automate the process of renewing my SSL certificates for a few different publicly accessible endpoints. I'm using Certify the Web's Certify SSL/TLS Certificate Management to complete the CSR and SSL generation and validation via Let's Encrypt and Certify DNS. This generates the .pfx files, which are then copied across the network to a location where my main "daily processing" application can access them and try to install them. I've been able to successfully get that application to install the certificates to the remote servers' certificate stores, but I'm unable to get the IIS 10 bindings reconfigured on the sites to use the new certificates.
For reference, here's the code for installing the certificate in the remote server's certificate store, which seems to work perfectly:
Imports System.Security.Cryptography.X509Certificates
Imports System.Security.Permissions
Imports Microsoft.Web.Administration
Private Sub AddCertificateToRemoteStore(ByVal HostName As String, ByVal StoreName As String, ByVal Certificate As X509Certificate2)
If HostName Is Nothing OrElse String.IsNullOrEmpty(HostName) Then
Throw New ArgumentNullException("HostName", "You must specify a server hostname")
ElseIf StoreName Is Nothing OrElse String.IsNullOrEmpty(StoreName) Then
Throw New ArgumentNullException("StoreName", "You must specify a certificate store name")
ElseIf Certificate Is Nothing Then
Throw New ArgumentNullException("Certificate", "A valid X509Certificate2 object is required")
Else
Dim CertStorePath As String = String.Format("\\{0}\{1}", HostName, StoreName)
Dim RootStorePath As String = String.Format("\\{0}\Root", HostName)
Dim IntermediateStorePath As String = String.Format("\\{0}\CA", HostName)
Using CertChain As New X509Chain
If CertChain.Build(Certificate) Then
Dim FindResults As X509Certificate2Collection
Dim StorePermissions As New StorePermission(PermissionState.Unrestricted)
With StorePermissions
.Flags = StorePermissionFlags.OpenStore Or StorePermissionFlags.AddToStore
.Assert()
End With
For C = 0 To CertChain.ChainElements.Count - 1
If C = 0 Then
Using CertificateStore As New X509Store(CertStorePath, StoreLocation.LocalMachine)
With CertificateStore
.Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)
If FindResults.Count <= 0 Then
.Add(CertChain.ChainElements(C).Certificate)
End If
FindResults.Clear()
.Close()
End With
End Using
ElseIf C = CertChain.ChainElements.Count - 1 Then
Using RootStore As New X509Store(RootStorePath, StoreLocation.LocalMachine)
With RootStore
.Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)
If FindResults.Count <= 0 Then
.Add(CertChain.ChainElements(C).Certificate)
End If
FindResults.Clear()
.Close()
End With
End Using
Else
Using IntermediateStore As New X509Store(IntermediateStorePath, StoreLocation.LocalMachine)
With IntermediateStore
.Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)
If FindResults.Count <= 0 Then
.Add(CertChain.ChainElements(C).Certificate)
End If
FindResults.Clear()
.Close()
End With
End Using
End If
Next C
End If
End Using
End If
End Sub
With the certificate successfully added to the store (I've verified that it's there through certlm.msc), the next obvious step is to apply the new certificate to the existing IIS 10 site's bindings so it can actually be used for SSL/TLS communication. Here's what I'm currently using to try to accomplish that with the Microsoft.Web.Administration namespace:
Private Sub ApplyCertificateBinding(ByVal HostName As String, ByVal StoreName As String, ByVal ActiveCertificate As X509Certificate2)
If HostName Is Nothing OrElse String.IsNullOrEmpty(HostName) Then
Throw New ArgumentNullException("HostName", "You must specify a server hostname")
ElseIf StoreName Is Nothing OrElse String.IsNullOrEmpty(StoreName) Then
Throw New ArgumentNullException("StoreName", "You must specify a certificate store name")
ElseIf ActiveCertificate Is Nothing Then
Throw New ArgumentNullException("ActiveCertificate", "A valid X509Certificate2 object is required")
Else
Dim SSLSiteName As String = ActiveCertificate.GetNameInfo(X509NameType.DnsName, False)
Dim HostSites As New List(Of Site)
Using HostManager As ServerManager = ServerManager.OpenRemote(HostName)
For Each Site As Site In HostManager.Sites
If Site.Name = SSLSiteName Then
HostSites.Add(Site)
Else
For Each Binding In Site.Bindings
If Binding.Host = SSLSiteName Then
HostSites.Add(Site)
Exit For
End If
Next Binding
End If
Next Site
For Each Site As Site In HostSites
For Each SiteBinding In Site.Bindings
If SiteBinding.Protocol = "https" Then
Dim NewBinding As Binding = Site.Bindings.CreateElement
NewBinding.CertificateStoreName = StoreName
NewBinding.Protocol = "https"
NewBinding.CertificateHash = ActiveCertificate.GetCertHash
NewBinding.BindingInformation = SiteBinding.BindingInformation
SiteBinding = NewBinding
HostManager.CommitChanges()
End If
Next SiteBinding
Site.Stop()
'PROBABLY A BETTER WAY TO HANDLE THIS
Do While Site.State <> ObjectState.Stopped
Loop
Site.Start()
'AND THIS
Do While Site.State <> ObjectState.Started
Loop
Next Site
End Using
End If
End Sub
This code gets all the way through the process without error, but it doesn't seem to actually make the necessary changes for the site to start using the new certificate. I manually restarted/refreshed the site from the IIS interface on the host, but it still doesn't seem to take effect. I've checked both the binding settings in IIS and the site itself (browser) and confirmed that it's still using the "old" certificate.
I've also tried to directly set the certificate hash of the SiteBinding object to the X509Certificate2.GetCertHash value, as well as assigning the SiteBinding object to the NewBinding object before trying to set the CertificateHash property as above. Unfortunately, both of these methods throw a NotSupportedException stating: The specified operation is not supported when a server name is specified.
Additionally, there are settings from the "live" SiteBinding object that can't be set on the NewBinding object (like the .Host property). All I really want to be able to do is to change the active certificate on that site and not muck around with any of the binding's other properties. The wording of the exception seems to indicate that what I'm trying to do can't be done remotely (at least, not with the Microsoft.Web.Administration API), but I can't imagine that there isn't a way to accomplish this goal. I'm sure I'm simply missing/overlooking something here, but my Google-fu is failing me and I need to get this project functional as soon as possible.
EDIT #1
I added the Site.Stop() and Site.Start() methods to restart the site from code, but it didn't make any difference. Plus, I'm sure there's probably a better way to implement that than what I've added to the code above.
EDIT #2
I've refactored some things to align with the suggestion from Joel Coehoorn in the comments. The code above represents the current state but still produces the same result: No exception occurs, but I cannot get the bindings updated to use the new certificate, even though it's apparently added to the store.
Just to triple-check, I went to the site's bindings in IIS and the new certificate does show up as available to apply to the site. I know there's a lot of blur, but the one highlighted in blue is what I'm trying to apply to the binding:
EDIT #3
After reading Conrado Clark's Developer Log entry titled Adding SSL Binding to a remote website using Microsoft.Web.Administration, I decided to try to add the NewBinding to IIS instead of just updating the existing one:
Site.Bindings.Add(NewBinding)
Site.Bindings.Remove(SiteBinding)
HostManager.CommitChanges()
This produced a different exception: Cannot add duplicate collection entry of type 'binding' with combined key attributes 'protocol, bindingInformation' respectively set to 'https, XXX.XXX.XXX.XXX:443:'
So, I tried removing the existing binding first:
Site.Bindings.Remove(SiteBinding)
Site.Bindings.Add(NewBinding)
HostManager.CommitChanges()
This time it made it through the first two steps (Site.Bindings.Remove() and Site.Bindings.Add()), but when it tried to execute HostManager.CommitChanges(), I got another "new" exception: A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520). Additionally, it "reset" the binding so there was no certificate installed on the site.
Just to see what would happen, I tried to commit the Site.Bindings.Remove() before trying to add it back.
Site.Bindings.Remove(SiteBinding)
HostManager.CommitChanges()
Site.Bindings.Add(NewBinding)
HostManager.CommitChanges()
The initial commit seemed to work fine (and the binding disappeared completely from IIS), but when it went to add the new binding, I got this: The configuration object is read only, because it has been committed by a call to ServerManager.CommitChanges(). If write access is required, use ServerManager to get a new reference.
I manually recreated the binding (thankfully I had taken a quick screenshot before I started messing with it), but that last error has given me an idea for my next attempt. I'm going to try to break the Add() and Remove() methods out to new methods where I can open new instances of the ServerManager object specifically for this purpose. I'll come back when I've had a chance to write/test that.
EDIT #4
I tried the above and still ended up with an error stating that A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520). So, just to see if I could determine the cause of the problem, I went to IIS and manually tried to apply the new certificate. I got the SAME EXACT ERROR from IIS! (Yes, I know... I probably should have checked that bit a long time ago, but here we are)
It looks like there's a problem with the certificate in the store. Digging around a little deeper, I found an old reference on the MSDN forums talking about this error being related to a missing private key. This makes it sound like I missed a step in the certificate installation process, so I guess I need to take another step back and figure out what's wrong with that method before proceeding.
a NotSupportedException stating: The specified operation is not supported when a server name is specified.
That's exactly what we should expect. Microsoft didn't develop that API to manage remote server's everything. As proof, you can see that even IIS Manager (built upon the same API) does not support managing server certificates of a remote machine.
If I were you, I will actually use other approaches, such as developing a dedicated small demon app to run on each IIS machines, so that actual communication via Microsoft.Web.Administration happens locally, not remotely.
I've actually gotten this working! As identified in my multiple EDITs to the OP, it seems the main problem actually had to do with the original import of the certificate. Once that issue was resolved, everything else pretty much fell into place. I've provided the full working code at the end if you want the TL;DR version, but here's what I found:
EXPLANATION/TROUBLESHOOTING
After a bunch of further research into the individual errors I encountered in my initial testing - specifically the A specified logon session does not exist. It may already have been terminated. error - I ran across this SO question: IIS 7 Error "A specified logon session does not exist. It may already have been terminated." when using https.
In the linked answer from user naimadswdn, they state that:
I fixed it by running:
certutil.exe -repairstore $CertificateStoreName $CertThumbPrint
where $CertificateStoreName is store name, and $CertThumbPrint is the thumbprint of imported certificate.
Another answer to that same question from user Ed Greaves provides a bit of explanation for the underlying cause of the problem:
This storage provider is a newer CNG provider and is not supported by IIS or .NET. You cannot access the key. Therefore you should use certutil.exe to install certificates in your scripts. Importing using the Certificate Manager MMC snap-in or IIS also works but for scripting, use certutil as follows:
The original question was asked about trying to import the certificate directly in IIS and, since my testing showed that I wasn't able to do that either, I went ahead and tried to repair the certificate from the command line on the server with certutil. I went back to IIS and, this time was able to successfully apply the certificate to the binding without error.
Since that worked, I reset the binding and tried again to set it to the new certificate through my code. No exception was encountered and, when I went to check the binding in IIS, it showed the correct certificate selected. I verified through my browser that it was showing the new certificate and everything seems to be working as expected/intended.
Of course, I want this process automated, so I can't be logging in to the server to repair certificates every 90 days. So, now I have two options:
Keep my existing code for adding the certificate to the store, then repair the certificate (as per naimadswdn's answer), or
Take Ed Greaves' suggestion and use certutil to perform the actual import of the certificate.
Since I already have it importing the certificate into the store without any exception being thrown, I decided to go with the former solution (for now, at least). Of course, I want to do this remotely, so I've chosen to use WMI to execute certutil on the server (see the CertUtilRepairCertificateInStore() method in the full code listing below).
(FINAL) TESTING
I reset the binding on the IIS site and deleted the certificate from the store to test my "new" process:
The certificate was successfully added to the store by the AddCertificateToRemoteStore() method, but I paused execution before allowing it to "repair" the certificate in the store.
While debugging was paused, I tried to manually apply the certificate to the binding in IIS. This resulted in the same logon session does not exist error.
I allowed the CertUtilRepairCertificateInStore() method to run the certutil on the server through WMI. I didn't get any exceptions.
I paused debugging again after the certificate was "repaired" and tried to manually apply the certifiate to the binding in IIS. Now the binding was successfully updated to use the new certificate.
I manually reset the binding in IIS to use the old certificate and allowed my ApplyCertificateBinding() method to execute.
This time, the method completed without throwing any exceptions, so I went into IIS and verified that it does, indeed, have the new certificate applied to the appropriate binding. As one last bit of verification, I went to my browser and checked the certificate from the site itself and it shows the correct new Let's Encrypt certificate. It seems that, along with some other minor tweaks along the way, the certutil -repairstore call was the final solution.
SOLUTION (CODE)
After all of that, I decided to keep my original code as-is and simply add the WMI bit to "repair" the certificate immediately after importing it to ensure it's ready to be applied to the binding. Yes, I could allow that to be handled in a Try/Catch block for the binding, but I'd rather just avoid the issue altogether. Here's a (mostly) complete listing of the functional code I'm using now and, so far, it seems to work exactly as I require/expect.
(quick edit - I moved the HostManager.CommitChanges() execution outside of the For/Next loop through the HostSites list because once the commit is executed, the code can't make any more changes until a new connection is opened.)
Imports System.Security.Cryptography.X509Certificates
Imports System.Security.Permissions
Imports Microsoft.Web.Administration
Friend Async Function InstallSSLCertificate(ByVal PFXFile As IO.FileInfo) As Task(Of X509Certificate2)
If PFXFile Is Nothing Then
Throw New ArgumentNullException("PFXFile", "You must provide a valid PFX certificate file")
ElseIf Not PFXFile.Exists OrElse PFXFile.Length <= 0 Then
Throw New ArgumentException("PFXFile", "You must provide a valid PFX certificate file")
Else
Dim CertPFX As X509Certificate2 = Nothing
Dim CertSubject As String = String.Empty
'THE GetOperationalCredentials() METHOD (not defined here) IS A CUSTOM UTILITY METHOD FOR RETRIEVING A SPECIFIC SET OF CREDENTIALS STORED ELSEWHERE
Dim PFXCredentials As Net.NetworkCredential = GetOperationalCredentials(SecurityOperation.PFX)
Dim UserCredentials As Net.NetworkCredential = GetOperationalCredentials(SecurityOperation.Server)
If Not PFXCredentials Is Nothing Then
'BUILD/EXTRACT THE X509Certificate2 OBJECT INFORMATION FROM THE PFX FILE
'MAKE SURE TO SET THE X509KeyStorageFlags.MachineKeySet AND X509KeyStorageFlags.PersistKeySet FLAGS TO
' ENSURE THE CERTIFICATE PERSISTS IN THE STORE
CertPFX = New X509Certificate2(PFXFile.FullName, PFXCredentials.SecurePassword, X509KeyStorageFlags.MachineKeySet Or X509KeyStorageFlags.PersistKeySet)
CertSubject = CertPFX.GetNameInfo(X509NameType.DnsName, False)
If Not CertSubject Is Nothing AndAlso Not String.IsNullOrEmpty(CertSubject.Trim) Then
If CertSubject.ToLower.Trim.StartsWith("ftps") Then
'THIS CONDITIONAL (not defined here) IS FOR HANDLING CERTIFICATE(S) THAT CANNOT BE INSTALLED FROM A CERTIFICATE STORE
If Not Await InstallSSLCertificateFromPEM(CertSubject, PFXFile, UserCredentials) Then
Return Nothing
End If
Else
If Not InstallCertificateFromPFX(CertPFX, UserCredentials) Then
Return Nothing
End If
End If
Else
Return Nothing
End If
Else
Return Nothing
End If
Return CertPFX
End If
End Function
Private Function InstallCertificateFromPFX(ByVal Certificate As X509Certificate2, ByVal Credentials As Net.NetworkCredential) As Boolean
Dim Subject As String = Certificate.GetNameInfo(X509NameType.DnsName, False)
Dim Hostname As String = String.Empty
Dim StoreName As String = String.Empty
If Subject.ToLower.Trim.StartsWith("www") Then
Hostname = "<WEBSITE_SERVER_NAME>"
StoreName = "WebHosting"
ElseIf Subject.ToLower.Trim.StartsWith("rdp") Then
Hostname = "<REMOTE_DESKTOP_SERVER_NAME>"
StoreName = "My"
End If
Try
AddCertificateToRemoteStore(Hostname, StoreName, Certificate, Credentials)
ApplyCertificateBinding(Hostname, StoreName, Certificate)
'THE CleanUpCertifcateStore() METHOD (not defined here) IS SOMETHING I INTEND TO
' IMPLEMENT TO GET RID OF OLD UNUSED/EXPIRED CERTIFICATES
CleanUpCertifcateStore(Hostname, StoreName, Certificate)
Return True
Catch ex As Exception
MessageBox.Show(ex.Message)
Return False
End Try
End Function
''' <summary>
''' Connect to the remote certificate store and import the details from a valid <see cref="X509Certificate2"/> object
''' </summary>
''' <param name="HostName">The hostname of the server where the certificate store is located</param>
''' <param name="StoreName">The name of the certificate store into which the certificate should be imported</param>
''' <param name="Certificate">A valid <see cref="X509Certificate2"/> object containing the details of the certificate to be imported</param>
''' <param name="Credentials">A valid <see cref="Net.NetworkCredential"/> object for passing to the certificate repair method for establishing a WMI connection</param>
Private Sub AddCertificateToRemoteStore(ByVal HostName As String, ByVal StoreName As String, ByVal Certificate As X509Certificate2, ByVal Credentials As Net.NetworkCredential)
If HostName Is Nothing OrElse String.IsNullOrEmpty(HostName) Then
Throw New ArgumentNullException("HostName", "You must specify a server hostname")
ElseIf StoreName Is Nothing OrElse String.IsNullOrEmpty(StoreName) Then
Throw New ArgumentNullException("StoreName", "You must specify a certificate store name")
ElseIf Certificate Is Nothing Then
Throw New ArgumentNullException("Certificate", "A valid X509Certificate2 object is required")
Else
'SET UP THE PATHS TO THE APPROPRIATE CERTIFICATE STORES
Dim CertStorePath As String = String.Format("\\{0}\{1}", HostName, StoreName)
Dim RootStorePath As String = String.Format("\\{0}\Root", HostName)
Dim IntermediateStorePath As String = String.Format("\\{0}\CA", HostName)
'USE THE X509Chain OBJECT TO MAKE IT EASIER TO IDENTIFY THE APPROPRIATE STORE FOR
' EACH CERTIFICATE EXTRACTED FROM THE PFX
Using CertChain As New X509Chain
If CertChain.Build(Certificate) Then
Dim FindResults As X509Certificate2Collection
Dim StorePermissions As New StorePermission(PermissionState.Unrestricted)
With StorePermissions
.Flags = StorePermissionFlags.OpenStore Or StorePermissionFlags.AddToStore
.Assert()
End With
For C = 0 To CertChain.ChainElements.Count - 1
If C = 0 Then
'FIRST ELEMENT IN THE CHAIN = CERTIFICATE FOR THE SITE
Using CertificateStore As New X509Store(CertStorePath, StoreLocation.LocalMachine)
With CertificateStore
.Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)
If FindResults.Count <= 0 Then
.Add(CertChain.ChainElements(C).Certificate)
End If
FindResults.Clear()
.Close()
End With
End Using
'REPAIR THE CERTIFICATE'S PROVIDER/PRIVATE KEY IN THE REMOTE STORE
CertUtilRepairCertificateInStore(HostName, StoreName, CertChain.ChainElements(C).Certificate, Credentials)
ElseIf C = CertChain.ChainElements.Count - 1 Then
'LAST ELEMENT IN THE CHAIN = ROOT CA CERTIFICATE
Using RootStore As New X509Store(RootStorePath, StoreLocation.LocalMachine)
With RootStore
.Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)
If FindResults.Count <= 0 Then
.Add(CertChain.ChainElements(C).Certificate)
End If
FindResults.Clear()
.Close()
End With
End Using
Else
'ANY ELEMENT BETWEEN THE FIRST AND LAST IN THE CHAIN = INTERMEDIATE CA CERTIFICATE(S)
Using IntermediateStore As New X509Store(IntermediateStorePath, StoreLocation.LocalMachine)
With IntermediateStore
.Open(OpenFlags.ReadWrite Or OpenFlags.OpenExistingOnly)
FindResults = .Certificates.Find(X509FindType.FindByThumbprint, CertChain.ChainElements(C).Certificate.Thumbprint, False)
If FindResults.Count <= 0 Then
.Add(CertChain.ChainElements(C).Certificate)
End If
FindResults.Clear()
.Close()
End With
End Using
End If
Next C
End If
End Using
End If
End Sub
''' <summary>
''' Use WMI to execute certutil.exe on the remote server to "repair" the certificate and correct issues with the provider/private key
''' </summary>
''' <param name="HostName">The hostname of the server where the certificate store is located</param>
''' <param name="StoreName">The name of the certificate store into which the certificate has been imported</param>
''' <param name="ActiveCertificate">A valid <see cref="X509Certificate2"/> object containing the details of the certificate to be repaired</param>
''' <param name="Credentials">A valid <see cref="Net.NetworkCredential"/> object for establishing the WMI connection</param>
Private Sub CertUtilRepairCertificateInStore(ByVal HostName As String, ByVal StoreName As String, ByVal ActiveCertificate As X509Certificate2, ByVal Credentials As Net.NetworkCredential)
Dim WMIOptions As New Management.ConnectionOptions
Dim WMIScope As Management.ManagementScope
Dim GetOptions As New Management.ObjectGetOptions
Dim WMIProcess As Management.ManagementClass
Dim WMIParameters As Management.ManagementBaseObject
With WMIOptions
.Username = Credentials.Domain & "\" & Credentials.UserName
.Password = Credentials.Password
.Impersonation = Management.ImpersonationLevel.Impersonate
.Authentication = Management.AuthenticationLevel.PacketPrivacy
.EnablePrivileges = True
End With
WMIScope = New Management.ManagementScope(String.Format("\\{0}\root\cimv2", HostName), WMIOptions)
WMIScope.Connect()
WMIProcess = New Management.ManagementClass(WMIScope, New Management.ManagementPath("root\cimv2:Win32_Process"), GetOptions)
WMIParameters = WMIProcess.GetMethodParameters("Create")
WMIParameters("CommandLine") = String.Format("cmd.exe /c C:\Windows\System32\certutil.exe -repairstore {0} {1}", StoreName, ActiveCertificate.Thumbprint)
WMIProcess.InvokeMethod("Create", WMIParameters, Nothing)
End Sub
''' <summary>
''' Connect to IIS on a remote host to apply a new certificate to a site's SSL bindings
''' </summary>
''' <param name="HostName">The hostname of the server where the certificate store is located</param>
''' <param name="StoreName">The name of the certificate store into which the certificate has been imported</param>
''' <param name="ActiveCertificate">A valid <see cref="X509Certificate2"/> object containing the details of the certificate that has been imported</param>
Private Sub ApplyCertificateBinding(ByVal HostName As String, ByVal StoreName As String, ByVal ActiveCertificate As X509Certificate2)
If HostName Is Nothing OrElse String.IsNullOrEmpty(HostName) Then
Throw New ArgumentNullException("HostName", "You must specify a server hostname")
ElseIf StoreName Is Nothing OrElse String.IsNullOrEmpty(StoreName) Then
Throw New ArgumentNullException("StoreName", "You must specify a certificate store name")
ElseIf ActiveCertificate Is Nothing Then
Throw New ArgumentNullException("ActiveCertificate", "A valid X509Certificate2 object is required")
Else
Dim SSLSiteName As String = ActiveCertificate.GetNameInfo(X509NameType.DnsName, False)
Dim HostSites As New List(Of Site)
Using HostManager As ServerManager = ServerManager.OpenRemote(HostName)
'FIND THE SITE(S) IN IIS THAT MATCH(ES) THE DETAILS FROM THE SSL CERTIFICATE
'>>> THIS IS **FAR FROM** BULLET-PROOF, BUT I AM NOT SURE HOW TO MAKE IT BETTER <<<
For Each Site As Site In HostManager.Sites
If Site.Name = SSLSiteName Then
HostSites.Add(Site)
Else
For Each Binding In Site.Bindings
If Binding.Host = SSLSiteName Then
HostSites.Add(Site)
Exit For
End If
Next Binding
End If
Next Site
For Each Site As Site In HostSites
'USE THE .ToList() METHOD TO BASICALLY MAKE AN IN-MEMORY "COPY" OF THE
' BindingCollection OBJECT WHERE CHANGES CAN BE MADE WITHOUT MODIFYING THE
' COLLECTION ITSELF (which would cause the For/Next loop to generate an
' exception whenever any changes are made to the site's bindings)
For Each SiteBinding In Site.Bindings.ToList
If SiteBinding.Protocol = "https" Then
If Not SiteBinding.CertificateHash.SequenceEqual(ActiveCertificate.GetCertHash) Then
'CANNOT JUST EDIT OR "REPLACE" AN EXISTING BINDING ON A REMOTE IIS HOST SO
' CREATE A NEW BINDING TO ADD TO THE SITE AFTER THE EXISTING BINDING HAS
' BEEN REMOVED
Dim NewBinding As Binding = Site.Bindings.CreateElement
NewBinding.CertificateStoreName = StoreName
NewBinding.Protocol = "https"
NewBinding.CertificateHash = ActiveCertificate.GetCertHash
NewBinding.BindingInformation = SiteBinding.BindingInformation
'THIS PROCESS MUST BE COMPLETED IN THIS ORDER
Site.Bindings.Remove(SiteBinding)
Site.Bindings.Add(NewBinding)
End If
End If
Next SiteBinding
'RESTARTING THE SITE IN IIS (there is almost certainly a better way to do this)
Site.Stop()
Do While Site.State <> ObjectState.Stopped
Loop
Site.Start()
Do While Site.State <> ObjectState.Started
Loop
Next Site
HostManager.CommitChanges()
End Using
End If
End Sub
I'm stepping into all of that from a method that looks for "pending" .pfx files that are waiting to be processd:
Private Async Sub CheckForNewSSLCertificates()
Dim PendingSSLFolder As DirectoryInfo = New DirectoryInfo("\\SERVER\Certify\PendingSSL\")
For Each PFXFile As FileInfo In PendingSSLFolder.GetFiles("*.pfx")
Dim CertPFX As Security.Cryptography.X509Certificates.X509Certificate2 = Await InstallSSLCertificate(PFXFile)
If Not CertPFX Is Nothing Then
'[ARCHIVE THE PFX]
End If
Next PFXFile
End Sub
I know the documentation is a bit sparse but, if you find ways to make this more effective/efficient, or if you have any questions about what it's doing here, please feel free to let me know. Eventually I may try to just use certutil to import the PFX and one-shot that process, but for now I just wanted to leave this here for anyone else who's trying to implement some "centralized" automation for SSL certificate management.

.NET Google API access token failing with no refresh token specified

I am trying to set up a class that can wrap around the .NET Google API so that I can use an Access Token that I have previously obtained to access a user's Google Drive. As of right now, I am just trying to get it to work so that I do not require a Refresh Token (more on that in a second). The ultimate goal is for somebody to go through a web page I have set up to authenticate where I obtain both an Access Token and a Refresh Token by directly calling to the Google Rest API (which I store in a database). They can then request to upload/download files onto their Drive on a different page which will first obtain the appropriate information from the database and then use the .NET Google API Library when accessing Drive.
However, when I attempt to access their Drive I get the the following error:
The access token has expired and could not be refreshed. Errors: refresh error, refresh error, refresh error
I know that the Access Token is valid because I obtain it only seconds earlier during my testing. Here is my code for setting up the Drive Service:
' NOTE: Code altered for brevity
Public Sub Initialize(accessToken As String)
' Set up the client secret information based on the default constants
Dim clientSecrets As New ClientSecrets()
clientSecrets.ClientId = DEFAULT_CLIENT_ID
clientSecrets.ClientSecret = DEFAULT_CLIENT_SECRET
' Set up a token based on the token data we got
' NOTE: Is it OK to leave some strings as NULL?
Dim token As New Responses.TokenResponse()
token.AccessToken = accessToken
token.RefreshToken = ""
token.TokenType = "Bearer"
token.IssuedUtc = DateTime.Now
token.ExpiresInSeconds = 3600
token.Scope = "drive"
token.IdToken = ""
' Set up a flow for the user credential
Dim init As New GoogleAuthorizationCodeFlow.Initializer()
init.ClientSecrets = clientSecrets
init.Scopes = New String() {DriveService.Scope.Drive}
init.Clock = Google.Apis.Util.SystemClock.Default
' Set up everything else and initialize the service
Dim baseInit As New BaseClientService.Initializer()
baseInit.HttpClientInitializer = New UserCredential(New GoogleAuthorizationCodeFlow(init), "user", token)
baseInit.ApplicationName = APP_NAME
_service = New DriveService(baseInit)
End Sub
Shortly after that, I then use the following code to request a folder so I can check to see if it exists or not.
Private Function GetDriveFolder(folderPath As String, ByRef folderIds As String(), Optional createMissingFolders As Boolean = False, Optional parentFolderId As String = "root") As Data.File
Dim creatingFolderPath As Boolean = False
Dim currentFolder As Data.File = Nothing
Dim folderPathSplit As String() = folderPath.Replace("/", "\").Trim("\").Split("\")
Dim folderIdList As New List(Of String)
folderIds = {}
' Loop through each folder in the path and seek each out until we reach the end
For x As Integer = 0 To folderPathSplit.Length - 1
Dim result As FileList = Nothing
If Not creatingFolderPath Then
' Build a list request which we will use to seek out the next folder
Dim request As FilesResource.ListRequest = _service.Files.List()
request.Q = "mimeType='application/vnd.google-apps.folder' and name='" & folderPathSplit(x) & "'"
If currentFolder Is Nothing Then
request.Q &= " and '" & EscapeDriveValue(parentFolderId) & "' in parents"
Else
request.Q &= " and '" & EscapeDriveValue(currentFolder.Id) & "' in parents"
End If
request.Spaces = "drive"
request.Fields = "files(id, name)"
' Execute the search, we should only get a single item back
' NOTE: Error thrown on this request
result = request.Execute()
End If
' So on.....
So, I'm just trying to get it to work with only the Access Token for the time being because if it ends up getting refreshed I'll need to know so that I can update my database. However, if I do include the Refresh Token I get the following error:
Error:"unauthorized_client", Description:"Unauthorized", Uri:""
I'm guessing this has something to do with the way I have configured my application through the Dev Console but if I authenticate through the Google API Library by having it launch a browser to get my credentials everything works fine. So, I'm really not sure where to go from here as I haven't found anybody having similar problems and the guides don't cover specifying your own Access Token.
Also, as a quick note this is the URL I am using when having the user authenticate:
String.Format("https://accounts.google.com/o/oauth2/v2/auth?client_id={0}&state={1}&redirect_uri={2}&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive&access_type=offline&include_granted_scopes=true&prompt=select_account%20consent&response_type=code", GOOGLEAPI_CLIENTID, validateState, redirectUri)
Thanks for the help!
If you have an access-token then the simplest way to create a google credential is to use the GoogleCredential.FromAccessToken() method passing in your access token.
This returns you a GoogleCredential instance which you can use to set the HttpClientInitializer property when building the DriveService.
If you then still get an error when accessing the drive service, then it's likely there's something incorrect in how you are asking for the access-token.

Web application working on iis7 but not on local machine

I have my VB.NET web application successfully built and compiled on ISS7 server but on the local machine when compiling through Visual studion 2013 it is
returning "System.Security.Cryptography.CryptographicException: The system cannot find the file specified." error.
Not sure what is happening but throws an error at Dim cert As New X509Certificate2(certFile, certFilePassword,X509KeyStorageFlags.MachineKeySet)
Below is the code where I am trying to use a certificate that is installed on my local machine.
Sub LoginED(ByVal token As String)
Try
'We need the ED user ID
lblEDUserID.Text = GetEDUserID().ToString()
'Track user login
InsertLogin(txtEDUsername.Text, True)
'Set login cookies for ED users
If lblEDUserID.Text = "0" Then
lblEDLoginStatus.Text = "You have entered an incorrect login. Please try again."
Else
Dim certFile As String = "VIP_Cert.pfx"
Dim certFilePassword As String = "password"
Dim cert As New X509Certificate2(certFile, certFilePassword,X509KeyStorageFlags.MachineKeySet)
Dim vipSoapInterfaceQueryServiceCert As New WebReference.QueryService
'Dim fs As FileStream = File.Open(certFile, FileMode.Open, FileAccess.ReadWrite)
'Dim filesize As Long = fs.Length
'Dim buffer(filesize) As Byte
'fs.Read(buffer, 0, filesize)
'byte[] buffer = new byte[fs.Length];
'int count = fs.Read(buffer, 0, buffer.Length);
'fs.Close()
lblEDLoginStatus.Text = "Activation "
vipSoapInterfaceQueryServiceCert.ClientCertificates.Add(cert)
Dim vipSoapInterfaceService As New WebReference.GetCredentialInfoRequestType()
vipSoapInterfaceService.credentialId = "SMR23324543"
vipSoapInterfaceService.credentialType = WebReference.CredentialTypeEnum.STANDARD_OTP
vipSoapInterfaceService.requestId = "ABCD"
Dim vipSoapInterfaceResponse As New WebReference.GetCredentialInfoResponseType()
vipSoapInterfaceResponse = vipSoapInterfaceQueryServiceCert.getCredentialInfo(vipSoapInterfaceService)
lblEDLoginStatus.Text = "WebService Response: " + vipSoapInterfaceResponse.credentialId + ", Status:" + vipSoapInterfaceResponse.credentialStatus
End If
Finally
What I have tried so far:
1) Tried to specify the complete path of where the certificate file is on the machine.
2) Tried different formats of certificates (.pfx; .p12; .cer)
3) Tried the certificate open, read and sent into a buffer to take the input from a buffer.
(I removed the buffer logic when deploying on IIS7 as I placed the .pfx certificate file in System32 folder and the code could automatically recognise the certificate. But the local would not recognize)
4) I tried removing the "X509KeyStorageFlags.MachineKeySet" parameter. (I use that for the IIS7 compilation)
Any help is appreciated!!
Thanks! I could figure out what needed.
I had to create an App Pool for the Certificate and then provide Full Control permissions to that pool

Stripe Payment with VB - 400 Bad Request

I'm trying to make a stripe payment work from a VB website. I know, I know, "I should use C#". I can't because the site is already in VB. Nothing I can do about it.
Anyway, I have most of it figured out:
User clicks submit button with valid info
Form submits to Stripe
Stripe sends a token back
A jQuery ajax function posts the data to donate/pay-by-stripe
I have this line of code in my Global.asax.vb
routes.MapRoute("pay-by-stripe", "donate/pay-by-stripe", New With{.controller = "Dynamic", .action = "PayByStripe"})
So my PayByStripe function in the Dynamic Controller looks like this:
Function PayByStripe()
''The Stripe Account API Token
Dim STR_Stripe_API_Token As String = "sk_test_*****"
''The Stripe API URL
Dim STR_Stripe_API_URL As [String] = "https://api.stripe.com/v1/charges"
''The Stripe Card Token
Dim token As String = HttpContext.Request.Form("token")
Dim description As String = HttpContext.Request.Form("description")
Dim amount As Single = HttpContext.Request.Form("amount")
''Creates a Web Client
Dim OBJ_Webclient As New System.Net.WebClient()
''Creates Credentials
Dim OBJ_Credentials As New System.Net.NetworkCredential(STR_Stripe_API_Token, "")
''Sets the Credentials on the Web Client
OBJ_Webclient.Credentials = OBJ_Credentials
''Creates a Transaction with Data that Will be Sent to Stripe
''Dim OBJ_Transaction As New System.Collections.Specialized.NameValueCollection()
Dim OBJ_Transaction As NameValueCollection = New NameValueCollection()
OBJ_Transaction.Add("amount", amount)
OBJ_Transaction.Add("currency", "usd")
OBJ_Transaction.Add("address-country", "US")
OBJ_Transaction.Add("description", "")
OBJ_Transaction.Add("card", token)
''The Stripe Response String
Dim STR_Response As String = Encoding.ASCII.GetString(OBJ_Webclient.UploadValues(STR_Stripe_API_URL, OBJ_Transaction))
'Response.Redirect("/donate/?transaction=success");
Return STR_Response
End Function
I'm getting a 400 bad request error on the STR_Response line:
Dim STR_Response As String = Encoding.ASCII.GetString(OBJ_Webclient.UploadValues(STR_Stripe_API_URL, OBJ_Transaction))
I'm a VB and Stripe noob, and not sure what this means. My main theory now is that it's because I don't have a /donate/pay-by-stripe/ page, but I don't know what I'd even put in there if I did create it.
Any help would be great!
That's a webservice you are calling, right?
A 400 Bad Request with a webservice means your XML request is malformed.
Example, in my request, part of it is a UTC in a certain date format. Example: <pp:utc>2013-05-24 2025</pp:utc>
So, if I were to malform my request to this <pp:utc>2013-05-24 2025</pp:utc2> it would result in:
HTTP/1.1 400 Bad Request
Cache-Control: private
Server: Microsoft-IIS/7.5
X-AspNet-Version: 2.0.5
So, check your request and make sure everything is properly formatted.
EDIT: just noticed I put the "incorrect" utc tags incorrectly.
Please notice the opening tag <pp:utc> is being closed with a </pp:utc2>, which is the reason why you see 400 bad request
I had to put my password in System.Net.NetworkCredentials, and address-country is not a usable field. The only usable fields when submitting a charge are amount, currency, description, and card (which is actually the token). This is the final, working version of my PayByStripe Function in my Dynamic Controller:
Function PayByStripe()
'' The Stripe Account API Token - change this for testing
Dim STR_Stripe_API_Token As String = ""
If (this_is_a_test) Then
' Test Secret Key
STR_Stripe_API_Token = "sk_test_***"
Else
' Prod Secret Key
STR_Stripe_API_Token = "sk_live_***"
End If
''The Stripe API URL
Dim STR_Stripe_API_URL As [String] = "https://api.stripe.com/v1/charges"
''The Stripe Card Token
Dim token As String = HttpContext.Request.Form("token")
Dim description As String = HttpContext.Request.Form("description")
Dim amount As Single = HttpContext.Request.Form("amount")
''Creates a Web Client
Dim OBJ_Webclient As New System.Net.WebClient()
''Creates Credentials
Dim OBJ_Credentials As New System.Net.NetworkCredential(STR_Stripe_API_Token, "YOUR PASSWORD FOR STRIPE")
''Sets the Credentials on the Web Client
OBJ_Webclient.Credentials = OBJ_Credentials
''Creates a Transaction with Data that Will be Sent to Stripe
Dim OBJ_Transaction As New System.Collections.Specialized.NameValueCollection()
OBJ_Transaction.Add("amount", amount)
OBJ_Transaction.Add("currency", "usd")
OBJ_Transaction.Add("description", description)
OBJ_Transaction.Add("card", token)
''The Stripe Response String
Dim STR_Response As String = Encoding.ASCII.GetString(OBJ_Webclient.UploadValues(STR_Stripe_API_URL, OBJ_Transaction))
Return STR_Response
End Function
I've never had to pass in my password when connecting to Stripe's API. Simply pass in your private API Key through an authorization header with no password. It may also help to pass in a version header as well, something Stripe recommends. The following lines of card are in C#, I know your question was in VB, but I'm sure you can easily adaptive this:
webrequest.Headers.Add("Stripe-Version", "2014-12-22");
webrequest.Headers.Add("Authorization", String.Concat("Basic ", (Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:", "sk_test_XXXXXXXXXXXXXXXXXXX"))))));
Also, it may help to know that Stripe sends a 400 Bad Request when an expired or invalid card token is sent.

VB.NET (WebRequest Authentication Issue)

I'm new to WebRequest authentication and have been researching how to authenticate w/ a couple websites to pull some excel data from them. Couple things I'm confused about is
a.) how to properly read a log from Fiddler (using this to pick up get/post data from the website authentication)
b.) how do use the data from Fiddler to program the VB.NET WebRequest properly.
I've been able to authenticate w/ websites that use simple authentication HTTPS, but any site that does any redirects/REST/cookie auth I'm lost...
Let me know if I can provide anymore detail.
Dim req As Net.HttpWebRequest = Net.WebRequest.Create(Url)
If Not Login = Nothing AndAlso Not Password = Nothing Then
Dim myCache As New System.Net.CredentialCache()
myCache.Add(New Uri(Url), "Basic", New System.Net.NetworkCredential(Login, Password))
req.Credentials = myCache
End If
Dim sr As New StreamReader(req.GetResponse().GetResponseStream())
Dim ss as string = sr.ReadToEnd
'Save it as excel & close stream
sr.Close()