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

105 lines
6.1 KiB
OpenEdge ABL

/**
* ─────────────────────────────────────────────────────────────────────────────────────────────────┐
* 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 <sumit@hicShopify.com>
* ──────────────────────────────────────────────────────────────────────────────────────────────────
*/
@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<String, Object> meta = (Map<String, Object>) 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<String, Object> payload = new Map<String, Object>{
'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"}');
}
}
}