Google Workspace 부가기능에서 Google 이외의 서비스에 연결하기

Google Workspace 부가기능 프로젝트는 Apps Script의 기본 제공고급 서비스를 사용하여 여러 Google 제품에 직접 연결할 수 있습니다.

Google 이외의 API 및 서비스에도 액세스할 수 있습니다. 서비스에 승인이 필요하지 않으면 일반적으로 적절한 UrlFetch 요청을 한 후 부가기능에서 응답을 해석하도록 할 수 있습니다.

하지만 Google 이외의 서비스에 승인이 필요한 경우 해당 서비스에 OAuth를 구성해야 합니다. Apps Script용 OAuth2 라이브러리 (OAuth1 버전도 있음)를 사용하면 이 프로세스를 더 쉽게 수행할 수 있습니다.

OAuth 서비스 사용

OAuth 서비스 객체를 사용하여 Google 이외의 서비스에 연결할 때 Google Workspace 부가기능은 승인이 필요한 경우를 감지하고 승인이 필요한 경우 승인 흐름을 호출해야 합니다.

승인 흐름은 다음과 같이 구성됩니다.

  1. 사용자에게 인증이 필요하다고 알리고 프로세스를 시작할 수 있는 링크를 제공합니다.
  2. Google 이외의 서비스에서 승인을 얻습니다.
  3. 보호된 리소스에 다시 액세스하기 위해 부가기능을 새로고침하는 중입니다.

Google 이외의 승인이 필요한 경우 Google Workspace 부가기능 인프라에서 이러한 세부정보를 처리합니다. 부가기능은 승인이 필요할 때만을 감지하고 필요할 때 승인 흐름을 호출하면 됩니다.

승인이 필요한지 감지

다음과 같은 다양한 이유로 보호되는 리소스에 액세스할 수 있는 권한이 요청에 없을 수 있습니다.

  • 액세스 토큰이 아직 생성되지 않았거나 만료되었습니다.
  • 액세스 토큰이 요청된 리소스를 포함하지 않습니다.
  • 액세스 토큰이 요청의 필수 범위를 포함하지 않습니다.

부가기능 코드가 이러한 사례를 감지합니다. OAuth 라이브러리 hasAccess() 함수를 통해 현재 서비스에 액세스할 수 있는지 확인할 수 있습니다. 또는 UrlFetchApp fetch() 요청을 사용할 때 muteHttpExceptions 매개변수를 true로 설정할 수 있습니다. 이렇게 하면 요청 실패 시 요청에서 예외가 발생하지 않으며 반환된 HttpResponse 객체의 요청 응답 코드와 콘텐츠를 검사할 수 있습니다.

부가기능에서 승인이 필요함을 감지하면 승인 흐름을 트리거해야 합니다.

승인 흐름 호출

카드 서비스를 사용해 AuthorizationException 객체를 만들고 속성을 설정한 후 throwException() 함수를 호출하여 승인 흐름을 호출합니다. 예외를 발생시키기 전에 다음 정보를 제공해야 합니다.

  1. 필수사항. 승인 URL 이 위치는 Google 이외의 서비스에서 지정하며 승인 과정이 시작될 때 사용자가 이동하는 위치입니다. setAuthorizationUrl() 함수를 사용하여 이 URL을 설정합니다.
  2. 필수사항. 리소스 표시 이름 문자열입니다. 승인이 요청될 때 사용자에게 리소스를 식별합니다. setResourceDisplayName() 함수를 사용하여 이 이름을 설정합니다.
  3. 맞춤 승인 메시지를 만드는 콜백 함수의 이름입니다. 이 콜백은 승인 처리를 위해 UI를 구성하는 빌드된 Card 객체의 배열을 반환합니다. 이는 선택사항입니다. 설정하지 않으면 기본 승인 카드가 사용됩니다. setCustomUiCallback() 함수를 사용하여 콜백 함수를 설정합니다.

Google 이외의 OAuth 구성 예시

이 코드 샘플은 OAuth가 필요한 Google 이외의 API를 사용하도록 부가기능을 구성하는 방법을 보여줍니다. Apps Script용 OAuth2를 사용하여 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();
}

맞춤 승인 메시지 만들기

Google 이외의 서비스 승인 카드

기본적으로 승인 메시지에는 브랜딩이 없으며 표시 이름 문자열만 사용하여 부가기능이 액세스하려는 리소스를 나타냅니다. 그러나 부가기능에서 동일한 용도로 사용되며 추가 정보와 브랜딩을 포함할 수 있는 맞춤설정된 승인 카드를 정의할 수 있습니다.

빌드된 Card 객체의 배열을 반환하는 맞춤 UI 콜백 함수를 구현하여 맞춤 메시지를 정의합니다. 이 배열에는 하나의 카드만 포함되어야 합니다. 헤더가 더 제공되면 목록에 표시되므로 사용자 환경에 혼란을 초래할 수 있습니다.

반품된 카드는 다음 단계를 따라야 합니다.

  • 부가기능에서 사용자를 대신하여 Google 이외의 서비스에 액세스할 수 있는 권한을 요청한다는 점을 사용자에게 명확히 알립니다.
  • 승인된 경우 부가기능이 어떤 작업을 할 수 있는지 명확히 설명합니다.
  • 사용자를 서비스의 승인 URL로 이동시키는 버튼이나 유사한 위젯을 포함합니다. 이 위젯의 기능이 사용자에게 명확하게 표시되는지 확인합니다.
  • 위의 위젯은 OpenLink 객체의 OnClose.RELOAD_ADD_ON 설정을 사용하여 승인을 받은 후 부가기능이 새로고침되도록 해야 합니다.
  • 승인 메시지에서 열리는 모든 링크는 HTTPS를 사용해야 합니다.

AuthorizationException 객체에서 setCustomUiCallback() 함수를 호출하여 카드를 사용하도록 승인 흐름을 지시합니다.

다음 예에서는 맞춤 승인 메시지 콜백 함수를 보여줍니다.

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

Google Workspace 앱에서 서드 파티 로그인 관리하기

Google Workspace 부가기능의 일반적인 애플리케이션 중 하나는 Google Workspace 호스트 애플리케이션 내에서 타사 시스템과 상호작용하기 위한 인터페이스를 제공하는 것입니다. Apps Script용 OAuth2 라이브러리를 사용하면 타사 서비스에 대한 연결을 만들고 관리할 수 있습니다.

서드 파티 시스템에서는 사용자가 사용자 ID, 비밀번호 또는 기타 사용자 인증 정보를 사용하여 로그인하도록 요구하는 경우가 많습니다. 사용자가 하나의 Google Workspace 호스트를 사용하는 동안 타사 서비스에 로그인하는 경우 다른 Google Workspace 호스트로 전환할 때 다시 로그인할 필요가 없도록 해야 합니다. 로그인 요청이 반복되지 않도록 사용자 속성이나 ID 토큰을 사용하세요. 이에 대해서는 다음 섹션에서 설명합니다.

사용자 속성 수

사용자의 로그인 데이터를 Apps Script의 사용자 속성에 저장할 수 있습니다. 예를 들어 로그인 서비스에서 자체 JWT를 만들어 사용자 속성에 기록하거나 서비스의 사용자 이름과 비밀번호를 기록할 수 있습니다.

사용자 속성은 부가기능의 스크립트 내에서 해당 사용자만 액세스할 수 있도록 범위가 지정됩니다. 다른 사용자 및 스크립트는 이러한 속성에 액세스할 수 없습니다. 자세한 내용은 PropertiesService를 참고하세요.

ID 토큰

Google ID 토큰을 서비스의 로그인 사용자 인증 정보로 사용할 수 있습니다. 이렇게 하면 싱글 사인온(SSO)을 수행할 수 있습니다. 사용자는 Google 호스트 앱에 있기 때문에 이미 Google에 로그인되어 있습니다.