將 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 Engine 應用程式 B 之前,將這個權杖新增至要求標頭。
  3. 應用程式 B 會使用 Google 的 OAuth 2.0 API 驗證權杖酬載。解碼後的酬載包含應用程式 A 的已驗證身分,也就是應用程式 A 預設服務帳戶的電子郵件地址。
  4. 應用程式 B 會將酬載中的身分與允許回應的身分清單進行比較。如果要求來自允許的應用程式,應用程式 B 會處理要求並做出回應。

OAuth 2.0 程序

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

App Identity API 與 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 清單中,查詢這個 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 身分識別。這樣一來,應用程式就能驗證及處理來自 Python 2 應用程式 (使用 App Identity API) 或 Python 3 應用程式 (使用 ID 權杖) 的要求。

    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 用戶端程式庫

如要讓 Python3 應用程式使用 google-auth 用戶端程式庫,請在與 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 替換為專案 ID。Google Cloud

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

    2. 輸入下列指令:

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

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

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

  3. 從應用程式傳送要求,並確認要求成功。如果您沒有可接收要求並使用 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 本機開發伺服器中執行更新後的範例。

驗證及處理要求

如要升級 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-auth 用戶端程式庫

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

  1. 在與 app.yaml 檔案相同的資料夾中建立 requirements.txt 檔案,並新增下列程式碼:

     google-auth==1.19.2
    

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

  2. 在應用程式的 app.yaml 檔案中,於 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 本機開發伺服器中執行應用程式,並從 Python 2 應用程式 (會使用 App Identity API) 和傳送 ID 權杖的 Python 3 應用程式傳送要求。

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

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

  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. 驗證這個權杖時,權杖酬載會包含新服務帳戶的電子郵件地址。