Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gmail Add-On: Oauth not being triggered

In the code below, 'Test' button triggers a function which calls an external endpoint to load data. However, nothing happens when the button is clicked and I get a 400 error in the console area saying Invalid Argument.

Code.gs

function buildAddOn(e) {
  // Create a section for that contains all user Labels.
  var section = CardService.newCardSection()  
  var action = CardService.newAction()
        .setFunctionName("testCall");

  var button = CardService.newTextButton().setText('Test').setOnClickAction(action);
  section.addWidget(CardService.newButtonSet().addButton(button)); 

//  section.addWidget(CardService.newTextParagraph()
//    .setText("This is a text paragraph widget. Multiple lines are allowed if needed.");)

  // Build the main card after adding the section.
  var card = CardService.newCardBuilder()
    .setHeader(CardService.newCardHeader()
    .setTitle('Authentication Card')
    .setImageUrl('https://www.gstatic.com/images/icons/material/system/1x/label_googblue_48dp.png'))
    .addSection(section)
    .build();

  return [card];
}

function testCall(){
  console.log("test");
  var data = accessProtectedResource('https://api.ssdf.io/v1.0/asd/4/174203','get');
  return CardService.newActionResponseBuilder()
      .setNotification(CardService.newNotification()
          .setType(CardService.NotificationType.INFO)
          .setText(data))
      .build();
}

authService.gs

/**
 * Attempts to access a non-Google API using a constructed service
 * object.
 *
 * If your add-on needs access to non-Google APIs that require OAuth,
 * you need to implement this method. You can use the OAuth1 and
 * OAuth2 Apps Script libraries to help implement it.
 *
 * @param {String} url         The URL to access.
 * @param {String} method_opt  The HTTP method. Defaults to GET.
 * @param {Object} headers_opt The HTTP headers. Defaults to an empty
 *                             object. The Authorization field is added
 *                             to the headers in this method.
 * @return {HttpResponse} the result from the UrlFetchApp.fetch() call.
 */
function accessProtectedResource(url, method_opt, headers_opt) {
  var service = getOAuthService();
  var maybeAuthorized = service.hasAccess();
  if (maybeAuthorized) {
    // A token is present, but it may be expired or invalid. Make a
    // request and check the response code to be sure.

    // Make the UrlFetch request and return the result.
    var accessToken = service.getAccessToken();
    var method = method_opt || 'get';
    var headers = headers_opt || {};
    headers['Authorization'] =
        Utilities.formatString('Bearer %s', accessToken);
    var resp = UrlFetchApp.fetch(url, {
      'headers': headers,
      'method' : method,
      'muteHttpExceptions': true, // Prevents thrown HTTP exceptions.
    });

    var code = resp.getResponseCode();
    if (code >= 200 && code < 300) {
      return resp.getContentText("utf-8"); // Success
    } else if (code == 401 || code == 403) {
       // Not fully authorized for this action.
       maybeAuthorized = false;
    } else {
       // Handle other response codes by logging them and throwing an
       // exception.
       console.error("Backend server error (%s): %s", code.toString(),
                     resp.getContentText("utf-8"));
       throw ("Backend server error: " + code);
    }
  }

  if (!maybeAuthorized) {
    // Invoke the authorization flow using the default authorization
    // prompt card.
    CardService.newAuthorizationException()
        .setAuthorizationUrl(service.getAuthorizationUrl())
        .setResourceDisplayName("Login to ....")
        .throwException();
  }
}

/**
 * Create a new OAuth service to facilitate accessing an API.
 * This example assumes there is a single service that the add-on needs to
 * access. Its name is used when persisting the authorized token, so ensure
 * it is unique within the scope of the property store. You must set the
 * client secret and client ID, which are obtained when registering your
 * add-on with the API.
 *
 * See the Apps Script OAuth2 Library documentation for more
 * information:
 *   https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
 *
 *  @return A configured OAuth2 service object.
 */
function getOAuthService() {
  return OAuth2.createService('auth')
      .setAuthorizationBaseUrl('https://app.ss.io/oauth/authorize')
      .setTokenUrl('https://api.ss.io/oauth/token')
      .setClientId('2361c9fbc5ba4b88813a3ef')
      .setClientSecret('f5d3a04f4asda30a52830e230e43727')
      .setScope('1')
      .setCallbackFunction('authCallback')
      .setCache(CacheService.getUserCache())
      .setPropertyStore(PropertiesService.getUserProperties());
}

/**
 * Boilerplate code to determine if a request is authorized and returns
 * a corresponding HTML message. When the user completes the OAuth2 flow
 * on the service provider's website, this function is invoked from the
 * service. In order for authorization to succeed you must make sure that
 * the service knows how to call this function by setting the correct
 * redirect URL.
 *
 * The redirect URL to enter is:
 * https://script.google.com/macros/d/<Apps Script ID>/usercallback
 *
 * See the Apps Script OAuth2 Library documentation for more
 * information:
 *   https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
 *
 *  @param {Object} callbackRequest The request data received from the
 *                  callback function. Pass it to the service's
 *                  handleCallback() method to complete the
 *                  authorization process.
 *  @return {HtmlOutput} a success or denied HTML message to display to
 *          the user. Also sets a timer to close the window
 *          automatically.
 */
function authCallback(callbackRequest) {
  var authorized = getOAuthService().handleCallback(callbackRequest);
  if (authorized) {
    return HtmlService.createHtmlOutput(
      'Success! <script>setTimeout(function() { top.window.close() }, 1);</script>');
  } else {
    return HtmlService.createHtmlOutput('Denied');
  }
}

/**
 * Unauthorizes the non-Google service. This is useful for OAuth
 * development/testing.  Run this method (Run > resetOAuth in the script
 * editor) to reset OAuth to re-prompt the user for OAuth.
 */
function resetOAuth() {
  getOAuthService().reset();
}
like image 577
Shyamal Parikh Avatar asked Mar 28 '19 10:03

Shyamal Parikh


1 Answers

All URLs in the function getOAuthService() have to be the original google-URLs of the example:

      // Set the endpoint URLs, which are the same for all Google services.
      .setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
      .setTokenUrl('https://accounts.google.com/o/oauth2/token')

and

      // Set the scopes to request (space-separated for Google services).
      .setScope('https://www.googleapis.com/auth/drive')

It might be possible to change the latter one, but only the path, not the domain, and you've to look in the API if and how it is adjustable. Also this scope-parameter differs in your example, but I don't know if '1' could be accepted.

Your own application get's a feedback if access is granted but is not involved in validating the authentication. Therefore you also need to get the access-token, I see it in your code: var accessToken = service.getAccessToken();, in the example it looks a bit different:

function makeRequest() {
  var driveService = getDriveService();
  var response = UrlFetchApp.fetch('https://www.googleapis.com/drive/v2/files?maxResults=10', {
    headers: {
      Authorization: 'Bearer ' + driveService.getAccessToken()
    }
  });
  // ...
}

see at the line with Authorization: 'Bearer ' + driveService.getAccessToken().

Your own server isn't (and shouldn't be) configured to handle authentication requests and therefore throws the 400 error. The API is built for use by Javascript on client-side, therefore I advise not to use it to authenticate on an own server. Nevertheless below I listed APIs for usage on your own server.

Using your own server for authentication
If you dismiss my advise to use the google-servers for authentication, then the scope of the tagged issues is getting larger, as server-configuration (apache, nginx, ...) and server-side programming-languages (PHP, Python, ...) might be involved.

You've to debug the headers then, what is sent exactly to the server and why the server can't handle it. You can debug the request and response in the browser's developer-tool (network panel) and check the error-files of the server.

The server has to acquire the google-service then by itself and do what would be done in frontend only by javascript if you'd follow the example you linked in your code.

In the example are three server-request made:
- one to authenticate
- one to get the authentication-token
- one to get the protected service
You've to keep in mind that all three steps have to be done then by your own server and that your server has to be able to answer on all three request-types.
An important question might be which of the three requests is producing the error?

You should extend your question then with the detailed problems you discover concerning communication with your server.

APIs and Examples for server-side OAuth-athentication
PHP: https://developers.google.com/api-client-library/php/auth/web-app
Python: https://developers.google.com/api-client-library/python/auth/web-app
Ruby: https://developers.google.com/api-client-library/ruby/auth/web-app
NodeJs: https://developers.google.com/apps-script/api/quickstart/nodejs
Java: https://developers.google.com/api-client-library/java/google-oauth-java-client/
Go: https://github.com/googleapis/google-api-go-client
.NET: https://developers.google.com/api-client-library/dotnet/apis/oauth2/v2

like image 113
David Avatar answered Nov 16 '22 00:11

David