Google Chat의 요청 확인하기

이 섹션에서는 HTTP 엔드포인트를 기반으로 빌드된 Google Chat 앱의 경우 엔드포인트에 대한 요청이 Chat에서 전송되었는지 확인하는 방법을 설명합니다.

채팅 앱의 엔드포인트에 상호작용 이벤트를 전달하기 위해 Google은 서비스에 요청을 보냅니다. Google에서 보낸 요청인지 확인하기 위해 Chat에는 엔드포인트에 대한 모든 HTTPS 요청의 Authorization 헤더에 Bearer 토큰이 포함되어 있습니다. 예를 들면 다음과 같습니다.

POST
Host: yourappurl.com
Authorization: Bearer AbCdEf123456
Content-Type: application/json
User-Agent: Google-Dynamite

앞의 예시에서 문자열 AbCdEf123456는 Bearer 승인 토큰입니다. 이는 Google에서 생성한 암호화 토큰입니다. Bearer 토큰 유형과 audience 필드의 값은 채팅 앱을 구성할 때 선택한 인증 대상 유형에 따라 다릅니다.

Cloud Functions 또는 Cloud Run을 사용하여 채팅 앱을 구현한 경우 Cloud IAM이 자동으로 토큰 확인을 처리합니다. Google Chat 서비스 계정을 승인된 호출자로 추가하기만 하면 됩니다. 앱이 자체 HTTP 서버를 구현하는 경우 오픈소스 Google API 클라이언트 라이브러리를 사용하여 Bearer 토큰을 확인할 수 있습니다.

채팅 앱에 대해 토큰이 확인되지 않으면 서비스는 HTTPS 응답 코드 401 (Unauthorized)로 요청에 응답해야 합니다.

Cloud Functions 또는 Cloud Run을 사용하여 요청 인증

함수 로직이 Cloud Functions 또는 Cloud Run을 사용하여 구현된 경우 채팅 앱 연결 설정인증 대상 필드에서 App URL를 선택하고 구성의 앱 URL이 Cloud 함수 또는 Cloud Run 엔드포인트의 URL과 일치하는지 확인해야 합니다.

그런 다음 Google Chat 서비스 계정 chat@system.gserviceaccount.com을 호출자로 승인해야 합니다.

다음 단계에서는 Cloud Functions (1세대)를 사용하는 방법을 보여줍니다.

Console

함수를 Google Cloud에 배포한 후 다음을 실행합니다.

  1. Google Cloud 콘솔에서 Cloud Functions 페이지로 이동합니다.

    Cloud Functions로 이동

  2. Cloud Functions 목록에서 수신 함수 옆의 체크박스를 클릭합니다. 함수 자체를 클릭하지 마세요.

  3. 화면 상단의 권한을 클릭합니다. 권한 패널이 열립니다.

  4. 주 구성원 추가를 클릭합니다.

  5. 새 주 구성원 필드에 chat@system.gserviceaccount.com를 입력합니다.

  6. 역할 선택 드롭다운 메뉴에서 Cloud Functions > Cloud Functions 호출자 역할을 선택합니다.

  7. 저장을 클릭합니다.

gcloud

gcloud functions add-iam-policy-binding 명령어를 사용합니다.

gcloud functions add-iam-policy-binding RECEIVING_FUNCTION \
  --member='serviceAccount:chat@system.gserviceaccount.com' \
  --role='roles/cloudfunctions.invoker'

RECEIVING_FUNCTION을 채팅 앱의 함수 이름으로 바꿉니다.

다음 단계에서는 Cloud Functions (2세대) 또는 Cloud Run 서비스를 사용하는 방법을 보여줍니다.

Console

함수 또는 서비스를 Google Cloud에 배포한 후 다음을 수행합니다.

  1. Google Cloud 콘솔에서 Cloud Run 페이지로 이동합니다.

    Cloud Run으로 이동

  2. Cloud Run 서비스 목록에서 수신 함수 옆에 있는 체크박스를 클릭합니다. 함수 자체를 클릭하지 마세요.

  3. 화면 상단의 권한을 클릭합니다. 권한 패널이 열립니다.

  4. 주 구성원 추가를 클릭합니다.

  5. 새 주 구성원 필드에 chat@system.gserviceaccount.com를 입력합니다.

  6. 역할 선택 드롭다운 메뉴에서 Cloud Run > Cloud Run 호출자 역할을 선택합니다.

  7. 저장을 클릭합니다.

gcloud

gcloud functions add-invoker-policy-binding 명령어를 사용합니다.

gcloud functions add-invoker-policy-binding RECEIVING_FUNCTION \
  --member='serviceAccount:chat@system.gserviceaccount.com'

RECEIVING_FUNCTION을 채팅 앱의 함수 이름으로 바꿉니다.

앱 URL ID 토큰으로 요청 인증

채팅 앱 연결 설정의 인증 대상 필드가 App URL로 설정된 경우 요청의 Bearer 승인 토큰은 Google이 서명한 OpenID Connect(OIDC) ID 토큰입니다. email 필드는 chat@system.gserviceaccount.com로 설정됩니다. audience 필드는 채팅 앱에 요청을 전송하도록 Google Chat을 구성한 URL로 설정됩니다. 예를 들어 채팅 앱의 구성된 엔드포인트가 https://example.com/app/인 경우 ID 토큰의 audience 필드는 https://example.com/app/입니다.

다음 샘플은 Bearer 토큰이 Google Chat에서 발급되었고 Google OAuth 클라이언트 라이브러리를 사용하여 앱을 타겟팅했는지 확인하는 방법을 보여줍니다.

Java

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collections;

import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
import com.google.api.client.http.apache.ApacheHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.json.JsonFactory;

/** Tool for verifying JWT Tokens for Apps in Google Chat. */
public class JWTVerify {
  // Bearer Tokens received by apps will always specify this issuer.
  static String CHAT_ISSUER = "chat@system.gserviceaccount.com";

  // Intended audience of the token, which is the URL of the app.
  static String AUDIENCE = "https://example.com/app/";

  // Get this value from the request's Authorization HTTPS header.
  // For example, for "Authorization: Bearer AbCdEf123456" use "AbCdEf123456".
  static String BEARER_TOKEN = "AbCdEf123456";

  public static void main(String[] args) throws GeneralSecurityException, IOException {
    JsonFactory factory = new GsonFactory();

    GoogleIdTokenVerifier verifier =
        new GoogleIdTokenVerifier.Builder(new ApacheHttpTransport(), factory)
        .setAudience(Collections.singletonList(AUDIENCE))
        .build();

    GoogleIdToken idToken = GoogleIdToken.parse(factory, BEARER_TOKEN);
    if (idToken == null) {
      System.out.println("Token cannot be parsed");
      System.exit(-1);
    }

    // Verify valid token, signed by CHAT_ISSUER, intended for a third party.
    if (!verifier.verify(idToken)
        || !idToken.getPayload().getEmailVerified()
        || !idToken.getPayload().getEmail().equals(CHAT_ISSUER)) {
      System.out.println("Invalid token");
      System.exit(-1);
    }

    // Token originates from Google and is targeted to a specific client.
    System.out.println("The token is valid");
  }
}

Python

import sys
from google.oauth2 import id_token
from google.auth.transport import requests

# Bearer Tokens received by apps will always specify this issuer.
CHAT_ISSUER = 'chat@system.gserviceaccount.com'

# Intended audience of the token, which is the URL of the app.
AUDIENCE = 'https://example.com/app/'

# Get this value from the request's Authorization HTTPS header.
# For example, for 'Authorization: Bearer AbCdEf123456' use 'AbCdEf123456'.
BEARER_TOKEN = 'AbCdEf123456'

try:
  # Verify valid token, signed by CHAT_ISSUER, intended for a third party.
  request = requests.Request()
  token = id_token.verify_oauth2_token(BEARER_TOKEN, request, AUDIENCE)

  if token['email'] != CHAT_ISSUER:
    sys.exit('Invalid token')
except:
  sys.exit('Invalid token')

# Token originates from Google and is targeted to a specific client.
print('The token is valid')

Node.js

import {OAuth2Client} from 'google-auth-library';

// Bearer Tokens received by apps will always specify this issuer.
const CHAT_ISSUER = 'chat@system.gserviceaccount.com';

// Intended audience of the token, which is the URL of the app.
const AUDIENCE = 'https://example.com/app/';

// Get this value from the request's Authorization HTTPS header.
// For example, for "Authorization: Bearer AbCdEf123456" use "AbCdEf123456"
const BEARER_TOKEN = 'AbCdEf123456';

const client = new OAuth2Client();

async function verify() {
  // Verify valid token, signed by CHAT_ISSUER, intended for a third party.
  try {
    const ticket = await client.verifyIdToken({
      idToken: BEARER_TOKEN,
      audience: AUDIENCE
    });
    if (!ticket.getPayload().email_verified
        || ticket.getPayload().email !== CHAT_ISSUER) {
      throw new Error('Invalid issuer');
    }
  } catch (unused) {
    console.error('Invalid token');
    process.exit(1);
  }

  // Token originates from Google and is targeted to a specific client.
  console.log('The token is valid');
}

verify();

프로젝트 번호 JWT로 요청 인증

채팅 앱 연결 설정의 인증 대상 필드가 Project Number (또는 설정되지 않음)로 설정된 경우 요청의 Bearer 승인 토큰은 chat@system.gserviceaccount.com에서 발급 및 서명한 자체 서명 JSON 웹 토큰 (JWT)입니다. audience 필드는 채팅 앱을 빌드하는 데 사용한 Google Cloud 프로젝트 번호로 설정됩니다. 예를 들어 채팅 앱의 클라우드 프로젝트 번호가 1234567890이면 JWT의 audience 필드는 1234567890입니다.

다음 샘플은 Bearer 토큰이 Google Chat에서 발급되었고 Google OAuth 클라이언트 라이브러리를 사용하여 프로젝트에서 타겟팅되었는지 확인하는 방법을 보여줍니다.

Java

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collections;

import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager;
import com.google.api.client.http.apache.ApacheHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.json.JsonFactory;

/** Tool for verifying JWT Tokens for Apps in Google Chat. */
public class JWTVerify {
  // Bearer Tokens received by apps will always specify this issuer.
  static String CHAT_ISSUER = "chat@system.gserviceaccount.com";

  // Url to obtain the public certificate for the issuer.
  static String PUBLIC_CERT_URL_PREFIX =
      "https://www.googleapis.com/service_accounts/v1/metadata/x509/";

  // Intended audience of the token, which is the project number of the app.
  static String AUDIENCE = "1234567890";

  // Get this value from the request's Authorization HTTPS header.
  // For example, for "Authorization: Bearer AbCdEf123456" use "AbCdEf123456".
  static String BEARER_TOKEN = "AbCdEf123456";

  public static void main(String[] args) throws GeneralSecurityException, IOException {
    JsonFactory factory = new GsonFactory();

    GooglePublicKeysManager.Builder keyManagerBuilder =
        new GooglePublicKeysManager.Builder(new ApacheHttpTransport(), factory);

    String certUrl = PUBLIC_CERT_URL_PREFIX + CHAT_ISSUER;
    keyManagerBuilder.setPublicCertsEncodedUrl(certUrl);

    GoogleIdTokenVerifier.Builder verifierBuilder =
        new GoogleIdTokenVerifier.Builder(keyManagerBuilder.build());
    verifierBuilder.setIssuer(CHAT_ISSUER);
    GoogleIdTokenVerifier verifier = verifierBuilder.build();

    GoogleIdToken idToken = GoogleIdToken.parse(factory, BEARER_TOKEN);
    if (idToken == null) {
      System.out.println("Token cannot be parsed");
      System.exit(-1);
    }

    // Verify valid token, signed by CHAT_ISSUER, intended for a third party.
    if (!verifier.verify(idToken)
        || !idToken.verifyAudience(Collections.singletonList(AUDIENCE))
        || !idToken.verifyIssuer(CHAT_ISSUER)) {
      System.out.println("Invalid token");
      System.exit(-1);
    }

    // Token originates from Google and is targeted to a specific client.
    System.out.println("The token is valid");
  }
}

Python

import sys

from google.oauth2 import id_token
from google.auth.transport import requests

# Bearer Tokens received by apps will always specify this issuer.
CHAT_ISSUER = 'chat@system.gserviceaccount.com'

# Url to obtain the public certificate for the issuer.
PUBLIC_CERT_URL_PREFIX = 'https://www.googleapis.com/service_accounts/v1/metadata/x509/'

# Intended audience of the token, which will be the project number of the app.
AUDIENCE = '1234567890'

# Get this value from the request's Authorization HTTPS header.
# For example, for 'Authorization: Bearer AbCdEf123456' use 'AbCdEf123456'.
BEARER_TOKEN = 'AbCdEf123456'

try:
  # Verify valid token, signed by CHAT_ISSUER, intended for a third party.
  request = requests.Request()
  certs_url = PUBLIC_CERT_URL_PREFIX + CHAT_ISSUER
  token = id_token.verify_token(BEARER_TOKEN, request, AUDIENCE, certs_url)

  if token['iss'] != CHAT_ISSUER:
    sys.exit('Invalid issuer')
except:
  sys.exit('Invalid token')

# Token originates from Google and is targeted to a specific client.
print('The token is valid')

Node.js

import fetch from 'node-fetch';
import {OAuth2Client} from 'google-auth-library';

// Bearer Tokens received by apps will always specify this issuer.
const CHAT_ISSUER = 'chat@system.gserviceaccount.com';

// Url to obtain the public certificate for the issuer.
const PUBLIC_CERT_URL_PREFIX =
    'https://www.googleapis.com/service_accounts/v1/metadata/x509/';

// Intended audience of the token, which is the project number of the app.
const AUDIENCE = '1234567890';

// Get this value from the request's Authorization HTTPS header.
// For example, for "Authorization: Bearer AbCdEf123456" use "AbCdEf123456"
const BEARER_TOKEN = 'AbCdEf123456';

const client = new OAuth2Client();

/** Verifies JWT Tokens for Apps in Google Chat. */
async function verify() {
  // Verify valid token, signed by CHAT_ISSUER, intended for a third party.
  try {
    const response = await fetch(PUBLIC_CERT_URL_PREFIX + CHAT_ISSUER);
    const certs = await response.json();
    const ticket = await client.verifySignedJwtWithCertsAsync(
        BEARER_TOKEN, certs, AUDIENCE, [CHAT_ISSUER]);
  } catch (unused) {
    console.error('Invalid token');
    process.exit(1);
  }

  // Token originates from Google and is targeted to a specific client.
  console.log('The token is valid');
}

verify();