/** * ─────────────────────────────────────────────────────────────────────────────────────────────────┐ * This web service class handle the custom respons which we send while authenticate user * Insert the user refresh token into the configuration custom settings. * Saved refresh token into installer User's org. * Shopify API Documentation: https://shopify.dev/docs/admin-api/rest/reference * Create Shopify Store: https://www.shopify.com/signup?itcat=identity&itterm=create-new-store * Shopify API Release 2020: https://www.shopify.in/partners/blog/shopify-api-release-january-2020 * What is Person Account in Salesforce ? https://help.salesforce.com/articleView?id=account_person.htm&type=5 * ────────────────────────────────────────────────────────────────────────────────────────────────── * @author Sumit * ────────────────────────────────────────────────────────────────────────────────────────────────── */ @RestResource(UrlMapping='/UpdateAccessToken/*') global with sharing class UpdateAccessToken { /** * Accepts an access token pushed in by the authenticator. * * WHAT WAS WRONG: this endpoint used to do * listofConfiguration[0].HIC_ShopifySync__Access_Token__c = JSONResponseBody; * i.e. it stored the ENTIRE RAW REQUEST BODY as the access token, unparsed and unvalidated, and never * touched Refresh_Token__c. Two consequences: * * 1. Any JSON body (e.g. {"access_token":"..."}) was stored verbatim, so Access_Token__c held a JSON * blob rather than a token. Every subsequent authenticated callout then failed, and because * the field was non-blank, nothing detected it: RefreshTokenKeepAlive's String.isBlank() passed and * ShopifyController.refreshTokenCheck handed the blob back as if the org were connected. * 2. It updated the access token WITHOUT updating the refresh token. Under mandatory Refresh Token * Rotation, if the caller obtained that access token via grant_type=refresh_token then the stored * refresh token has ALREADY been consumed and is dead — so the org looks freshly authenticated * while its refresh token is guaranteed to fail on next use. That is the "worked for days, then * asks to re-initialize" signature. * * Now: the body is parsed, only a real token value is accepted, and if the caller supplies a rotated * refresh token alongside it BOTH are written in one DML via the shared, compare-and-swap-protected * persistRotatedToken. Failures are reported instead of swallowed. */ @HTTPPost global static void updatetoken(){ Restrequest request = restContext.request; Restresponse response = restContext.response; String rawBody = (request != null && request.requestBody != null) ? request.requestBody.toString().trim() : null; if (String.isBlank(rawBody)) { if (response != null) response.statusCode = 400; return; } String accessToken; String refreshToken; try { Map meta = (Map) JSON.deserializeUntyped(rawBody); if (meta != null) { // Accept the packaged field name and the plain OAuth name. Object a = meta.containsKey('HIC_ShopifySync__Access_Token__c') ? meta.get('HIC_ShopifySync__Access_Token__c') : meta.get('access_token'); Object r = meta.containsKey('HIC_ShopifySync__Refresh_Token__c') ? meta.get('HIC_ShopifySync__Refresh_Token__c') : meta.get('refresh_token'); accessToken = (a == null) ? null : String.valueOf(a).trim(); refreshToken = (r == null) ? null : String.valueOf(r).trim(); } } catch (Exception parseEx) { // Not JSON. Treat the body as a bare token only if it cannot be a structured payload — // never store something that merely looks like JSON/HTML. if (!rawBody.startsWith('{') && !rawBody.startsWith('[') && !rawBody.startsWith('<') && !rawBody.contains(' ') && rawBody.length() >= 20) { accessToken = rawBody; } } if (String.isBlank(accessToken) || accessToken.equalsIgnoreCase('null') || accessToken.equalsIgnoreCase('undefined')) { System.debug(LoggingLevel.ERROR, 'SME_TOKEN UpdateAccessToken: no usable access token in body — refusing to store it'); if (response != null) { response.statusCode = 400; response.addHeader('Content-Type', 'application/json'); response.responseBody = Blob.valueOf( '{"error":"no_access_token","error_description":"body did not contain a usable access token"}'); } return; } Map payload = new Map{ 'HIC_ShopifySync__Access_Token__c' => accessToken }; if (String.isNotBlank(refreshToken)) { payload.put('HIC_ShopifySync__Refresh_Token__c', refreshToken); } Boolean stored = AuthenticateAllApexClass.persistRotatedToken(JSON.serialize(payload)); if (response != null) { response.statusCode = stored ? 200 : 500; response.addHeader('Content-Type', 'application/json'); response.responseBody = Blob.valueOf(stored ? '{"status":"ok"}' : '{"error":"token_not_stored","error_description":"access token could not be persisted"}'); } } }