2

I am trying to use OAuth JWT Token Bearer Flow to connect to the Salesforce REST API from C#. I have configured a Connected App within Salesforce and uploaded the certificate used to sign the token request. I have enabled all available OAuth scopes.

This is the C# code used to call Salesforce.

public static dynamic GetAccessToken()
{
    // certificate
    var certificate = new X509Certificate2(@"C:\Temp\miketest.pfx");

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

    // claimset
    var expiryDate = GetExpiryDate();
    var claimset = new
    {
        iss = "3MVG9Y6d_Btp4xp43PirpOECJJz5VZ7iFn54V1KCtfvFBvS9RMyenB.Hx7cavL.GAkTx4yabyPV0Zk5T2nrvU",
        prn = "myusername@domain.com",
        aud = "https://login.salesforce.com",
        exp = expiryDate
    };

    var ser = new JavaScriptSerializer();

    // encoded header
    var headerSerialized = ser.Serialize(header);
    var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
    var headerEncoded = Convert.ToBase64String(headerBytes);

    // encoded claimset
    var claimsetSerialized = ser.Serialize(claimset);
    var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
    var claimsetEncoded = Convert.ToBase64String(claimsetBytes);

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

    // signiture
    var rsa = certificate.PrivateKey as RSACryptoServiceProvider;
    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 = Convert.ToBase64String(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 = ser.Deserialize<dynamic>(response);

    return result;
}

private 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;
}

This is the request

POST https://login.salesforce.com/services/oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: login.salesforce.com
Content-Length: 729
Expect: 100-continue
Connection: Keep-Alive
assertion=eyJhbGciOiJS.......Z633ERQ%3d%3d&grant_type=urn%3aietf%3aparams%3aoauth%3agrant-type%3ajwt-bearer

And the response

HTTP/1.1 400 Bad Request
Date: Sun, 19 Oct 2014 23:03:25 GMT
Set-Cookie: BrowserId=pBV8Q_i6RfePkxjH67e96Q;Path=/;Domain=.salesforce.com;Expires=Thu, 18-     Dec-2014 23:03:25 GMT
 Expires: Thu, 01 Jan 1970 00:00:00 GMT
 Pragma: no-cache
 Cache-Control: no-cache, no-store
 Date: Sun, 19 Oct 2014 23:03:25 GMT
 Set-Cookie: BrowserId=-RgZomTjQIiwKzVT8ZnibQ;Path=/;Domain=.salesforce.com;Expires=Thu,      18-Dec-2014 23:03:25 GMT
 Expires: Thu, 01 Jan 1970 00:00:00 GMT
 Pragma: no-cache
 Cache-Control: no-cache, no-store
 Content-Type: application/json;charset=UTF-8
 Transfer-Encoding: chunked

 {"error_description":"invalid client credentials","error":"invalid_client"}

If anyone has this working for a .Net client, any help or advice would be greatly appreciated.

  • Can you post a sample JWT created with your code, so we can check it for errors? – metadaddy Oct 20 '14 at 16:00
3

You are using plain base64 encoding, but JWT uses base64url, where - and _ are used in place of + and / respectively, with no = padding. You could do something like:

static string ToBase64UrlString(byte[] input) {
    return Convert.ToBase64String(input).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
  • 2
    Thanks for your prompt answer. This has solved the problem. After correcting the encoding I then received the error description "user hasn't approved this consumer". This was resolved by setting the permitted users for the Connected App to "Admin approved users are pre-authorized" and assigned a permission set with access to the connected app to the user defined in the token request. Thanks again for your help. – Mike Oct 21 '14 at 3:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.