.netjwt.net-4.5bearer-token

Difficulty signing JWT in .NET Framework 4.5 using pre-generated ES512 private key


The goal

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-----

Environment

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.)

Roadblocks

Things I've tried

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.


Solution

  • 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