Mit One Tap über Google anmelden – Aufforderung für das Web

1. Einführung

Dieses Codelab baut auf dem Codelab „Mit Google anmelden“-Schaltfläche für das Web auf. Sie sollten es also zuerst durcharbeiten.

In diesem Codelab verwenden Sie die JavaScript-Bibliothek Google Identity Services und One Tap-Aufforderungen, um die Nutzeranmeldung auf statischen und dynamischen Webseiten mithilfe von HTML- und JavaScript-APIs hinzuzufügen.

One Tap-Aufforderung

Außerdem richten wir einen serverseitigen Anmeldeendpunkt ein, um JWT-ID-Tokens zu überprüfen.

Lerninhalte

  • Serverseitigen Anmeldeendpunkt zum Überprüfen von ID-Tokens einrichten
  • So fügen Sie einer Webseite eine One Tap-Aufforderung für die Anmeldung über Google hinzu 
    • als statisches HTML-Element und
    • dynamisch mit JavaScript.
  • So verhält sich die Aufforderung für One Tap

Voraussetzungen

  1. Grundkenntnisse in HTML, CSS, JavaScript und den Chrome-Entwicklertools (oder einem ähnlichen Tool).
  2. Ein Ort zum Bearbeiten und Hosten von HTML- und JavaScript-Dateien.
  3. Die Client-ID, die Sie im vorherigen Codelab erhalten haben.
  4. Eine Umgebung, in der eine einfache Python-App ausgeführt werden kann.

Los gehts!

2. Anmeldeendpunkt einrichten

Zuerst erstellen wir ein Python-Script, das als einfacher Webserver fungiert, und richten die Python-Umgebung ein, die zum Ausführen des Scripts erforderlich ist.

Bei lokaler Ausführung stellt das Skript die Landingpage sowie statische HTML- und dynamische One Tap-Seiten für den Browser bereit. Es akzeptiert POST-Anfragen, decodiert das im Parameter „credential“ enthaltene JWT und prüft, ob es von der OAuth-Plattform von Google Identity ausgestellt wurde.

Nachdem ein JWT decodiert und geprüft wurde, wird das Skript zur Landingpage index.html weitergeleitet, um die Ergebnisse anzuzeigen.

Kopieren Sie das Folgende in eine Datei mit dem Namen simple-server.py:

"""Very basic web server to handle GET and POST requests."""
from http.server import SimpleHTTPRequestHandler
import json
import socketserver
from typing import Dict, Optional, Tuple
import urllib.parse
from urllib.parse import parse_qs

from google.auth.transport import requests as google_auth_requests
from google.oauth2 import id_token


""" NOTE: You'll need to change this """
CLIENT_ID = (
    "PUT_YOUR_WEB_CLIENT_ID_HERE"
)

""" these may change for a Cloud IDE, but good as-is for local termainals """
SERVER_ADDRESS = "0.0.0.0"
PORT = 3000
TARGET_HTML_PAGE_URL = f"http://localhost:{PORT}/"
""" and this is the end of constants you might need to change """

HTTP_STATUS_OK = 200
HTTP_STATUS_BAD_REQUEST = 400
HTTP_STATUS_UNAUTHORIZED = 401
HTTP_STATUS_INTERNAL_SERVER_ERROR = 500
HTTP_STATUS_FOUND = 303  # For redirection after decode and verify
OIDC_SERVER = "accounts.google.com"


class OIDCJWTReceiver(SimpleHTTPRequestHandler):
  """Request handler to securely process a Google ID token response."""

  def _validate_csrf(self, request_parameters: Dict) -> Tuple[bool, str]:
    """Validates the g_csrf_token to protect against CSRF attacks."""
    csrf_token_body = request_parameters.get("g_csrf_token")
    if not csrf_token_body:
      return False, "g_csrf_token not found in POST body."

    csrf_token_cookie = None
    cookie_header = self.headers.get("Cookie")
    if cookie_header:
      cookie_pairs = (c.split("=", 1) for c in cookie_header.split(";"))
      cookies = {k.strip(): v.strip() for k, v in cookie_pairs}
      csrf_token_cookie = cookies.get("g_csrf_token")
    if not csrf_token_cookie:
      return False, "g_csrf_token not found in cookie."

    if csrf_token_body != csrf_token_cookie:
      return False, "CSRF token mismatch."

    return True, "CSRF token validated successfully."

  def _parse_and_validate_credential(
      self, request_parameters: Dict
  ) -> Optional[Tuple[Optional[Dict], str]]:
    """Parse POST data, extract, decode and validate user credential."""
    credential = request_parameters.get("credential")
    if not credential:
      return None, "Credential not provided"

    try:
      id_info = id_token.verify_oauth2_token(
          credential, google_auth_requests.Request(), CLIENT_ID
      )
      return id_info, ""
    except ValueError as e:
      return None, f"Error during JWT decode: {e}"
    except Exception as e:
      return None, f"Unexpected error during credential validation: {e}"

  def _redirect_to_html(self, response_data: Dict) -> None:
    """Redirect to the target HTML page with data in the URL fragment."""
    try:
      json_data = json.dumps(response_data)
      encoded_data = urllib.parse.quote(json_data)
      redirect_url = f"http://localhost:{PORT}/#data={encoded_data}"
      self.send_response(HTTP_STATUS_FOUND)
      self.send_header("Location", redirect_url)
      self.send_header("Connection", "close")
      self.end_headers()
    except Exception as e:
      print(f"An error occurred during redirection: {e}")
      self.send_response(HTTP_STATUS_INTERNAL_SERVER_ERROR)
      self.send_header("Content-type", "text/plain")
      self.send_header("Connection", "close")
      self.end_headers()
      self.wfile.write(f"A redirect error occurred: {e}".encode("utf-8"))

  def _send_bad_request(self, message: str) -> None:
    """Sends a 400 Bad Request response."""
    self.send_response(HTTP_STATUS_BAD_REQUEST)
    self.send_header("Content-type", "text/plain")
    self.send_header("Connection", "close")
    self.end_headers()
    self.wfile.write(message.encode("utf-8"))

  def do_POST(self):
    """Handle POST requests for the /user-login path."""
    if self.path != "/user-login":
      self.send_error(404, "File not found")
      return

    try:
      content_length = int(self.headers.get("Content-Length", 0))
      post_data_bytes = self.rfile.read(content_length)
      post_data_str = post_data_bytes.decode("utf-8")
      request_parameters = {
          key: val[0]
          for key, val in parse_qs(post_data_str).items()
          if len(val) == 1
      }

      csrf_valid, csrf_message = self._validate_csrf(request_parameters)
      if not csrf_valid:
        print(f"CSRF verify failure: {csrf_message}")
        self._send_bad_request(f"CSRF verify failure: {csrf_message}")
        return

      decoded_id_token, error_message = self._parse_and_validate_credential(
          request_parameters
      )

      response_data = {}
      if decoded_id_token:
        response_data["status"] = "success"
        response_data["message"] = decoded_id_token
      elif error_message:
        response_data["status"] = "error"
        response_data["message"] = error_message
      else:
        response_data["status"] = "error"
        response_data["message"] = "Unknown error during JWT validation"

      self._redirect_to_html(response_data)

    except Exception as e:
      self._redirect_to_html(
          {"status": "error", "error_message": f"Internal server error: {e}"}
      )


with socketserver.TCPServer(("", PORT), OIDCJWTReceiver) as httpd:
  print(
      f"Serving HTTP on {SERVER_ADDRESS} port"
      f" {PORT} (http://{SERVER_ADDRESS}:{PORT}/)"
  )
  httpd.serve_forever()

Da wir überprüfen werden, ob das JWT für Ihren Client ausgestellt wurde, indem wir das Feld „audience“ (aud) des ID-Tokens verwenden, muss Ihre Python-App wissen, welche Client-ID verwendet wird. Ersetzen Sie dazu PUT_YOUR_WEB_CLIENT_ID_HERE durch die Client-ID, die Sie im vorherigen Codelab zur Schaltfläche „Über Google anmelden“ verwendet haben.

Python-Umgebung

Richten wir die Umgebung zum Ausführen des Webserver-Skripts ein.

Sie benötigen Python 3.8 oder höher sowie einige Pakete, die bei der JWT-Bestätigung und -Decodierung helfen.

$ python3 --version

Wenn Ihre Version von python3 älter als 3.8 ist, müssen Sie möglicherweise den Shell-PATH ändern, damit die erwartete Version gefunden wird, oder eine neuere Version von Python auf Ihrem System installieren.

Erstellen Sie als Nächstes eine Datei mit dem Namen requirements.txt, in der die Pakete aufgeführt sind, die wir zum Decodieren und Überprüfen von JWTs benötigen:

google-auth

Führen Sie diese Befehle im selben Verzeichnis wie simple-server.py und requirements.txt aus, um eine virtuelle Umgebung zu erstellen und Pakete nur für diese App zu installieren:

$ python3 -m venv env
$ source env/bin/activate
(env) $ pip install -r requirements.txt

Starten Sie nun den Server. Wenn alles richtig funktioniert, wird Folgendes angezeigt:

(env) $ python3 ./simple-server.py
Serving HTTP on 0.0.0.0 port 3000 (http://0.0.0.0:3000/) ...

Cloudbasierte IDEs

Dieses Codelab wurde für die Ausführung in einem lokalen Terminal und auf localhost entwickelt. Mit einigen Änderungen kann es jedoch auch auf Plattformen wie Replit oder Glitch verwendet werden. Für jede Plattform gelten eigene Einrichtungsanforderungen und Standardeinstellungen für die Python-Umgebung. Daher müssen Sie wahrscheinlich einige Dinge wie TARGET_HTML_PAGE_URL und die Python-Einrichtung ändern.

Auf Glitch würden Sie beispielsweise weiterhin ein requirements.txt hinzufügen, aber auch eine Datei mit dem Namen start.sh erstellen, um den Python-Server automatisch zu starten:

python3 ./simple-server.py

Die vom Python-Skript und den HTML-Dateien verwendeten URLs müssen ebenfalls auf die externe URL Ihrer Cloud IDE aktualisiert werden. Das sieht dann so aus: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/". Da in den HTML-Dateien in diesem Codelab standardmäßig auch „localhost“ verwendet wird, müssen Sie sie mit der externen Cloud IDE-URL aktualisieren: data-login_uri="https://your-project-name.glitch.me/user-login".

3. Landingpage erstellen

Als Nächstes erstellen wir eine Landingpage, auf der die Ergebnisse der Anmeldung mit One Tap angezeigt werden. Auf der Seite wird das decodierte JWT-ID-Token oder ein Fehler angezeigt. Über ein Formular auf der Seite kann auch ein JWT an den Anmeldeendpunkt auf unserem Python-HTTP-Server gesendet werden, wo es decodiert und überprüft wird. Es verwendet ein CSRF-Double-Submit-Cookie und einen POST-Anfrageparameter, sodass derselbe user-login-Serverendpunkt wie in den gsi/client-HTML- und JavaScript-API-Beispielen im Codelab wiederverwendet werden kann.

Speichern Sie dies im Terminal in einer Datei mit dem Namen index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JWT Verification</title>
    <style>
      body { font-family: sans-serif; margin: 0; }
      .top-nav {
        text-align: center; padding: 15px 0; background-color: #f8f9fa;
        border-bottom: 1px solid #dee2e6; width: 100%;
      }
      .top-nav a {
        margin: 0 15px; text-decoration: none; font-weight: bold;
        color: #007bff; font-size: 1.1em;
      }
      .top-nav a:hover { text-decoration: underline; }
      .page-container { padding: 20px; }
      pre {
        background-color: #f9f9f9; padding: 10px; overflow-x: auto;
        white-space: pre-wrap; word-break: break-all;
      }
      .error { color: red; }
      .success { color: green; }
      #jwt-form { margin-bottom: 20px; flex: 1; }
      fieldset {
        border: 1px solid #ddd; padding: 10px; margin: 0;
        min-width: 0; flex: 1; }
      legend { font-weight: bold; margin-bottom: 5px; }
      textarea { width: 100%; box-sizing: border-box; vertical-align: top; }
      button[type="submit"] { padding: 8px 15px; margin-top: 10px; }
      @media (min-width: 1024px) {
        .main-content { display: flex; gap: 20px; }
        .main-content > #jwt-form,
        .main-content > .result-container {
          flex-basis: 50%; /* Each item takes up 50% of the width */
          margin-bottom: 0; /* Remove bottom margin when side-by-side */
          display: flex; /* Make the result container a flex container */
          flex-direction: column; /* Stack children vertically */
          flex: 1; /* Allows the result container to grow and shrink */
        }
      }
    </style>
  </head>
  <body>
    <nav class="top-nav">
      <a href="static-page.html">One Tap Static Page</a>
      <a href="dynamic-page.html">One Tap Dynamic Page</a>
      <a href="prompt-outcomes.html">Prompt behaviors</a>
    </nav>
    <div class="page-container">
      <h1>JWT Verification</h1>

      <div class="main-content">
        <form id="jwt-form" action="/user-login" method="post">
          <fieldset>
            <legend>Encoded JWT ID Token</legend>
            <textarea id="credential" name="credential" rows="5"
                      cols="50"></textarea>
            <button type="submit">Verify JWT</button>
          </fieldset>
        </form>

        <section class="result-container">
          <fieldset>
            <legend>Decode and verify result</legend>
            <p id="status"></p>
            <pre id="result"></pre>
          </fieldset>
        </section>
      </div>
    </div>
    <script>
      const statusElement = document.getElementById("status");
      const resultElement = document.getElementById("result");

      const handleResponse = (responseData) => {
        const { status, message } = responseData;
        const result = message
          ? status === "success"
            ? JSON.stringify(message, null, 2)
            : message
          : "";

        statusElement.textContent = status;
        resultElement.textContent = result;
        statusElement.className = "";
        if (status === "success") {
          statusElement.classList.add("success");
        } else if (status === "error") {
          statusElement.classList.add("error");
        }
      };

      const getEncodedDataFromHash = (hash) => {
        const urlParams = new URLSearchParams(hash.substring(1));
        return urlParams.get("data");
      };

      const processHashData = (hash) => {
        const encodedData = getEncodedDataFromHash(hash);
        if (encodedData) {
          try {
            const jsonData = JSON.parse(decodeURIComponent(encodedData));
            handleResponse(jsonData);
            history.pushState(
              "",
              document.title,
              window.location.pathname + window.location.search,
            );
          } catch (error) {
            handleResponse({
              status: "error",
              message: "Error parsing data from URL: " + error,
            });
          }
        }
      };

      window.addEventListener("load",
                              () => processHashData(window.location.hash));
      window.addEventListener("hashchange",
                              () => processHashData(window.location.hash));
    </script>

    <script>
      document.addEventListener("DOMContentLoaded", () => {

        const generateRandomString = (length) => {
        const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
                      "abcdefghijklmnopqrstuvwxyz" +
                      "0123456789";
          let result = "";
          for (let i = 0; i < length; i++) {
            result += chars.charAt(Math.floor(Math.random() * chars.length));
          }
          return result;
        };

        const csrfToken = generateRandomString(12);
        document.cookie = `g_csrf_token=${csrfToken};path=/;SameSite=Lax`;
        const form = document.getElementById("jwt-form");
        const hiddenInput = document.createElement("input");
        hiddenInput.setAttribute("type", "hidden");
        hiddenInput.setAttribute("name", "g_csrf_token");
        hiddenInput.setAttribute("value", csrfToken);
        form.appendChild(hiddenInput);
      });
    </script>
  </body>
</html>

Webserver und JWT-Decodierung testen

Bevor wir versuchen, mit One Tap zu arbeiten, prüfen wir, ob die Serverendpunktumgebung eingerichtet ist und funktioniert.

Rufen Sie die Landingpage http://localhost:3000/ auf und klicken Sie auf die Schaltfläche JWT überprüfen.

Sie sollten Folgendes sehen:

JWT-Überprüfung ohne Inhalt

Durch Drücken der Schaltfläche wird ein POST mit dem Inhalt des Eingabefelds an das Python-Script gesendet. Das Script erwartet ein codiertes JWT im Eingabefeld und versucht daher, die Nutzlast zu decodieren und zu überprüfen. Anschließend werden Sie zur Landingpage weitergeleitet, um die Ergebnisse zu sehen.

Aber Moment mal, es gab kein JWT – ist das nicht fehlgeschlagen? Ja, aber mit Stil!

Da das Feld leer war, wird ein Fehler angezeigt. Geben Sie nun einen beliebigen Text in das Eingabefeld ein und drücken Sie die Schaltfläche noch einmal. Der Vorgang schlägt mit einem anderen Decodierungsfehler fehl.

Sie können ein codiertes, von Google ausgestelltes JWT-ID-Token in das Eingabefeld einfügen und das Python-Skript decodieren, überprüfen und anzeigen lassen. Alternativ können Sie https://jwt.io verwenden, um ein codiertes JWT zu prüfen.

4. Statische HTML-Seite

Als Nächstes richten wir One Tap für HTML-Seiten ein, ohne JavaScript zu verwenden. Das kann für statische Websites oder für Caching-Systeme und CDNs nützlich sein.

Fügen Sie zuerst dieses Codebeispiel in eine Datei mit dem Namen static-page.html ein:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://accounts.google.com/gsi/client" async></script>
    <link rel="icon" href="data:," />
  </head>
  <body>
    <h1>Google One Tap static HTML page</h1>
    <div
      id="g_id_onload"
      data-client_id="PUT_YOUR_WEB_CLIENT_ID_HERE"
      data-ux_mode="redirect"
      data-login_uri="http://localhost:3000/user-login"
    ></div>
  </body>
</html>

Öffnen Sie als Nächstes static-page.html und ersetzen Sie PUT_YOUR_WEB_CLIENT_ID_HERE durch die Client-ID, die Sie im vorherigen Codelab zur Schaltfläche „Über Google anmelden“ verwendet haben.

Was passiert dadurch?

Alle HTML-Elemente mit einem id von g_id_onload und den zugehörigen Datenattributen werden zum Konfigurieren der Google Identity Services-Bibliothek (gsi/client) verwendet. Außerdem wird die Aufforderung zur Einmalanmeldung angezeigt, wenn das Dokument im Browser geladen wird.

Das Attribut data-login_uri ist der URI, der nach der Anmeldung des Nutzers eine POST-Anfrage vom Browser empfängt. Diese Anfrage enthält das von Google ausgestellte codierte JWT.

Eine vollständige Liste der One Tap-Optionen finden Sie im HTML-Codegenerator und in der HTML-API-Referenz.

Anmelden

Klicken Sie auf http://localhost:3000/static-page.html.

Die Aufforderung zur Einmalanmeldung sollte in Ihrem Browser angezeigt werden.

One Tap-Benutzeroberfläche

Klicken Sie auf Weiter als, um sich anzumelden.

Nach der Anmeldung sendet Google eine POST-Anfrage an den Anmeldeendpunkt Ihres Python-Servers. Die Anfrage enthält ein codiertes JWT, das von Google signiert ist.

Der Server verwendet dann einen der öffentlichen Signaturschlüssel von Google, um zu prüfen, ob das JWT von Google erstellt und signiert wurde. Anschließend wird die Zielgruppe decodiert und geprüft, ob sie mit Ihrer Client-ID übereinstimmt. Als Nächstes wird eine CSRF-Prüfung durchgeführt, um sicherzustellen, dass der Cookie-Wert und der Wert des Anfrageparameters im POST-Text übereinstimmen. Wenn das nicht der Fall ist, liegt mit Sicherheit ein Problem vor.

Schließlich wird auf der Landingpage das erfolgreich bestätigte JWT als JSON-formatiertes ID-Token-Nutzeranmeldedaten angezeigt.

Bestätigtes JWT

Häufige Fehler

Es gibt mehrere Möglichkeiten, wie der Anmeldevorgang fehlschlagen kann. Die häufigsten Gründe sind:

  • data-client_id fehlt oder ist falsch. In diesem Fall wird in der DevTools-Konsole ein Fehler angezeigt und die Aufforderung zur Einmalanmeldung funktioniert nicht.
  • data-login_uri ist nicht verfügbar, da ein falscher URI eingegeben wurde, der Webserver nicht gestartet wurde oder er den falschen Port überwacht. In diesem Fall scheint die Aufforderung zur Einmalanmeldung zu funktionieren, aber auf dem Tab „Netzwerk“ der Entwicklertools wird ein Fehler angezeigt, wenn die Anmeldedaten zurückgegeben werden.
  • Der Hostname oder Port, den Ihr Webserver verwendet, ist nicht in den autorisierten JavaScript-Quellen für Ihre OAuth-Client-ID aufgeführt. Sie sehen die Konsolennachricht When fetching the ID assertion endpoint, a 400 HTTP response code was received. (Beim Abrufen des ID-Bestätigungsendpunkts wurde der HTTP-Antwortcode 400 empfangen). Wenn Sie dies während dieses Codelab sehen, prüfen Sie, ob sowohl http://localhost/ als auch http://localhost:3000 aufgeführt sind.

5. Dynamische Seite

Jetzt zeigen wir One Tap mit einem JavaScript-Aufruf an. In diesem Beispiel wird One Tap immer beim Laden der Seite angezeigt. Sie können das Prompt aber auch nur bei Bedarf einblenden. Sie können beispielsweise prüfen, ob die Sitzung des Nutzers älter als 28 Tage ist, und die Anmeldeaufforderung noch einmal anzeigen.

Fügen Sie dieses Codebeispiel in eine Datei mit dem Namen dynamic-page.html ein.

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://accounts.google.com/gsi/client" async></script>
    <link rel="icon" href="data:," />
  </head>
  <body>
    <h1>Google One Tap dynamic page</h1>
    <script>
      const generateRandomString = (length) => {
        const array = new Uint8Array(length / 2);
        window.crypto.getRandomValues(array);
        return Array.from(array, (byte) =>
          byte.toString(16).padStart(2, "0")
        ).join("");
      };

      const setCookie = (name, value) => {
        document.cookie = '${name}=${value};path=/;SameSite=Lax';
      };

      const getCookie = (name) => {
        const nameEQ = name + "=";
        const ca = document.cookie.split(";");
        for (let i = 0; i < ca.length; i++) {
          let c = ca[i];
          while (c.charAt(0) == " ") c = c.substring(1, c.length);
          if (c.indexOf(nameEQ) == 0)
            return c.substring(nameEQ.length, c.length);
        }
        return null;
      };

      function handleResponse(rsp) {
        console.log("ID Token received from Google: ", rsp.credential);
        console.log("Submitting token to server via dynamic form POST.");

        const form = document.createElement("form");
        form.method = "POST";
        form.action = "http://" + window.location.host + "/user-login";

        // Add the credential and CSRF cookie value asa hidden fields
        const hiddenField = document.createElement("input");
        hiddenField.type = "hidden";
        hiddenField.name = "credential";
        hiddenField.value = rsp.credential;
        form.appendChild(hiddenField);

        const csrfToken = getCookie("g_csrf_token");
        if (csrfToken) {
          console.log("Found g_csrf_token cookie, adding to form.");
          const csrfField = document.createElement("input");
          csrfField.type = "hidden";
          csrfField.name = "g_csrf_token";
          csrfField.value = csrfToken;
          form.appendChild(csrfField);
        } else {
          console.warn(
            "Warning: g_csrf_token cookie not found. POSTing without it."
          );
        }

        document.body.appendChild(form);
        form.submit();
      }

      window.onload = function () {
        const csrfToken = generateRandomString(12);
        setCookie("g_csrf_token", csrfToken);
        console.log("CSRF token cookie set on page load:", csrfToken);

        google.accounts.id.initialize({
          client_id: "PUT_YOUR_WEB_CLIENT_ID_HERE",
          ux_mode: "popup",
          callback: handleResponse,
        });

        google.accounts.id.prompt(); // Display the One Tap prompt
      };
    </script>
  </body>
</html>

Öffnen Sie dynamic-page.html und ersetzen Sie PUT_YOUR_WEB_CLIENT_ID_HERE durch die Client-ID, die Sie im vorherigen Codelab zur Schaltfläche „Über Google anmelden“ verwendet haben.

Dieser Code besteht aus HTML und JavaScript und hat mehrere Aufgaben:

  • Konfiguriert die Google Identity Services-Bibliothek (gsi/client) durch Aufrufen von google.accounts.id.initialize().
  • generiert ein CSRF-Cookie (Cross-Site Request Forgery),
  • fügt einen Callback-Handler hinzu, um das codierte JWT von Google zu empfangen und es über einen Formular-POST an unseren /user-login-Endpunkt des Python-Skripts zu senden, und
  • zeigt den One Tap-Prompt mit google.accounts.id.prompt() an.

Eine vollständige Liste der Einstellungen für die Einmalanmeldung finden Sie in der JavaScript API-Referenz.

Melden wir uns an!

Öffnen Sie in Ihrem Browser http://localhost:3000/dynamic-page.html.

Das Verhalten des One Tap-Aufforderungsfensters ist dasselbe wie im statischen HTML-Szenario. Auf dieser Seite wird jedoch ein JavaScript-Callback-Handler definiert, um ein CSRF-Cookie zu erstellen, das JWT von Google zu empfangen und es per POST-Anfrage an den user-login-Endpunkt des Python-Servers zu senden. Die HTML-API führt diese Schritte automatisch für Sie aus.

Chrome-Tab „Netzwerk“

6. Prompt-Verhalten

Wir probieren jetzt einige Dinge mit One Tap aus, da die Aufforderung im Gegensatz zur Schaltfläche nicht immer angezeigt wird. Sie kann vom Browser und vom Nutzer geschlossen oder deaktiviert werden.

Speichern Sie dies zuerst in einer Datei mit dem Namen prompt-outcomes.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Google One Tap Prompt behaviors</title>
    <style>
      body { font-family: sans-serif; padding: 20px; }
      #log {
        border: 1px solid #ccc; background-color: #f0f0f0;
        padding: 15px; margin-top: 20px;
        white-space: pre-wrap; font-family: monospace;
      }
      .success { color: green; }
      .error { color: red; }
      .info { color: blue; }
      .warning { color: orange; }
    </style>
  </head>
  <body>
    <h1>One Tap behaviors</h1>
    <p>Open the developer console to see detailed logs.</p>
    <div id="log">Awaiting events...</div>

    <script src="https://accounts.google.com/gsi/client" async defer></script>

    <script>
      // logging utility to display event and notification info
      const logElement = document.getElementById("log");
      function log(message, type = "info") {
        const timestamp = new Date().toLocaleTimeString();
        logElement.innerHTML +=
          `\n<span class="${type}">[${timestamp}] ${message}</span>`;
        console.log(`[${type.toUpperCase()}] ${message}`);
      }

      function decodeJwt(jwt) {
        try {
          const parts = jwt.split(".");
          if (parts.length !== 3) {
            throw new Error("Invalid JWT structure");
          }
          const base64Url = parts[1];
          const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
          const jsonPayload = decodeURIComponent(
            atob(base64)
              .split("")
              .map(function (c) {
                return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
              })
              .join(""),
          );
          return JSON.parse(jsonPayload);
        } catch (e) {
          log(`Error decoding JWT: ${e.message}`, "error");
          return null;
        }
      }

      /* Handles the credential response after a user signs in. */
      function handleCredentialResponse(credentialResponse) {
        log("Credential Response received.", "success");

        const credential = credentialResponse.credential;
        log(`Credential JWT: ${credential.substring(0, 30)}...`);

        // For demonstration, we decode the JWT on the client side.
        // REMEMBER: Always verify the token on your backend server!
        const payload = decodeJwt(credential);
        if (payload) {
          log(`Welcome, ${payload.name}! (Email: ${payload.email})`);
          log("Decoded JWT Payload: " + JSON.stringify(payload, null, 2));
        }
      }

      /* Handles notifications about the One-Tap prompt's UI status. */
      function handlePromptMomentNotification(notification) {
        log(`Prompt Moment Notification received.`, "info");

        if (notification.isNotDisplayed()) {
          const reason = notification.getNotDisplayedReason();
          log(`Prompt not displayed. Reason: <strong>${reason}</strong>`,
              "error");
        }
        if (notification.isSkippedMoment()) {
          const reason = notification.getSkippedReason();
          log(`Prompt was skipped. Reason: <strong>${reason}</strong>`,
              "warning");
          if (reason === "auto_cancel") {
            log("may have called prompt() multiple times in a row.");
          } else if (reason === "user_cancel") {
            log("The user manually closed the prompt.");
          }
        }
        if (notification.isDismissedMoment()) {
          const reason = notification.getDismissedReason();
          log(`Prompt dismissed. Reason: <strong>${reason}</strong>`, "info");
          if (reason === "credential_returned") {
            log("Expected, credential sent to the JS handler.");
          } else if (reason === "cancel_called") {
            log("programmatic call to google.accounts.id.cancel().");
          }
        }
      }

      window.onload = function () {
        try {
          google.accounts.id.initialize({
            client_id: "PUT_YOUR_WEB_CLIENT_ID_HERE",
            callback: handleCredentialResponse,
            ux_mode: "popup",
          });

          google.accounts.id.prompt(handlePromptMomentNotification);
          log("One Tap initialized. Waiting for prompt...");
        } catch (e) {
          log(`Initialization Error: ${e.message}`, "error");
        }
      };
    </script>
  </body>
</html>

Öffnen Sie als Nächstes prompt-outcomes.html, ersetzen Sie PUT_YOUR_WEB_CLIENT_ID_HERE durch Ihre Client-ID und speichern Sie die Datei.

Öffnen Sie in Ihrem Browser http://localhost:3000/prompt-outcomes.html.

Seitenklicks

Klicken Sie zuerst auf eine beliebige Stelle außerhalb des One Tap-Prompts. Sowohl auf der Seite als auch in der Konsole sollte die Meldung The request has been aborted. (Die Anfrage wurde abgebrochen.) protokolliert werden.

Anmelden

Melden Sie sich dann einfach wie gewohnt an. Sie sehen Protokollierungs- und Benachrichtigungsaktualisierungen, mit denen sich beispielsweise eine Nutzersitzung einrichten oder aktualisieren lässt.

Prompt schließen

Laden Sie die Seite neu und drücken Sie nach der Anzeige von One Tap auf das „X“ in der Titelleiste. Diese Meldung sollte in der Konsole protokolliert werden:

  • Der Nutzer hat die Aufforderung abgelehnt oder verworfen. Der exponentielle Cool-down-Mechanismus der API wurde ausgelöst.

Während des Tests wird die Abkühlung ausgelöst. Während der Wartezeit wird die Aufforderung für die Einmalanmeldung nicht angezeigt. Während des Testens möchten Sie das Gerät wahrscheinlich lieber zurücksetzen, als auf das automatische Zurücksetzen zu warten – es sei denn, Sie möchten wirklich einen Kaffee trinken oder nach Hause gehen und schlafen. So setzen Sie die Wartezeit zurück:

  • Klicken Sie links in der Adressleiste des Browsers auf das Symbol für Websiteinformationen.
  • drücken Sie die Schaltfläche „Berechtigungen zurücksetzen“ und
  • Aktualisieren Sie die Seite.

Nachdem Sie den Cooldown zurückgesetzt und die Seite neu geladen haben, wird die Aufforderung für die Ein-Klick-Zahlung angezeigt.

7. Fazit

In diesem Codelab haben Sie einige Dinge gelernt, z. B. wie Sie One Tap nur mit statischem HTML oder dynamisch mit JavaScript anzeigen.

Sie haben einen sehr einfachen Python-Webserver für lokale Tests eingerichtet und die erforderlichen Schritte zum Decodieren und Validieren von ID-Tokens kennengelernt.

Sie haben die häufigsten Arten der Interaktion von Nutzern mit dem One Tap-Hinweis und des Schließens des Hinweises ausprobiert und haben eine Webseite, mit der Sie das Verhalten des Hinweises debuggen können.

Glückwunsch!

Als zusätzliche Aufgabe können Sie noch einmal versuchen, One Tap in den verschiedenen unterstützten Browsern zu verwenden.

Diese Links können Ihnen bei den nächsten Schritten helfen:

Häufig gestellte Fragen