googleworkspace / apps-script-oauth2

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

How to connect to Esty ? #466

Open Niji-Bemani opened 11 months ago

Niji-Bemani commented 11 months ago

Hi ! I am trying to use your library to connect to the new Etsy API (v3) that require OAuth2. I used the Twitter sample, and tried to adapt it for Etsy.

I can generate the authorization url and see the Etsy page to grant access. However, the redirection fails with this error : Error: Error retrieving token: Invalid authorization header (line 605, file "Service", project "OAuth2"), and I have no idea what part should be changed to make it work.

Here is my code :

var CLIENT_ID = '...';
var CLIENT_SECRET = '...';

/**
 * Authorizes and makes a request to the Etsy API v3
 * OAuth 2.0 Making requests on behalf of users
 * https://developers.etsy.com/documentation/essentials/authentication#step-3-request-an-access-token
 */
function run() {
  var service = getService_();
  if (service.hasAccess()) {
    var url = 'https://api.etsy.com/v3/application/openapi-ping';
    var response = UrlFetchApp.fetch(url, {
      headers: {
        Authorization: 'Bearer ' + service.getAccessToken()
      },
      muteHttpExceptions: true
    });
    var result = JSON.parse(response.getContentText());
    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();
  PropertiesService.getUserProperties().deleteProperty('code_challenge');
  PropertiesService.getUserProperties().deleteProperty('code_verifier');
}

/**
 * Configures the service.
 */
function getService_() {
  pkceChallengeVerifier();
  var userProps = PropertiesService.getUserProperties();
  return OAuth2.createService('Etsy')
  // Set the endpoint URLs.
      .setAuthorizationBaseUrl('https://www.etsy.com/oauth/connect')
      .setTokenUrl(
          'https://api.etsy.com/v3/public/oauth/token?code_verifier=' + userProps.getProperty('code_verifier'))

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

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

  // Set the property store where authorized tokens should be persisted.
      .setPropertyStore(userProps)

  // Set the scopes to request (space-separated).
      .setScope('address_r address_w')

  // Add parameters in the authorization url
      .setParam('response_type', 'code')
      .setParam('code_challenge_method', 'S256')
      .setParam('code_challenge', userProps.getProperty('code_challenge'))

      .setTokenHeaders({
        'Authorization': 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET),
        'Content-Type': 'application/x-www-form-urlencoded',
      });
}

/**
 * Handles the OAuth callback.
 */
function authCallback(request) {
  var service = getService_();
  var authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('Success!');
  } else {
    return HtmlService.createHtmlOutput('Denied.');
  }
}

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

/**
 * Generates code_verifier & code_challenge for PKCE
 */
function pkceChallengeVerifier() {
  var userProps = PropertiesService.getUserProperties();
  if (!userProps.getProperty('code_verifier')) {
    var verifier = '';
    var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';

    for (var i = 0; i < 128; i++) {
      verifier += possible.charAt(Math.floor(Math.random() * possible.length));
    }

    var sha256Hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, verifier);

    var challenge = Utilities.base64Encode(sha256Hash)
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
    userProps.setProperty('code_verifier', verifier);
    userProps.setProperty('code_challenge', challenge);
  }
}
Khnaz35 commented 11 months ago

Based on the information from Etsy's API documentation, the error you're encountering might be related to how the OAuth token is requested. Here are the key points to consider for the OAuth token request:

POST Request Format: The access token should be requested using a POST request to https://api.etsy.com/v3/public/oauth/token​​.

Request Body Parameters: The request body must include the following parameters in application/x-www-form-urlencoded format:

grant_type: Must always be authorization_code. client_id: Your Etsy App API Key string. redirect_uri: The same redirect_uri value used in the prior authorization code request. code: The authorization code received from Etsy after granting access. code_verifier: The PKCE code verifier preimage of the code_challenge used in the prior authorization code request​​. Header Format: The format for the authorization header in your token request seems correct. It uses the 'Basic' format with the CLIENT_ID and CLIENT_SECRET encoded in Base64. Ensure that this is consistent with Etsy's requirement.

PKCE Verification: Make sure that the PKCE code_verifier and code_challenge are correctly generated and used. The code_verifier should be the preimage of the code_challenge used in the authorization request

Niji-Bemani commented 11 months ago

Thank you for your reply ! I tried to change my code, but without success.

The access token should be requested using a POST request

I removed the parameter ?code_verifier=' + userProps.getProperty('code_verifier') from the token url. So I have :

.setTokenUrl('https://api.etsy.com/v3/public/oauth/token')

and I set the method to POST (although the default value is post according to the JSDoc) :

.setTokenMethod('POST')

The request body must include the following parameters [...]

I see a specific method for grant_type with :

.setGrantType('authorization_code')

but I don't understand how to set the others parameters. Could you provide an example ?

Niji-Bemani commented 11 months ago

It seems that the others parameters can be set with setTokenPayloadHandler(tokenHandler)

And a tokenHandler already has some parameters I need :

Name Type Description
code string The authorization code.
client_id string The client ID.
client_secret string The client secret.
redirect_uri string The redirect URI.
grant_type string The type of grant requested.

So, I need to set an additional parameter code_verifier, and remove the client_secret :

function getService_() {
   ...
   .setTokenPayloadHandler(etsyTokenHandler);
}

function etsyTokenHandler(payload) {
  payload.code_verifier = PropertiesService.getUserProperties().getProperty('code_verifier');

  if (payload.client_secret) {
    delete payload.client_secret;
  }

  return payload;
}

Unfortunately, I am stuck with the same error : Error retrieving token: Invalid authorization header. I would like to check the information I submit, but I don't know how to see them.

Khnaz35 commented 11 months ago

try something like this


var CLIENT_ID = '...';  // Replace with your Client ID
var CLIENT_SECRET = '...';  // Replace with your Client Secret

/**
 * Configures the OAuth2 service.
 */
function getService_() {
  pkceChallengeVerifier();
  var userProps = PropertiesService.getUserProperties();
  return OAuth2.createService('Etsy')
    .setAuthorizationBaseUrl('https://www.etsy.com/oauth/connect')
    .setTokenUrl('https://api.etsy.com/v3/public/oauth/token')
    .setClientId(CLIENT_ID)
    .setClientSecret(CLIENT_SECRET)
    .setCallbackFunction('authCallback')
    .setPropertyStore(userProps)
    .setScope('address_r address_w')
    .setParam('response_type', 'code')
    .setParam('code_challenge_method', 'S256')
    .setParam('code_challenge', userProps.getProperty('code_challenge'))
    .setTokenHeaders({
      'Authorization': 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET),
      'Content-Type': 'application/x-www-form-urlencoded',
    })
    .setTokenPayloadHandler(etsyTokenHandler);
}

function etsyTokenHandler(payload) {
  payload.code_verifier = PropertiesService.getUserProperties().getProperty('code_verifier');
  delete payload.client_secret;
  return payload;
}

function authCallback(request) {
  var service = getService_();
  var authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('Success!');
  } else {
    return HtmlService.createHtmlOutput('Denied.');
  }
}

// Include other necessary functions (run, reset, pkceChallengeVerifier) as per your existing code.
Niji-Bemani commented 11 months ago

Thanks for your help, I have exactly the same code as you, excepted this extra line :

.setGrantType('authorization_code')

But I tried, with and without this line, and I get the same error.

I just saw in Etsy documentation, that I need to register a callback URL :

https://developers.etsy.com/documentation/essentials/authentication#redirect-uris

All redirect_uri parameter values must match a precise callback URL string registered with Etsy. These values can be provided by editing your application at etsy.com/developers/your-apps.

I tried to set https://script.google.com/macros/d/{MY_SCRIPT_ID}/usercallback, but without success. It seems that Etsy does not accept this type or URI, but I have no idea what they want 😓 I contacted the support to get more information about that.

Khnaz35 commented 11 months ago

Have you gone through this documentions ? https://developer.etsy.com/documentation/essentials/authentication/

rcknr commented 10 months ago

@Niji-Bemani I'd be willing to help you but it is such a hustle to get approved for an app on this platform!