Fazer login com o Google One Tap na Web

1. Introdução

Este codelab se baseia no botão "Fazer login com o Google" para a Web. Portanto, conclua esse codelab primeiro.

Neste codelab, você vai usar a biblioteca JavaScript Google Identity Services e os comandos com um toque para adicionar o login do usuário a páginas da Web estáticas e dinâmicas usando APIs HTML e JavaScript.

Um pedido do One Tap

Também vamos configurar um endpoint de login do lado do servidor para verificar tokens de ID JWT.

Conteúdo

  • Como configurar um endpoint de login do lado do servidor para verificar tokens de ID
  • Como adicionar um pedido de um toque do Google One a uma página da Web
    • como um elemento HTML estático; e
    • dinamicamente usando JavaScript.
  • Como o pedido do One Tap se comporta

O que é necessário

  1. Conhecimento básico de HTML, CSS, JavaScript e Chrome DevTools (ou equivalente).
  2. Um lugar para editar e hospedar arquivos HTML e JavaScript.
  3. O ID do cliente obtido no codelab anterior.
  4. Um ambiente capaz de executar um app Python básico.

Vamos lá!

2. Configurar um endpoint de login

Primeiro, vamos criar um script Python que funcione como um servidor da Web básico e configurar o ambiente Python necessário para executá-lo.

Executado localmente, o script disponibiliza a página de destino e as páginas HTML estáticas e dinâmicas do One Tap para o navegador. Ele aceita solicitações POST, decodifica o JWT contido no parâmetro de credencial e valida se ele foi emitido pela plataforma OAuth do Google Identity.

Depois de decodificar e verificar um JWT, o script redireciona para a página de destino index.html para mostrar os resultados.

Copie o seguinte para um arquivo chamado 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()

Como vamos verificar se o JWT foi emitido para seu cliente usando o campo "público-alvo" (aud) do token de ID, seu app Python precisa saber qual ID do cliente está sendo usado. Para fazer isso, substitua PUT_YOUR_WEB_CLIENT_ID_HERE pelo ID do cliente usado no codelab anterior do botão "Fazer login com o Google".

Ambiente Python

Vamos configurar o ambiente para executar o script do servidor da Web.

Você vai precisar do Python 3.8 ou mais recente e de alguns pacotes para ajudar na verificação e decodificação do JWT.

$ python3 --version

Se a versão do python3 for anterior à 3.8, talvez seja necessário mudar o PATH do shell para que a versão esperada seja encontrada ou instalar uma versão mais recente do Python no sistema.

Em seguida, crie um arquivo chamado requirements.txt que liste os pacotes necessários para decodificação e verificação de JWT:

google-auth

Execute estes comandos no mesmo diretório de simple-server.py e requirements.txt para criar um ambiente virtual e instalar pacotes apenas para este app:

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

Agora inicie o servidor. Se tudo estiver funcionando corretamente, você vai ver isto:

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

Ambientes de desenvolvimento integrado baseados na nuvem

Este codelab foi projetado para ser executado em um terminal local e no localhost, mas, com algumas mudanças, pode ser usado em plataformas como Replit ou Glitch. Cada plataforma tem requisitos de configuração e padrões de ambiente Python próprios. Portanto, talvez seja necessário mudar algumas coisas, como TARGET_HTML_PAGE_URL e a configuração do Python.

Por exemplo, no Glitch, você ainda adicionaria um requirements.txt, mas também criaria um arquivo chamado start.sh para iniciar automaticamente o servidor Python:

python3 ./simple-server.py

Os URLs usados pelo script Python e pelos arquivos HTML também precisam ser atualizados para o URL externo da sua IDE do Cloud. Assim, teríamos algo como: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/". Como os arquivos HTML deste codelab também usam localhost por padrão, você precisará atualizá-los com o URL externo do Cloud IDE: data-login_uri="https://your-project-name.glitch.me/user-login".

3. Criar uma página de destino

Em seguida, vamos criar uma página de destino que mostre os resultados do login com um toque. A página mostra o token de ID JWT decodificado ou um erro. Um formulário na página também pode ser usado para enviar um JWT ao endpoint de login no nosso servidor HTTP Python, onde ele é decodificado e verificado. Ele usa um cookie de envio duplo de CSRF e um parâmetro de solicitação POST para reutilizar o mesmo endpoint de servidor user-login dos exemplos de API gsi/client HTML e JavaScript no codelab.

No terminal, salve isso em um arquivo chamado 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>

Testar o servidor da Web e a decodificação do JWT

Antes de tentar trabalhar com o One Tap, vamos verificar se o ambiente de endpoint do servidor está configurado e funcionando.

Acesse a página de destino, http://localhost:3000/, e pressione o botão Verificar JWT.

Você vai ver isto:

Verificação de JWT sem conteúdo

Ao pressionar o botão, um POST com o conteúdo do entryfield é enviado ao script Python. O script espera que um JWT codificado esteja presente no campo de entrada. Por isso, ele tenta decodificar e verificar o payload. Depois, ele redireciona de volta para a página de destino para mostrar os resultados.

Mas espera, não havia um JWT... isso não falhou? Sim, mas com cuidado!

Como o campo estava vazio, um erro é exibido. Agora, digite um texto (qualquer texto que você quiser) no campo de entrada e pressione o botão novamente. Ele falha com um erro de decodificação diferente.

Você pode colar um token de ID JWT emitido pelo Google codificado no campo de entrada e fazer com que o script Python decodifique, verifique e mostre o token para você. Também é possível usar https://jwt.io para inspecionar qualquer JWT codificado.

4. Página HTML estática

Agora vamos configurar o recurso "Um toque" para funcionar em páginas HTML sem usar JavaScript. Isso pode ser útil para sites estáticos ou para sistemas de cache e CDNs.

Comece adicionando este exemplo de código a um arquivo chamado 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>

Em seguida, abra static-page.html e substitua PUT_YOUR_WEB_CLIENT_ID_HERE pelo ID do cliente usado no codelab anterior do botão "Fazer login com o Google".

O que isso faz?

Qualquer elemento HTML com um id de g_id_onload e seus atributos de dados são usados para configurar a biblioteca dos Serviços de identidade do Google (gsi/client). Ele também mostra a solicitação do One Tap quando o documento é carregado no navegador.

O atributo data-login_uri é o URI que vai receber uma solicitação POST do navegador depois que o usuário fizer login. Essa solicitação contém o JWT codificado emitido pelo Google.

Confira o gerador de código HTML e a referência da API HTML para uma lista completa de opções do recurso "Um toque".

Login

Clique em http://localhost:3000/static-page.html.

O aviso do recurso "Um toque" vai aparecer no navegador.

Interface do One Tap

Pressione Continuar como para fazer login.

Depois do login, o Google envia uma solicitação POST ao endpoint de login do seu servidor Python. A solicitação contém um JWT codificado assinado pelo Google.

A partir daí, o servidor usa uma das chaves de assinatura públicas do Google para verificar se o Google criou e assinou o JWT. Em seguida, ele decodifica e verifica se o público-alvo corresponde ao seu ID do cliente. Em seguida, uma verificação de CSRF é feita para garantir que o valor do cookie e o valor do parâmetro de solicitação no corpo POST sejam iguais. Se não estiverem, isso é um sinal de problema.

Por fim, a página de destino mostra o JWT verificado com sucesso como uma credencial de usuário de token de ID formatada em JSON.

JWT verificado

Erros comuns

O fluxo de login pode falhar de várias maneiras. Alguns dos motivos mais comuns são:

  • data-client_id está faltando ou incorreto. Nesse caso, você vai ver um erro no console do DevTools, e a solicitação do One Tap não vai funcionar.
  • O data-login_uri não está disponível porque um URI incorreto foi inserido, o servidor da Web não foi iniciado ou está detectando a porta errada. Se isso acontecer, a solicitação de um toque vai parecer funcionar, mas um erro vai aparecer na guia "Rede" do DevTools quando a credencial for retornada.
  • O nome do host ou a porta que seu servidor da Web está usando não está listado nas Origens JavaScript autorizadas do seu ID do cliente OAuth. Você vai ver uma mensagem no console: "Ao buscar o endpoint de declaração de ID, um código de resposta HTTP 400 foi recebido.". Se isso acontecer durante este codelab, verifique se http://localhost/ e http://localhost:3000 estão listados.

5. Página dinâmica

Agora vamos mostrar o recurso "Um toque" usando uma chamada JavaScript. Neste exemplo, sempre vamos mostrar o toque único quando a página for carregada, mas você pode escolher mostrar a solicitação apenas quando necessário. Por exemplo, você pode verificar se a sessão do usuário tem mais de 28 dias e mostrar o pedido de login novamente.

Adicione este exemplo de código a um arquivo chamado 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>

Abra dynamic-page.html e substitua PUT_YOUR_WEB_CLIENT_ID_HERE pelo ID do cliente usado no codelab anterior do botão "Fazer login com o Google".

Esse código é uma mistura de HTML e JavaScript e faz várias coisas:

  • configura a biblioteca dos Serviços de identidade do Google (gsi/client) chamando google.accounts.id.initialize();
  • gera um cookie de falsificação de solicitação entre sites (CSRF),
  • adiciona um manipulador de callback para receber o JWT codificado do Google e enviá-lo usando um POST de formulário para o endpoint /user-login do script Python; e
  • mostra a solicitação de um toque usando google.accounts.id.prompt().

A lista completa de configurações do One Tap pode ser encontrada na referência da API JavaScript.

Vamos fazer login!

Abra http://localhost:3000/dynamic-page.html no navegador.

O comportamento da solicitação de um toque é o mesmo do cenário de HTML estático, exceto que esta página define um manipulador de callback JavaScript para criar um cookie CSRF, receber o JWT do Google e enviá-lo por POST ao endpoint user-login do servidor Python. A API HTML realiza essas etapas automaticamente para você.

Guia &quot;Rede&quot; do Chrome

6. Comportamentos de comandos

Vamos testar algumas coisas com o toque único, porque, ao contrário do botão, a solicitação nem sempre é exibida. Ele pode ser dispensado, fechado ou desativado pelo navegador e pelo usuário.

Primeiro, salve este código em um arquivo chamado 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>

Em seguida, abra prompt-outcomes.html, substitua PUT_YOUR_WEB_CLIENT_ID_HERE pelo seu ID do cliente e salve o arquivo.

No navegador, abra http://localhost:3000/prompt-outcomes.html.

Cliques na página

Clique em qualquer lugar fora da solicitação do One Tap. Você vai ver "A solicitação foi cancelada." registrado na página e no console.

Fazer login

Em seguida, faça login normalmente. Você vai encontrar atualizações de registro e notificação que podem ser usadas para acionar algo como estabelecer ou atualizar uma sessão de usuário.

Fechar o comando

Agora, atualize a página e, depois que o One Tap aparecer, pressione o "X" na barra de título. Esta mensagem precisa ser registrada no console:

  • "O usuário recusou ou dispensou o aviso. O resfriamento exponencial da API foi acionado."

Durante o teste, você vai acionar o resfriamento. Durante o período de espera, a solicitação de um toque não é exibida. Durante o teste, talvez seja melhor redefinir o dispositivo em vez de esperar que ele seja redefinido automaticamente, a menos que você queira tomar um café ou ir para casa dormir um pouco. Para redefinir o período de espera:

  • Clique no ícone "Informações do site" no lado esquerdo da barra de endereço do navegador.
  • pressione o botão "Redefinir permissões" e
  • atualize a página.

Depois de redefinir o período de espera e recarregar a página, o aviso do One Tap vai aparecer.

7. Conclusão

Neste codelab, você aprendeu algumas coisas, como exibir o recurso "Um toque" usando apenas HTML estático ou dinamicamente com JavaScript.

Você configurou um servidor da Web Python muito básico para testes locais e aprendeu as etapas necessárias para decodificar e validar tokens de ID.

Você já testou as maneiras mais comuns de os usuários interagirem com o pedido de um toque e o dispensarem, além de ter uma página da Web que pode ser usada para depurar o comportamento do pedido.

Parabéns!

Para um crédito extra, tente voltar e usar o recurso com um toque nos diferentes navegadores compatíveis.

Estes links podem ajudar com as próximas etapas:

Perguntas frequentes