Google Chat의 요청 확인하기

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

상호작용 이벤트를 Chat 앱의 엔드포인트로 디스패치하기 위해 Google은 서비스에 HTTPS 요청을 보냅니다. 요청이 Google에서 전송되었는지 확인하기 위해 Chat은 모든 HTTPS 요청의 Authorization 헤더 (및 요청 본문의 authorizationEventObject.systemIdToken 필드)에 Google 서명 OpenID Connect (OIDC) ID 토큰을 Bearer 토큰으로 포함합니다. 예를 들면 다음과 같습니다.

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

위 예의 AbCdEf123456 문자열은 베어러 승인 토큰입니다. 이 암호화 토큰은 채팅 앱의 프로젝트별 고유 서비스 계정(service-<var>PROJECT_NUMBER</var>@gcp-sa-gsuiteaddons.iam.gserviceaccount.com)으로 서명되며 audience 필드는 채팅 앱 구성 시 채팅 앱에 구성된 HTTP 엔드포인트 URL로 설정됩니다.

Google Cloud 콘솔의 Chat API 구성 탭에 있는 연결 설정 섹션에서 Chat 앱의 서비스 계정 이메일 주소를 복사할 수 있습니다.

  1. Google Cloud 콘솔에서 메뉴 > API 및 서비스 > 사용 설정된 API 및 서비스 > Google Chat API > 구성으로 이동합니다.

    Google Chat API 구성으로 이동

  2. 대화형 기능 > 연결 설정에서 서비스 계정 이메일 (service-<var>PROJECT_NUMBER</var>@gcp-sa-gsuiteaddons.iam.gserviceaccount.com)을 복사합니다.

Cloud Run 함수 또는 Cloud Run을 사용하여 Chat 앱을 구현한 경우 Chat 앱의 서비스 계정에 Cloud Run Invoker (roles/run.invoker) 역할을 부여하면 Cloud IAM에서 토큰 확인을 자동으로 처리합니다. 앱에서 자체 HTTP 서버를 구현하는 경우 오픈소스 Google API 클라이언트 라이브러리를 사용하여 Bearer 토큰을 확인할 수 있습니다.

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

Cloud Run Functions를 사용하여 요청 인증

Cloud Run Functions 또는 Cloud Run을 사용하여 함수 로직을 구현하는 경우 Chat 앱의 연결 설정에 있는 트리거에서 구성된 HTTP 엔드포인트 URL이 Cloud Run 함수 엔드포인트의 URL과 일치하는지 확인합니다.

그런 다음 다음 단계를 따라 Chat 앱의 서비스 계정(service-<var>PROJECT_NUMBER</var>@gcp-sa-gsuiteaddons.iam.gserviceaccount.com, Chat API 구성 탭의 연결 설정 섹션에서 복사됨)을 호출자로 승인합니다.

콘솔

Google Cloud에 함수 또는 서비스를 배포한 후 다음 단계를 따르세요.

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

    Cloud Run으로 이동

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

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

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

  5. 새 주 구성원 필드에 Chat 앱의 서비스 계정 이메일 주소(service-<var>PROJECT_NUMBER</var>@gcp-sa-gsuiteaddons.iam.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:service-PROJECT_NUMBER@gcp-sa-gsuiteaddons.iam.gserviceaccount.com'

다음을 바꿉니다.

  • RECEIVING_FUNCTION: Chat 앱의 함수 이름입니다.
  • PROJECT_NUMBER: Chat 앱의 서비스 계정 이메일 주소에 있는 프로젝트 번호입니다.

ID 토큰으로 HTTP 요청 인증

HTTP 엔드포인트의 경우 요청의 베어러 승인 토큰은 Google 서명 OpenID Connect (OIDC) ID 토큰입니다. email 필드는 Chat 앱의 서비스 계정 이메일 주소(service-<var>PROJECT_NUMBER</var>@gcp-sa-gsuiteaddons.iam.gserviceaccount.com)로 설정되고 audience 필드는 요청을 수신하도록 구성된 HTTP 엔드포인트 URL로 설정됩니다. 예를 들어 Chat 앱의 구성된 엔드포인트가 https://example.com/app/인 경우 ID 토큰의 audience 필드는 https://example.com/app/입니다.

HTTP 엔드포인트가 IAM 기반 인증을 지원하는 서비스 (예: Cloud Run)에서 호스팅되지 않는 경우 이 인증 방법을 사용하는 것이 좋습니다.

다음 샘플은 Google OAuth 클라이언트 라이브러리를 사용하여 Google에서 채팅 앱에 대해 발급하고 앱의 엔드포인트를 타겟팅한 Bearer 토큰을 확인하는 방법을 보여줍니다.

자바

java/chat/secured-app/src/main/java/com/google/chat/app/secured/App.java
/**
 * Determine whether a Google Workspace add-on request is legitimate.
 * 
 * @param event Event sent from Google Workspace add-on
 * @param authorization Authorization header from the request
 * @return {boolean} Whether the request is legitimate
 */
private boolean verifyAddOnRequest(JsonNode event, String authorization) throws Exception {
  JsonFactory factory = JacksonFactory.getDefaultInstance();

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

  String bearer = authorization.substring("Bearer ".length(), authorization.length());
  GoogleIdToken idToken = GoogleIdToken.parse(factory, bearer);
  return idToken != null
    && verifier.verify(idToken)
    && idToken.getPayload().getEmailVerified()
    && idToken.getPayload().getEmail().equals(SERVICE_ACCOUNT_EMAIL);
}

Python

python/chat/secured-app/main.py
def verifyAddOnRequest() -> bool:
  """Determine whether a Google Workspace add-on request is legitimate.

  Args:
    request: Request sent from Google Workspace add-on

  Returns:
    Whether the request is legitimate
  """
  try:
    bearer = request.headers.get('Authorization')[len("Bearer "):]
    token = id_token.verify_oauth2_token(bearer, requests.Request(), HTTP_ENDPOINT)
    return token['email'] == SERVICE_ACCOUNT_EMAIL

  except:
    return False

Node.js

node/chat/secured-app/index.js
/**
 * Determine whether a Google Workspace add-on request is legitimate.
 * 
 * @param {Object} req Request sent from Google Workspace add-on
 * @return {boolean} Whether the request is legitimate
 */
async function verifyAddOnRequest(req) {
  try {
    const authorization = req.headers.authorization;
    const idToken = authorization.substring('Bearer '.length, authorization.length);
    const ticket = await new OAuth2Client().verifyIdToken({idToken, audience: HTTP_ENDPOINT});
    return ticket.getPayload().email_verified
        && ticket.getPayload().email === SERVICE_ACCOUNT_EMAIL;
  } catch (unused) {
    return false;
  }
}

부가기능이 아닌 Chat 앱: Google Chat의 요청 확인

다음 문서는 Google Workspace 부가기능이 아닌 Chat 앱에 적용됩니다. 부가기능이 아닌 Chat 앱을 이전하려면 Google Chat 앱을 Google Workspace 부가기능으로 변환하기를 참고하세요.

연결 설정에서 HTTP 엔드포인트 URL로 구성된 부가기능이 아닌 Chat 앱의 경우, Bearer 토큰의 유형과 audience 필드의 값은 Chat 앱을 구성할 때 선택한 인증 대상의 유형에 따라 달라지며 요청은 공유 서비스 계정 chat@system.gserviceaccount.com로 서명됩니다.

Cloud Run Functions를 사용하여 요청 인증 (부가기능이 아닌 Chat 앱)

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

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

콘솔

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을 Chat 앱의 함수 이름으로 바꿉니다.

ID 토큰으로 HTTP 요청 인증 (부가기능이 아닌 Chat 앱)

부가기능이 아닌 Chat 앱의 연결 설정의 인증 대상 필드가 HTTP 엔드포인트 URL로 설정된 경우 요청의 베어러 인증 토큰은 Google 서명 OpenID Connect(OIDC) ID 토큰입니다. email 필드는 chat@system.gserviceaccount.com로 설정됩니다. 인증 대상 필드는 Google Chat이 부가기능이 아닌 Chat 앱에 요청을 보내도록 구성한 URL로 설정됩니다. 예를 들어 Chat 앱의 구성된 엔드포인트가 https://example.com/app/인 경우 ID 토큰의 인증 대상 필드는 https://example.com/app/입니다.

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

자바

java/basic-app/src/main/java/com/google/chat/app/basic/App.java
String CHAT_ISSUER = "chat@system.gserviceaccount.com";
JsonFactory factory = JacksonFactory.getDefaultInstance();

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

GoogleIdToken idToken = GoogleIdToken.parse(factory, bearer);
return idToken != null
    && verifier.verify(idToken)
    && idToken.getPayload().getEmailVerified()
    && idToken.getPayload().getEmail().equals(CHAT_ISSUER);

Python

python/basic-app/main.py
# Bearer Tokens received by apps will always specify this issuer.
CHAT_ISSUER = 'chat@system.gserviceaccount.com'

try:
    # Verify valid token, signed by CHAT_ISSUER, intended for a third party.
    request = requests.Request()
    token = id_token.verify_oauth2_token(bearer, request, AUDIENCE)
    return token['email'] == CHAT_ISSUER

except:
    return False

Node.js

node/basic-app/index.js
// Bearer Tokens received by apps will always specify this issuer.
const chatIssuer = 'chat@system.gserviceaccount.com';

// Verify valid token, signed by chatIssuer, intended for a third party.
try {
  const ticket = await client.verifyIdToken({
    idToken: bearer,
    audience: audience
  });
  return ticket.getPayload().email_verified
      && ticket.getPayload().email === chatIssuer;
} catch (unused) {
  return false;
}

프로젝트 번호 JWT로 요청 인증 (부가기능이 아닌 채팅 앱)

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

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

자바

java/basic-app/src/main/java/com/google/chat/app/basic/App.java
String CHAT_ISSUER = "chat@system.gserviceaccount.com";
JsonFactory factory = JacksonFactory.getDefaultInstance();

GooglePublicKeysManager keyManagerBuilder =
    new GooglePublicKeysManager.Builder(new ApacheHttpTransport(), factory)
        .setPublicCertsEncodedUrl(
            "https://www.googleapis.com/service_accounts/v1/metadata/x509/" + CHAT_ISSUER)
        .build();

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

GoogleIdToken idToken = GoogleIdToken.parse(factory, bearer);
return idToken != null
    && verifier.verify(idToken)
    && idToken.verifyAudience(Collections.singletonList(AUDIENCE))
    && idToken.verifyIssuer(CHAT_ISSUER);

Python

python/basic-app/main.py
# Bearer Tokens received by apps will always specify this issuer.
CHAT_ISSUER = 'chat@system.gserviceaccount.com'

try:
    # Verify valid token, signed by CHAT_ISSUER, intended for a third party.
    request = requests.Request()
    certs_url = 'https://www.googleapis.com/service_accounts/v1/metadata/x509/' + CHAT_ISSUER
    token = id_token.verify_token(bearer, request, AUDIENCE, certs_url)
    return token['iss'] == CHAT_ISSUER

except:
    return False

Node.js

node/basic-app/index.js
// Bearer Tokens received by apps will always specify this issuer.
const chatIssuer = 'chat@system.gserviceaccount.com';

// Verify valid token, signed by CHAT_ISSUER, intended for a third party.
try {
  const response = await fetch('https://www.googleapis.com/service_accounts/v1/metadata/x509/' + chatIssuer);
  const certs = await response.json();
  await client.verifySignedJwtWithCertsAsync(
    bearer, certs, audience, [chatIssuer]);
  return true;
} catch (unused) {
  return false;
}