推播訂閱的驗證

如果推送訂閱項目使用驗證,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":"[email protected]",    "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 資訊主頁中按一下「授權」

  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,請在「憑證」頁面中尋找「用戶端 ID」IAP-App-Engine-app

版權聲明

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 	}  	// Get the Cloud Pub/Sub-generated JWT in the "Authorization" header. 	authHeader := r.Header.Get("Authorization") 	if authHeader == "" || len(strings.Split(authHeader, " ")) != 2 { 		http.Error(w, "Missing Authorization header", http.StatusBadRequest) 		return 	} 	token := strings.Split(authHeader, " ")[1] 	// Verify and decode the JWT. 	// If you don't need to control the HTTP client used you can use the 	// convenience method idtoken.Validate instead of creating a Validator. 	v, err := idtoken.NewValidator(r.Context(), option.WithHTTPClient(a.defaultHTTPClient)) 	if err != nil { 		http.Error(w, "Unable to create Validator", http.StatusBadRequest) 		return 	} 	// Please change http://example.com to match with the value you are 	// providing while creating the subscription. 	payload, err := v.Validate(r.Context(), token, "http://example.com") 	if err != nil { 		http.Error(w, fmt.Sprintf("Invalid Token: %v", err), http.StatusBadRequest) 		return 	} 	if payload.Issuer != "accounts.google.com" && payload.Issuer != "https://accounts.google.com" { 		http.Error(w, "Wrong Issuer", http.StatusBadRequest) 		return 	}  	// IMPORTANT: you should validate claim details not covered by signature 	// and audience verification above, including: 	//   - Ensure that `payload.Claims["email"]` is equal to the expected service 	//     account set up in the push subscription settings. 	//   - Ensure that `payload.Claims["email_verified"]` is set to true. 	if payload.Claims["email"] != "[email protected]" || payload.Claims["email_verified"] != true { 		http.Error(w, "Unexpected email identity", http.StatusBadRequest) 		return 	}  	var pr pushRequest 	if err := json.NewDecoder(r.Body).Decode(&pr); err != nil { 		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest) 		return 	}  	a.messagesMu.Lock() 	defer a.messagesMu.Unlock() 	// Limit to ten. 	a.messages = append(a.messages, pr.Message.Data) 	if len(a.messages) > maxMessages { 		a.messages = a.messages[len(a.messages)-maxMessages:] 	}  	fmt.Fprint(w, "OK") } 

Java

@WebServlet(value = "/pubsub/authenticated-push") public class PubSubAuthenticatedPush extends HttpServlet {   private final String pubsubVerificationToken = System.getenv("PUBSUB_VERIFICATION_TOKEN");   private final MessageRepository messageRepository;   private final GoogleIdTokenVerifier verifier =       new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new GsonFactory())           /**            * Please change example.com to match with value you are providing while creating            * subscription as provided in @see <a            * href="https://github.com/GoogleCloudPlatform/java-docs-samples/tree/main/appengine-java8/pubsub">README</a>.            */           .setAudience(Collections.singletonList("example.com"))           .build();   private final Gson gson = new Gson();    @Override   public void doPost(HttpServletRequest req, HttpServletResponse resp)       throws IOException, ServletException {      // Verify that the request originates from the application.     if (req.getParameter("token").compareTo(pubsubVerificationToken) != 0) {       resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);       return;     }     // Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.     String authorizationHeader = req.getHeader("Authorization");     if (authorizationHeader == null         || authorizationHeader.isEmpty()         || authorizationHeader.split(" ").length != 2) {       resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);       return;     }     String authorization = authorizationHeader.split(" ")[1];      try {       // 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       // messsages have prompted a single push server to handle them, in which       // case they would all share the same token for a limited time window.       GoogleIdToken idToken = verifier.verify(authorization);        GoogleIdToken.Payload payload = idToken.getPayload();       // IMPORTANT: you should validate claim details not covered by signature       // and audience verification above, including:       //   - Ensure that `payload.getEmail()` is equal to the expected service       //     account set up in the push subscription settings.       //   - Ensure that `payload.getEmailVerified()` is set to true.        messageRepository.saveToken(authorization);       messageRepository.saveClaim(payload.toPrettyString());       // parse message object from "message" field in the request body json       // decode message data from base64       Message message = getMessage(req);       messageRepository.save(message);       // 200, 201, 204, 102 status codes are interpreted as success by the Pub/Sub system       resp.setStatus(102);       super.doPost(req, resp);     } catch (Exception e) {       resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);     }   }    private Message getMessage(HttpServletRequest request) throws IOException {     String requestBody = request.getReader().lines().collect(Collectors.joining("\n"));     JsonElement jsonRoot = JsonParser.parseString(requestBody).getAsJsonObject();     String messageStr = jsonRoot.getAsJsonObject().get("message").toString();     Message message = gson.fromJson(messageStr, Message.class);     // decode from base64     String decoded = decode(message.getData());     message.setData(decoded);     return message;   }    private String decode(String data) {     return new String(Base64.getDecoder().decode(data));   }    PubSubAuthenticatedPush(MessageRepository messageRepository) {     this.messageRepository = messageRepository;   }    public PubSubAuthenticatedPush() {     this(MessageRepositoryImpl.getInstance());   } }

Node.js

app.post('/pubsub/authenticated-push', jsonBodyParser, async (req, res) => {   // Verify that the request originates from the application.   if (req.query.token !== PUBSUB_VERIFICATION_TOKEN) {     res.status(400).send('Invalid request');     return;   }    // Verify that the push request originates from Cloud Pub/Sub.   try {     // Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.     const bearer = req.header('Authorization');     const [, token] = bearer.match(/Bearer (.*)/);     tokens.push(token);      // 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.     const ticket = await authClient.verifyIdToken({       idToken: token,       audience: 'example.com',     });      const claim = ticket.getPayload();      // IMPORTANT: you should validate claim details not covered     // by signature and audience verification above, including:     //   - Ensure that `claim.email` is equal to the expected service     //     account set up in the push subscription settings.     //   - Ensure that `claim.email_verified` is set to true.      claims.push(claim);   } catch (e) {     res.status(400).send('Invalid token');     return;   }    // The message is a unicode string encoded in base64.   const message = Buffer.from(req.body.message.data, 'base64').toString(     'utf-8'   );    messages.push(message);    res.status(200).send(); });

Python

@app.route("/push-handlers/receive_messages", methods=["POST"]) def receive_messages_handler():     # Verify that the request originates from the application.     if request.args.get("token", "") != current_app.config["PUBSUB_VERIFICATION_TOKEN"]:         return "Invalid request", 400      # Verify that the push request originates from Cloud Pub/Sub.     try:         # Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.         bearer_token = request.headers.get("Authorization")         token = bearer_token.split(" ")[1]         TOKENS.append(token)          # Verify and decode the JWT. `verify_oauth2_token` verifies         # the JWT signature, the `aud` claim, and the `exp` claim.         # Note: For high volume push requests, it would save some network         # overhead if you verify the tokens offline by downloading Google's         # Public Cert and decode them using the `google.auth.jwt` module;         # 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.         claim = id_token.verify_oauth2_token(             token, requests.Request(), audience="example.com"         )          # IMPORTANT: you should validate claim details not covered by signature         # and audience verification above, including:         #   - Ensure that `claim["email"]` is equal to the expected service         #     account set up in the push subscription settings.         #   - Ensure that `claim["email_verified"]` is set to true.          CLAIMS.append(claim)     except Exception as e:         return f"Invalid token: {e}\n", 400      envelope = json.loads(request.data.decode("utf-8"))     payload = base64.b64decode(envelope["message"]["data"])     MESSAGES.append(payload)     # Returning any 2xx status indicates successful receipt of the message.     return "OK", 200

Ruby

post "/pubsub/authenticated-push" do   halt 400 if params[:token] != PUBSUB_VERIFICATION_TOKEN    begin     bearer = request.env["HTTP_AUTHORIZATION"]     token = /Bearer (.*)/.match(bearer)[1]     claim = Google::Auth::IDTokens.verify_oidc token, aud: "example.com"      # IMPORTANT: you should validate claim details not covered by signature     # and audience verification above, including:     #   - Ensure that `claim["email"]` is equal to the expected service     #     account set up in the push subscription settings.     #   - Ensure that `claim["email_verified"]` is set to true.      claims.push claim   rescue Google::Auth::IDTokens::VerificationError => e     puts "VerificationError: #{e.message}"     halt 400, "Invalid token"   end    message = JSON.parse request.body.read   payload = Base64.decode64 message["message"]["data"]    messages.push payload end

如要瞭解上述程式碼範例中使用的環境變數 PUBSUB_VERIFICATION_TOKEN,請參閱「撰寫及回應 Pub/Sub 訊息」。

如需驗證不記名 JWT 的其他範例,請參閱網站專用的 Google 登入指南。如要進一步瞭解 OpenID 符記,請參閱 OpenID Connect 指南,包括有助於驗證 JWT 的用戶端程式庫清單。

從其他 Google Cloud 服務驗證

Cloud Run 和 App Engine 函式會驗證 Pub/Sub 產生的權杖,藉此驗證來自 Pub/Sub 的 HTTP 呼叫。您唯一需要進行的設定,就是將必要的 IAM 角色授予呼叫端帳戶。

如要瞭解這些服務的不同用途,請參閱下列指南和教學課程:

Cloud Run

App Engine

Cloud Run 函式