Search code examples
c#.net-coreoauthjwtsalesforce

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


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('/', '_');
    }

Solution

  • 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('/', '_');
        }