googleworkspace / apps-script-oauth2

An OAuth2 library for Google Apps Script.
https://developers.google.com/apps-script/
Apache License 2.0
1.53k stars 428 forks source link

OAuth2 Authentication Issue: ZohoCRM Successful, ZohoRecruit Fails with Identical Code #476

Closed techlover11 closed 5 months ago

techlover11 commented 5 months ago

Hello team,

I have successfully implemented OAuth2 authentication with ZohoCRM and have been able to retrieve data without issues using the following endpoint and scope:

However, when attempting to use an identical script with a different endpoint and scope for ZohoRecruit, the script fails to work:

The OAuth2 response seems to be successful, as I receive the following tokens and parameters:

{
  refresh_token: "1000.4xxxxxxxxxxxxxxxxxxxx",
  expiresAt: 1.711720326E9,
  scope: "ZohoRecruit.modules.READ",
  expires_in: 3600.0,
  access_token: "1000.3yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
  api_domain: "https://www.zohoapis.eu",
  token_type: "Bearer"
}

To further investigate, I utilized Postman to authenticate with OAuth2 for the ZohoRecruit endpoint /recruit/v2/org with the scope ZohoRecruit.modules.READ, and it worked as expected.

In Postman, I used the following parameters, which may be relevant for troubleshooting:

I am seeking guidance on the following:

I appreciate any insights or suggestions you can provide.

Thank you!

[Code used in the script]


/**
 * This sample demonstrates how to connect to the Zoho CRM API.
 * @see https://www.zoho.com/crm/developer/docs/api/oauth-overview.html
 */

var CLIENT_ID = '1000.aaaaaaaaaaaaaaaaaaaaaaaaaaaa';
var CLIENT_SECRET = 'ccccccccccccccccccccccccccccccccccccccccccccccccccc';

function writeToSheet(content) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  sheet.appendRow([content]); // Write the entire content to the first cell of a new row
}
function saveToDrive(content) {
  var fileName = 'API Error Response ' + new Date().toISOString();
  DriveApp.createFile(fileName, content, MimeType.PLAIN_TEXT);
}

/**
 * Authorizes and makes a request to the Zoho CRM API.
 */
function run() {
  var service = getService_();
  if (service.hasAccess()) {
    // Retrieve the API server from the token.
    var apiServer = service.getToken().api_domain;
    //var url = apiServer + '/crm/v2/org';
    var url = apiServer + '/recruit/v2/org';
    var response = UrlFetchApp.fetch(url, {
      method: 'get',
      headers: {
        'Authorization': 'Bearer ' + service.getAccessToken(),
        'Content-Type': 'application/json'
      },
      muteHttpExceptions: true
    });

    // writeToSheet(response.getContentText());
    saveToDrive(response.getContentText());

    // Log the status code and headers to understand the response better
    Logger.log(response.getResponseCode());
    Logger.log(response.getHeaders());

    // Log the raw content of the response
    Logger.log(response.getContentText());

    // Attempt to parse the response as JSON, with error handling
    var result;
    try {
      result = JSON.parse(response.getContentText());
    } catch (e) {
      Logger.log('Failed to parse response as JSON:');
      Logger.log(response.getContentText());
      return; // Exit the function or handle the error as needed
    }

    // Proceed with processing the result if it's successfully parsed
    // Logger.log(JSON.stringify(result, null, 2));

  } else {
    var authorizationUrl = service.getAuthorizationUrl();
    Logger.log('Open the following URL and re-run the script: %s',
        authorizationUrl);
  }
}

/**
 * Reset the authorization state, so that it can be re-tested.
 */
function reset() {
  getService_().reset();
}

/**
 * Configures the service.
 * @param {string} optAccountServer The account server to use when requesting
 *     tokens.
 */
function getService_(optAccountServer) {
  var service = OAuth2.createService('Zoho')
      // Set the authorization base URL.
      .setAuthorizationBaseUrl('https://accounts.zoho.eu/oauth/v2/auth')

      // Set the client ID and secret.
      .setClientId(CLIENT_ID)
      .setClientSecret(CLIENT_SECRET)

      // Set scopes. See
      // https://www.zoho.com/crm/developer/docs/api/oauth-overview.html#scopes.
      .setScope('ZohoRecruit.modules.READ')
      //.setScope('ZohoRECRUIT.modules.all')
      //.setScope('ZohoRECRUIT.org.all')
      //.setScope('ZohoRecruit.org.all')
      //.setScope('ZohoCRM.org.all')    

      // Set the name of the callback function that should be invoked to
      // complete the OAuth flow.
      .setCallbackFunction('authCallback')

      // Set the access type to "offline" to get a refresh token.
      .setParam('access_type', 'offline')
      // Set prompt to "consent" to ensure a refresh token is retrieved.
      .setParam('prompt', 'consent')

      // Set the property store where authorized tokens should be persisted.
      .setPropertyStore(PropertiesService.getUserProperties())
      .setCache(CacheService.getUserCache());

      var token = service.getToken();
      Logger.log(token); // This will show you the structure of the token object

  // Set the token URL using the account server passed in or previously stored.
  var accountServer = optAccountServer ||
      service.getStorage().getValue('account-server');
  if (accountServer) {
    service.setTokenUrl(accountServer + '/oauth/v2/token');
  }
  return service;
}

/**
 * Handles the OAuth callback.
 */
function authCallback(request) {
  var accountServer = request.parameter['accounts-server'];
  var service = getService_(accountServer);
  var authorized = service.handleCallback(request);
  if (authorized) {
    // Save the account server in the service's storage.
    service.getStorage().setValue('account-server', accountServer);
    return HtmlService.createHtmlOutput('Success!');
  } else {
    return HtmlService.createHtmlOutput('Denied.');
  }
}

/**
 * Logs the redict URI to register.
 */
function logRedirectUri() {
  Logger.log(OAuth2.getRedirectUri());
}
erickoledadevrel commented 5 months ago

What was the actual error message you got? It could be that the OAuth is working fine, but you are using the API in some way that isn't compatible.

techlover11 commented 5 months ago

There is no actual error message, just a generic error page ""Sorry the page you have requested was not found"

The interesting thing: it is a Zoho CRM page and not a Zoho Recruit error page.

This is why one idea is that the request URL / endpoint is somehow using a CRM endpoint instead of Recruit endpoint

<html><head><title>Zoho CRM - Error</title><link rel="SHORTCUT ICON" href="https&#x3a;&#x2f;&#x2f;static.zohocdn.com&#x2f;crm&#x2f;images&#x2f;favicon_cbfca4856ba4bfb37be615b152f95251_.ico" /><link href="https://static.zohocdn.com/crm/CRMClient/css/default_theme_23524a7da62146af9811df20b1f50421_.css" rel="stylesheet" type="text/css"/><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Pragma" content="no-cache">

<div class="crmErrorPgCont crmErrorHeight" data-zcqa="crm_error_page" data-errormessage="Sorry the page you have requested was not found"><div class="crmErrorPgBody"><div class="crmErrorPgImgContr"><!-- <span class="crmErrorPgImg"></span> -->
<img class="crmErrorPgImg" data-image-mode='true' src='https://static.zohocdn.com/crm/images/crm-iam-errorPage_lightmode_2eb563c9ea6d989a69e2be6276245761_.png'/></div><div class="crmErrorInfoSection"><p class="crmErrorPgErrText1">Invalid URL</p> 
<p class="crmErrorPgErrText2">Unable to process your request as the URL <span class='crmErrorUrl'>&#x2f;recruit&#x2f;v2&#x2f;org</span> is invalid.</p> <!-- No I18N -->
<a class="crmErrorPgHomeLink" href="/crm/ShowHomePage.do">
<button data-zcqa="home_errorPage" class = "primarybtn" >Go to Home page</button>
</a></div></div><div class="crmErrorPgfooter"><div class="crmErrorPgfooterCont"><img src="https&#x3a;&#x2f;&#x2f;static.zohocdn.com&#x2f;crm&#x2f;CRMClient&#x2f;images&#x2f;crm_logo_04536e35e8162d631b95cf42593491cd_.svg"  alt="CRM" class="crmLogoImg"></div><span class="crmErrorPgfooterTxt">© 2024 Zoho Corp.All rights reserved.</span></div></div></body></html>

Check out the these 3 calls (without being logged in) https://recruit.zoho.eu/recruit/v2/org (looks legit) https://crm.zoho.eu/recruit/v2/org (looks like the generic error page from above) ---> my assumption is that this call is somehow constructed https://crm.zoho.eu/crm/v2/org (looks legit)

(I expected the first one, but potentially get the second one - but cannot prove this)

techlover11 commented 5 months ago

(solved)

I kind of solved the issue myself.

Here is an explanation for everybody else:

My code / the boilerplate code calls api_domain + '/recruit/v2/org';

However, api_domain from bearer token ALWAYS seems to be https://www.zohoapis.eu

BUT https://www.zohoapis.eu/recruit/v2/org seems to result in a generic error So I had to call https://recruit.zoho.eu/recruit/v2/org explicitly in order to not get the error

---> For non Zoho CRM use cases, you cannot use the built in var url = apiServer + '/recruit/v2/org';

function recruitOrg() {
  var service = getService_();
  if (service.hasAccess()) {
    var url = 'https://recruit.zoho.eu/recruit/v2/org';
    var response = UrlFetchApp.fetch(url, {
      method: 'get',
      headers: {
        'Authorization': 'Bearer ' + service.getAccessToken(),
        'Content-Type': 'application/json'
      },
      muteHttpExceptions: true
    });

    // Log the status code and headers to understand the response better
    Logger.log(response.getResponseCode());
    Logger.log(response.getHeaders());

    // Log the raw content of the response
    Logger.log(response.getContentText());

    // Attempt to parse the response as JSON, with error handling
    var result;
    try {
      result = JSON.parse(response.getContentText());
      // Log the parsed JSON response
      Logger.log(JSON.stringify(result, null, 2));
    } catch (e) {
      Logger.log('Failed to parse response as JSON:');
      Logger.log(response.getContentText());
      // Optionally, handle the error as needed
    }
  } else {
    var authorizationUrl = service.getAuthorizationUrl();
    Logger.log('Open the following URL and re-run the script: %s', authorizationUrl);
  }
}