Web için Google One Tap istemiyle oturum açma

1. Giriş

Bu codelab, Web için Google ile oturum açma düğmesi codelab'i temel aldığından önce bu codelab'i tamamladığınızdan emin olun.

Bu codelab'de, HTML ve JavaScript API'lerini kullanarak statik ve dinamik web sayfalarına kullanıcı oturum açma özelliği eklemek için Google Kimlik Hizmetleri JavaScript kitaplığını ve One Tap istemlerini kullanacaksınız.

One Tap istemi

Ayrıca, JWT kimlik jetonlarını doğrulamak için sunucu tarafında bir giriş uç noktası da ayarlayacağız.

Neler öğreneceksiniz?

  • Kimlik jetonlarını doğrulamak için sunucu tarafı giriş uç noktası oluşturma
  • Web sayfasına Google ile oturum açarken One Tap istemi ekleme
    • statik bir HTML öğesi olarak ve
    • JavaScript kullanarak dinamik olarak.
  • One Tap isteminin davranışı

İhtiyacınız olanlar

  1. HTML, CSS, JavaScript ve Chrome Geliştirici Araçları (veya benzeri) hakkında temel bilgi.
  2. HTML ve JavaScript dosyalarını düzenleyip barındırabileceğiniz bir yer.
  3. Önceki codelab'de edinilen istemci kimliği.
  4. Temel bir Python uygulamasını çalıştırabilen bir ortam.

Haydi başlayalım!

2. Giriş uç noktası ayarlama

İlk olarak, temel bir web sunucusu görevi gören bir Python komut dosyası oluşturup bunu çalıştırmak için gereken Python ortamını ayarlayacağız.

Yerel olarak çalıştırılan komut dosyası, açılış sayfasını, statik HTML'yi ve dinamik tek dokunuşla ödeme sayfalarını tarayıcıya sunar. POST isteklerini kabul eder, kimlik bilgisi parametresinde bulunan JWT'yi çözer ve Google Identity'nin OAuth platformu tarafından verildiğini doğrular.

Bir JWT'nin kodu çözülüp doğrulandıktan sonra komut dosyası, sonuçları göstermek için index.html açılış sayfasına yönlendirir.

Bu kodu simple-server.py adlı bir dosyaya kopyalayın:

"""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()

JWT'nin, kimlik jetonunun kitle (aud) alanı kullanılarak istemcinize verildiğini doğrulayacağımız için Python uygulamanızın hangi istemci kimliğinin kullanıldığını bilmesi gerekir. Bunu yapmak için PUT_YOUR_WEB_CLIENT_ID_HERE yerine, önceki Google ile Oturum Açma düğmesi codelab'inde kullandığınız istemci kimliğini girin.

Python ortamı

Web sunucusu komut dosyasını çalıştırmak için ortamı ayarlayalım.

JWT doğrulama ve kod çözme işlemlerinde yardımcı olması için Python 3.8 veya sonraki bir sürümün yanı sıra birkaç paket gerekir.

$ python3 --version

Python3 sürümünüz 3.8'den düşükse beklenen sürümün bulunması için kabuk PATH'inizi değiştirmeniz veya sisteminize Python'ın daha yeni bir sürümünü yüklemeniz gerekebilir.

Ardından, JWT kod çözme ve doğrulama için ihtiyacımız olan paketlerin listelendiği requirements.txt adlı bir dosya oluşturun:

google-auth

Sanal ortam oluşturmak ve paketleri yalnızca bu uygulama için yüklemek üzere simple-server.py ve requirements.txt ile aynı dizinde aşağıdaki komutları çalıştırın:

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

Şimdi sunucuyu başlatın. Her şey düzgün çalışıyorsa şu mesajı görürsünüz:

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

Bulut tabanlı IDE'ler

Bu codelab, yerel bir terminalde ve localhost'ta çalışacak şekilde tasarlanmıştır ancak bazı değişikliklerle Replit veya Glitch gibi platformlarda da kullanılabilir. Her platformun kendi kurulum şartları ve varsayılan Python ortamı vardır. Bu nedenle, TARGET_HTML_PAGE_URL ve Python kurulumu gibi birkaç şeyi değiştirmeniz gerekebilir.

Örneğin, Glitch'te requirements.txt eklemeye devam edersiniz ancak Python sunucusunu otomatik olarak başlatmak için start.sh adlı bir dosya da oluşturursunuz:

python3 ./simple-server.py

Python komut dosyası ve HTML dosyaları tarafından kullanılan URL'lerin de Cloud IDE'nizin harici URL'siyle güncellenmesi gerekir. Bu nedenle, TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/" gibi bir şey elde ederiz. Bu codelab'deki HTML dosyaları da varsayılan olarak localhost'u kullandığından bunları harici Cloud IDE URL'siyle güncellemeniz gerekir: data-login_uri="https://your-project-name.glitch.me/user-login".

3. Açılış sayfası oluşturma

Ardından, tek dokunmayla oturum açma sonuçlarını gösteren bir açılış sayfası oluşturacağız. Sayfada, kodu çözülmüş JWT kimlik jetonu veya bir hata gösterilir. Sayfadaki bir form, JWT'yi Python HTTP sunucumuzdaki giriş uç noktasına göndermek için de kullanılabilir. Burada JWT'nin kodu çözülür ve doğrulanır. Aynı user-login sunucu uç noktasını, codelab'deki gsi/client HTML ve JavaScript API örnekleriyle birlikte yeniden kullanabilmek için CSRF çift gönderme çerezi ve POST isteği parametresi kullanır.

Terminalinizde bunu index.html adlı bir dosyaya kaydedin:

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

Web sunucusunu ve JWT kod çözme işlemini test etme

Tek Dokunma ile çalışmayı denemeden önce sunucu uç nokta ortamının ayarlandığından ve çalıştığından emin olacağız.

http://localhost:3000/ açılış sayfasına gidin ve JWT'yi Doğrula düğmesine basın.

Aşağıdaki bilgileri görürsünüz:

İçerik olmadan JWT doğrulama

Düğmeye basıldığında, giriş alanının içeriğiyle birlikte Python komut dosyasına bir POST isteği gönderilir. Komut dosyası, giriş alanında kodlanmış bir JWT olmasını beklediği için yükü çözmeye ve doğrulamaya çalışır. Ardından, sonuçları göstermek için açılış sayfasına geri yönlendirilir.

Ancak JWT yoktu. Bu işlem başarısız olmadı mı? Evet, ama zarif bir şekilde!

Alan boş olduğundan bir hata gösterilir. Şimdi giriş alanına biraz metin (istediğiniz herhangi bir metin) girip düğmeye tekrar basın. Farklı bir kod çözme hatasıyla başarısız oluyor.

Giriş alanına Google tarafından verilmiş kodlanmış bir JWT kimlik jetonu yapıştırabilir ve Python komut dosyasının bunu sizin için kodunu çözmesini, doğrulamasını ve görüntülemesini sağlayabilirsiniz. Alternatif olarak, kodlanmış JWT'leri incelemek için https://jwt.io adresini kullanabilirsiniz.

4. Statik HTML sayfası

Şimdi de JavaScript kullanmadan HTML sayfalarında çalışacak şekilde tek dokunma özelliğini ayarlayacağız. Bu, statik siteler veya önbelleğe alma sistemleri ve CDN'ler için yararlı olabilir.

İlk olarak bu kod örneğini static-page.html adlı bir dosyaya ekleyin:

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

Ardından static-page.html dosyasını açın ve PUT_YOUR_WEB_CLIENT_ID_HERE yerine önceki Google ile oturum açma düğmesi codelab'inde kullandığınız istemci kimliğini girin.

Peki bu ne işe yarar?

id değeri g_id_onload olan tüm HTML öğeleri ve veri özellikleri, Google Kimlik Hizmetleri kitaplığını (gsi/client) yapılandırmak için kullanılır. Ayrıca, belge tarayıcıya yüklendiğinde tek dokunma istemini gösterir.

data-login_uri özelliği, kullanıcı oturum açtıktan sonra tarayıcıdan POST isteği alacak URI'dir. Bu istek, Google tarafından verilen kodlanmış JWT'yi içerir.

Tek dokunma seçeneklerinin tam listesi için HTML kodu oluşturma aracına ve HTML API referansına göz atın.

Oturum açma

http://localhost:3000/static-page.html adresini tıklayın.

Tarayıcınızda tek dokunma istemini görmeniz gerekir.

One Tap kullanıcı arayüzü

Oturum açmak için Şu kullanıcı olarak devam et'e basın.

Oturum açtıktan sonra Google, Python sunucunuzun giriş uç noktasına bir POST isteği gönderir. İstek, Google tarafından imzalanmış kodlanmış bir JWT içeriyor.

Sunucu, Google'ın JWT'yi oluşturup imzaladığını doğrulamak için Google'ın ortak imzalama anahtarlarından birini kullanır. Ardından, kitleyi kod çözerek Müşteri Kimliğinizle eşleşip eşleşmediğini doğrular. Ardından, POST gövdesindeki çerez değeri ile istek parametresi değerinin eşit olduğundan emin olmak için bir CSRF kontrolü yapılır. Bu durumda sorun yaşamanız kaçınılmazdır.

Son olarak, açılış sayfasında başarıyla doğrulanmış JWT, JSON biçimli bir kimlik jetonu kullanıcı kimlik bilgisi olarak gösterilir.

Doğrulanmış JWT

Yaygın görülen hatalar

Oturum açma akışının başarısız olmasının çeşitli nedenleri vardır. Bu durumun en yaygın nedenlerinden bazıları şunlardır:

  • data-client_id eksik veya yanlışsa. Bu durumda, Geliştirici Araçları konsolunda bir hata görürsünüz ve tek dokunma istemi çalışmaz.
  • data-login_uri, yanlış URI girildiği, web sunucusu başlatılmadığı veya yanlış bağlantı noktasında dinleme yapıldığı için kullanılamıyor. Bu durumda, tek dokunma istemi çalışıyor gibi görünür ancak kimlik bilgisi döndürüldüğünde DevTools ağ sekmesinde bir hata görünür.
  • Web sunucunuzun kullandığı ana makine adı veya bağlantı noktası, OAuth istemci kimliğinizin Yetkili JavaScript kaynakları listesinde yer almıyor. "When fetching the ID assertion endpoint, a 400 HTTP response code was received." (Kimlik onaylama uç noktası getirilirken 400 HTTP yanıt kodu alındı) şeklinde bir konsol mesajı görürsünüz. Bu codelab sırasında bunu görürseniz hem http://localhost/ hem de http://localhost:3000 öğesinin listelendiğinden emin olun.

5. Dinamik sayfa

Şimdi de tek dokunma özelliğini bir JavaScript çağrısı kullanarak göstereceğiz. Bu örnekte, sayfa yüklendiğinde her zaman tek dokunma seçeneğini gösteriyoruz ancak istemi yalnızca gerektiğinde göstermeyi de tercih edebilirsiniz. Örneğin, kullanıcının oturumunun 28 günden eski olup olmadığını kontrol edebilir ve oturum açma istemini tekrar gösterebilirsiniz.

Bu kod örneğini dynamic-page.html adlı bir dosyaya ekleyin.

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

dynamic-page.html dosyasını açın ve PUT_YOUR_WEB_CLIENT_ID_HERE kısmını, önceki Google ile oturum açma düğmesi codelab'inde kullandığınız istemci kimliği ile değiştirin.

Bu kod, HTML ve JavaScript'in bir karışımıdır ve çeşitli işlemler yapar:

  • google.accounts.id.initialize() işlevini çağırarak Google Identity Services kitaplığını (gsi/client) yapılandırır,
  • Siteler Arası İstek Sahteciliği (CSRF) çerezi oluşturur,
  • Google'dan kodlanmış JWT'yi almak ve bunu Python komut dosyamızın /user-login uç noktasına bir form POST'u kullanarak göndermek için bir geri çağırma işleyicisi ekler ve
  • google.accounts.id.prompt() kullanarak One Tap istemini gösterir.

Tek Dokunma ayarlarının tam listesini JavaScript API referansında bulabilirsiniz.

Oturum açalım.

Tarayıcınızda http://localhost:3000/dynamic-page.html adresini açın.

Tek dokunma istemi davranışı, bu sayfanın CSRF çerezi oluşturmak, JWT'yi Google'dan almak ve Python sunucusunun user-login uç noktasına POST etmek için bir JavaScript geri çağırma işleyicisi tanımlaması dışında statik HTML senaryosuyla aynıdır. HTML API, bu adımları sizin için otomatik olarak gerçekleştirir.

Chrome Network (Ağ) sekmesi

6. İstem davranışları

Bu nedenle, düğmeden farklı olarak istem her zaman gösterilmediğinden Tek Dokunma ile bazı şeyleri deneyelim. Tarayıcı ve kullanıcı tarafından kapatılabilir veya devre dışı bırakılabilir.

Öncelikle bunu prompt-outcomes.html adlı bir dosyaya kaydedin:

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

Ardından, prompt-outcomes.html dosyasını açın, PUT_YOUR_WEB_CLIENT_ID_HERE yerine müşteri kimliğinizi girin ve dosyayı kaydedin.

Tarayıcınızda http://localhost:3000/prompt-outcomes.html adresini açın.

Sayfa tıklamaları

Öncelikle Tek Dokunma isteminin dışındaki herhangi bir yeri tıklayın. Hem sayfaya hem de konsola "İstek iptal edildi." mesajının kaydedildiğini görürsünüz.

Oturum aç

Ardından, normal şekilde oturum açmanız yeterlidir. Kullanıcı oturumu oluşturma veya yenileme gibi işlemleri tetiklemek için kullanılabilecek günlük kaydı ve bildirim güncellemelerini görürsünüz.

İstemi kapatma

Şimdi sayfayı yeniden yükleyin ve Tek Dokunma gösterildikten sonra başlık çubuğundaki "X"e basın. Bu mesaj konsola kaydedilmelidir:

  • "Kullanıcı, istemi reddetti veya kapattı. API eksponansiyel soğutma tetiklendi."

Test sırasında soğutma işlemini tetiklersiniz. Bekleme süresi boyunca tek dokunma istemi gösterilmez. Test sırasında, otomatik olarak sıfırlanmasını beklemek yerine sıfırlamak isteyebilirsiniz. Tabii kahve içmek veya eve gidip uyumak istemiyorsanız. Bekleme süresini sıfırlamak için:

  • Tarayıcının adres çubuğunun sol tarafındaki "site bilgileri" simgesini tıklayın.
  • "İzinleri Sıfırla" düğmesine basın ve
  • Sayfayı yeniden yükleyin.

Sıklık sınırlaması sıfırlandıktan ve sayfa yeniden yüklendikten sonra tek dokunma istemi gösterilir.

7. Sonuç

Bu codelab'de, yalnızca statik HTML kullanarak veya JavaScript ile dinamik olarak Tek Dokunma'yı görüntüleme gibi birkaç şey öğrendiniz.

Yerel test için çok basit bir Python web sunucusu kurdunuz ve kimlik jetonlarının kodunu çözmek ve doğrulamak için gereken adımları öğrendiniz.

Kullanıcıların tek dokunma istemiyle etkileşim kurup bu istemi kapatmak için kullandığı en yaygın yöntemleri denediniz ve istem davranışında hata ayıklamak için kullanılabilecek bir web sayfanız var.

Tebrikler!

Ekstra bilgi için, desteklenen farklı tarayıcılarda geri dönüp Tek Dokunma'yı kullanmayı deneyin.

Aşağıdaki bağlantılar, sonraki adımlarda size yardımcı olabilir:

Sık sorulan sorular