Many thanks to Topaco for pointing me in the right direction. Using their code, I was able to make some more progress. But, when I tried their solution, I still ran into this weird could not find file specified error. However, all I needed to do was adjust some IIS settings to resolve that issue. I set up a new app pool, enabled 32-bit applications, and set "Load user profile" to True.
Unfortunately, I still couldn't generate valid tokens. One issue was getting the right date format, so after some more Googling I modified some code I found to get dates generating in the correct format.
Later, after meeting with a developer from the payment processor, I realized I was trying to validate my tokens incorrectly. I didn't have the public key to go with my private key, so I was doomed to get "Invalid Token" on jwt.io. I then checked the tokens generated with Topaco's first solution using CngKey
's, and my token was indeed valid. Once I knew there wasn't an issue with the tokens, I also fixed the API link I was using and voila, my requests were going through!
All in all, here's a simplified version of the code I have ended up with. All this requires is installing jose-jwt and RestSharp NuGet packages.
Imports Jose
Imports RestSharp
Imports System.Net
Imports System.Net.Http
Imports System.Security.Cryptography
Imports System.Web.Http
Module MyModule
Public Const KEY_ID = "...."
Public Const PRIVATE_KEY =
"-----BEGIN PRIVATE KEY-----
.....
-----END PRIVATE KEY-----"
'Gets proper format for JWT time
Public Function GetSecondsSinceEpoch(utcTime As DateTime) As Int64
Return (utcTime - New DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds
End Function
'Removes the enclosure and any whitespace
Public Shared Function ConvertPkcs8PemToDer(ByVal pkcs8Pem As String) As Byte()
Dim body As String = pkcs8Pem.Replace("-----BEGIN PRIVATE KEY-----", "").Replace("-----END PRIVATE KEY-----", "").Replace(Environment.NewLine, "")
Dim reg = New Regex("\s")
body = reg.Replace(body, "")
Return Convert.FromBase64String(body)
End Function
'Creates a UUID-formatted string.
Public Shared Function CreateJti() As String
Return Guid.NewGuid().ToString()
End Function
Public Function MakeMyRequest(uri As String, json_body As String) As String
'Fixes a weird .NET bug
ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 Or SecurityProtocolType.Tls12 Or SecurityProtocolType.Tls11 Or SecurityProtocolType.Tls
'Create RestRequest things
Dim rest_uri As New Uri(uri) 'Uri of the API to call
Dim rest_client As New RestClient With {.BaseUrl = rest_uri}
Dim rest_request As New RestRequest With {.Method = Method.POST} 'Or whatever Method your API requires
Dim rest_response As New RestResponse
'Add JSON body and headers
rest_request.AddJsonBody(json_body) 'Make sure this matches the API specifications
rest_request.AddHeader("accept", "application/json")
rest_request.AddHeader("Content-Type", "application/json")
'Conver the PEM string to Pkcs8 format, acceptable to CngKey.Import()
Dim privatePkcs8Der As Byte() = ConvertPkcs8PemToDer(PRIVATE_KEY)
Dim privateEcKey As CngKey = CngKey.Import(privatePkcs8Der, CngKeyBlobFormat.Pkcs8PrivateBlob)
'Claims and headers
Dim iss = "MyIssuer"
Dim aud = "MyAudience"
Dim iat = GetSecondsSinceEpoch(Now().ToUniversalTime()) '.ToUniversalTime ensures your local UTC offset doesn't affect the timestamp
Dim exp = GetSecondsSinceEpoch(Now().AddMinutes(5).ToUniversalTime())
Dim kid = KEY_ID
Dim headers = New Dictionary(Of String, Object)() From {
{"alg", "ES512"},
{"typ", "JWT"},
{"kid", KEY_ID} 'Key ID may not be necessary for your scenario, but put whatever extra parameters are required here like username, password, etc
}
Dim payload = New Dictionary(Of String, Object)() From {
{"iss", iss},
{"aud", aud},
{"iat", iat},
{"exp", exp},
{"jti", CreateJti()}
}
'Generate the token using the payload, CngKey, ES512 algorithm, and extra headers
Dim token = Jose.JWT.Encode(payload, privateEcKey, JwsAlgorithm.ES512, headers)
'Excecute the request
rest_request.AddHeader("Authorization", "Bearer " & token)
rest_response = rest_client.Execute(rest_request)
'Check for your API's designated response code
If rest_response.StatusCode <> 201 Then Throw New Exception("An error occurred processing the request.")
Return rest_response.Content
End Function
End Module