Prompt di accesso con Google One Tap per il web

1. Introduzione

Questo codelab si basa sul codelab Pulsante Accedi con Google per il web, quindi assicurati di completarlo prima.

In questo codelab, utilizzerai la libreria JavaScript Servizi di identità Google e i prompt One Tap per aggiungere l'accesso degli utenti a pagine web statiche e dinamiche utilizzando le API HTML e JavaScript.

Un prompt One Tap

Configureremo anche un endpoint di accesso lato server per verificare i token ID JWT.

Obiettivi didattici

  • Come configurare un endpoint di accesso lato server per verificare i token ID
  • Come aggiungere una richiesta One Tap di Google a una pagina web
    • come elemento HTML statico e
    • in modo dinamico utilizzando JavaScript.
  • Comportamento della richiesta One Tap

Che cosa ti serve

  1. Conoscenza di base di HTML, CSS, JavaScript e Chrome DevTools (o equivalente).
  2. Uno spazio per modificare e ospitare file HTML e JavaScript.
  3. L'ID client ottenuto nel codelab precedente.
  4. Un ambiente in grado di eseguire un'app Python di base.

Iniziamo.

2. Configurare un endpoint di accesso

Innanzitutto, creeremo uno script Python che funge da server web di base e configureremo l'ambiente Python necessario per eseguirlo.

Se eseguito localmente, lo script fornisce al browser la pagina di destinazione e le pagine HTML statiche e One Tap dinamiche. Accetta le richieste POST, decodifica il JWT contenuto nel parametro delle credenziali e verifica che sia stato emesso dalla piattaforma OAuth di Google Identity.

Dopo aver decodificato e verificato un JWT, lo script reindirizza alla pagina di destinazione index.html per visualizzare i risultati.

Copia questo codice in un file denominato 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()

Poiché verificheremo che il JWT sia stato emesso per il tuo client utilizzando il campo audience (aud) del token ID, la tua app Python deve sapere quale ID client viene utilizzato. Per farlo, sostituisci PUT_YOUR_WEB_CLIENT_ID_HERE con l'ID client che hai utilizzato nel codelab precedente del pulsante Accedi con Google.

Ambiente Python

Configuriamo l'ambiente per eseguire lo script del server web.

Avrai bisogno di Python 3.8 o versioni successive, oltre ad alcuni pacchetti per facilitare la verifica e la decodifica di JWT.

$ python3 --version

Se la tua versione di python3 è precedente alla 3.8, potrebbe essere necessario modificare il PATH della shell in modo che venga trovata la versione prevista o installare una versione più recente di Python sul sistema.

Successivamente, crea un file denominato requirements.txt che elenca i pacchetti necessari per la decodifica e la verifica JWT:

google-auth

Esegui questi comandi nella stessa directory di simple-server.py e requirements.txt per creare un ambiente virtuale e installare i pacchetti solo per questa app:

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

Ora avvia il server e, se tutto funziona correttamente, vedrai questo messaggio:

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

IDE basati su cloud

Questo codelab è stato progettato per essere eseguito in un terminale locale e su localhost, ma con alcune modifiche può essere utilizzato su piattaforme come Replit o Glitch. Ogni piattaforma ha i propri requisiti di configurazione e impostazioni predefinite dell'ambiente Python, quindi probabilmente dovrai modificare alcune cose come TARGET_HTML_PAGE_URL e la configurazione di Python.

Ad esempio, su Glitch devi comunque aggiungere un requirements.txt, ma anche creare un file denominato start.sh per avviare automaticamente il server Python:

python3 ./simple-server.py

Anche gli URL utilizzati dallo script Python e dai file HTML devono essere aggiornati all'URL esterno del tuo Cloud IDE. Quindi avremo qualcosa di simile a: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/" e poiché i file HTML in questo codelab utilizzano per impostazione predefinita anche localhost, dovrai aggiornarli con l'URL esterno di Cloud IDE: data-login_uri="https://your-project-name.glitch.me/user-login".

3. Creare una pagina di destinazione

Successivamente, creeremo una pagina di destinazione che mostri i risultati dell'accesso con One Tap. La pagina mostra il token ID JWT decodificato o un errore. Un modulo nella pagina può essere utilizzato anche per inviare un JWT all'endpoint di accesso sul nostro server HTTP Python, dove viene decodificato e verificato. Utilizza un cookie CSRF double-submit e un parametro di richiesta POST in modo da poter riutilizzare lo stesso endpoint server user-login degli esempi di API HTML e JavaScript nel codelab.gsi/client

Nel terminale, salva questo codice in un file denominato 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>

Testa il server web e la decodifica JWT

Prima di provare a utilizzare One Tap, ci assicureremo che l'ambiente dell'endpoint server sia configurato e funzionante.

Vai alla pagina di destinazione, http://localhost:3000/, e premi il pulsante Verifica JWT.

Dovresti vedere questo

Verifica JWT senza contenuti

Se premi il pulsante, viene inviato un POST con i contenuti del campo di inserimento allo script Python. Lo script prevede che nel campo di inserimento sia presente un JWT codificato, quindi tenta di decodificare e verificare il payload. Successivamente, viene reindirizzato alla pagina di destinazione per visualizzare i risultati.

Ma aspetta, non c'era un JWT… non è andato tutto storto? Sì, ma con grazia.

Poiché il campo era vuoto, viene visualizzato un errore. Ora prova a inserire un testo qualsiasi nel campo di inserimento e a premere di nuovo il pulsante. L'operazione non va a buon fine a causa di un errore di decodifica diverso.

Puoi incollare un token ID JWT emesso da Google codificato nel campo di inserimento e fare in modo che lo script Python lo decodifichi, verifichi e visualizzi... oppure puoi utilizzare https://jwt.io per esaminare qualsiasi JWT codificato.

4. Pagina HTML statica

Ok, ora configureremo One Tap in modo che funzioni sulle pagine HTML senza utilizzare JavaScript. Questo può essere utile per i siti statici o per i sistemi di memorizzazione nella cache e le CDN.

Inizia aggiungendo questo codice campione a un file denominato static-page.html:

<!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>

Successivamente, apri static-page.html e sostituisci PUT_YOUR_WEB_CLIENT_ID_HERE con l'ID client che hai utilizzato nel codelab precedente del pulsante Accedi con Google.

Che cosa fa?

Qualsiasi elemento HTML con un id di g_id_onload e i relativi attributi di dati vengono utilizzati per configurare la libreria Google Identity Services (gsi/client). Inoltre, viene visualizzata la richiesta One Tap quando il documento viene caricato nel browser.

L'attributo data-login_uri è l'URI che riceverà una richiesta POST dal browser dopo che l'utente avrà eseguito l'accesso. Questa richiesta contiene il JWT codificato emesso da Google.

Consulta il generatore di codice HTML e il riferimento API HTML per un elenco completo delle opzioni di One Tap.

Accedi

Fai clic su http://localhost:3000/static-page.html.

Dovresti visualizzare il prompt One Tap nel browser.

UI di One Tap

Premi Continua come per accedere.

Dopo l'accesso, Google invia una richiesta POST all'endpoint di accesso del tuo server Python. La richiesta contiene un JWT codificato firmato da Google.

Da qui, il server utilizza una delle chiavi di firma pubbliche di Google per verificare che Google abbia creato e firmato il JWT. Successivamente, decodifica e verifica che il segmento di pubblico corrisponda al tuo ID cliente. Successivamente, viene eseguito un controllo CSRF per verificare che il valore del cookie e il valore del parametro di richiesta nel corpo POST siano uguali. In caso contrario, è un segnale sicuro di problemi.

Infine, la pagina di destinazione mostra il JWT verificato correttamente come credenziale utente del token ID formattata in JSON.

JWT verificato

Errori comuni

Esistono diversi modi in cui il flusso di accesso può non andare a buon fine. Alcuni dei motivi più comuni sono:

  • data-client_id manca o è errato. In questo caso, nella console DevTools viene visualizzato un errore e il prompt Accedi con un tocco non funziona.
  • data-login_uri non è disponibile perché è stato inserito un URI errato, il server web non è stato avviato o è in ascolto sulla porta errata. In questo caso, il prompt One Tap sembra funzionare, ma nella scheda di rete di DevTools viene visualizzato un errore quando vengono restituite le credenziali.
  • Il nome host o la porta utilizzati dal server web non sono elencati in Origini JavaScript autorizzate per l'ID client OAuth. Viene visualizzato il messaggio della console: "When fetching the ID assertion endpoint, a 400 HTTP response code was received.". Se visualizzi questo messaggio durante questo codelab, controlla che siano elencati sia http://localhost/ sia http://localhost:3000.

5. Pagina dinamica

Ora mostreremo One Tap utilizzando una chiamata JavaScript. In questo esempio, mostreremo sempre One Tap al caricamento della pagina, ma potresti scegliere di mostrare la richiesta solo quando necessario. Ad esempio, puoi verificare se la sessione dell'utente ha più di 28 giorni e visualizzare nuovamente la richiesta di accesso.

Aggiungi questo codice di esempio a un file denominato dynamic-page.html.

<!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>

Apri dynamic-page.html e sostituisci PUT_YOUR_WEB_CLIENT_ID_HERE con l'ID client che hai utilizzato nel codelab precedente del pulsante Accedi con Google.

Questo codice è un mix di HTML e JavaScript e svolge diverse funzioni:

  • configura la libreria Google Identity Services (gsi/client) chiamando google.accounts.id.initialize(),
  • genera un cookie Cross-Site Request Forgery (CSRF),
  • aggiunge un gestore di callback per ricevere il JWT codificato da Google e inviarlo utilizzando un POST del modulo all'endpoint dello script Python /user-login e
  • visualizza la richiesta One Tap utilizzando google.accounts.id.prompt().

Un elenco completo delle impostazioni di One Tap è disponibile nei riferimenti per l'API JavaScript.

Accediamo.

Apri http://localhost:3000/dynamic-page.html nel browser.

Il comportamento del prompt One Tap è lo stesso dello scenario HTML statico, tranne per il fatto che questa pagina definisce un gestore di callback JavaScript per creare un cookie CSRF, ricevere il JWT da Google e inviarlo tramite POST all'endpoint user-login del server Python. L'API HTML esegue automaticamente questi passaggi.

Scheda Rete di Chrome

6. Comportamenti dei prompt

Proviamo alcune cose con One Tap, perché a differenza del pulsante, il prompt non viene sempre visualizzato. Può essere chiuso o disattivato dal browser e dall'utente.

Innanzitutto, salva questo codice in un file denominato 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>

Poi apri prompt-outcomes.html, sostituisci PUT_YOUR_WEB_CLIENT_ID_HERE con il tuo ID client e salva il file.

Nel browser, apri http://localhost:3000/prompt-outcomes.html

Clic sulla pagina

Inizia facendo clic in un punto qualsiasi al di fuori del prompt One Tap. Dovresti visualizzare il messaggio "La richiesta è stata interrotta." registrato sia nella pagina che nella console.

Accedi

Dopodiché, accedi normalmente. Vedrai aggiornamenti di logging e notifiche che possono essere utilizzati per attivare un'azione, ad esempio l'apertura o l'aggiornamento di una sessione utente.

Chiudi il prompt

Ora ricarica la pagina e, dopo che viene visualizzato One Tap, premi la "X" nella barra del titolo. Questo messaggio deve essere registrato nella console:

  • "L'utente ha rifiutato o ignorato la richiesta. API exponential cool down triggered."

Durante il test, attiverai il raffreddamento. Durante il periodo di attesa, la richiesta One Tap non viene visualizzata. Durante i test, probabilmente vorrai ripristinarlo anziché attendere che si ripristini automaticamente, a meno che tu non voglia davvero andare a prendere un caffè o tornare a casa a dormire. Per reimpostare il periodo di raffreddamento:

  • Fai clic sull'icona"Informazioni sul sito" sul lato sinistro della barra degli indirizzi del browser.
  • premi il pulsante "Reimposta autorizzazioni" e
  • ricarica la pagina.

Dopo aver reimpostato il periodo di attesa e ricaricato la pagina, verrà visualizzato il prompt One Tap.

7. Conclusione

In questo codelab hai imparato alcune cose, ad esempio come visualizzare One Tap utilizzando solo HTML statico o in modo dinamico con JavaScript.

Hai configurato un server web Python molto semplice per i test locali e hai appreso i passaggi necessari per decodificare e convalidare i token ID.

Hai provato i modi più comuni in cui gli utenti interagiscono con il prompt One Tap e lo chiudono e hai una pagina web che può essere utilizzata per eseguire il debug del comportamento del prompt.

Complimenti!

Per un ulteriore credito, prova a tornare indietro e a utilizzare One Tap nei diversi browser che supporta.

Questi link possono aiutarti con i passaggi successivi:

Domande frequenti