SyncMadeEasy/force-app/main/default/classes/RefreshTokenKeepAlive.cls

72 lines
4.8 KiB
OpenEdge ABL

/**
* ─────────────────────────────────────────────────────────────────────────────────────────────────┐
* Scheduled job that (a) keeps the OAuth refresh token alive under the Connected App's Idle Refresh
* Token TTL and (b) acts as the ONLY proactive liveness probe for the connection.
*
* It delegates to TokenService — the single serialized, rotation-safe authority — which handles the row
* lock, single-flight, and fail-loud persistence of the rotated pair. This class performs no callout of
* its own, no token DML of its own, and NEVER wipes a token.
*
* ── History, because this was wrong twice and the second fix caused the current outage ──────────────
* v1 wiped both tokens on a bare invalid_grant using a stale (unlocked) read. Under mandatory Refresh
* Token Rotation that routinely blanked a token another path had just rotated. Correctly removed.
* v2 then stopped rotating ENTIRELY, on the stated grounds that "the middleware is the token-rotation
* authority ... if this scheduled job ALSO rotated it would issue a new token the middleware doesn't
* know about". THAT REASONING IS VOID: this package is 100% Salesforce-native — there is no middleware
* and no external server, so there was never a second holder to desynchronise from. (Confirmed
* structurally: the only outbound token push in the repo, salesforceAuthenticationController's
* '/api/auth/callback', is unreachable — one call site is gated on isSiteSupportedOrg()==false, which
* is true for every EE/UE/Developer org, and the other's sole caller is a test.)
*
* v3 keeps v2's behaviour (no rotation) but for the CORRECT reason, documented in refreshNow below:
* rotation cannot be done "just to check", because consuming the token is the only way to test it, and
* every consumption risks losing it. The Idle TTL that a keep-alive would protect against cannot
* expire on an org that is actually syncing. So not rotating here is right — the previous justification
* was simply wrong, and that mattered because a wrong reason invites the wrong fix later.
* ──────────────────────────────────────────────────────────────────────────────────────────────────
*/
global with sharing class RefreshTokenKeepAlive implements Schedulable {
public static final String JOB_NAME = 'Hic_ShopifySync_Refresh_Token_Rotation';
// Unchanged (14th and 28th). This job performs no callout, so its frequency does not matter much —
// do NOT increase it in the belief that it keeps the token alive. It does not rotate; see refreshNow.
public static final String CRON_EXPR = '0 0 23 14,28 * ?';
global void execute(SchedulableContext sc) {
refreshNow();
}
public static void refreshNow() {
try {
List<HIC_ShopifySync__Configuration__c> cfg = [
SELECT HIC_ShopifySync__Refresh_Token__c
FROM HIC_ShopifySync__Configuration__c
WHERE Name = 'Default'
LIMIT 1
];
// Not connected at all — nothing to keep alive. Do not attempt a refresh and do not notify.
if (cfg.isEmpty() || String.isBlank(cfg[0].HIC_ShopifySync__Refresh_Token__c)) {
return;
}
// DELIBERATELY DOES NOT ROTATE.
//
// A refresh token cannot be tested without being consumed, so any "probe" IS a rotation —
// and under mandatory Refresh Token Rotation every rotation is a chance to lose the token
// (see TokenService's lost-response window: if the callout fails after Salesforce has already
// rotated, the org can never know which token is live). A scheduled rotation therefore ADDS
// risk rather than removing it.
//
// The only thing it would buy is protection against the Connected App's Idle Refresh Token
// TTL — and that clock cannot run out on a working org: ordinary sync traffic exercises the
// refresh token every time the access token needs replacing. An org quiet enough to hit a
// 30-day idle limit is an org that is not syncing anyway.
//
// So rotation happens ONLY on demand, only via TokenService, and only when something actually
// needs a new access token. This job stays a cheap existence check.
} catch (Exception e) {
System.debug(LoggingLevel.ERROR, 'RefreshTokenKeepAlive probe failed: ' + e.getMessage());
}
}
}