OAuth2 refresh token rotation

OAuth2 refresh token rotation

In the Summer '26 release, Salesforce began globally enforcing refresh token rotation for new External Client Apps. This adds another layer of complexity to OAuth2 authentication for unattended API connections.

Affected installations

Any that use OAuth2 authentication. This does not affect Partner API logins, however those will be retired in Summer '27 at which point all connections must use OAuth2 tokens.

Supported releases

S4S introduces support for configurable token storage mechanisms beginning with the following versions:

  • S4S 2.917.26216 (Sitecore 9.1 and 9.2)

  • S4S 2.1012.26216 (Sitecore 10.1)

  • S4S 2.1023.26216 (Sitecore 10.2)

  • S4S 2.1033.26216 (Sitecore 10.3)

  • S4S 2.1043.26216 (Sitecore 10.4)

  • G4S 2.2.26216

Salesforce restrictions

S4S administrators should be aware of the following.

  1. The primary restriction is the enforcement of refresh token rotation. i.e. when exchanging a refresh token for an access token/session id, Salesforce may issue a replacement refresh token and expire the previous one.

  2. Any attempt to use an expired refresh token will revoke the connection entirely, immediately.

  3. There is now a limit of 5 connections per App, after which oldest is automatically revoked. If in doubt, create a distinct App for each service.

Configuration: Salesforce App

S4S now supports all refresh token rotation options. However as token rotation presents the highest risk of Salesforce automatically revoking connections, you may consider increasing the validity period. If your organization allows, review the External Client App policy settings. When tokens are suitably secured this does not pose an increased security risk.

image-20260730-233105.png

Some refresh token policy configurations can cause unexpected results, with newly rotated refresh tokens expiring before the configured period. For simplicity we recommend using the Expire refresh token if not used for specific time option, with a validity period of 30 days or greater (which matches the enforced idle TTL setting). Ensure that a new connection is made at least every 30 days to keep it alive.

Configuration: Sitecore database

Choose a location in one of the Sitecore databases that is accessible to the applicable service(s). Create a folder - the S4S package does not include this by default.

image-20260728-033045.png

Review the S4S.TokenRotation.-prefixed settings in /App_Config/Modules/S4S/S4S.config.

<sitecore> <settings> <!-- Token rotation settings may be applied via Sitecore patch for convenience. Do not add top-level tokenRotation section if specified here. --> <setting name="S4S.TokenRotation.enabled" value="false" /> <setting name="S4S.TokenRotation.tokenStore" value="FuseIT.S4S.WebToSalesforce.Connection.SitecoreDbTokenStore" /> <setting name="S4S.TokenRotation.storeLocation" value="core:en:/sitecore/system/Settings/Security/SalesforceTokens" /> <setting name="S4S.TokenRotation.encryptionKey" value="" /> <setting name="S4S.TokenRotation.waitTimeout" value="" /> </settings> </sitecore>

Make sure the enabled value is true and review the storeLocation value. It must contain three parts, colon-separated. Configure these to suit your installation.

  1. Database name

  2. Language code. As Sitecore may return different items depending on context language, this is locked to one explicit language. It doesn’t matter which as this is never exposed to users.

  3. Full path to the folder. Don’t share this folder with anything else, per-connection items will be created and overwritten here as needed.

Make sure the /App_Config/Modules/S4S/S4S.config file contains the initialize pipeline processor.

<sitecore> <settings></settings> <pipelines> <initialize> <!-- S4S initialize pipeline, required to register refresh token rotation store. --> <processor type="FuseIT.S4S.WebToSalesforce.Pipelines.S4SInitialize, FuseIT.S4S.WebToSalesforce" /> </initialize> </pipelines> </sitecore>

Configuration: file store

Alternatively you may also use the generic G4S file store. This requires a writable directory in the filesystem.

<setting name="S4S.TokenRotation.tokenStore" value="FuseIT.Sitecore.SalesforceConnector.Authentication.FileTokenStore" /> <setting name="S4S.TokenRotation.storeLocation" value="D:\secure\tokens" />

Encryption

If the encryptionKey configuration attribute has a value, tokens will be encrypted before saving to persistent storage. Set this to a 32-character string, e.g. a UUID. If not specified, tokens will not be encrypted.

<setting name="S4S.TokenRotation.encryptionKey" value="f5229b8c4b804c108b1bef515148f42c" />

Changing the encryption key will render stored tokens inaccessible.

Configuration: Sitecore patch files vs .NET settings section

The underlying G4S SDK loads the above configuration via a standard .NET settings section from the core (web.config or app.config style) config file(s).

  • For most, the Sitecore patch file system is more convenient. The S4SInitialize pipeline applies these values as described above; this pipeline must run at startup for patch files to be used.

  • For legacy reasons, Sitecore patch file settings can only be used where the <FuseIT.Sitecore.SalesforceConnector> section does not exist. If you’ve added this and cannot remove it, the token rotation configuration will need to be included in this location. More information.

CM-CD / multiple service environments

In most cases S4S needs to connect to Salesforce in both the CM and CD instances. Connection strings need to be configured for each.

Distinct connections (recommended)

It is still technically possible for both services to share the same connection, provided they can access a shared token store location. However we no longer recommend this configuration as it may trigger concurrency problems should both services attempt to connect at once. This risks not only one of the services failing to receive an updated token, but if an expired token is used the connection will be revoked entirely.

Best practice is to use a distinct connection for each service. The authentication process should be followed, i.e. a distinct initial refresh token generated, for each. To further guarantee that one service cannot affect connections for another, create a unique External Client App for each.

Experimental: shared connections with database item locking

For those who wish to share the same refresh token between services, S4S does provide an experimental feature to handle concurrency issues using the Sitecore editing lock on database items. This only applies when using the SitecoreDbTokenStore implementation, and requires a configuration setting to enable.

<!-- EXPERIMENTAL: enable database item locking for cross-service sharing of refresh tokens. --> <setting name="S4S.TokenRotation.itemLocking" value="true" />

This functionality may be dependent on Sitecore locking settings. Enable debug logging and check for the presence of relevant lines in the log file to verify this is operating.

DEBUG FuseIT.S4S.WebToSalesforce.Connection.SitecoreDbTokenStore - Locking refresh token item for connection S4SConnString. ... DEBUG FuseIT.S4S.WebToSalesforce.Connection.SitecoreDbTokenStore - Releasing lock on refresh token item for connection S4SConnString.

General operation

  • Once correctly configured rotated refresh tokens are automatically stored and used.

  • Your connection strings are never modified.

  • When token rotation is enabled, an in-memory service maps the original refresh token to the latest rotated token. This data is lost when the service restarts.

  • Rotated tokens are also mapped to persistent storage if a token store is configured. This data remains between service restarts, and populates the in-memory map automatically.

  • The most recent rotated token is always supplied based on the initial token. If the initial token (i.e. in the configured connection string) is changed it will not be substituted until a new connection is made.

Programmatic connections

Token rotation functionality requires a unique connection name, normally assigned from a configured connection string. If initiating Salesforce sessions programmatically, either:

  • Implement custom token rotation storage by handling the SalesforceSession.OnLoginDetailsChange event, or

  • Specify a unique connection name using the optional string parameter to the RefreshTokenLoginDetails.InstanceFrom...() methods. Continue to pass the initial refresh token for each subsequent use, and it will be substituted for the latest rotated token.

var loginDetails = RefreshTokenLoginDetails.InstanceFromRefreshToken(tokens.refresh_token, appCredentials.Hostname, appConnStringName, connectionName) as RefreshTokenLoginDetails;

Advanced: custom token store

Should you wish to persist rotated tokens in a different manner, it is possible to create your own token store implementation. Create a class that implements FuseIT.Sitecore.SalesforceConnector.Authentication.ITokenStore.

/// <summary> /// Interface for persistent rotated authentication token storage. /// </summary> /// <remarks>Values passed to implementations may be encrypted, depending on configuration.</remarks> public interface ITokenStore { /// <summary> /// Initialize token store. /// </summary> /// <remarks>Throw on error so caller knows token store is not usable.</remarks> void Initialize(string location); /// <summary> /// Get rotated token. /// </summary> /// <param name="connectionId">Connection identifier.</param> /// <param name="key">Key for the token, generally the original token.</param> /// <returns>Rotated token, or null if none.</returns> /// <remarks>Thread locking is handled by caller.</remarks> string Get(string connectionId, string key); /// <summary> /// Set rotated token. /// </summary> /// <param name="connectionId">Connection identifier.</param> /// <param name="key">Key for the token, generally the original token.</param> /// <param name="value">The rotated value to store.</param> /// <remarks>Thread locking is handled by caller.</remarks> void Set(string connectionId, string key, string value); /// <summary> /// Wait for a custom per-connection lock. /// </summary> /// <param name="connectionId">Connection identifier.</param> /// <remarks>Optional; return Task.FromResult(0) if not implemented. Timeout is handled by caller.</remarks> Task WaitForLock(string connectionId); /// <summary> /// Release a custom per-connection lock. /// </summary> /// <param name="connectionId">Connection identifier.</param> /// <remarks>Optional; should be implemented when WaitForLock() is.</remarks> void ReleaseLock(string connectionId); }

In order for this to be available, it must be registered once via the following call prior to any Salesforce session instantiation. A Sitecore initialize pipeline is suitable for this purpose.

if (TokenRotationService.IsRotationEnabled) { // Register MyTokenStore class for persistant token storage. // Must implement ITokenStore and have a parameterless constructor. TokenRotationService.RegisterStore<MyTokenStore>(); }

Specify the token store in the tokenRotation configuration element and set the required storeLocation value, if any.

<setting name="S4S.TokenRotation.tokenStore" value="My.Full.Namespace.MyTokenStore" />