推送訂閱的驗證

如果推送訂閱項目使用驗證,Pub/Sub 服務會簽署 JSON Web Token (JWT),並在推送要求的授權標頭中傳送 JWT。JWT 包含憑證附加資訊和簽章。

訂閱者可以驗證 JWT,並確認下列事項:

  • 聲明內容正確無誤。
  • Pub/Sub 服務已簽署聲明。

如果訂閱者使用防火牆,就無法接收推送要求。如要接收推送要求,請關閉防火牆並驗證 JWT。如果訂閱者有防火牆,您可能會收到 403 permission denied 錯誤訊息。

事前準備

JWT 格式

JWT 是由標頭、聲明集和簽章組成的 OpenIDConnect JWT。Pub/Sub 服務會將 JWT 編碼為以英文句點分隔的 base64 字串。

舉例來說,下列授權標頭包含經過編碼的 JWT:

"Authorization" : "Bearer
eyJhbGciOiJSUzI1NiIsImtpZCI6IjdkNjgwZDhjNzBkNDRlOTQ3MTMzY2JkNDk5ZWJjMWE2MWMzZDVh
YmMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXpwIjoiMTEzNzc0M
jY0NDYzMDM4MzIxOTY0IiwiZW1haWwiOiJnYWUtZ2NwQGFwcHNwb3QuZ3NlcnZpY2VhY2NvdW50LmNvb
SIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NTAxODU5MzUsImlhdCI6MTU1MDE4MjMzNSwia
XNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTEzNzc0MjY0NDYzMDM4MzIxO
TY0In0.QVjyqpmadTyDZmlX2u3jWd1kJ68YkdwsRZDo-QxSPbxjug4ucLBwAs2QePrcgZ6hhkvdc4UHY
4YF3fz9g7XHULNVIzX5xh02qXEH8dK6PgGndIWcZQzjSYfgO-q-R2oo2hNM5HBBsQN4ARtGK_acG-NGG
WM3CQfahbEjZPAJe_B8M7HfIu_G5jOLZCw2EUcGo8BvEwGcLWB2WqEgRM0-xt5-UPzoa3-FpSPG7DHk7
z9zRUeq6eB__ldb-2o4RciJmjVwHgnYqn3VvlX9oVKEgXpNFhKuYA-mWh5o7BCwhujSMmFoBOh6mbIXF
cyf5UiVqKjpqEbqPGo_AvKvIQ9VTQ" 

標頭和憑證附加資訊集都是 JSON 字串。經過解碼後,會呈現以下這樣的格式:

{"alg":"RS256","kid":"7d680d8c70d44e947133cbd499ebc1a61c3d5abc","typ":"JWT"}

{
   "aud":"https://example.com",
   "azp":"113774264463038321964",
   "email":"gae-gcp@appspot.gserviceaccount.com",
   "sub":"113774264463038321964",
   "email_verified":true,
   "exp":1550185935,
   "iat":1550182335,
   "iss":"https://accounts.google.com"
  }

傳送至推送端點的要求所附加的權杖,最多可能已過期一小時。

設定 Pub/Sub 以進行推送驗證

以下範例說明如何將推送授權服務帳戶設為您選擇的服務帳戶,以及如何將 iam.serviceAccountTokenCreator 角色授予service-{PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com 服務代理程式

控制台

  1. 前往「Pub/Sub Subscriptions」(Pub/Sub 訂閱項目) 頁面。

    前往「訂閱」頁面

  2. 按一下「Create Subscription」 (建立訂閱項目)

  3. 在「Subscription ID」(訂閱 ID) 欄位中輸入名稱。

  4. 選取主題。

  5. 在「傳送類型」選取「推送」

  6. 輸入端點網址。

  7. 勾選「啟用驗證」

  8. 選取服務帳戶。

  9. 確認服務代理程式 service-{PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com 具備專案IAM 資訊主頁iam.serviceAccountTokenCreator 角色。如果服務帳戶尚未獲得該角色,請在 IAM 資訊主頁中按一下「授權」Grant

  10. 選用:輸入目標對象。

  11. 點選「建立」

gcloud

# Configure the push subscription
gcloud pubsub subscriptions (create|update|modify-push-config) ${SUBSCRIPTION} \
 --topic=${TOPIC} \
 --push-endpoint=${PUSH_ENDPOINT_URI} \
 --push-auth-service-account=${SERVICE_ACCOUNT_EMAIL} \
 --push-auth-token-audience=${OPTIONAL_AUDIENCE_OVERRIDE}

# Your service agent
# `service-{PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com` needs to have the
# `iam.serviceAccountTokenCreator` role.
PUBSUB_SERVICE_ACCOUNT="service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
 --member="serviceAccount:${PUBSUB_SERVICE_ACCOUNT}"\
 --role='roles/iam.serviceAccountTokenCreator'

為推送訂閱項目啟用驗證時,您可能會遇到 permission deniednot authorized 錯誤。 如要解決這個問題,請授予啟動訂閱項目建立或更新作業的主體服務帳戶 iam.serviceAccounts.actAs 權限。詳情請參閱「建立推送訂閱」一文中的「驗證」。

如果您使用已驗證的推送訂閱項目,搭配以 Identity-Aware Proxy 保護的 App Engine 應用程式,則必須提供 IAP 用戶端 ID 做為推送驗證權杖目標對象。如要在 App Engine 應用程式上啟用 IAP,請參閱「啟用 IAP」。如要尋找 IAP 用戶端 ID,請在「憑證」IAP-App-Engine-app頁面中尋找「用戶端 ID」。

版權聲明

JWT 可用來驗證憑證附加資訊 (包括 emailaud 憑證附加資訊) 是否由 Google 簽署。如要進一步瞭解如何使用 Google 的 OAuth 2.0 API 進行驗證和授權作業,請參閱 OpenID Connect 一文。

有兩種機制可讓這些憑證附加資訊更具實用價值。首先,Pub/Sub 會要求進行 CreateSubscription、UpdateSubscription 或 ModifyPushConfig 呼叫的使用者或服務帳戶,皆需擁有推送授權服務帳戶的 iam.serviceAccounts.actAs 權限。例如 roles/iam.serviceAccountUser 角色。

其次,系統會嚴格控管用來簽署符記的憑證存取權。如要建立符記,Pub/Sub 必須使用不同的簽署服務帳戶身分 (即服務代理程式 service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com) 來呼叫內部 Google 服務。簽署服務帳戶必須具備 iam.serviceAccounts.getOpenIdToken 權限,或在推送授權服務帳戶 (或推送授權服務帳戶的任何上層資源,例如專案) 中擁有「服務帳戶權杖建立者」角色 (roles/iam.serviceAccountTokenCreator)。

驗證權杖

驗證 Pub/Sub 傳送至推送端點的權杖時,請採取下列步驟:

  • 使用簽名驗證功能檢查權杖完整性。
  • 確認權杖中的 emailaudience 權杖附加資訊與推送訂閱項目設定中設定的值相符。

以下範例說明如何驗證向未受 Identity-Aware Proxy 保護的 App Engine 應用程式提出的推送要求。如果您的 App Engine 應用程式受到 IAP 保護,含有 IAP JWT 的 HTTP 要求標頭為 x-goog-iap-jwt-assertion,且必須經過驗證

通訊協定

請求:

GET https://oauth2.googleapis.com/tokeninfo?id_token={BEARER_TOKEN}

回應:

200 OK
{
    "alg": "RS256",
    "aud": "example.com",
    "azp": "104176025330667568672",
    "email": "{SERVICE_ACCOUNT_NAME}@{YOUR_PROJECT_NAME}.iam.gserviceaccount.com",
    "email_verified": "true",
    "exp": "1555463097",
    "iat": "1555459497",
    "iss": "https://accounts.google.com",
    "kid": "3782d3f0bc89008d9d2c01730f765cfb19d3b70e",
    "sub": "104176025330667568672",
    "typ": "JWT"
}

C#

在嘗試這個範例之前,請先按照快速入門:使用用戶端程式庫中的 C# 設定操作說明進行操作。詳情請參閱 Pub/Sub C# API 參考文件

/// <summary>
/// Extended JWT payload to match the pubsub payload format.
/// </summary>
public class PubSubPayload : JsonWebSignature.Payload
{
    [JsonProperty("email")]
    public string Email { get; set; }
    [JsonProperty("email_verified")]
    public string EmailVerified { get; set; }
}
/// <summary>
/// Handle authenticated push request coming from pubsub.
/// See the full sample in https://github.com/GoogleCloudPlatform/dotnet-docs-samples/blob/main/appengine/flexible/Pubsub/Pubsub.Sample/Controllers/HomeController.cs
/// </summary>
[HttpPost]
[Route("/AuthPush")]
public async Task<IActionResult> AuthPushAsync([FromBody] PushBody body, [FromQuery] string token)
{
    // Get the Cloud Pub/Sub-generated "Authorization" header.
    string authorizaionHeader = HttpContext.Request.Headers["Authorization"];
    string verificationToken = token ?? body.message.attributes["token"];
    // JWT token comes in `Bearer <JWT>` format substring 7 specifies the position of first JWT char.
    string authToken = authorizaionHeader.StartsWith("Bearer ") ? authorizaionHeader.Substring(7) : null;
    if (verificationToken != _options.VerificationToken || authToken is null)
    {
        return new BadRequestResult();
    }
    // Verify and decode the JWT.
    // Note: For high volume push requests, it would save some network
    // overhead if you verify the tokens offline by decoding them using
    // Google's Public Cert; caching already seen tokens works best when
    // a large volume of messages have prompted a single push server to
    // handle them, in which case they would all share the same token for
    // a limited time window.
    var payload = await JsonWebSignature.VerifySignedTokenAsync<PubSubPayload>(authToken);

    // IMPORTANT: you should validate payload details not covered
    // by signature and audience verification above, including:
    //   - Ensure that `payload.Email` is equal to the expected service
    //     account set up in the push subscription settings.
    //   - Ensure that `payload.Email_verified` is set to true.

    var messageBytes = Convert.FromBase64String(body.message.data);
    string message = System.Text.Encoding.UTF8.GetString(messageBytes);
    s_authenticatedMessages.Add(message);
    return new OkResult();
}

Go

// receiveMessagesHandler validates authentication token and caches the Pub/Sub
// message received.
func (a *app) receiveMessagesHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
		return
	}

	// Verify that the request originates from the application.
	// a.pubsubVerificationToken = os.Getenv("PUBSUB_VERIFICATION_TOKEN")
	if token, ok := r.URL.Query()["token"]; !ok || len(token) != 1 || token[0] != a.pubsubVerificationToken {
		http.Error(w, "Bad token", http.StatusBadRequest)
		return