How to generate JWT Bearer Flow OAuth access tokens from a .net core client?

Viewed 2440

I'm having trouble getting my .NET Core client to generate OAuth access tokens for a salesforce endpoint that requires OAuth of type 'JWT Bearer Flow'.

It seems there are limited .NET Framework examples that show a .NET client doing this, however none that show a .NET Core client doing it e.g. https://salesforce.stackexchange.com/questions/53662/oauth-jwt-token-bearer-flow-returns-invalid-client-credentials

So in my .NET Core 3.1 app i've generated a self signed certificate, added the private key to the above example's code when loading in the certificate, however a System.InvalidCastExceptionexception exception occurs on this line:

var rsa = certificate.GetRSAPrivateKey() as RSACryptoServiceProvider;

Exception:

System.InvalidCastException: 'Unable to cast object of type 'System.Security.Cryptography.RSACng' to type 'System.Security.Cryptography.RSACryptoServiceProvider'.'

It appears that this private key is used in the JWT Bearer Flow as part of the signature, and perhaps RSACryptoServiceProvider is not used in .NET core as it was in .NET Framework.

My question is this - is there actually a way in .NET Core to generate access tokens for the OAuth JWT Bearer Flow?

Full code that I'm using:

static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
        var token = GetAccessToken();
    }

    static dynamic GetAccessToken()
    {
        // get the certificate
        var certificate = new X509Certificate2(@"C:\temp\cert.pfx");

        // create a header
        var header = new { alg = "RS256" };

        // create a claimset
        var expiryDate = GetExpiryDate();
        var claimset = new
        {
            iss = "xxxxxx",
            prn = "xxxxxx",
            aud = "https://test.salesforce.com",
            exp = expiryDate
        };

        // encoded header
        var headerSerialized = JsonConvert.SerializeObject(header);
        var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
        var headerEncoded = ToBase64UrlString(headerBytes);

        // encoded claimset
        var claimsetSerialized = JsonConvert.SerializeObject(claimset);
        var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
        var claimsetEncoded = ToBase64UrlString(claimsetBytes);

        // input
        var input = headerEncoded + "." + claimsetEncoded;
        var inputBytes = Encoding.UTF8.GetBytes(input);

        // signature
        var rsa = (RSACryptoServiceProvider) certificate.GetRSAPrivateKey();

        var cspParam = new CspParameters
        {
            KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
            KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
        };
        var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
        var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
        var signatureEncoded = ToBase64UrlString(signatureBytes);

        // jwt
        var jwt = headerEncoded + "." + claimsetEncoded + "." + signatureEncoded;

        var client = new WebClient();
        client.Encoding = Encoding.UTF8;
        var uri = "https://login.salesforce.com/services/oauth2/token";
        var content = new NameValueCollection();

        content["assertion"] = jwt;
        content["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";

        string response = Encoding.UTF8.GetString(client.UploadValues(uri, "POST", content));

        var result = JsonConvert.DeserializeObject<dynamic>(response);

        return result;
    }

    static int GetExpiryDate()
    {
        var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
        var currentUtcTime = DateTime.UtcNow;

        var exp = (int)currentUtcTime.AddMinutes(4).Subtract(utc0).TotalSeconds;

        return exp;
    }

    static string ToBase64UrlString(byte[] input)
    {
        return Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_');
    }
2 Answers

Well - it turns out posting to stackoverflow gets the brain cogs turning.

The answer ended up being doing a deep dive to find a similar issue here and using the solution from x509certificate2 sign for jwt in .net core 2.1

I ended up replacing the following code:

var cspParam = new CspParameters
{
      KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
      KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
};
var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
var signatureEncoded = ToBase64UrlString(signatureBytes);

With this code which makes use of the System.IdentityModel.Tokens.Jwt nuget package:

var signingCredentials = new X509SigningCredentials(certificate, "RS256");
var signature = JwtTokenUtilities.CreateEncodedSignature(input, signingCredentials);

Full code after solution:

static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
        var token = GetAccessToken();
    }

    static dynamic GetAccessToken()
    {
        // get the certificate
        var certificate = new X509Certificate2(@"C:\temp\cert.pfx");

        // create a header
        var header = new { alg = "RS256" };

        // create a claimset
        var expiryDate = GetExpiryDate();
        var claimset = new
        {
            iss = "xxxxx",
            prn = "xxxxx",
            aud = "https://test.salesforce.com",
            exp = expiryDate
        };

        // encoded header
        var headerSerialized = JsonConvert.SerializeObject(header);
        var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
        var headerEncoded = ToBase64UrlString(headerBytes);

        // encoded claimset
        var claimsetSerialized = JsonConvert.SerializeObject(claimset);
        var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
        var claimsetEncoded = ToBase64UrlString(claimsetBytes);

        // input
        var input = headerEncoded + "." + claimsetEncoded;
        var inputBytes = Encoding.UTF8.GetBytes(input);

        var signingCredentials = new X509SigningCredentials(certificate, "RS256");
        var signature = JwtTokenUtilities.CreateEncodedSignature(input, signingCredentials);

        // jwt
        var jwt = headerEncoded + "." + claimsetEncoded + "." + signature;

        var client = new WebClient();
        client.Encoding = Encoding.UTF8;
        var uri = "https://test.salesforce.com/services/oauth2/token";
        var content = new NameValueCollection();

        content["assertion"] = jwt;
        content["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";

        string response = Encoding.UTF8.GetString(client.UploadValues(uri, "POST", content));

        var result = JsonConvert.DeserializeObject<dynamic>(response);

        return result;
    }

    static int GetExpiryDate()
    {
        var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
        var currentUtcTime = DateTime.UtcNow;

        var exp = (int)currentUtcTime.AddMinutes(4).Subtract(utc0).TotalSeconds;

        return exp;
    }

    static string ToBase64UrlString(byte[] input)
    {
        return Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_');
    }

I am replying to this question just because such a similar answer would have helped me a lot when I landed on this page the first time.

First of all you don't have to generate the JWT from the C# client.

To generate a JWT token you can use this website: https://jwt.io/

There is a very well done video showing how to generate a JWT token: https://www.youtube.com/watch?v=cViU2-xVscA&t=1680s

Once generated, use it from your C# client to call the get access_token endpoint https://developer.salesforce.com/docs/atlas.en-us.api_iot.meta/api_iot/qs_auth_access_token.htm (Watch the video on YT)

If all is correct you will get the access_token

To run the API calls, all you need is the access_token and not the JWT.

Once you have it add it to the HTTP calls like this

public static void AddBearerToken(this HttpRequestMessage request, string accessToken)
    {
        request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
    }

From time to time the access_token will expire. To check its validity you can call the token introspect api https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oidc_token_introspection_endpoint.htm&type=5

You need to pass two additional parameters: client_id and client_secret

  • The client_id is the Consumer Key. You get it from the Connected App in Salesforce
  • The client_server is the Consumer Secret. You get it from Connected App in Salesforce

If the introspect token API returns a response with

{ active: false, ...  }

it means that the access_token is expired and you need to issue a new one. To issue a new access_token simply call the "/services/oauth2/token" again using the same JWT.

Related