Perintah Login dengan Google Sekali Ketuk untuk Web

1. Pengantar

Codelab ini dibuat berdasarkan codelab Tombol Login dengan Google untuk Web, jadi pastikan Anda menyelesaikannya terlebih dahulu.

Dalam codelab ini, Anda akan menggunakan library JavaScript Google Identity Services dan dialog Sekali Ketuk untuk menambahkan login pengguna ke halaman web statis dan dinamis menggunakan API HTML dan JavaScript.

Dialog Sekali Ketuk

Kita juga akan menyiapkan endpoint login sisi server untuk memverifikasi token ID JWT.

Yang akan Anda pelajari

  • Cara menyiapkan endpoint login sisi server untuk memverifikasi token ID
  • Cara menambahkan dialog sekali ketuk Google One ke halaman web
    • sebagai elemen HTML statis, dan
    • secara dinamis menggunakan JavaScript.
  • Perilaku perintah Sekali Ketuk

Yang Anda butuhkan

  1. Pengetahuan dasar tentang HTML, CSS, JavaScript, dan Chrome DevTools (atau yang setara).
  2. Tempat untuk mengedit dan menghosting file HTML dan JavaScript.
  3. Client ID yang diperoleh dalam codelab sebelumnya.
  4. Lingkungan yang dapat menjalankan aplikasi Python dasar.

Ayo mulai!

2. Menyiapkan endpoint login

Pertama, kita akan membuat skrip Python yang berfungsi sebagai server web dasar dan menyiapkan lingkungan Python yang diperlukan untuk menjalankannya.

Saat dijalankan secara lokal, skrip ini menayangkan halaman landing, HTML statis, dan halaman login sekali ketuk dinamis ke browser. API ini menerima permintaan POST, mendekode JWT yang ada dalam parameter kredensial, dan memvalidasi bahwa JWT tersebut dikeluarkan oleh platform OAuth Google Identity.

Setelah mendekode dan memverifikasi JWT, skrip akan dialihkan ke halaman landing index.html untuk menampilkan hasilnya.

Salin kode ini ke dalam file bernama 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()

Karena kita akan memverifikasi bahwa JWT dikeluarkan untuk klien Anda menggunakan kolom audiens (aud) token ID, aplikasi Python Anda perlu mengetahui Client ID mana yang sedang digunakan. Untuk melakukannya, ganti PUT_YOUR_WEB_CLIENT_ID_HERE dengan ID Klien yang Anda gunakan di codelab tombol Login Dengan Google sebelumnya.

Lingkungan Python

Siapkan lingkungan untuk menjalankan skrip server web.

Anda memerlukan Python 3.8 atau yang lebih baru beserta beberapa paket untuk membantu verifikasi dan dekode JWT.

$ python3 --version

Jika versi python3 Anda lebih rendah dari 3.8, Anda mungkin perlu mengubah PATH shell agar versi yang diharapkan ditemukan atau menginstal versi Python yang lebih baru di sistem Anda.

Selanjutnya, buat file bernama requirements.txt yang mencantumkan paket yang kita butuhkan untuk dekode dan verifikasi JWT:

google-auth

Jalankan perintah ini di direktori yang sama dengan simple-server.py dan requirements.txt untuk membuat lingkungan virtual dan menginstal paket hanya untuk aplikasi ini:

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

Sekarang mulai server dan jika semuanya berfungsi dengan baik, Anda akan melihat ini:

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

IDE berbasis cloud

Codelab ini dirancang untuk dijalankan di terminal lokal dan localhost, tetapi dengan beberapa perubahan, codelab ini dapat digunakan di platform seperti Replit atau Glitch. Setiap platform memiliki persyaratan penyiapan dan default lingkungan Python-nya sendiri, jadi Anda mungkin perlu mengubah beberapa hal seperti TARGET_HTML_PAGE_URL dan penyiapan Python.

Misalnya, di Glitch, Anda tetap akan menambahkan requirements.txt, tetapi juga membuat file bernama start.sh untuk memulai server Python secara otomatis:

python3 ./simple-server.py

URL yang digunakan oleh skrip Python dan file HTML juga perlu diupdate ke URL eksternal Cloud IDE Anda. Jadi, kita akan memiliki sesuatu seperti ini: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/" dan karena file HTML di seluruh codelab ini juga secara default menggunakan localhost, Anda harus memperbaruinya dengan URL Cloud IDE eksternal: data-login_uri="https://your-project-name.glitch.me/user-login".

3. Membuat halaman landing

Selanjutnya, kita akan membuat halaman landing yang menampilkan hasil login dengan Login Sekali Ketuk. Halaman ini menampilkan token ID JWT yang didekode atau error. Formulir di halaman juga dapat digunakan untuk mengirim JWT ke endpoint login di server HTTP Python kami, tempat JWT didekode dan diverifikasi. Contoh ini menggunakan cookie pengiriman ganda CSRF dan Parameter Permintaan POST sehingga dapat menggunakan kembali endpoint server user-login yang sama dengan contoh gsi/client HTML dan JavaScript API dalam codelab.

Di terminal Anda, simpan kode ini dalam file bernama 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>

Menguji server web dan decoding JWT

Sebelum mencoba dan menggunakan Login Sekali Ketuk, kita akan memastikan lingkungan endpoint server disiapkan dan berfungsi.

Buka halaman landing, http://localhost:3000/, lalu tekan tombol Verify JWT.

Anda akan melihat ini

Verifikasi JWT tanpa konten

Menekan tombol akan mengirimkan POST dengan konten entryfield ke skrip Python. Skrip mengharapkan JWT yang dienkode ada di kolom entri, sehingga mencoba mendekode dan memverifikasi payload. Setelah itu, halaman akan dialihkan kembali ke halaman landing untuk menampilkan hasilnya.

Tapi tunggu, tidak ada JWT... bukankah ini akan gagal? Ya, tetapi dengan sopan.

Karena kolom kosong, error akan ditampilkan. Sekarang coba masukkan beberapa teks (teks apa pun yang Anda inginkan) di kolom entri, lalu tekan tombol lagi. Proses ini gagal dengan error decoding yang berbeda.

Anda dapat menempelkan token ID JWT yang dikeluarkan Google dan dienkode di kolom entri, lalu skrip Python akan mendekode, memverifikasi, dan menampilkannya untuk Anda... atau Anda dapat menggunakan https://jwt.io untuk memeriksa JWT yang dienkode.

4. Halaman HTML statis

Oke, sekarang kita akan menyiapkan Login Sekali Ketuk agar berfungsi di halaman HTML tanpa menggunakan JavaScript. Hal ini dapat berguna untuk situs statis atau untuk sistem caching dan CDN.

Mulailah dengan menambahkan contoh kode ini ke dalam file bernama 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>

Selanjutnya, buka static-page.html dan ganti PUT_YOUR_WEB_CLIENT_ID_HERE dengan Client ID yang Anda gunakan di codelab tombol Login Dengan Google sebelumnya.

Jadi, apa fungsinya?

Elemen HTML apa pun dengan id g_id_onload dan atribut datanya digunakan untuk mengonfigurasi library Layanan Identitas Google (gsi/client). Elemen ini juga menampilkan dialog sekali ketuk saat dokumen dimuat di browser.

Atribut data-login_uri adalah URI yang akan menerima permintaan POST dari browser setelah pengguna login. Permintaan ini berisi JWT yang dienkode dan dikeluarkan oleh Google.

Lihat generator kode HTML dan referensi API HTML untuk mengetahui daftar lengkap opsi Login Sekali Klik.

Login

Klik http://localhost:3000/static-page.html.

Anda akan melihat dialog login Sekali Ketuk ditampilkan di browser Anda.

UI Sekali Ketuk

Tekan Lanjutkan sebagai untuk login.

Setelah login, Google akan mengirim permintaan POST ke endpoint login server Python Anda. Permintaan berisi JWT yang dienkode dan ditandatangani oleh Google.

Dari sana, server menggunakan salah satu kunci penandatanganan publik Google untuk memverifikasi bahwa Google membuat dan menandatangani JWT. Kemudian, API akan mendekode dan memverifikasi apakah audiens cocok dengan Client ID Anda. Selanjutnya, pemeriksaan CSRF dilakukan untuk memastikan nilai Cookie dan nilai parameter permintaan di isi POST sama. Jika tidak, itu adalah tanda pasti adanya masalah.

Terakhir, halaman landing menampilkan JWT yang berhasil diverifikasi sebagai kredensial pengguna token ID berformat JSON.

JWT terverifikasi

Error yang biasa terjadi

Ada beberapa penyebab alur login gagal. Beberapa alasan yang paling umum adalah:

  • data-client_id tidak ada atau salah. Dalam hal ini, Anda akan melihat error di konsol DevTools dan dialog login Sekali Ketuk tidak akan berfungsi.
  • data-login_uri tidak tersedia karena URI yang salah dimasukkan, server web tidak dimulai, atau memproses port yang salah. Jika hal ini terjadi, perintah Satu Ketuk akan tampak berfungsi, tetapi error akan terlihat di tab jaringan DevTools saat kredensial ditampilkan.
  • Nama host atau port yang digunakan server web Anda tidak tercantum dalam Authorized JavaScript origins untuk Client ID OAuth Anda. Anda akan melihat pesan konsol: "Saat mengambil endpoint pernyataan ID, kode respons HTTP 400 diterima.". Jika Anda melihatnya selama codelab ini, periksa apakah http://localhost/ dan http://localhost:3000 tercantum.

5. Halaman dinamis

Sekarang kita akan menampilkan Login Sekali Ketuk menggunakan panggilan JavaScript. Dalam contoh ini, kita akan selalu menampilkan Login Sekali Ketuk saat halaman dimuat, tetapi Anda dapat memilih untuk menampilkan dialog hanya saat diperlukan. Misalnya, Anda dapat memeriksa apakah sesi pengguna sudah lebih dari 28 hari dan menampilkan perintah login lagi.

Tambahkan contoh kode ini ke dalam file bernama 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>

Buka dynamic-page.html dan ganti PUT_YOUR_WEB_CLIENT_ID_HERE dengan ID Klien yang Anda gunakan di codelab tombol Login Dengan Google sebelumnya.

Kode ini adalah campuran HTML dan JavaScript, yang melakukan beberapa hal:

  • mengonfigurasi library Google Identity Services (gsi/client) dengan memanggil google.accounts.id.initialize(),
  • membuat cookie Pemalsuan Permintaan Lintas Situs (CSRF),
  • menambahkan pengendali callback untuk menerima JWT yang dienkode dari Google dan mengirimkannya menggunakan POST formulir ke endpoint /user-login skrip Python kami, dan
  • menampilkan perintah Satu Ketuk menggunakan google.accounts.id.prompt().

Daftar lengkap setelan Login Sekali Ketuk dapat ditemukan di referensi JavaScript API.

Ayo login!

Buka http://localhost:3000/dynamic-page.html di browser Anda.

Perilaku perintah Login Sekali Ketuk sama dengan skenario HTML statis, kecuali halaman ini menentukan pengendali callback JavaScript untuk membuat cookie CSRF, menerima JWT dari Google, dan mempostingnya ke endpoint user-login server Python. HTML API melakukan langkah-langkah ini secara otomatis untuk Anda.

Tab Jaringan Chrome

6. Perilaku perintah

Jadi, mari kita coba beberapa hal dengan Satu Ketuk, karena tidak seperti tombol, perintahnya tidak selalu ditampilkan. Notifikasi ini dapat ditutup, dihentikan, atau dinonaktifkan oleh browser dan pengguna.

Pertama, simpan kode ini dalam file bernama 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>

Selanjutnya, buka prompt-outcomes.html, ganti PUT_YOUR_WEB_CLIENT_ID_HERE dengan Client ID Anda, lalu simpan file.

Di browser Anda, buka http://localhost:3000/prompt-outcomes.html

Klik halaman

Mulai dengan mengklik di mana saja di luar perintah Satu Ketuk. Anda akan melihat "Permintaan telah dibatalkan." yang dicatat ke halaman dan konsol.

Login

Selanjutnya, cukup login seperti biasa. Anda akan melihat pembaruan logging dan notifikasi yang dapat digunakan untuk memicu sesuatu seperti membuat atau memuat ulang sesi pengguna.

Tutup perintah

Sekarang, muat ulang halaman dan setelah Login Sekali Ketuk ditampilkan, tekan'X' di kolom judulnya. Pesan ini harus dicatat ke konsol:

  • "Pengguna menolak atau menutup dialog. Pendinginan eksponensial API dipicu."

Selama pengujian, Anda akan memicu pendinginan. Selama periode tunggu, perintah Satu Ketuk tidak ditampilkan. Selama pengujian, Anda mungkin ingin meresetnya daripada menunggu hingga direset secara otomatis... kecuali jika Anda benar-benar ingin pergi minum kopi atau pulang ke rumah dan tidur. Untuk mereset periode tunggu:

  • Klik ikon "informasi situs" di sisi kiri kolom URL browser,
  • tekan tombol "Reset Izin", dan
  • muat ulang halaman.

Setelah mereset periode tunggu dan memuat ulang halaman, dialog login Sekali Ketuk akan ditampilkan.

7. Kesimpulan

Jadi, dalam codelab ini, Anda telah mempelajari beberapa hal, seperti cara menampilkan Login Sekali Klik hanya menggunakan HTML statis atau secara dinamis dengan JavaScript.

Anda telah menyiapkan server web Python yang sangat mendasar untuk pengujian lokal dan mempelajari langkah-langkah yang diperlukan untuk mendekode dan memvalidasi token ID.

Anda telah mencoba cara paling umum pengguna berinteraksi dengan dan menutup perintah Login Sekali Ketuk serta memiliki halaman web yang dapat digunakan untuk men-debug perilaku perintah.

Selamat!

Untuk mendapatkan nilai tambahan, coba kembali dan gunakan Login Sekali Ketuk di browser berbeda yang mendukungnya.

Link berikut dapat membantu Anda melakukan langkah berikutnya:

Pertanyaan umum (FAQ)