כניסה באמצעות הודעה מ-Google בלחיצה אחת באתרים

כניסה באמצעות חשבון Google בלחיצה אחת באתרים

מידע על Codelab זה

subjectהעדכון האחרון: יולי 9, 2025
account_circleנכתב על ידי Brian Daugherty

1.‏ מבוא

ה-codelab הזה מבוסס על ה-codelab Sign in with Google button for Web, לכן חשוב להשלים אותו קודם.

ב-codelab הזה תשתמשו בספריית JavaScript של Google Identity Services ובהנחיות בלחיצה אחת כדי להוסיף כניסת משתמשים לדפי אינטרנט סטטיים ודינמיים באמצעות ממשקי API של HTML ו-JavaScript.

הנחיה לכניסה בהקשה אחת

בנוסף, נגדיר נקודת קצה להתחברות בצד השרת כדי לאמת אסימוני JWT מזהים.

מה תלמדו

  • איך מגדירים נקודת קצה (endpoint) להתחברות בצד השרת כדי לאמת טוקנים של מזהים
  • איך מוסיפים בקשה לכניסה באמצעות Google One Tap לדף אינטרנט
    • כפריט HTML סטטי, וגם
    • באופן דינמי באמצעות JavaScript.
  • איך פועלת ההודעה 'כניסה בלחיצה אחת'

מה צריך בשביל להצטרף

  1. ידע בסיסי ב-HTML, ב-CSS, ב-JavaScript וב-Chrome DevTools (או בכלי מקביל).
  2. מקום לעריכה ולאירוח של קובצי HTML ו-JavaScript.
  3. מזהה הלקוח שהתקבל בסדנת הקוד הקודמת.
  4. סביבה שיכולה להריץ אפליקציית Python בסיסית.

קדימה!

2.‏ הגדרת נקודת קצה לכניסה

קודם ניצור סקריפט Python שפועל כשרת אינטרנט בסיסי, ונגדיר את סביבת Python שנדרשת להרצת הסקריפט.

הסקריפט פועל באופן מקומי ומציג לדפדפן את דף הנחיתה, את דפי ה-HTML הסטטיים ואת דפי ה-HTML הדינמיים של 'הצטרפות בלחיצה אחת'. הוא מקבל בקשות 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()

אנחנו הולכים לאמת שה-JWT הונפק ללקוח שלכם באמצעות שדה הקהל (aud) של טוקן הזהות, ולכן אפליקציית Python צריכה לדעת באיזה מזהה לקוח נעשה שימוש. כדי לעשות את זה, מחליפים את PUT_YOUR_WEB_CLIENT_ID_HERE במזהה הלקוח שבו השתמשתם ב-codelab הקודם של כניסה באמצעות חשבון Google.

סביבת Python

בואו נגדיר את הסביבה להרצת הסקריפט של שרת האינטרנט.

תצטרכו Python בגרסה 3.8 ואילך, וגם כמה חבילות שיעזרו לכם לאמת ולפענח את ה-JWT.

$ python3 --version

אם גרסת python3 שלכם היא מתחת ל-3.8, יכול להיות שתצטרכו לשנות את נתיב המעטפת כדי שהגרסה הצפויה תימצא, או להתקין גרסה חדשה יותר של Python במערכת.

לאחר מכן, יוצרים קובץ בשם 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. לכל פלטפורמה יש דרישות הגדרה משלה והגדרות ברירת מחדל של סביבת Python, כך שכנראה תצטרכו לשנות כמה דברים כמו TARGET_HTML_PAGE_URL והגדרת Python.

לדוגמה, ב-Glitch עדיין מוסיפים requirements.txt אבל גם יוצרים קובץ בשם start.sh כדי להפעיל את שרת Python באופן אוטומטי:

python3 ./simple-server.py

צריך גם לעדכן את כתובות ה-URL שבהן נעשה שימוש בסקריפט Python ובקובצי ה-HTML לכתובת ה-URL החיצונית של סביבת הפיתוח המשולבת (IDE) ב-Cloud. לכן, נקבל משהו כזה: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/" ומכיוון שקובצי ה-HTML ב-codelab הזה משתמשים גם ב-localhost כברירת מחדל, תצטרכו לעדכן אותם עם כתובת ה-URL החיצונית של Cloud IDE: data-login_uri="https://your-project-name.glitch.me/user-login".

3.‏ יצירת דף נחיתה

בשלב הבא ניצור דף נחיתה שבו יוצגו התוצאות של הכניסה באמצעות One Tap. בדף מוצג טוקן המזהה של JWT אחרי פענוח או שגיאה. אפשר גם להשתמש בטופס בדף כדי לשלוח JWT לנקודת הקצה של ההתחברות בשרת ה-HTTP של Python, שם הוא מפוענח ומאומת. הוא משתמש בקובץ Cookie של שליחה כפולה של CSRF ובפרמטר של בקשת POST כדי שיוכל לעשות שימוש חוזר באותה נקודת קצה (endpoint) של השרת כמו בדוגמאות של HTML ו-JavaScript API ב-codelab.user-logingsi/client

בטרמינל, שומרים את הטקסט הזה בקובץ בשם 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/ ולוחצים על הלחצן Verify JWT (אימות JWT).

אתם אמורים לראות את ההודעה הזו

אימות JWT ללא תוכן

לחיצה על הלחצן שולחת POST עם התוכן של שדה ההזנה לסקריפט Python. הסקריפט מצפה שיהיה JWT מקודד בשדה הקלט, ולכן הוא מנסה לפענח ולאמת את מטען הייעודי (payload). לאחר מכן המשתמש מועבר חזרה לדף הנחיתה כדי לראות את התוצאות.

אבל רגע, לא היה JWT... אז למה זה לא נכשל? כן, אבל בצורה חלקה!

מכיוון שהשדה היה ריק, מוצגת שגיאה. עכשיו מנסים להזין טקסט (כל טקסט שרוצים) בשדה ההזנה ולוחצים שוב על הלחצן. הפעולה נכשלת עם שגיאת פענוח אחרת.

אפשר להדביק אסימון מזהה JWT מקודד שהונפק על ידי Google בשדה ההזנה, ולתת לסקריפט Python לפענח, לאמת ולהציג אותו בשבילכם… או שאפשר להשתמש בכתובת https://jwt.io כדי לבדוק כל JWT מקודד.

4.‏ דף HTML סטטי

עכשיו נגדיר את התכונה 'אישור בלחיצה אחת' כך שתפעל בדפי HTML בלי להשתמש ב-JavaScript. האפשרות הזו יכולה להיות שימושית לאתרים סטטיים או למערכות שמירה במטמון ולרשתות 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 בClient ID שבו השתמשתם ב-codelab הקודם של לחצן הכניסה באמצעות חשבון Google.

אז מה זה עושה?

כל רכיב HTML עם id של g_id_onload ומאפייני הנתונים שלו משמשים להגדרת ספריית שירותי הזהות של Google‏ (gsi/client). בנוסף, מוצגת ההנחיה 'הצטרפות בלחיצה אחת' כשהמסמך נטען בדפדפן.

מאפיין data-login_uri הוא ה-URI שיקבל בקשת POST מהדפדפן אחרי שהמשתמש ייכנס לחשבון. הבקשה הזו מכילה את אסימון ה-JWT המקודד שהונפק על ידי Google.

לרשימה מלאה של אפשרויות לשימוש בתכונה 'הצטרפות בלחיצה אחת', אפשר לעיין במחולל קוד ה-HTML ובהפניה ל-HTML API.

כניסה

לוחצים על http://localhost:3000/static-page.html.

ההנחיה 'בלחיצה אחת' תוצג בדפדפן.

ממשק משתמש של &#39;לחיצה אחת&#39;

לוחצים על המשך בתור כדי להיכנס לחשבון.

אחרי הכניסה לחשבון, Google שולחת בקשת POST לנקודת הקצה של הכניסה לשרת Python. הבקשה מכילה JWT מקודד שחתום על ידי Google.

מכאן, השרת משתמש באחד ממפתחות החתימה הציבוריים של Google כדי לאמת ש-Google יצרה את ה-JWT וחתמה עליו. לאחר מכן המערכת מפענחת את הקהל ומוודאת שהוא תואם למזהה הלקוח. לאחר מכן מתבצעת בדיקת CSRF כדי לוודא שהערך של ה-Cookie והערך של פרמטר הבקשה בגוף של בקשת ה-POST שווים. אם הם לא מופיעים, זו אינדיקציה ברורה לבעיה.

לבסוף, בדף הנחיתה מוצג ה-JWT שאומת בהצלחה כפרטי כניסה של משתמש עם אסימון מזהה בפורמט JSON.

JWT מאומת

שגיאות נפוצות

יש כמה סיבות לכך שתהליך הכניסה ייכשל. אלה כמה מהסיבות הנפוצות ביותר לכך:

  • data-client_id חסר או שגוי. במקרה כזה תופיע שגיאה במסוף כלי הפיתוח וההנחיה לכניסה בלחיצה אחת לא תפעל.
  • data-login_uri לא זמין כי הוזן מזהה URI שגוי, שרת האינטרנט לא הופעל או שהוא מאזין ליציאה שגויה. במקרה כזה, ההנחיה 'הצטרפות בלחיצה אחת' תיראה כאילו היא פועלת, אבל שגיאה תופיע בכרטיסייה 'רשת' בכלי הפיתוח כשפרטי הכניסה יוחזרו.
  • שם המארח או היציאה שבהם שרת האינטרנט שלכם משתמש לא מופיעים במקורות ה-JavaScript המורשים של מזהה הלקוח של OAuth. תוצג הודעה במסוף: When fetching the ID assertion endpoint, a 400 HTTP response code was received. (כשמתבצעת אחזור של נקודת הקצה של הצהרת הזהות, מתקבל קוד תגובת HTTP‏ 400). אם אתם רואים את זה במהלך ה-codelab, בדקו שגם http://localhost/ וגם http://localhost:3000 מופיעים ברשימה.

5.‏ דף דינמי

עכשיו נציג את התכונה 'הצטרפות בלחיצה אחת' באמצעות קריאה של JavaScript. בדוגמה הזו, תמיד נציג את ההנחיה 'הצטרפות בלחיצה אחת' כשהדף נטען, אבל אפשר לבחור להציג את ההנחיה רק כשצריך. לדוגמה, אפשר לבדוק אם חלפו יותר מ-28 ימים מאז שהמשתמש התחבר לחשבון, ואם כן, להציג שוב את ההנחיה להתחבר.

מוסיפים את דוגמת הקוד הזו לקובץ בשם 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 בClient ID שבו השתמשתם ב-codelab הקודם של לחצן הכניסה באמצעות חשבון Google.

הקוד הזה הוא שילוב של HTML ו-JavaScript, והוא מבצע כמה פעולות:

  • מגדיר את ספריית שירותי הזהויות של Google‏ (gsi/client) על ידי קריאה ל-google.accounts.id.initialize(),
  • יוצר קובץ Cookie של זיוף בקשות בין אתרים (CSRF),
  • מוסיף פונקציית callback לקבלת אסימון ה-JWT המקודד מ-Google ושליחתו באמצעות POST של טופס לנקודת הקצה של סקריפט Python /user-login, וגם
  • ההנחיה 'הצטרפות בלחיצה אחת' מוצגת באמצעות google.accounts.id.prompt().

רשימה מלאה של הגדרות One Tap זמינה בהפניות JavaScript API.

בואו נתחבר!

פותחים את http://localhost:3000/dynamic-page.html בדפדפן.

ההתנהגות של ההנחיה 'הצטרפות בלחיצה אחת' זהה לתרחיש של HTML סטטי, אלא שהדף הזה מגדיר פונקציית קריאה חוזרת (callback) של JavaScript כדי ליצור קובץ Cookie של CSRF, לקבל את ה-JWT מ-Google ולשלוח אותו לנקודת הקצה user-login של שרת Python. ה-API של HTML מבצע את השלבים האלה באופן אוטומטי.

הכרטיסייה &#39;רשת&#39; ב-Chrome

6.‏ התנהגויות של הנחיות

לכן, ננסה כמה דברים עם אימות בלחיצה אחת, כי בניגוד ללחצן, ההנחיה לא תמיד מוצגת. הדפדפן והמשתמש יכולים לסגור או להשבית את ההודעה.

קודם שומרים את הקוד הבא בקובץ בשם 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

קליקים בדף

כדי להתחיל, לוחצים על מקום כלשהו מחוץ להנחיה 'לחיצה אחת'. ההודעה The request has been aborted (הבקשה בוטלה) אמורה להופיע ביומן גם בדף וגם במסוף.

כניסה

אחרי כן, פשוט נכנסים לחשבון כרגיל. יוצגו לכם עדכונים בנוגע לרישום ביומן ולהתראות, שאפשר להשתמש בהם כדי להפעיל פעולות כמו יצירה או רענון של סשן משתמש.

סגירת ההנחיה

עכשיו טוענים מחדש את הדף, ואחרי שמוצג אישור בלחיצה אחת, לוחצים על ה-X בסרגל הכותרת שלו. ההודעה הזו אמורה להירשם במסוף:

  • User declined or dismissed prompt. הופעלה השהיה מעריכית לפני ניסיון חוזר (exponential cool down) של API."

במהלך הבדיקה, תפעילו את הקירור. במהלך תקופת הצינון, לא מוצגת בקשת ההסכמה בלחיצה אחת. במהלך הבדיקה, כדאי לאפס את המכשיר במקום לחכות לאיפוס אוטומטי... אלא אם אתם רוצים ללכת לשתות קפה או לחזור הביתה ולישון. כדי לאפס את תקופת הצינון:

  • לוחצים על סמל פרטי האתר בצד ימין של סרגל הכתובות בדפדפן,
  • לוחצים על הלחצן 'איפוס ההרשאות'.
  • לטעון מחדש את הדף.

אחרי איפוס תקופת ההמתנה וטעינה מחדש של הדף, תוצג ההנחיה 'הצטרפות בלחיצה אחת'.

7.‏ סיכום

בשיעור הזה למדתם כמה דברים, כמו איך להציג את התכונה 'הצטרפות בלחיצה אחת' באמצעות HTML סטטי בלבד או באופן דינמי באמצעות JavaScript.

הגדרתם שרת אינטרנט בסיסי מאוד של Python לבדיקה מקומית, ולמדתם את השלבים הדרושים לפענוח ולאימות של טוקנים של מזהים.

התנסיתם בדרכים הנפוצות ביותר שבהן משתמשים מבצעים אינטראקציה עם ההנחיה 'הצטרפות בלחיצה אחת' וסוגרים אותה, ויש לכם דף אינטרנט שאפשר להשתמש בו כדי לנפות באגים בהתנהגות של ההנחיה.

מעולה!

כדי לקבל נקודות בונוס, אפשר לנסות לחזור אחורה ולהשתמש בכניסה בלחיצה אחת בדפדפנים השונים שנתמכים.

הקישורים הבאים יכולים לעזור לכם בשלבים הבאים:

שאלות נפוצות