Kết nối với các dịch vụ không phải của Google từ một tiện ích bổ sung của Google Workspace

Dự án Tiện ích bổ sung Google Workspace có thể kết nối trực tiếp với nhiều sản phẩm của Google bằng các dịch vụ tích hợp sẵnnâng cao của Apps Script.

Bạn cũng có thể truy cập các API và dịch vụ không phải của Google. Nếu dịch vụ không cần được cho phép, thì thông thường, bạn chỉ cần đưa ra một yêu cầu UrlFetch thích hợp rồi yêu cầu tiện ích bổ sung của bạn diễn giải phản hồi.

Tuy nhiên, nếu dịch vụ không phải của Google yêu cầu uỷ quyền, bạn phải định cấu hình OAuth cho dịch vụ đó. Bạn có thể làm cho quá trình này dễ dàng hơn bằng cách sử dụng thư viện OAuth2 cho Apps Script (ngoài ra còn có phiên bản OAuth1).

Sử dụng dịch vụ OAuth

Khi sử dụng đối tượng dịch vụ OAuth để kết nối với một dịch vụ không phải của Google, Tiện ích bổ sung của Google Workspace cần phát hiện thời điểm cần phải ủy quyền và gọi quy trình uỷ quyền khi cần.

Quy trình uỷ quyền bao gồm:

  1. Thông báo cho người dùng rằng cần xác thực và cung cấp một đường liên kết để bắt đầu quy trình.
  2. Nhận được sự uỷ quyền từ dịch vụ không phải của Google.
  3. Làm mới tiện ích bổ sung để thử truy cập lại vào tài nguyên được bảo vệ.

Khi cần sự cho phép của Google không phải của Google, cơ sở hạ tầng của Tiện ích bổ sung Google Workspace sẽ xử lý những thông tin chi tiết này. Tiện ích bổ sung của bạn chỉ cần phát hiện thời điểm cần uỷ quyền và gọi luồng uỷ quyền khi cần.

Phát hiện rằng uỷ quyền là bắt buộc

Một yêu cầu có thể không được phép truy cập vào một tài nguyên được bảo vệ vì nhiều lý do, chẳng hạn như:

  • Mã truy cập chưa được tạo hoặc đã hết hạn.
  • Mã truy cập không bao gồm tài nguyên được yêu cầu.
  • Mã truy cập không bao gồm phạm vi bắt buộc của yêu cầu.

Mã tiện ích bổ sung của bạn sẽ phát hiện được những trường hợp này. Hàm hasAccess() của thư viện OAuth có thể cho biết hiện bạn có quyền truy cập vào một dịch vụ hay không. Ngoài ra, khi sử dụng các yêu cầu UrlFetchApp fetch(), bạn có thể đặt tham số muteHttpExceptions thành true. Điều này ngăn yêu cầu gửi một trường hợp ngoại lệ khi yêu cầu không thành công, đồng thời cho phép bạn kiểm tra nội dung và mã phản hồi yêu cầu trong đối tượng HttpResponse được trả về.

Khi tiện ích bổ sung phát hiện thấy cần phải uỷ quyền, tiện ích bổ sung sẽ kích hoạt quy trình uỷ quyền.

Gọi quy trình uỷ quyền

Bạn gọi quy trình uỷ quyền bằng cách sử dụng Dịch vụ thẻ để tạo đối tượng AuthorizationException, đặt các thuộc tính của đối tượng đó rồi gọi hàm throwException(). Trước khi loại bỏ ngoại lệ, bạn cần cung cấp thông tin sau:

  1. Bắt buộc. URL uỷ quyền. Giá trị này do dịch vụ không phải của Google chỉ định và là vị trí mà người dùng được đưa đến khi quy trình uỷ quyền bắt đầu. Bạn đặt URL này bằng hàm setAuthorizationUrl().
  2. Bắt buộc. Chuỗi tên hiển thị tài nguyên. Xác định tài nguyên cho người dùng khi họ yêu cầu uỷ quyền. Bạn đặt tên này bằng hàm setResourceDisplayName().
  3. Tên của hàm callback tạo lời nhắc tuỳ chỉnh về việc uỷ quyền. Lệnh gọi lại này trả về một mảng các đối tượng Card đã tạo có nhiệm vụ soạn giao diện người dùng để xử lý uỷ quyền. Thao tác này là không bắt buộc; nếu bạn không đặt thẻ uỷ quyền mặc định, hệ thống sẽ sử dụng thẻ đó. Bạn đặt hàm callback bằng hàm setCustomUiCallback().

Ví dụ về cấu hình OAuth không phải của Google

Mã mẫu này cho biết cách định cấu hình tiện ích bổ sung để sử dụng API không phải của Google yêu cầu OAuth. API này sử dụng OAuth2 cho Apps Script để tạo dịch vụ truy cập vào 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();
}

Tạo lời nhắc cấp quyền tuỳ chỉnh

thẻ uỷ quyền dịch vụ không phải của Google

Theo mặc định, lời nhắc uỷ quyền không có thương hiệu nào và chỉ sử dụng chuỗi tên hiển thị để cho biết tài nguyên mà tiện ích bổ sung đang cố gắng truy cập. Tuy nhiên, tiện ích bổ sung của bạn có thể xác định một thẻ uỷ quyền tuỳ chỉnh phục vụ cùng một mục đích và có thể bao gồm thông tin và thương hiệu bổ sung.

Bạn xác định lời nhắc tuỳ chỉnh bằng cách triển khai hàm callback giao diện người dùng tuỳ chỉnh. Hàm này trả về một mảng các đối tượng Card đã tạo. Mảng này chỉ được chứa một thẻ duy nhất. Nếu bạn cung cấp thêm mã, tiêu đề của những thành phần đó sẽ hiển thị trong danh sách, điều này có thể dẫn đến trải nghiệm người dùng khó hiểu.

Thẻ được trả về phải thực hiện những việc sau:

  • Hãy nói rõ với người dùng rằng tiện ích bổ sung đang yêu cầu cấp quyền truy cập vào một dịch vụ không phải của Google thay mặt họ.
  • Hãy nêu rõ những việc tiện ích bổ sung có thể làm nếu được cho phép.
  • Chứa một nút hoặc tiện ích tương tự đưa người dùng đến URL uỷ quyền của dịch vụ. Hãy đảm bảo người dùng có thể nắm rõ chức năng của tiện ích này.
  • Tiện ích ở trên phải sử dụng chế độ cài đặt OnClose.RELOAD_ADD_ON trong đối tượng OpenLink để đảm bảo tiện ích bổ sung sẽ tải lại sau khi nhận được uỷ quyền.
  • Tất cả đường liên kết được mở từ lời nhắc cấp quyền phải sử dụng HTTPS.

Bạn chuyển hướng quy trình uỷ quyền để sử dụng thẻ của mình bằng cách gọi hàm setCustomUiCallback() trên đối tượng AuthorizationException.

Ví dụ sau đây cho thấy một hàm gọi lại lời nhắc uỷ quyền tuỳ chỉnh:

/**
 * 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();
  }
}

Quản lý hoạt động đăng nhập qua bên thứ ba trên các ứng dụng của Google Workspace

Một ứng dụng phổ biến cho Tiện ích bổ sung của Google Workspace là cung cấp giao diện để tương tác với hệ thống của bên thứ ba ngay trong ứng dụng lưu trữ của Google Workspace. Thư viện OAuth2 cho Apps Script có thể giúp bạn tạo và quản lý các kết nối với các dịch vụ của bên thứ ba.

Các hệ thống bên thứ ba thường yêu cầu người dùng đăng nhập bằng mã nhận dạng người dùng, mật khẩu hoặc thông tin xác thực khác. Khi người dùng đăng nhập vào dịch vụ bên thứ ba của bạn trong khi đang sử dụng một máy chủ lưu trữ Google Workspace, bạn phải đảm bảo họ không phải đăng nhập lại khi chuyển sang một máy chủ lưu trữ Google Workspace khác. Để ngăn các yêu cầu đăng nhập lặp lại, hãy sử dụng thuộc tính người dùng hoặc mã thông báo giá trị nhận dạng. Điều này được giải thích trong các phần sau.

Thuộc tính người dùng

Bạn có thể lưu trữ dữ liệu đăng nhập của người dùng trong thuộc tính người dùng của Apps Script. Ví dụ: bạn có thể tạo JWT của riêng mình từ dịch vụ đăng nhập của họ và ghi lại trong thuộc tính người dùng, hoặc ghi lại tên người dùng và mật khẩu cho dịch vụ của họ.

Các thuộc tính người dùng nằm trong phạm vi mà chỉ người dùng đó mới có thể truy cập vào những thuộc tính đó trong tập lệnh của tiện ích bổ sung. Người dùng khác và các tập lệnh khác không thể truy cập vào các thuộc tính này. Hãy xem PropertiesService để biết thêm thông tin chi tiết.

Mã thông báo giá trị nhận dạng

Bạn có thể sử dụng mã thông báo giá trị nhận dạng của Google làm thông tin đăng nhập cho dịch vụ của mình. Đây là một cách để đạt được tính năng đăng nhập một lần. Người dùng đã đăng nhập vào Google vì họ đang sử dụng ứng dụng lưu trữ của Google.