با درخواست Google One Tap for Web وارد سیستم شوید

۱. مقدمه

این codelab بر اساس دکمه ورود با گوگل برای codelab وب ساخته شده است، بنابراین حتماً ابتدا آن را تکمیل کنید.

در این آزمایشگاه کد، شما از کتابخانه جاوا اسکریپت خدمات هویت گوگل و اعلان‌های One Tap برای اضافه کردن ورود کاربر به صفحات وب استاتیک و پویا با استفاده از APIهای HTML و جاوا اسکریپت استفاده خواهید کرد.

یک اعلان با یک لمس

همچنین یک نقطه پایانی ورود به سیستم سمت سرور برای تأیید توکن‌های JWT ID راه‌اندازی خواهیم کرد.

آنچه یاد خواهید گرفت

  • نحوه تنظیم یک نقطه پایانی ورود به سیستم سمت سرور برای تأیید توکن‌های شناسه
  • نحوه اضافه کردن اعلان Google One Tap به یک صفحه وب
    • به عنوان یک عنصر HTML استاتیک، و
    • به صورت پویا با استفاده از جاوا اسکریپت.
  • نحوه عملکرد دکمه One Tap

آنچه نیاز دارید

  1. دانش پایه در مورد HTML، CSS، جاوا اسکریپت و Chrome DevTools (یا معادل آن).
  2. مکانی برای ویرایش و میزبانی فایل‌های HTML و جاوا اسکریپت.
  3. شناسه کلاینت که در کدلب قبلی به دست آمده است.
  4. محیطی که بتواند یک برنامه پایتون پایه را اجرا کند.

بزن بریم!

۲. یک نقطه پایانی ورود به سیستم راه‌اندازی کنید

ابتدا، یک اسکریپت پایتون ایجاد می‌کنیم که به عنوان یک وب سرور پایه عمل می‌کند و محیط پایتون لازم برای اجرای آن را راه‌اندازی می‌کنیم.

این اسکریپت که به صورت محلی اجرا می‌شود، صفحه فرود و صفحات HTML استاتیک و صفحات One Tap دینامیک را به مرورگر ارائه می‌دهد. درخواست‌های POST را می‌پذیرد، JWT موجود در پارامتر اعتبارنامه را رمزگشایی می‌کند و تأیید می‌کند که توسط پلتفرم OAuth متعلق به Google Identity صادر شده است.

پس از رمزگشایی و تأیید JWT، اسکریپت برای نمایش نتایج به صفحه فرود index.html هدایت می‌شود.

این را در فایلی به نام 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()

از آنجایی که قرار است با استفاده از فیلد audience (aud) مربوط به توکن ID، تأیید کنیم که JWT به کلاینت شما ارسال شده است، برنامه پایتون شما باید بداند از کدام Client ID استفاده می‌شود. برای انجام این کار، PUT_YOUR_WEB_CLIENT_ID_HERE با Client ID که در codelab قبلی در بخش دکمه ورود با گوگل استفاده کردید، جایگزین کنید.

محیط پایتون

بیایید محیط را برای اجرای اسکریپت وب سرور تنظیم کنیم.

برای کمک به تأیید و رمزگشایی JWT به پایتون ۳.۸ یا بالاتر به همراه چند بسته نیاز خواهید داشت.

$ python3 --version

اگر نسخه پایتون ۳ شما کمتر از ۳.۸ است، ممکن است لازم باشد مسیر پوسته خود را تغییر دهید تا نسخه مورد انتظار پیدا شود یا نسخه جدیدتری از پایتون را روی سیستم خود نصب کنید.

در مرحله بعد، فایلی با نام requirements.txt ایجاد کنید که بسته‌های مورد نیاز برای رمزگشایی و تأیید JWT را فهرست کند:

google-auth

این دستورات را در همان دایرکتوری simple-server.py و requirements.txt اجرا کنید تا یک محیط مجازی ایجاد شود و بسته‌ها فقط برای این برنامه نصب شوند:

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

حالا سرور را اجرا کنید و اگر همه چیز درست کار کند، این را خواهید دید:

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

IDE های مبتنی بر ابر

این codelab برای اجرا در ترمینال محلی و localhost طراحی شده است، اما با کمی تغییر می‌توان از آن در پلتفرم‌هایی مانند Replit یا Glitch نیز استفاده کرد. هر پلتفرم الزامات راه‌اندازی و پیش‌فرض‌های محیط پایتون خود را دارد، بنابراین احتمالاً باید چند مورد مانند TARGET_HTML_PAGE_URL و تنظیمات پایتون را تغییر دهید.

برای مثال، در Glitch شما همچنان یک requirements.txt اضافه می‌کنید، اما فایلی به نام start.sh نیز ایجاد می‌کنید تا سرور پایتون به طور خودکار شروع به کار کند:

python3 ./simple-server.py

آدرس‌های اینترنتی (URL) استفاده شده توسط اسکریپت پایتون و فایل‌های HTML نیز باید به آدرس اینترنتی خارجی Cloud IDE شما به‌روزرسانی شوند. بنابراین چیزی شبیه به این خواهیم داشت: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/" و از آنجایی که فایل‌های HTML در سراسر این codelab نیز به طور پیش‌فرض از localhost استفاده می‌کنند، باید آنها را با آدرس اینترنتی خارجی Cloud IDE به‌روزرسانی کنید: data-login_uri="https://your-project-name.glitch.me/user-login" .

۳. یک صفحه فرود ایجاد کنید

در مرحله بعد، یک صفحه فرود ایجاد خواهیم کرد که نتایج ورود به سیستم با One Tap را نمایش می‌دهد. این صفحه، توکن JWT ID رمزگشایی شده یا یک خطا را نمایش می‌دهد. همچنین می‌توان از فرم موجود در صفحه برای ارسال JWT به نقطه پایانی ورود به سیستم در سرور HTTP پایتون ما استفاده کرد که در آنجا رمزگشایی و تأیید می‌شود. این فرم از یک کوکی ارسال دوگانه CSRF و پارامتر درخواست POST استفاده می‌کند تا بتواند از همان نقطه پایانی سرور user-login مانند نمونه‌های gsi/client HTML و JavaScript API در codelab استفاده مجدد کند.

در ترمینال خود، این را در فایلی با نام 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>

تست وب سرور و رمزگشایی JWT

قبل از اینکه بخواهیم با One Tap کار کنیم، مطمئن می‌شویم که محیط نقطه پایانی سرور راه‌اندازی و کار می‌کند.

به صفحه اصلی، http://localhost:3000/ ، بروید و دکمه تأیید JWT را فشار دهید.

باید اینو ببینی

تأیید JWT بدون محتوا

با فشردن دکمه، یک POST حاوی محتوای فیلد ورودی به اسکریپت پایتون ارسال می‌شود. اسکریپت انتظار دارد یک JWT کدگذاری شده در فیلد ورودی وجود داشته باشد، بنابراین سعی می‌کند تا payload را رمزگشایی و تأیید کند. پس از آن، برای نمایش نتایج، به صفحه فرود هدایت می‌شود.

اما صبر کنید، JWT وجود نداشت... مگر این شکست نخورد؟ بله، اما به طرز زیبایی!

از آنجایی که فیلد خالی بود، خطایی نمایش داده می‌شود. حالا سعی کنید متنی (هر متنی که دوست دارید) را در فیلد ورودی وارد کنید و دوباره دکمه را فشار دهید. با خطای رمزگشایی متفاوتی مواجه می‌شوید.

می‌توانید یک توکن JWT ID کدگذاری شده توسط گوگل را در فیلد ورودی قرار دهید و اسکریپت پایتون آن را رمزگشایی، تأیید و نمایش دهد... یا می‌توانید از https://jwt.io برای بررسی هر JWT کدگذاری شده استفاده کنید.

۴. صفحه HTML استاتیک

بسیار خب، حالا ما One Tap را طوری تنظیم می‌کنیم که بدون استفاده از جاوا اسکریپت روی صفحات HTML کار کند. این می‌تواند برای سایت‌های استاتیک یا سیستم‌های ذخیره‌سازی موقت و CDNها مفید باشد.

با اضافه کردن این نمونه کد به فایلی به نام 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>

سپس، static-page.html را باز کنید و PUT_YOUR_WEB_CLIENT_ID_HERE با شناسه کلاینتی که در codelab قبلی برای دکمه ورود با گوگل استفاده کردید، جایگزین کنید.

خب این چیکار می‌کنه؟

هر عنصر HTML با id g_id_onload و ویژگی‌های داده آن برای پیکربندی کتابخانه سرویس‌های هویت گوگل ( gsi/client ) استفاده می‌شود. همچنین هنگام بارگذاری سند در مرورگر، اعلان One Tap را نمایش می‌دهد.

ویژگی data-login_uri آدرس اینترنتی (URI) است که پس از ورود کاربر، یک درخواست POST از مرورگر دریافت می‌کند. این درخواست شامل JWT کدگذاری شده‌ای است که توسط گوگل صادر شده است.

برای مشاهده لیست کاملی از گزینه‌های One Tap، به مولد کد HTML و مرجع HTML API مراجعه کنید.

ورود

روی http://localhost:3000/static-page.html کلیک کنید.

باید عبارت One Tap را در مرورگر خود مشاهده کنید.

رابط کاربری تک لمسی

برای ورود، روی «ادامه » کلیک کنید.

پس از ورود به سیستم، گوگل یک درخواست POST به نقطه پایانی ورود به سیستم سرور پایتون شما ارسال می‌کند. این درخواست حاوی یک JWT کدگذاری شده است که توسط گوگل امضا شده است.

از آنجا، سرور از یکی از کلیدهای امضای عمومی گوگل برای تأیید اینکه گوگل JWT را ایجاد و امضا کرده است، استفاده می‌کند. سپس مخاطب را رمزگشایی و تأیید می‌کند که با شناسه کلاینت شما مطابقت دارد. در مرحله بعد، یک بررسی CSRF انجام می‌شود تا مطمئن شود مقدار کوکی و مقدار پارامتر درخواست در بدنه POST برابر هستند. اگر اینطور نباشد، نشانه قطعی مشکل است.

در نهایت، صفحه فرود، JWT تأیید شده با موفقیت را به عنوان یک شناسه توکن با فرمت JSON برای اعتبارنامه کاربر نمایش می‌دهد.

JWT تأیید شده

خطاهای رایج

چندین دلیل برای عدم موفقیت روند ورود به سیستم وجود دارد. برخی از رایج‌ترین دلایل عبارتند از:

  • data-client_id موجود نیست یا نادرست است، در این صورت در کنسول DevTools خطایی مشاهده خواهید کرد و اعلان One Tap کار نخواهد کرد.
  • data-login_uri در دسترس نیست زیرا یک URI نادرست وارد شده است، سرور وب شروع نشده است یا به پورت اشتباهی گوش می‌دهد. در این صورت، به نظر می‌رسد که One Tap prompt کار می‌کند، اما هنگام بازگشت اعتبارنامه، خطایی در تب شبکه DevTools مشاهده خواهد شد.
  • نام میزبان یا پورتی که وب سرور شما استفاده می‌کند در Authorized JavaScript origins برای OAuth Client ID شما فهرست نشده است. شما یک پیام کنسولی با این مضمون مشاهده خواهید کرد: " هنگام واکشی نقطه پایانی ادعای شناسه، یک کد پاسخ HTTP با کد ۴۰۰ دریافت شد. ". اگر در طول این آزمایش کد با این مشکل مواجه شدید، بررسی کنید که هر دو http://localhost/ و http://localhost:3000 فهرست شده باشند.

۵. صفحه پویا

حالا ما با استفاده از یک فراخوانی جاوا اسکریپت، One Tap را نمایش می‌دهیم. در این مثال، ما همیشه One Tap را هنگام بارگذاری صفحه نمایش می‌دهیم، اما می‌توانید انتخاب کنید که این اعلان فقط در صورت نیاز نمایش داده شود. برای مثال، می‌توانید بررسی کنید که آیا جلسه کاربر بیش از ۲۸ روز قدمت دارد یا خیر و دوباره اعلان ورود را نمایش دهید.

این نمونه کد را به فایلی با نام 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>

dynamic-page.html را باز کنید و PUT_YOUR_WEB_CLIENT_ID_HERE با شناسه کلاینتی که در کدنویسی دکمه ورود با گوگل قبلی استفاده کردید، جایگزین کنید.

این کد ترکیبی از HTML و جاوا اسکریپت است و چندین کار انجام می‌دهد:

  • کتابخانه سرویس‌های هویت گوگل ( gsi/client ) را با فراخوانی google.accounts.id.initialize() پیکربندی می‌کند،
  • یک کوکی جعل درخواست بین سایتی ( CSRF ) تولید می‌کند،
  • یک کنترل‌کننده‌ی فراخوانی اضافه می‌کند تا JWT کدگذاری‌شده را از گوگل دریافت کند و آن را با استفاده از فرم POST به نقطه‌ی پایانی اسکریپت پایتون /user-login ما ارسال کند، و
  • با استفاده از google.accounts.id.prompt() اعلان One Tap را نمایش می‌دهد.

لیست کاملی از تنظیمات One Tap را می‌توانید در مرجع API جاوا اسکریپت پیدا کنید.

بیایید وارد سیستم شویم!

آدرس http://localhost:3000/dynamic-page.html را در مرورگر خود باز کنید.

رفتار اعلان با یک لمس (One Tap prompt) مشابه سناریوی HTML استاتیک است، با این تفاوت که این صفحه یک کنترل‌کننده‌ی فراخوانی جاوااسکریپت برای ایجاد یک کوکی CSRF، دریافت JWT از گوگل و ارسال آن به نقطه‌ی پایانی user-login سرور پایتون تعریف می‌کند. API HTML این مراحل را به طور خودکار برای شما انجام می‌دهد.

برگه شبکه کروم

۶. رفتارهای سریع

بنابراین بیایید چند مورد را با One Tap امتحان کنیم، زیرا برخلاف دکمه، اعلان همیشه نمایش داده نمی‌شود. می‌تواند توسط مرورگر و کاربر رد، بسته یا غیرفعال شود.

ابتدا، این را در فایلی با نام 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>

سپس، prompt-outcomes.html را باز کنید، PUT_YOUR_WEB_CLIENT_ID_HERE را با شناسه کلاینت خود جایگزین کنید و سپس فایل را ذخیره کنید.

در مرورگر خود، http://localhost:3000/prompt-outcomes.html را باز کنید.

کلیک‌های صفحه

با کلیک کردن در هر جایی خارج از اعلان One Tap شروع کنید. باید عبارت « درخواست لغو شده است. » را هم در صفحه و هم در کنسول مشاهده کنید.

ورود

در مرحله بعد، به طور عادی وارد سیستم شوید. به‌روزرسانی‌های گزارش‌گیری و اعلان‌ها را مشاهده خواهید کرد که می‌توانند برای ایجاد یا به‌روزرسانی جلسه کاربری استفاده شوند.

اعلان را ببندید

حالا صفحه را دوباره بارگذاری کنید و پس از نمایش One Tap، روی علامت «X» در نوار عنوان آن کلیک کنید. این پیام باید در کنسول ثبت شود:

  • کاربر درخواست را رد کرد یا آن را رد کرد. کاهش سرعت نمایی API آغاز شد.

در طول آزمایش، شما حالت خنک شدن را فعال می‌کنید. در طول دوره خنک شدن، اعلان One Tap نمایش داده نمی‌شود. در طول آزمایش، احتمالاً می‌خواهید آن را مجدداً تنظیم کنید تا اینکه منتظر بمانید تا به طور خودکار تنظیم مجدد شود... مگر اینکه واقعاً بخواهید برای نوشیدن قهوه یا رفتن به خانه و خوابیدن بروید. برای تنظیم مجدد حالت خنک شدن:

  • روی نماد «اطلاعات سایت» در سمت چپ نوار آدرس مرورگر کلیک کنید،
  • دکمه‌ی «بازنشانی مجوزها» را فشار دهید، و
  • صفحه را دوباره بارگذاری کنید.

پس از تنظیم مجدد زمان آماده‌سازی و بارگذاری مجدد صفحه، پیام «یک ضربه» نمایش داده می‌شود.

۷. نتیجه‌گیری

بنابراین در این آزمایشگاه کد، چند نکته را یاد گرفتید، مثلاً نحوه نمایش One Tap با استفاده از HTML استاتیک یا به صورت پویا با جاوا اسکریپت.

شما یک وب سرور پایتون بسیار ساده را برای آزمایش محلی راه‌اندازی کردید و مراحل لازم برای رمزگشایی و اعتبارسنجی توکن‌های شناسه را آموختید.

شما با رایج‌ترین روش‌های تعامل کاربران با اعلان تک‌ضربه‌ای و رد کردن آن کار کرده‌اید و یک صفحه وب دارید که می‌تواند برای اشکال‌زدایی رفتار اعلان استفاده شود.

تبریک می‌گویم!

برای اعتبار بیشتر، سعی کنید به عقب برگردید و از One Tap در مرورگرهای مختلفی که پشتیبانی می‌کند استفاده کنید.

این لینک‌ها می‌توانند در مراحل بعدی به شما کمک کنند:

سوالات متداول