I'm trying to make a simple HTTP Post request to an e-payment processing API, and their specifications require me to add a Bearer token to the payload. I.e., rest_request.AddHeader("Authorization", "Bearer " + myToken)
, the token
being a JWT like "abcdefg.hijklmop.qrstuvwxyz"
.
They have provided an ES512 private PEM key to encode into the JWT as a secret, but I have struggled to understand what exactly I need to do to my key (formatted like below).
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
I am using .NET Framework 4.5 (it's a legacy system), VB.NET for language, and I have been given a key ID and private key by the API provider.
The API provider needs iss
, aud
, iat
, exp
, kid
, and jti
values in the headers and claims. (I'm not 100% sure what the issuer and audience values should be or why they should be what they are, so any pointers there would also be helpful.)
ECDSa.ImportFromPem
does exist, but only in .NET 6.message
string used for? Does it matter?x
, y
, and d
parameters.PemReader
to no avail.CngKey
object. (This is my best lead so far.)Tldr, I have an ES512 PEM key stored as a string that I need to encode as a secret in a JWT Bearer token, but I am stuck in .NET Framework 4.5 and know very little about encryption. Everything I've tried has either crashed or resulted in a 401 unauthorized
error response from the API endpoint.
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