將 App Identity 遷移至 OIDC ID 權杖

在 Python 2 執行階段中執行的應用程式向其他 App Engine 應用程式傳送要求時,可以使用 App Engine App Identity API 宣告自己的身分。接收要求的應用程式可以使用這個身分,判斷是否應處理要求。

如果 Python 3 應用程式在傳送要求至其他 App Engine 應用程式時,需要宣告自己的身分,您可以使用由 Google OAuth 2.0 API 核發及解碼的 OpenID Connect (OIDC) ID 權杖。

以下簡要說明如何使用 OIDC ID 權杖來斷言及驗證身分:

  1. 名為「App A」的 App Engine 應用程式會從 Google Cloud 執行階段環境擷取 ID 權杖。
  2. 應用程式 A 會在將要求傳送至 App B (另一個 App Engine 應用程式) 之前,將這個符記新增至要求標頭。
  3. App B 會使用 Google 的 OAuth 2.0 API 驗證權杖酬載。經解碼的酬載包含已驗證的 App A 身分,其形式為 App A 預設服務帳戶的電子郵件地址。
  4. App B 會將酬載中的身分與可回應的身分清單進行比較。如果要求來自已核准的應用程式,則應用程式 B 會處理要求並回應。

OAuth 2.0 程序

本指南說明如何更新 App Engine 應用程式,以便使用 OpenID Connect (OIDC) ID 權杖來宣告身分,並更新其他 App Engine 應用程式,以便在處理要求前使用 ID 權杖驗證身分。

App Identity 和 OIDC API 之間的主要差異

  • Python 2 執行階段中的應用程式不需要明確宣告身分。當應用程式使用 httpliburlliburllib2 Python 程式庫,或 App Engine 網址擷取服務來傳送外連要求時,執行階段會使用 App Engine 網址擷取服務提出要求。如果要求傳送至 appspot.com 網域,網址擷取服務會在要求中加入 X-Appengine-Inbound-Appid 標頭,自動宣告要求應用程式的身分。該標頭包含應用程式 ID (也稱為專案 ID)。

    Python 3 執行階段中的應用程式確實需要明確斷言身分,方法是從 Google Cloud 執行階段環境擷取 OIDC ID 權杖,然後將其新增至要求標頭。您必須更新所有傳送要求至其他 App Engine 應用程式的程式碼,讓要求包含 OIDC ID 權杖。

  • 要求中的 X-Appengine-Inbound-Appid 標頭包含傳送要求的應用程式專案 ID。

    Google 的 OIDC ID 權杖酬載不會直接識別應用程式本身的專案 ID。相反地,權杖會提供服務帳戶的電子郵件地址,藉此識別應用程式執行的服務帳戶。您需要新增一些程式碼,才能從權杖酬載中擷取使用者名稱。

    如果該服務帳戶是專案的應用程式層級預設 App Engine 服務帳戶,您可以在服務帳戶的電子郵件地址中找到專案 ID。地址中的使用者名稱部分與專案 ID 相同。在這種情況下,接收應用程式程式碼可以在允許要求的專案 ID 清單中查詢這項資訊。

    不過,如果要求應用程式使用使用者管理的服務帳戶,而非預設的 App Engine 服務帳戶,則接收應用程式只能驗證該服務帳戶的身份,而不會定義要求應用程式的專案 ID。在這種情況下,接收應用程式必須維護允許的服務帳戶電子郵件地址清單,而非允許的專案 ID 清單。

  • URL Fetch API 呼叫的配額與 Google 授予憑證的 OAuth 2.0 API 配額不同。您可以在 Google Cloud 主控台的 OAuth 同意畫面中,查看每天可授予的符記數量上限。無論是 URL Fetch、App Identity API 還是 Google 的 OAuth 2.0 API,都不會產生帳單費用。

轉換程序總覽

如要將 Python 應用程式遷移至使用 OIDC API 來斷言及驗證身分,請按照下列步驟操作:

  1. 在需要向其他 App Engine 應用程式傳送要求時,應用程式必須宣告身分:

    1. 請等到應用程式在 Python 3 環境中執行,再遷移至 ID 權杖。

      雖然可以在 Python 2 執行階段使用 ID 權杖,但 Python 2 中的步驟相當複雜,且只在您更新應用程式以便在 Python 3 執行階段中執行時才需要使用。

    2. 應用程式在 Python 3 中執行後,請更新應用程式,以便要求 ID 權杖,並將權杖新增至要求標頭。

  2. 在需要先驗證身分才能處理要求的應用程式中:

    1. 首先,請升級 Python 2 應用程式,同時支援 ID 權杖和 App Identity API 身分。這樣一來,應用程式就能驗證並處理來自使用 App Identity API 的 Python 2 應用程式,或使用 ID 權杖的 Python 3 應用程式的要求。

    2. 升級後的 Python 2 應用程式穩定後,請將其遷移至 Python 3 執行階段。請繼續支援 ID 權杖和 App Identity API 身分,直到您確定應用程式不再需要支援舊版應用程式的要求為止。

    3. 當您不再需要處理舊版 App Engine 應用程式的要求時,請移除用於驗證 App Identity API 身分的程式碼。

  3. 測試應用程式後,請先部署處理要求的應用程式。接著,部署更新後的 Python 3 應用程式,該應用程式會使用 ID 權杖來斷言身分。

聲明身分

等待應用程式在 Python 3 環境中執行,然後按照下列步驟升級應用程式,以便使用 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 等虛擬環境中安裝依附元件。

新增程式碼來斷言身分

搜尋程式碼,找出所有向其他 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:傳遞您要傳送要求的應用程式網址。這麼做可將符記繫結至要求,並防止其他應用程式使用符記。

      為求清楚明確,即使您為應用程式使用自訂網域,我們仍建議您傳遞 App Engine 為接收要求的特定服務建立的 appspot.com 網址。

  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 權杖,請按照下列步驟操作:

  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-ID 替換為 Google Cloud 專案的 ID。

      服務帳戶金鑰檔案現在會下載到您的電腦。您可以任意移動及重新命名此檔案。請務必妥善保存此檔案,因為此檔案可當做服務帳戶進行驗證。如果您遺失檔案,或檔案遭到未經授權的使用者存取,請刪除服務帳戶金鑰,並建立新的金鑰

    2. 輸入下列指令:

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

    service-account-key 替換為含有您下載的服務帳戶金鑰的檔案絕對路徑。

  2. 在匯出 GOOGLE_APPLICATION_CREDENTIALS 環境變數的相同 Shell 中,啟動 Python 應用程式

  3. 從應用程式傳送要求,並確認要求成功。如果您尚未擁有可接收要求並使用 ID 權杖驗證身分的應用程式,請按照下列步驟操作:

    1. 下載「incoming」應用程式範例
    2. 在範例的 main.py 檔案中,將 Google Cloud 專案的 ID 新增至 allowed_app_ids。例如:

       allowed_app_ids = [
          '<APP_ID_1>',
          '<APP_ID_2>',
          'my-project-id'
        ]
      
    3. 在 Python 2 本機開發伺服器中執行更新後的範例。

驗證及處理要求

如要將 Python 2 應用程式升級為在處理要求前使用 ID 權杖或 App Identity API 身分,請按照下列步驟操作:

  1. 安裝 google-auth 用戶端程式庫。

  2. 更新程式碼,執行下列操作:

    1. 如果要求包含 X-Appengine-Inbound-Appid 標頭,請使用該標頭驗證身分。在 Python 2 等舊版執行階段中執行的應用程式會包含這個標頭。

    2. 如果要求不包含 X-Appengine-Inbound-Appid 標頭,請檢查 OIDC ID 權杖。如果有符記,請驗證符記酬載並檢查傳送者的身分

  3. 測試更新內容。

為 Python 2 應用程式安裝 Google 驗證用戶端程式庫

如要讓 google-auth 用戶端程式庫可供 Python 2 應用程式使用,請按照下列步驟操作:

  1. app.yaml 檔案所在的資料夾中建立 requirements.txt 檔案,然後加入以下行:

     google-auth==1.19.2
    

    建議您使用 1.19.2 版的 Cloud Logging 用戶端程式庫,因為它支援 Python 2.7 應用程式。

  2. 在應用程式的 app.yaml 檔案中,如果尚未指定 SSL 程式庫,請在 libraries 區段中指定 SSL 程式庫:

    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: token」。

  3. 使用 google.oauth2.id_token.verify_oauth2_token(token, request, audience) 驗證及擷取已解碼的權杖酬載。請在方法呼叫中加入下列參數:

    • token:傳遞從傳入要求中擷取的權杖。
    • request:傳遞新的 google.auth.transport.Request 物件。

    • audience:傳遞目前應用程式 (傳送驗證要求的應用程式) 的網址。Google 授權伺服器會將這個網址與權杖最初產生時提供的網址進行比較。如果網址不相符,系統就不會驗證權杖,授權伺服器也會傳回錯誤。

  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.
"""

import logging

from google.auth.transport import requests
from google.oauth2 import id_token
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 權杖或 X-Appengine-Inbound-Appid 標頭來驗證要求,請在 Python 2 本機開發伺服器中執行應用程式,並從使用 App Identity API 的 Python 2 應用程式和傳送 ID 權杖的 Python 3 應用程式傳送要求。

如果您尚未更新應用程式以傳送 ID 權杖:

  1. 下載「requesting」應用程式範例

  2. 測試應用程式斷言更新一文所述,將服務帳戶憑證新增至本地環境。

  3. 使用標準 Python 3 指令啟動 Python 3 範例應用程式

  4. 從範例應用程式傳送要求,並確認要求成功。

部署應用程式

準備好部署應用程式後,請執行下列操作:

  1. 在 App Engine 上測試應用程式

  2. 如果應用程式執行時沒有發生錯誤,請使用流量分配功能,逐步增加更新後應用程式的流量。請密切監控應用程式是否有任何問題,再將更多流量導向更新版應用程式。

使用其他服務帳戶宣告身分

要求 ID 權杖時,要求預設會使用 App Engine 預設服務帳戶的身分。驗證權杖時,權杖酬載會包含預設服務帳戶的電子郵件地址,該電子郵件地址會對應至應用程式的專案 ID。

根據預設,App Engine 預設服務帳戶的權限非常高。這個帳戶可以查看及編輯整個Google Cloud 專案,因此在大多數情況下,如果應用程式需要透過 Cloud 服務進行驗證,就不適合使用這個帳戶。

不過,在宣告應用程式身分時,使用預設服務帳戶是安全的,因為您只會使用 ID 權杖來驗證傳送要求的應用程式身分。這個程序不會考量或需要服務帳戶的實際權限

如果您仍想使用其他服務帳戶提出 ID 權杖要求,請執行下列操作:

  1. 將名為 GOOGLE_APPLICATION_CREDENTIALS 的環境變數設為包含服務帳戶憑證的 JSON 檔案路徑。請參閱安全儲存這些憑證的建議。

  2. 請使用 google.oauth2.id_token.fetch_id_token(request, audience) 擷取 ID 權杖。

  3. 驗證此權杖時,權杖酬載會包含新服務帳戶的電子郵件地址。