[SalesForce] OAuth JWT Token Bearer Flow returns Invalid Client Credentials

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.

Best Answer

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