/** * ─────────────────────────────────────────────────────────────────────────────────────────────────┐ * PKCE (RFC 7636) helper for Connected Apps that have "Require Proof Key for Code Exchange" enabled. * Generates the code_verifier / code_challenge pair, and packs/unpacks the verifier inside the * OAuth `state` parameter so it survives the round-trip through the browser and back to Apex * (works across orgs for the publisher-Site flow, and across browser tabs for the LWC flow). * ────────────────────────────────────────────────────────────────────────────────────────────────── */ global with sharing class PkceUtil { private static final String STATE_DELIMITER = '__pkce__'; global static String generateCodeVerifier() { Blob random = Crypto.generateAesKey(256); return base64UrlEncode(random); } global static String generateCodeChallenge(String codeVerifier) { if (String.isBlank(codeVerifier)) return ''; Blob hash = Crypto.generateDigest('SHA-256', Blob.valueOf(codeVerifier)); return base64UrlEncode(hash); } global static String embedVerifierInState(String existingState, String verifier) { String safeState = existingState == null ? '' : existingState; return safeState + STATE_DELIMITER + (verifier == null ? '' : verifier); } global static String extractVerifier(String stateParam) { if (String.isBlank(stateParam) || !stateParam.contains(STATE_DELIMITER)) return ''; Integer i = stateParam.indexOf(STATE_DELIMITER); return stateParam.substring(i + STATE_DELIMITER.length()); } global static String extractOriginalState(String stateParam) { if (String.isBlank(stateParam) || !stateParam.contains(STATE_DELIMITER)) return stateParam; return stateParam.substring(0, stateParam.indexOf(STATE_DELIMITER)); } @AuraEnabled global static Map issuePkceForState(String existingState) { String verifier = generateCodeVerifier(); String challenge = generateCodeChallenge(verifier); String state = embedVerifierInState(existingState, verifier); Map result = new Map(); result.put('codeChallenge', challenge); result.put('state', state); return result; } private static String base64UrlEncode(Blob input) { return EncodingUtil.base64Encode(input) .replace('+', '-') .replace('/', '_') .replace('=', ''); } }