In this post I will explain my approach to the Salesforce – Sitecore integration. In fact code that is going to be described here can be used in almost every .net application.
Available Sitecore – Salesforce integration
Sitecore provides an integration to everyone who needs to synchronize the data between the Sitecore and Salesforce – the “Salesforce Connect” extension.
You can download it here: https://dev.sitecore.net/Downloads/Salesforce_Connect.aspx
If you do not know which version you should install, check it here (comatibility table): https://support.sitecore.com/kb?id=kb_article_view&sysparm_article=KB1000576
The natural question that probably comes to your mind right now is ‘why did not I use it when I had a need to connect to salesforce’ – this is a really good question.
The answer is – because available extension is focused on the syncing contacts between Salesforce and Sitecore, when in our case we just wanted to send some data to Salesforce and the synchronization was being made on the different level.
In other words, available extension did not meet our needs.
Few general words about the integration
Integration with Salesforce is not in any kind special – it is just an API that is managed by the Salesforce developers. An endpoints and parameters can differ but the common thing is token generation that I decided to describe in this post because it took me a while to understand how to generate the correct JWT bearer token that will be honored by the Salesforce endpoint.
Documentation about it is available on the Salesforce help portal : https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5
Documentation contains the Java code that I had to transform into .NET one what sometimes was not so obvious.
What do you need to generate OAuth 2.0 JWT bearer token
To make communication possible you must have generated certificate that is going to be used by Salesforce and your Sitecore instance. When certificate will be installed on the Salesforce side you can continute configuration on your side.
To generate the JWT token you need to gather the following information:
- iss – this is OAuth client_id (provided by Salesforce)
- aud – this is authorization server’s url (login.salesfroce.com for production and test.salesforce.com for test environments – provided by Salesforce)
- sub – the username of account used to connect to salesforce (usually email – provided by Salesforce)
- exp – timestamp of the expiration (provided by Salesforce)
Generation process
All of that data need to be later encoded to base64 string:
private string GenerateClaimsString()
{
var iss = this._salesforceConfigurationService.GetJwtClaimsIss();
var sub = this._salesforceConfigurationService.GetJwtClaimsSub();
var aud = this._salesforceConfigurationService.GetJwtClaimsAud();
var exp = this._salesforceConfigurationService.GetJwtClaimsExp();
var claims = $"{{\"iss\": \"{iss}\", \"sub\": \"{sub}\", \"aud\": \"{aud}\", \"exp\": \"{exp}\"}}";
return this.Base64Encoder(claims);
}
But it is not only encoding by the standard Convert.ToBase64String method. We must also remove some of the chars from the generated string:
private string Base64Encoder(string valueToEncode)
{
byte[] valueToEncodeAsBytes = System.Text.Encoding.UTF8.GetBytes(valueToEncode);
return Convert.ToBase64String(valueToEncodeAsBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
When the values are encoded and unwanted chars are removed from the encoded string we need to add to it predefined JWT header. Header has a very similar structure to the claims and can be hardcoded with value:
"{\"alg\":\"RS256\"}"
Here is the code that can do that:
private string GenerateJwtHeaderString()
{
var headerValue = SalesforceConstants.Api.Values.Header;
return this.Base64Encoder(headerValue);
}
After all operations we have two strings that we can use to build the assertion used later in the authorization request.
var assertion = this.GenerateJwtHeaderString();
assertion += ".";
assertion += this.GenerateClaimsString();
As you can see two strings are again connected with the dot sign.
But this is not the end – now we are going to use our certificate to sign the assertion. Full code of assertion generation will look like this:
var assertion = this.GenerateJwtHeaderString();
assertion += ".";
assertion += this.GenerateClaimsString();
this._assertion = assertion + "." + this.SignAndGeneratePayloadString(assertion);
Where SignAndGeneratePayloadString method looks like this:
private string SignAndGeneratePayloadString(string payload)
{
X509Certificate2 certificate = new X509Certificate2(this._salesforceConfigurationService.GetCertPath(), this._salesforceConfigurationService.GetCertPass(), X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
using (var privateKey = certificate.GetRSAPrivateKey())
{
var signedData = privateKey.SignData(System.Text.Encoding.UTF8.GetBytes(payload), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signedData).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
}
As you may noticed SignAndGeneratePayloadString method uses certificate to ‘sign’ the assertion data – to make it work you need to load the certificate from the disk (it must be p12 certificate file) and have certificate password to read it.
The result of signing is again concatenated with the dot and original assertion data.
When the assertion is finally ready, you can authenticate with Salesforce API and Request Access Token for further Salesforce communication.
Summary
If you compare implementation from this blog post with implementation from the Salesforce’s help page you will notice few major differences like:
- usage of p12 certificate file instead of jks
- additional string operations on the generated/encoded string values
If you want you can check the full implementation here: https://github.com/lskowronski/SitecoreSalesforce/blob/main/SalesforceJwtService.cs
Because transition from JKS file to p12 file can be also tricky, I will describe it in the another blog post that is going to be published soon – stay tuned!