앱 ID를 OIDC ID 토큰으로 마이그레이션

Python 2 런타임에서 실행되는 앱은 다른 App Engine 앱으로 요청을 전송할 때 App Engine App Identity API를 사용하여 해당 ID를 어설션할 수 있습니다. 요청을 수신하는 앱은 이 ID를 사용하여 요청을 처리할지 여부를 결정할 수 있습니다.

다른 App Engine 앱으로 요청을 전송할 때 Python 3 앱에서 해당 ID를 어설션해야 할 경우 Google의 OAuth 2.0 API에서 실행되고 디코딩되는 OpenID Connect(OIDC) ID 토큰을 사용할 수 있습니다.

OIDC ID 토큰을 사용한 ID를 어설션하고 확인하는 방법에 대한 개요는 다음과 같습니다.

  1. 'App A'라는 App Engine 앱이 Google Cloud 런타임 환경에서 ID 토큰을 검색합니다.
  2. App A는 또 다른 App Engine 앱인 App B로 요청을 전송하기 직전에 이 토큰을 요청 헤더에 추가합니다.
  3. App B는 Google의 OAuth 2.0 API를 사용하여 토큰 페이로드를 확인합니다. 디코딩된 페이로드에는 App A의 기본 서비스 계정의 이메일 주소 형식으로 확인된 App A의 ID가 포함됩니다.
  4. App B는 페이로드의 ID를 응답이 허용되는 ID 목록과 비교합니다. 허용된 앱에서 온 요청인 경우 App B가 요청을 처리하고 응답합니다.

OAuth 2.0 프로세스

이 가이드에서는 OpenID Connect(OIDC) ID 토큰을 사용하여 ID를 어설션할 수 있도록 다른 App Engine 앱을 업데이트하는 방법과 요청을 처리하기 전 ID 토큰을 사용하여 ID를 확인할 수 있도록 App Engine 앱을 업데이트하는 방법을 설명합니다.

앱 ID와 OIDC API의 주요 차이점

  • Python 2 런타임의 앱은 ID를 명시적으로 어설션하지 않아도 됩니다. 앱이 httplib, urllib, urllib2 Python 라이브러리 또는 App Engine URL Fetch 서비스를 사용하여 아웃바운드 요청을 전송할 때 런타임은 App Engine URL Fetch 서비스를 사용하여 요청을 수행합니다. 요청을 appspot.com 도메인으로 전송하는 경우 URL Fetch는 X-Appengine-Inbound-Appid 헤더를 요청에 추가하여 요청 앱의 ID를 자동으로 어설션합니다. 헤더에는 앱의 애플리케이션 ID(프로젝트 ID라고도 함)가 포함됩니다.

    Python 3 런타임의 앱은 Google Cloud 런타임 환경에서 OIDC ID 토큰을 검색하고 이를 요청 헤더에 추가하여 ID를 명시적으로 어설션해야 합니다. 요청에 OIDC ID 토큰이 포함되도록 다른 App Engine 앱에 요청을 전송하는 모든 코드를 업데이트해야 합니다.

  • 요청의 X-Appengine-Inbound-Appid 헤더에는 요청을 전송한 앱의 프로젝트 ID가 포함됩니다.

    Google의 OIDC ID 토큰의 페이로드는 앱 자체의 프로젝트 ID를 직접 식별하지 않습니다. 대신 토큰은 서비스 계정의 이메일 주소를 제공하여 앱이 실행 중인 서비스 계정을 식별합니다. 토큰 페이로드에서 사용자 이름을 추출하려면 코드를 추가해야 합니다.

    서비스 계정이 프로젝트의 앱 수준 기본 App Engine 서비스 계정인 경우 서비스 계정의 이메일 주소에서 프로젝트 ID를 찾을 수 있습니다. 주소의 사용자 이름 부분은 프로젝트 ID와 동일합니다. 이 경우 수신 앱 코드가 요청을 허용하는 프로젝트 ID 목록에서 이를 찾을 수 있습니다.

    하지만 요청하는 앱이 기본 App Engine 서비스 계정 대신 사용자 관리 서비스 계정을 사용하는 경우 수신 앱은 요청하는 앱의 프로젝트 ID를 정의할 필요가 없는 해당 서비스 계정의 ID만 확인할 수 있습니다. 이 경우 수신 앱은 허용된 프로젝트 ID 목록 대신 허용되는 서비스 계정 이메일 목록을 유지해야 합니다.

  • URL Fetch API 호출의 할당량은 토큰을 부여하기 위한 Google의 OAuth 2.0 API 할당량과 다릅니다. Google Cloud 콘솔 OAuth 동의 화면에서 하루에 부여할 수 있는 최대 토큰 수를 확인할 수 있습니다. URL Fetch, App Identity API, Google OAuth 2.0 API 모두 비용이 발생하지 않습니다.

이전 프로세스 개요

OIDC API를 사용하여 ID를 어설션하고 확인하도록 Python 앱을 마이그레이션하려면 다음 안내를 따르세요.

  1. 다른 App Engine 앱에 요청을 전송할 때 ID를 어설션해야 하는 앱에서 다음을 수행합니다.

    1. Python 3 환경에서 앱이 실행되어 ID 토큰으로 마이그레이션할 때까지 기다립니다.

      Python 2 런타임에서 ID 토큰을 사용할 수 있지만 Python 2에서의 단계는 복잡하며 Python 3 런타임에서 실행되도록 앱을 업데이트할 때까지만 일시적으로 필요합니다.

    2. Python 3에서 앱이 실행된 후에는 ID 토큰을 요청하고 토큰을 요청 헤더에 추가하도록 앱을 업데이트합니다.

  2. 요청을 처리하기 전 ID를 확인해야 하는 앱에서 다음을 수행해야 합니다.

    1. 먼저 ID 토큰과 App Identity API ID를 지원하도록 Python 2 앱을 업그레이드합니다. 이렇게 하면 앱이 App Identity API를 사용하는 Python 2 앱 또는 ID 토큰을 사용하는 Python 3 앱의 요청을 확인하고 처리할 수 있습니다.

    2. 업그레이드한 Python 2 앱이 안정화되면 이를 Python 3 런타임으로 마이그레이션합니다. 이전 앱의 요청을 더 이상 지원할 필요가 없을 때까지 ID 토큰과 App Identity API ID를 모두 계속 지원합니다.

    3. 이전 App Engine 앱의 요청을 더 이상 처리할 필요가 없으면 App Identity API ID를 확인하는 코드를 삭제합니다.

  3. 앱을 테스트한 후 요청을 먼저 처리하는 앱을 배포합니다. 그런 다음 ID를 어설션하기 위해 ID 토큰을 사용하는 업데이트된 Python 3 앱을 배포합니다.

ID 어설션

Python 3 환경에서 앱이 실행될 때까지 대기한 후 다음 단계에 따라 ID 토큰으로 ID를 어설션하도록 앱을 업그레이드합니다.

  1. google-auth 클라이언트 라이브러리를 설치합니다.

  2. Google의 OAuth 2.0 API에서 ID 토큰을 요청하고 요청을 전송하기 전 요청 헤더에 토큰을 추가하도록 코드를 추가합니다.

  3. 업데이트를 테스트합니다.

Python 3 앱용 google-auth 클라이언트 라이브러리 설치

google-auth 클라이언트 라이브러리를 Python3 앱에서 사용할 수 있도록 하려면 app.yaml 파일과 동일한 폴더에 requirements.txt 파일을 만들고 다음 줄을 추가합니다.

     google-auth

앱을 배포하면 App Engine이 requirements.txt 파일에 정의된 모든 종속 항목을 다운로드합니다.

로컬 개발의 경우 venv와 같은 가상 환경에 종속 항목을 설치하는 것이 좋습니다.

ID를 어설션하기 위해 코드 추가

코드를 검색하여 다른 App Engine 앱으로 요청을 전송하는 모든 코드를 찾습니다. 요청을 전송하기 전 다음을 수행하도록 해당 코드를 업데이트합니다.

  1. 다음 가져오기를 추가합니다.

    from google.auth.transport import requests as reqs
    from google.oauth2 import id_token
    
  2. google.oauth2.id_token.fetch_id_token(request, audience)를 사용하여 ID 토큰을 검색합니다. 메서드 호출에 다음 매개변수를 포함합니다.

    • request: 전송할 요청 객체를 전달합니다.
    • audience: 요청을 전송할 앱의 URL을 전달합니다. 그러면 토큰이 요청에 결합되고 다른 앱에서 사용되지 않습니다.

      명확하고 구체적으로 하기 위해 앱에 커스텀 도메인을 사용하는 경우에도 요청을 수신하는 특정 서비스에 대해 App Engine에서 생성된 appspot.com URL을 전달하는 것이 좋습니다.

  3. 요청 객체에서 다음 헤더를 설정합니다.

    'Authorization': 'ID {}'.format(token)
    

예를 들면 다음과 같습니다.

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from flask import Flask, render_template, request
from google.auth.transport import requests as reqs
from google.oauth2 import id_token
import requests

app = Flask(__name__)


@app.route("/", methods=["GET"])
def index():
    return render_template("index.html")


@app.route("/", methods=["POST"])
def make_request():
    url = request.form["url"]
    token = id_token.fetch_id_token(reqs.Request(), url)

    resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})

    message = f"Response when calling {url}:\n\n"
    message += resp.text

    return message, 200, {"Content-type": "text/plain"}

ID를 어설션하기 위해 업데이트 테스트

앱을 로컬에서 실행하고 앱이 ID 토큰을 성공적으로 전송할 수 있는지 테스트하려면 다음 안내를 따르세요.

  1. 이 단계에 따라 기본 App Engine 서비스 계정의 사용자 인증 정보를 로컬 환경에서 사용할 수 있도록 설정합니다. Google OAuth API가 ID 토큰을 생성하려면 이러한 사용자 인증 정보가 필요합니다.

    1. 다음 gcloud 명령어를 입력하여 프로젝트의 기본 App Engine 계정의 서비스 계정 키를 검색합니다.

      gcloud iam service-accounts keys create ~/key.json --iam-account project-ID@appspot.gserviceaccount.com

      project-IDGoogle Cloud 프로젝트의 ID로 바꿉니다.

      이제 서비스 계정 키 파일이 머신에 다운로드됩니다. 원하는 경우 이 파일을 이동하고 이름을 변경할 수 있습니다. 이 파일은 서비스 계정으로 인증하는 데 사용할 수 있으므로 안전하게 저장되어야 합니다. 이 파일을 분실하거나 파일이 승인되지 않은 사용자에게 노출된 경우에는 서비스 계정 키를 삭제하고 새 키를 만듭니다.

    2. 다음 명령어를 입력합니다.

      <code>export GOOGLE_APPLICATION_CREDENTIALS=<var>service-account-key</var></code>
      

    다운로드한 서비스 계정 키가 포함된 파일의 절대 경로 이름으로 service-account-key를 바꿉니다.

  2. GOOGLE_APPLICATION_CREDENTIALS 환경 변수를 내보낸 동일한 셸에서 Python 앱을 시작합니다.

  3. 앱에서 요청을 전송하고 성공하는지 확인합니다. 요청을 수신하고 ID 토큰을 사용하여 ID를 확인할 수 있는 앱이 아직 없으면 다음 안내를 따르세요.

    1. 샘플 '수신' 앱을 다운로드합니다.
    2. 샘플의 main.py 파일에서 Google Cloud 프로젝트의 ID를 allowed_app_ids에 추가합니다. 예를 들면 다음과 같습니다.

       allowed_app_ids = [
          '<APP_ID_1>',
          '<APP_ID_2>',
          'my-project-id'
        ]
      
    3. Python 2 로컬 개발 서버에서 업데이트된 샘플을 실행합니다.

요청 확인 및 처리

요청을 처리하기 전 ID 토큰 또는 App Identity API ID를 사용하도록 Python 2 앱을 업그레이드하려면 다음 안내를 따르세요.

  1. google-auth 클라이언트 라이브러리를 설치합니다.

  2. 다음을 수행하도록 코드를 업데이트합니다.

    1. 요청에 X-Appengine-Inbound-Appid 헤더가 포함된 경우 이 헤더를 사용하여 ID를 확인합니다. Python 2와 같은 이전 런타임에서 실행되는 앱에 이 헤더가 포함됩니다.

    2. 요청에 X-Appengine-Inbound-Appid 헤더가 없으면 OIDC ID 토큰을 확인합니다. 토큰이 있으면 토큰 페이로드 및 발신자의 ID를 확인합니다.

  3. 업데이트를 테스트합니다.

Python 2 앱용 google-auth 클라이언트 라이브러리 설치

Python 2 앱에서 google-auth 클라이언트 라이브러리를 사용할 수 있도록 하려면 다음 안내를 따르세요.

  1. app.yaml 파일과 동일한 폴더에 requirements.txt 파일을 만들고 다음 줄을 추가합니다.

     google-auth==1.19.2
    

    Python 2.7 앱을 지원하므로 Cloud Logging 클라이언트 라이브러리의 1.19.2 버전을 사용하는 것이 좋습니다.

  2. 앱의 app.yaml 파일에서 아직 SSL 라이브러리가 지정되지 않았으면 libraries 섹션에서 지정합니다.

    libraries:
    - name: ssl
      version: latest
    
  3. 디렉터리를 만들어 lib/ 같은 타사 라이브러리를 저장합니다. 그런 다음 pip install을 사용하여 디렉터리에 라이브러리를 설치합니다. 예를 들면 다음과 같습니다.

    pip install -t lib -r requirements.txt
  4. app.yaml 파일과 동일한 폴더에 appengine_config.py 파일을 만듭니다. appengine_config.py 파일에 다음을 추가합니다.

    # appengine_config.py
    import pkg_resources
    from google.appengine.ext import vendor
    
    # Set path to your libraries folder.
    path = 'lib'
    # Add libraries installed in the path folder.
    vendor.add(path)
    # Add libraries to pkg_resources working set to find the distribution.
    pkg_resources.working_set.add_entry(path)

    앞의 예시의 appengine_config.py 파일은 lib 폴더가 현재 작업 디렉터리에 있다고 가정합니다. lib가 항상 현재 작업 디렉터리에 있다고 보장할 수 없으면 lib 폴더에 전체 경로를 지정합니다. 예를 들면 다음과 같습니다.

    import os
    path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'lib')

로컬 개발의 경우 Python 2용 virtualenv와 같은 가상 환경에 종속 항목을 설치하는 것이 좋습니다.

요청 확인을 위한 코드 업데이트

코드를 검색하여 X-Appengine-Inbound-Appid 헤더의 값을 가져오는 모든 코드를 찾습니다. 다음을 수행하도록 해당 코드를 업데이트합니다.

  1. 다음 가져오기를 추가합니다.

    from google.auth.transport import requests as reqs
    from google.oauth2 import id_token
    
  2. 수신 요청에 X-Appengine-Inbound-Appid 헤더가 없으면 Authorization 헤더를 찾고 값을 검색합니다.

    헤더 값은 형식이 'ID: 토큰'으로 지정됩니다.

  3. google.oauth2.id_token.verify_oauth2_token(token, request, audience)를 사용하여 디코딩된 토큰 페이로드를 확인하고 검색합니다. 메서드 호출에 다음 매개변수를 포함합니다.

    • token: 수신 요청에서 추출한 토큰을 전달합니다.
    • request: 새 google.auth.transport.Request 객체를 전달합니다.

    • audience: 현재 앱(확인 요청을 전송하는 앱)의 URL을 전달합니다. Google의 승인 서버는 이 URL을 원래 토큰을 생성할 때 제공된 URL과 비교합니다. URL이 일치하지 않으면 토큰이 확인되지 않고 승인 서버가 오류를 반환합니다.

  4. verify_oauth2_token 메서드는 토큰을 생성한 앱의 기본 서비스 계정의 이메일 주소를 비롯하여 여러 이름/값 쌍이 포함된 디코딩된 토큰 페이로드를 반환합니다.

  5. 토큰 페이로드의 이메일 주소에서 사용자 이름을 추출합니다.

    사용자 이름은 요청을 전송한 앱의 프로젝트 ID와 동일합니다. 이 값은 X-Appengine-Inbound-Appid 헤더에 이전에 반환된 동일한 값입니다.

  6. 허용되는 프로젝트 ID 목록에 사용자 이름/프로젝트 ID가 있으면 요청을 처리합니다.

예를 들면 다음과 같습니다.

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Authenticate requests coming from other App Engine instances.
"""

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

import logging
import webapp2


def get_app_id(request):
    # Requests from App Engine Standard for Python 2.7 will include a
    # trustworthy X-Appengine-Inbound-Appid. Other requests won't have
    # that header, as the App Engine runtime will strip it out
    incoming_app_id = request.headers.get("X-Appengine-Inbound-Appid", None)
    if incoming_app_id is not None:
        return incoming_app_id

    # Other App Engine apps can get an ID token for the App Engine default
    # service account, which will identify the application ID. They will
    # have to include at token in an Authorization header to be recognized
    # by this method.
    auth_header = request.headers.get("Authorization", None)
    if auth_header is None:
        return None

    # The auth_header must be in the form Authorization: Bearer token.
    bearer, token = auth_header.split()
    if bearer.lower() != "bearer":
        return None

    try:
        info = id_token.verify_oauth2_token(token, requests.Request())
        service_account_email = info["email"]
        incoming_app_id, domain = service_account_email.split("@")
        if domain != "appspot.gserviceaccount.com":  # Not App Engine svc acct
            return None
        else:
            return incoming_app_id
    except Exception as e:
        # report or log if desired, as here:
        logging.warning("Request has bad OAuth2 id token: {}".format(e))
        return None


class MainPage(webapp2.RequestHandler):
    allowed_app_ids = ["other-app-id", "other-app-id-2"]

    def get(self):
        incoming_app_id = get_app_id(self.request)

        if incoming_app_id is None:
            self.abort(403)

        if incoming_app_id not in self.allowed_app_ids:
            self.abort(403)

        self.response.write("This is a protected page.")


app = webapp2.WSGIApplication([("/", MainPage)], debug=True)

ID 확인을 위한 업데이트 테스트

앱이 ID 토큰 또는 X-Appengine-Inbound-Appid 헤더를 사용하여 요청을 확인할 수 있는지 테스트하려면 Python 2 로컬 개발 서버에서 앱을 실행하고 Python 2 앱(App Identity API 사용) 및 Python 3 앱(ID 토큰 전송)에서 요청을 전송합니다.

ID 토큰을 전송하도록 앱을 업데이트하지 않은 경우 다음 안내를 따르세요.

  1. 샘플 '요청' 앱을 다운로드합니다.

  2. 앱 어설션을 위한 업데이트 테스트에 설명된 대로 로컬 환경에 서비스 계정 사용자 인증 정보를 추가합니다.

  3. 표준 Python 3 명령어를 사용하여 Python 3 샘플 앱을 시작합니다.

  4. 샘플 앱에서 요청을 전송하고 성공하는지 확인합니다.

앱 배포

앱을 배포할 준비가 되었으면 다음을 수행해야 합니다.

  1. App Engine에서 앱을 테스트합니다.

  2. 오류 없이 앱이 실행되면 트래픽 분할을 사용하여 업데이트된 앱의 트래픽을 천천히 늘립니다. 앱을 면밀히 모니터링하여 문제가 없는 것을 확인한 후 더 많은 트래픽을 업데이트된 앱으로 라우팅합니다.

ID를 어설션하기 위해 다른 서비스 계정 사용

ID 토큰을 요청하면 요청에 기본적으로 App Engine 기본 서비스 계정의 ID가 사용됩니다. 토큰을 확인할 때 토큰 페이로드에는 앱의 프로젝트 ID에 매핑되는 기본 서비스 계정의 이메일 주소가 포함됩니다.

App Engine 기본 서비스 계정에는 기본적으로 매우 높은 수준의 권한이 포함됩니다. 이 계정은 전체 Google Cloud 프로젝트를 보고 수정할 수 있으므로, 대부분의 경우 앱에 클라우드 서비스 인증이 필요할 때 사용하기에 적합하지 않습니다.

하지만 사용자는 요청을 전송한 앱의 ID를 확인하기 위해 ID 토큰을 사용하는 유일한 사용자이기 때문에 앱 ID를 어설션할 때 기본 서비스 계정을 사용해도 안전합니다. 서비스 계정에 부여된 실제 권한은 이 프로세스 중에 고려되거나 필요하지 않습니다.

그래도 ID 토큰 요청에 대해 다른 서비스 계정을 선호하는 경우 다음을 수행합니다.

  1. GOOGLE_APPLICATION_CREDENTIALS라는 환경 변수를 서비스 계정의 사용자 인증 정보가 포함된 JSON 파일의 경로로 설정합니다. 이러한 사용자 인증 정보의 안전한 보관에 대한 권장사항을 참조하세요.

  2. google.oauth2.id_token.fetch_id_token(request, audience)를 사용하여 ID 토큰을 검색합니다.

  3. 이 토큰을 확인할 때 토큰 페이로드에는 새 서비스 계정의 이메일 주소가 포함됩니다.