Cómo conectarse a servicios que no son de Google desde un complemento de Google Workspace

Tu proyecto de complemento de Google Workspace puede conectarse directamente a muchos productos de Google con los servicios integrados y avanzados de Apps Script.

También puedes acceder a las API y los servicios que no son de Google. Si el servicio no requiere autorización, por lo general, puedes hacer una solicitud UrlFetch adecuada y, luego, hacer que tu complemento interprete la respuesta.

Sin embargo, si el servicio que no es de Google requiere autorización, debes configurar OAuth para ese servicio. Puedes facilitar este proceso con la biblioteca de OAuth2 para Apps Script (también hay una versión de OAuth1).

Usa un servicio de OAuth

Cuando usas un objeto de servicio de OAuth para conectarte a un servicio que no es de Google, el complemento de Google Workspace debe detectar cuándo se requiere la autorización y, cuando lo esté, invocar el flujo de autorización.

El flujo de autorización consta de los siguientes elementos:

  1. Alertar al usuario que se necesita la autenticación y proporcionar un vínculo para iniciar el proceso
  2. Adquirir la autorización del servicio ajeno a Google
  3. Actualizando el complemento para volver a intentar el acceso al recurso protegido.

Cuando se necesita una autorización ajena a Google, la infraestructura del complemento de Google Workspace controla estos detalles. Tu complemento solo necesita detectar cuándo se necesita la autorización y, luego, invocar el flujo de autorización cuando sea necesario.

Cómo detectar la autorización requerida

Una solicitud puede no tener autorización para acceder a un recurso protegido por varias razones, como las siguientes:

  • El token de acceso aún no se generó o venció.
  • El token de acceso no cubre el recurso solicitado.
  • El token de acceso no cubre los alcances requeridos de la solicitud.

El código del complemento debería detectar estos casos. La función hasAccess() de la biblioteca de OAuth puede indicarte si actualmente tienes acceso a un servicio. Como alternativa, cuando usas solicitudes UrlFetchApp fetch(), puedes establecer el parámetro muteHttpExceptions en true. Esto evita que la solicitud arroje una excepción en caso de error de solicitud y te permite examinar el código y el contenido de respuesta de la solicitud en el objeto HttpResponse que se muestra.

Cuando el complemento detecta que se requiere autorización, debe activar el flujo de autorización.

Invoca el flujo de autorización

Puedes invocar el flujo de autorización con el servicio de Card para crear un objeto AuthorizationException, configurar sus propiedades y, luego, llamar a la función throwException(). Antes de generar la excepción, debes proporcionar lo siguiente:

  1. Obligatorio. Una URL de autorización. Esto lo especifica el servicio que no es de Google y es la ubicación a la que se dirige al usuario cuando comienza el flujo de autorización. Para configurarla, usa la función setAuthorizationUrl().
  2. Obligatorio. Una string de nombre visible de recurso. Identifica el recurso para el usuario cuando se solicita la autorización. Este nombre se establece con la función setResourceDisplayName().
  3. El nombre de una función de devolución de llamada que crea un mensaje de autorización personalizado. Esta devolución de llamada muestra un arreglo de objetos Card compilados que componen una IU para manejar la autorización. Esto es opcional; si no se establece, se usa la tarjeta de autorización predeterminada. Configura la función de devolución de llamada con la función setCustomUiCallback().

Ejemplo de configuración de OAuth que no es de Google

En esta muestra de código, se indica cómo configurar un complemento para usar una API que no sea de Google y que requiera OAuth. Se usa OAuth2 para Apps Script a fin de crear un servicio a fin de acceder a la API.

/**
 * 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("Display name to show to the user")
        .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('SERVICE_NAME')
      .setAuthorizationBaseUrl('SERVICE_AUTH_URL')
      .setTokenUrl('SERVICE_AUTH_TOKEN_URL')
      .setClientId('CLIENT_ID')
      .setClientSecret('CLIENT_SECRET')
      .setScope('SERVICE_SCOPE_REQUESTS')
      .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();
}

Crear una solicitud de autorización personalizada

tarjeta de autorización que no pertenece a Google

De forma predeterminada, una solicitud de autorización no tiene ninguna marca y solo usa la string del nombre visible para indicar a qué recurso intenta acceder el complemento. Sin embargo, tu complemento puede definir una tarjeta de autorización personalizada que tenga el mismo propósito y pueda incluir información adicional y desarrollo de la marca.

Para definir un mensaje personalizado, implementa una función de devolución de llamada de la IU personalizada que muestre un arreglo de objetos Card compilados. Este array debe contener solo una tarjeta. Si se proporcionan más encabezados, se muestran sus encabezados en una lista, lo que puede generar una experiencia del usuario confusa.

La tarjeta que se muestra debe cumplir con los siguientes requisitos:

  • Explícale al usuario que el complemento solicita permiso para acceder a un servicio ajeno a Google en su nombre.
  • Deje en claro qué puede hacer el complemento si está autorizado.
  • Contener un botón o un widget similar que lleve al usuario a la URL de autorización del servicio Asegúrate de que la función de este widget sea obvia para el usuario.
  • El widget anterior debe usar la configuración OnClose.RELOAD_ADD_ON en su objeto OpenLink para garantizar que el complemento se vuelva a cargar después de recibir la autorización.
  • Todos los vínculos abiertos desde el mensaje de autorización deben usar HTTPS.

Debes dirigir el flujo de autorización para que use tu tarjeta mediante una llamada a la función setCustomUiCallback() en tu objeto AuthorizationException.

En el siguiente ejemplo, se muestra una función de devolución de llamada para un mensaje de autorización personalizada:

/**
 * Returns an array of cards that comprise the customized authorization
 * prompt. Includes a button that opens the proper authorization link
 * for a non-Google service.
 *
 * When creating the text button, using the
 * setOnClose(CardService.OnClose.RELOAD_ADD_ON) function forces the add-on
 * to refresh once the authorization flow completes.
 *
 * @return {Card[]} The card representing the custom authorization prompt.
 */
function create3PAuthorizationUi() {
  var service = getOAuthService();
  var authUrl = service.getAuthorizationUrl();
  var authButton = CardService.newTextButton()
      .setText('Begin Authorization')
      .setAuthorizationAction(CardService.newAuthorizationAction()
          .setAuthorizationUrl(authUrl));

  var promptText =
      'To show you information from your 3P account that is relevant' +
      ' to the recipients of the email, this add-on needs authorization' +
      ' to: <ul><li>Read recipients of the email</li>' +
      '         <li>Read contact information from 3P account</li></ul>.';

  var card = CardService.newCardBuilder()
      .setHeader(CardService.newCardHeader()
          .setTitle('Authorization Required'))
      .addSection(CardService.newCardSection()
          .setHeader('This add-on needs access to your 3P account.')
          .addWidget(CardService.newTextParagraph()
              .setText(promptText))
          .addWidget(CardService.newButtonSet()
              .addButton(authButton)))
      .build();
  return [card];
}

/**
 * When connecting to the non-Google service, pass the name of the
 * custom UI callback function to the AuthorizationException object
 */
function accessProtectedResource(url, method_opt, headers_opt) {
  var service = getOAuthService();
  if (service.hasAccess()) {
    // Make the UrlFetch request and return the result.
    // ...
  } else {
    // Invoke the authorization flow using a custom authorization
    // prompt card.
    CardService.newAuthorizationException()
        .setAuthorizationUrl(service.getAuthorizationUrl())
        .setResourceDisplayName("Display name to show to the user")
        .setCustomUiCallback('create3PAuthorizationUi')
        .throwException();
  }
}

Cómo administrar accesos de terceros en apps de Google Workspace

Una aplicación común para los complementos de Google Workspace es proporcionar una interfaz a fin de interactuar con un sistema de terceros desde una aplicación host de Google Workspace. La biblioteca OAuth2 para Apps Script puede ayudarte a crear y administrar conexiones a servicios de terceros.

Los sistemas de terceros a menudo requieren que el usuario acceda con un ID de usuario, contraseña u otra credencial. Cuando un usuario accede a tu servicio de terceros mientras usa un host de Google Workspace, debes asegurarte de que no tenga que volver a acceder cuando cambie a otro host de Google Workspace. Para evitar solicitudes de acceso repetidas, usa propiedades de usuario o tokens de ID. Estos se explican en las siguientes secciones.

Propiedades del usuario

Puedes almacenar los datos de acceso de un usuario en sus propiedades. Por ejemplo, puedes crear tu propio JWT desde el servicio de acceso y registrarlo en una propiedad del usuario, o registrar el nombre de usuario y la contraseña de su servicio.

Las propiedades del usuario tienen un alcance que solo puede acceder a ese usuario dentro de la secuencia de comandos de tu complemento. Otros usuarios y otras secuencias de comandos no pueden acceder a estas propiedades. Consulta PropertiesService para obtener más detalles.

Tokens de ID

Puedes usar un token de ID de Google como credencial de acceso para tu servicio. Esta es una forma de lograr el inicio de sesión único. Los usuarios ya accedieron a Google porque están en una app host de Google.