Authentifizierung für Push-Abonnements

Wenn für ein Push-Abo die Authentifizierung verwendet wird, signiert der Pub/Sub-Dienst ein JWT und sendet es im Autorisierungsheader der Push-Anfrage. Das JWT enthält Ansprüche und eine Signatur.

Abonnenten können das JWT prüfen und Folgendes überprüfen:

  • Die Ansprüche sind korrekt.
  • Der Pub/Sub-Dienst hat die Ansprüche signiert.

Wenn Abonnenten eine Firewall verwenden, können sie keine Push-Anfragen empfangen. Um Push-Anfragen zu erhalten, müssen Sie die Firewall deaktivieren und das JWT prüfen.

Hinweise

JWT-Format

Das JWT ist ein OpenIDConnect-JWT, das aus einem Header, einem Anforderungssatz und einer Signatur besteht. Der Pub/Sub-Dienst codiert das JWT als Base64-String mit Punkttrennzeichen.

Der folgende Autorisierungsheader enthält beispielsweise ein codiertes 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" 

Der Header und der Anforderungssatz sind JSON-Strings. Nach der Decodierung nehmen sie die folgende Form an:

{"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"
  }

Die an Anfragen angehängten Tokens an Push-Endpunkte können bis zu einer Stunde alt sein.

Pub/Sub für die Push-Authentifizierung konfigurieren

Im folgenden Beispiel wird gezeigt, wie Sie das Dienstkonto für die Push-Authentifizierung auf ein beliebiges Dienstkonto festlegen und dem service-{PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com-Dienst-Agenten die Rolle iam.serviceAccountTokenCreator zuweisen.

Console

  1. Rufen Sie die Seite Pub/Sub-Abos auf.

    Zur Seite "Abos"

  2. Klicken Sie auf Abo erstellen.

  3. Geben Sie im Feld Abo-ID einen Namen ein.

  4. Wählen Sie ein Thema aus.

  5. Wählen Sie als Zustellungstyp Push aus.

  6. Geben Sie eine Endpunkt-URL ein.

  7. Klicken Sie das Kästchen Authentifizierung aktivieren an.

  8. Wählen Sie ein Dienstkonto aus.

  9. Der Dienst-Agent service-{PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com muss im IAM-Dashboard Ihres Projekts die Rolle iam.serviceAccountTokenCreator haben. Wenn dem Dienstkonto die Rolle noch nicht zugewiesen wurde, klicken Sie im IAM-Dashboard auf Zuweisen.

  10. Optional: Geben Sie eine Zielgruppe ein.

  11. Klicken Sie auf Erstellen.

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'

Wenn Sie die Authentifizierung für ein Push-Abo aktivieren, kann der Fehler permission denied oder not authorized auftreten. Weisen Sie dem Hauptkonto, das das Abo erstellt oder aktualisiert, die Berechtigung iam.serviceAccounts.actAs für das Dienstkonto zu, um dieses Problem zu beheben. Weitere Informationen finden Sie unter Authentifizierung im Abschnitt „Push-Abos erstellen“.

Wenn Sie ein authentifiziertes Push-Abo mit einer App Engine-Anwendung verwenden, die mit Identity-Aware Proxy gesichert ist, müssen Sie die IAP-Client-ID als Zielgruppe für das Push-Authentifizierungstoken angeben. Informationen zum Aktivieren von IAP in Ihrer App Engine-Anwendung finden Sie unter IAP aktivieren. Die IAP-Client-ID finden Sie auf der Seite Anmeldedaten unter IAP-App-Engine-app Client-ID.

Ansprüche

Mit dem JWT kann sichergestellt werden, dass die Anforderungen (einschließlich der email- und aud-Anforderungen) von Google unterzeichnet werden. Weitere Informationen dazu, wie die OAuth 2.0 APIs von Google sowohl für die Authentifizierung als auch für die Autorisierung verwendet werden können, finden Sie unter OpenID Connect.

Es gibt zwei Mechanismen, mit denen Sie diese Anforderungen nutzbar machen. Erstens muss in Pub/Sub der Nutzer oder das Dienstkonto, das den Aufruf „CreateSubscription“, „UpdateSubscription“ oder „ModifyPushConfig“ ausführt, eine Rolle mit der Berechtigung iam.serviceAccounts.actAs für das Konto des Push-Authentifizierungsdienstes haben. Ein Beispiel für eine solche Rolle ist die Rolle roles/iam.serviceAccountUser.

Zweitens wird der Zugriff auf die zum Signieren der Tokens verwendeten Zertifikate streng kontrolliert. Damit Pub/Sub das Token erstellen kann, muss es einen internen Google-Dienst unter Verwendung einer separaten Dienstkontoidentität zum Signieren aufrufen. Das ist der Dienstagent service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com. Dieses signierende Dienstkonto muss die Berechtigung iam.serviceAccounts.getOpenIdToken oder die Rolle Ersteller von Dienstkonto-Tokens (roles/iam.serviceAccountTokenCreator) für das Dienstkonto für die Push-Authentifizierung oder für eine übergeordnete Ressource wie das Projekt des Dienstkontos für die Push-Authentifizierung haben.

Tokens validieren

Die Validierung von Tokens, die von Pub/Sub an den Push-Endpunkt gesendet werden, umfasst folgende Aufgaben:

  • Tokenintegrität durch Signaturvalidierung prüfen
  • Achten Sie darauf, dass die Anforderungen E-Mail und Zielgruppe im Token mit den Werten übereinstimmen, die in der Konfiguration des Push-Abo festgelegt wurden.

Das folgende Beispiel zeigt, wie Sie eine Push-Anfrage bei einer App Engine-Anwendung authentifizieren, die nicht mit Identity-Aware Proxy gesichert ist. Wenn Ihre App Engine-Anwendung mit IAP gesichert ist, lautet der HTTP-Anfrageheader, der das IAP-JWT enthält, x-goog-iap-jwt-assertion und muss entsprechend validiert werden.

Protokoll

Anfrage:

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

Antwort:

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#

Bevor Sie dieses Beispiel testen, folgen Sie der Einrichtungsanleitung für C# in der Schnellstart-Anleitung: Clientbibliotheken verwenden. Weitere Informationen finden Sie in der Referenzdokumentation zu 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.
        /// </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"] != "test-service-account-email@example.com" || 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

Informationen zur Umgebungsvariablen PUBSUB_VERIFICATION_TOKEN, die in den Codebeispielen oben verwendet wird, finden Sie unter Pub/Sub-Nachrichten schreiben und beantworten.

Weitere Beispiele zur Validierung des Inhaber-JWT finden Sie im Handbuch zu Google Log-in für Websites. Eine umfassendere Übersicht über OpenID-Tokens finden Sie im Handbuch zu OpenID Connect. Dort finden Sie auch eine Liste von Clientbibliotheken, mit denen JWTs validiert werden können.

Authentifizierung über andere Google Cloud-Dienste

Cloud Run, App Engine und Cloud Run Functions authentifizieren HTTP-Aufrufe von Pub/Sub, indem von Pub/Sub generierte Tokens verifiziert werden. Sie müssen dem Anruferkonto nur die erforderlichen IAM-Rollen gewähren.

In den folgenden Anleitungen und Tutorials finden Sie Informationen zu verschiedenen Anwendungsfällen mit diesen Diensten:

Cloud Run

App Engine

Cloud Run-Funktionen

  • HTTP-Trigger: Ihr Dienstkonto für die Push-Authentifizierung muss die Rolle roles/cloudfunctions.invoker haben, um eine Funktion aufzurufen, wenn Sie Pub/Sub-Push-Anfragen als HTTP-Trigger für die Funktion verwenden möchten.
  • Google Cloud Pub/Sub-Trigger: IAM-Rollen und Berechtigungen werden automatisch konfiguriert, wenn Sie eine Funktion mit Pub/Sub-Triggern aufrufen.