Customizing Sitecore – Salesforce Marketing Cloud connector to Support OAuth 2.0

Salesforce Marketing Cloud (SFMC) has integrations that allow for the Sitecore Connect software for Salesforce Marketing Cloud connector (SFMC Connector) to communicate with SFMC. When Sitecore built the Content Exchange 1.0 version it relied on what Salesforce now calls a “legacy package” or integration. As of August 1, 2019, SFMC moved to support OAuth 2.0 and stopped support allowing the creation of legacy integrations.

As of August 1, 2019, Marketing Cloud has removed the ability to create legacy packages. All new packages are enhanced packages. You can still use legacy authentication and API requests with existing legacy packages.

https://developer.salesforce.com/docs/atlas.en-us.mc-app-development.meta/mc-app-development/create-integration-legacy.htm

This broke the SFMC 1.0 connector which relied on the legacy Salesforce integration and was cataloged in Sitecore reference number 332757.

Ok, so how can we customize Sitecore to correct something like this while a product update is being worked on?


Disclaimer: When finally getting around to publish this post I realized that a new version was released by Sitecore earlier this month to address the issue. So if that is all you need you can just grab the 2.0 update. View the release notes here


In light of that update earlier this month, this post now has a two-fold purpose,

  1. If you had run across this in the past, use the new connector 2.0 for Sitecore 9.1.
  2. If you are curious about how to replace this functionality with your own Token logic continue reading.

Customizing the SFMC Connector

Understand the extension points

First, we need to understand where and how the current implementation is performing the Authentication with SFMC. A good starting point is always showconfig.aspx – to review dependencies, events, pipelines, etc.

Additionally, Sitecore support will provide valuable information when possible. This is the info form the Sitecore support team.

As a starting point of your possible customization, I would suggest you take a look at:
 – the “Sitecore.Connector.Sfmc.DataAccess.RestSecurityTokenDataAccessor” class that is used to get access token and that contains/defines request body.
 – the “Sitecore.Connector.Sfmc.ServicesConfigurator” class that is used to define implementation types for different SFMC-related services:
    <configurator type=”Sitecore.Connector.Sfmc.ServicesConfigurator, Sitecore.Connector.Sfmc” />

-Sitecore support team

Now we have both the class that is doing the work and also the ServicesConfigurator class responsible for service registration. Sitecore based their DI implementation on the Microsoft.Extensions.DependencyInjection abstractions from ASP.NET Core.

You can implement the IServicesConfigurator interface to add services in code or from the services/register nodes in config.

Looking at the ServicesConfigurator class it was easy to identify where I could inject my own Access token handler to use the OAuth 2.0 API Integration.

Lets look in dotPeek…

So as Sitecore support had mentioned, the RestSecurityTokenDataAccessor class handles getting the authentication token, which is what we will need to change to use OAuth 2.0 Client Credentials grant type to get our Access Token.

Now if we look at the class Sitecore.Connector.Sfmc.DataAccess.RestSecurityTokenDataAccessor we can see what is responsible for the current token retrieval.

The method GetTokenAsync() which returns a ValueDataAccessorResult.

This method uses the injected RestDataAccessor instance to handle the HTTP Request/Response to obtain the access token.

Now let us customize…


Note: this code was written for POC purposes only to evaluate the SFMC integration. If you do something similar be sure to implement best practices around resiliency, patterns like Retry or Circut Breaker, similar to what Sitecore had implemented. Polly jumps out.


So first, we want to inject our own class in place of RestSecurityTokenDataAccessor as we saw in the ServicesConfigurator previously.

To do so we will create our own ServicesConfigurator class and replace this dependency with the new class we will create to do the work.

public class ServicesConfigurator : IServicesConfigurator
    {
        [ExcludeFromCodeCoverage]
        public void Configure(IServiceCollection serviceCollection)
        {
            var serviceDescriptor = serviceCollection.FirstOrDefault(descriptor => descriptor.ServiceType.Name is "SecurityTokenDataAccessor");
            serviceCollection.Remove(serviceDescriptor);
            serviceCollection.AddSingleton<SecurityTokenDataAccessor, Digitas.Feature.Salesforce.RestSecurityTokenDataAccessor>();

            serviceCollection.AddTransient<IRestClient, RestClient>();
            serviceCollection.AddTransient<ITokenResultRepository, TokenResultRepository>();
        }
    }

In the above, we are removing the existing service and adding our custom one. Again if you do something similar in production code do it responsibly and handle for load order of the Configurators.

Then add the new Conifurator config file

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
    <sitecore>
        <services>
            <configurator type= "Digitas.Feature.Salesforce.ServicesConfigurator, Digitas.Feature.Salesforce"/>
        </services>
    </sitecore>
</configuration>

And now we will create our class that inherits the abstract SecurityTokenDataAccessor class and add implement its methods to perform the Access Token retrieval.

public class RestSecurityTokenDataAccessor : SecurityTokenDataAccessor
    {
        private readonly ITokenResultRepository _tokenResultRepository;

        public RestSecurityTokenDataAccessor(ITokenResultRepository tokenResultRepository)
        {
            this._tokenResultRepository = tokenResultRepository;
        }

        protected SfmcSettings SfmcSettings { get; }

        public override Task<ValueDataAccessorResult<TokenResult>> GetTokenAsync()
        {
            return this.ProcessTokenRequestAsync<TokenResult>();
        }

        public virtual async Task<ValueDataAccessorResult<TResult>> ProcessTokenRequestAsync<TResult>()
        {
            var response = await this._tokenResultRepository.GetTokenAsync();

            return response.IsSuccessful
                    ? new ValueDataAccessorResult<TResult>(
                        this.CreateTokenResult<TResult>(response.Data.AccessToken, response.Data.ExpiresIn),
                        response.StatusCode)
                    : new ValueDataAccessorResult<TResult>(new ErrorResult(ErrorCode.NotAuthorizedResultErrorCode, "sample error", "", 0, null), HttpStatusCode.Forbidden);
        }

        private TResult CreateTokenResult<TResult>(string accessToken, long expiresIn) 
        {
            return (TResult)(new TokenResult(accessToken, expiresIn) as object);
        }
    }

In the ProcessTokenRequestAsync method, you will see the POC code that uses a Token Repository class to fetch the AccessToken and then this method exposes a TokenResult or error information to the caller.

Here is the repository class that uses RestSharp to retrieve the token and expiration.

    public class TokenResultRepository : ITokenResultRepository
    {
        private readonly SfmcSettings _sfmcSettings;
        private readonly IRestClient _client;

        public TokenResultRepository(SfmcSettings sfmcSettings, IRestClient client)
        {
            this._sfmcSettings = sfmcSettings;
            this._client = client;

            Assert.IsNotNull(sfmcSettings.TokenEndpointUri, "this._tokenEndpointUri != null");
            Log.Info(
                $"{nameof(this.GetTokenAsync)}Salesforce API Authentication::Sending GET request to \"{sfmcSettings.TokenEndpointUri}\".", this);
        }

        public async Task<IRestResponse<DeserializedTokenResult>> GetTokenAsync()
        {
            this._client.BaseUrl = this._sfmcSettings.TokenEndpointUri;
            this._client.Authenticator = new HttpBasicAuthenticator("client-app", "secret");
            
            //get access token
            var request = new RestRequest(Method.POST);

            //set up the correct headers/params
            this.AddClientCredentialsGrantTypeHeaders(request);

            //create the response
            return await this._client.ExecuteTaskAsync<DeserializedTokenResult>(request, CancellationToken.None);
        }

        private void AddClientCredentialsGrantTypeHeaders(IRestRequest request)
        {
            ....
        }
    }

After deploying and configuring Sitecore, you can create media items and see them populate in Salesforce

Leave a Reply

Leave a Reply

Your email address will not be published. Required fields are marked *