वेब के लिए, 'Google से साइन इन करें' One Tap प्रॉम्प्ट

1. परिचय

यह कोडलैब, वेब के लिए 'Google से साइन इन करें' बटन कोडलैब पर आधारित है. इसलिए, पहले इसे पूरा करना न भूलें.

इस कोडलैब में, आपको Google Identity Services JavaScript लाइब्रेरी और One Tap प्रॉम्प्ट का इस्तेमाल करना होगा. इससे, एचटीएमएल और JavaScript API का इस्तेमाल करके, स्टैटिक और डाइनैमिक वेब पेजों पर उपयोगकर्ता साइन-इन की सुविधा जोड़ी जा सकेगी.

One Tap का प्रॉम्प्ट

हम JWT आईडी टोकन की पुष्टि करने के लिए, सर्वर साइड लॉगिन एंडपॉइंट भी सेट अप करेंगे.

आपको क्या सीखने को मिलेगा

  • आईडी टोकन की पुष्टि करने के लिए, सर्वर-साइड लॉगिन एंडपॉइंट को सेट अप करने का तरीका
  • किसी वेब पेज पर Google One Tap प्रॉम्प्ट जोड़ने का तरीका
    • के तौर पर इस्तेमाल किया जा सकता है.
    • JavaScript का डाइनैमिक तरीके से इस्तेमाल किया जा रहा हो.
  • One Tap प्रॉम्प्ट कैसे काम करता है

आपको किन चीज़ों की ज़रूरत होगी

  1. एचटीएमएल, सीएसएस, JavaScript, और Chrome DevTools (या मिलते-जुलते टूल) की बुनियादी जानकारी.
  2. एचटीएमएल और JavaScript फ़ाइलों में बदलाव करने और उन्हें होस्ट करने की जगह.
  3. पिछले कोडलैब में मिला क्लाइंट आईडी.
  4. ऐसा एनवायरमेंट जिसमें बुनियादी Python ऐप्लिकेशन चलाया जा सकता हो.

आइए, शुरू करते हैं!

2. लॉगिन एंडपॉइंट सेट अप करना

सबसे पहले, हम एक Python स्क्रिप्ट बनाएंगे, जो बुनियादी वेब सर्वर के तौर पर काम करती है. साथ ही, इसे चलाने के लिए ज़रूरी Python एनवायरमेंट सेट अप करेगी.

स्थानीय तौर पर चलने वाली स्क्रिप्ट, ब्राउज़र को लैंडिंग पेज, स्टैटिक एचटीएमएल, और डाइनैमिक One Tap पेज दिखाती है. यह POST अनुरोध स्वीकार करता है, क्रेडेंशियल पैरामीटर में मौजूद JWT को डिकोड करता है, और पुष्टि करता है कि इसे Google Identity के OAuth प्लैटफ़ॉर्म ने जारी किया है.

जेडब्ल्यूटी को डिकोड करने और उसकी पुष्टि करने के बाद, स्क्रिप्ट नतीजे दिखाने के लिए 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()

हम आईडी टोकन के ऑडियंस (aud) फ़ील्ड का इस्तेमाल करके, इस बात की पुष्टि करने जा रहे हैं कि आपके क्लाइंट को JWT जारी किया गया था. इसलिए, आपके Python ऐप्लिकेशन को यह जानना होगा कि किस क्लाइंट आईडी का इस्तेमाल किया जा रहा है. इसके लिए, PUT_YOUR_WEB_CLIENT_ID_HERE को उस क्लाइंट आईडी से बदलें जिसका इस्तेमाल आपने 'Google से साइन इन करें' बटन के पिछले कोडलैब में किया था.

Python एनवायरमेंट

वेब सर्वर स्क्रिप्ट को चलाने के लिए, इनवायरनमेंट सेट अप करते हैं.

JWT की पुष्टि करने और उसे डिकोड करने के लिए, आपको Python 3.8 या उसके बाद के वर्शन के साथ-साथ कुछ पैकेज की ज़रूरत होगी.

$ python3 --version

अगर आपके पास python3 का 3.8 से कम वर्शन है, तो आपको अपने शेल PATH को बदलना पड़ सकता है, ताकि आपको सही वर्शन मिल सके. इसके अलावा, अपने सिस्टम पर 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/) ...

क्लाउड-आधारित आईडीई

इस कोडलैब को स्थानीय टर्मिनल और localhost पर चलाने के लिए डिज़ाइन किया गया था. हालांकि, कुछ बदलावों के साथ इसका इस्तेमाल Replit या Glitch जैसे प्लैटफ़ॉर्म पर भी किया जा सकता है. हर प्लैटफ़ॉर्म के लिए, सेटअप की अलग-अलग ज़रूरतें होती हैं. साथ ही, Python एनवायरमेंट के डिफ़ॉल्ट वैल्यू भी अलग-अलग होती हैं. इसलिए, आपको TARGET_HTML_PAGE_URL और Python सेटअप जैसी कुछ चीज़ों में बदलाव करना पड़ सकता है.

उदाहरण के लिए, Glitch पर अब भी requirements.txt जोड़ा जाएगा. हालांकि, Python सर्वर को अपने-आप शुरू करने के लिए, start.sh नाम की फ़ाइल भी बनाई जाएगी:

python3 ./simple-server.py

Python स्क्रिप्ट और एचटीएमएल फ़ाइलों में इस्तेमाल किए गए यूआरएल को भी, अपने Cloud IDE के बाहरी यूआरएल पर अपडेट करना होगा. इसलिए, हमारे पास कुछ ऐसा होगा: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/" और इस कोडलैब में मौजूद सभी एचटीएमएल फ़ाइलें, डिफ़ॉल्ट रूप से localhost का इस्तेमाल करती हैं. इसलिए, आपको उन्हें बाहरी Cloud IDE यूआरएल: data-login_uri="https://your-project-name.glitch.me/user-login" से अपडेट करना होगा.

3. लैंडिंग पेज बनाना

इसके बाद, हम एक ऐसा लैंडिंग पेज बनाएंगे जो One Tap से साइन इन करने के नतीजे दिखाता है. पेज पर, डिकोड किया गया JWT आईडी टोकन या गड़बड़ी दिखती है. पेज पर मौजूद फ़ॉर्म का इस्तेमाल, हमारे Python एचटीटीपी सर्वर पर लॉगिन एंडपॉइंट पर JWT भेजने के लिए भी किया जा सकता है. यहां JWT को डिकोड और पुष्टि की जाती है. यह सीएसआरएफ़ डबल-सबमिट कुकी और पोस्ट अनुरोध पैरामीटर का इस्तेमाल करता है, ताकि वह कोडलैब में gsi/client एचटीएमएल और JavaScript API के उदाहरणों के तौर पर उसी user-login सर्वर एंडपॉइंट का फिर से इस्तेमाल कर सके.

अपने टर्मिनल में, इसे 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 की पुष्टि करना

बटन दबाने पर, एंट्री फ़ील्ड के कॉन्टेंट के साथ Python स्क्रिप्ट को एक POST भेजा जाता है. स्क्रिप्ट को उम्मीद है कि एंट्री फ़ील्ड में कोड में बदला गया JWT मौजूद होगा. इसलिए, यह पेलोड को डिकोड करने और उसकी पुष्टि करने की कोशिश करती है. इसके बाद, यह नतीजे दिखाने के लिए लैंडिंग पेज पर वापस रीडायरेक्ट करता है.

लेकिन रुकिए, कोई JWT नहीं था... क्या यह काम नहीं कर रहा था? हां, लेकिन शानदार तरीके से!

फ़ील्ड खाली होने की वजह से, गड़बड़ी का मैसेज दिखता है. अब एंट्री फ़ील्ड में कोई टेक्स्ट डालें और बटन को फिर से दबाएं. डिकोड करने में कोई दूसरी गड़बड़ी होती है.

एंट्री फ़ील्ड में, Google से जारी किया गया कोड किया गया JWT आईडी टोकन चिपकाया जा सकता है. इसके बाद, Python स्क्रिप्ट को डिकोड करने, उसकी पुष्टि करने, और उसे आपके लिए दिखाने के लिए कहा जा सकता है. इसके अलावा, किसी भी कोड किए गए JWT की जांच करने के लिए, https://jwt.io का इस्तेमाल किया जा सकता है.

4. स्टैटिक एचटीएमएल पेज

ठीक है, अब हम One Tap को सेट अप करेंगे, ताकि यह बिना किसी JavaScript के एचटीएमएल पेजों पर काम कर सके. यह स्टैटिक साइटों या कैश मेमोरी सिस्टम और सीडीएन के लिए फ़ायदेमंद हो सकता है.

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 से बदलें जिसका इस्तेमाल आपने 'Google से साइन इन करें' बटन के पिछले कोडलैब में किया था.

तो यह क्या करता है?

g_id_onload का id और उसके डेटा एट्रिब्यूट वाला कोई भी एचटीएमएल एलिमेंट, Google Identity Services लाइब्रेरी (gsi/client) को कॉन्फ़िगर करने के लिए इस्तेमाल किया जाता है. यह ब्राउज़र में दस्तावेज़ लोड होने पर, One Tap प्रॉम्प्ट भी दिखाता है.

data-login_uri एट्रिब्यूट वह यूआरआई होता है जिसे उपयोगकर्ता के साइन इन करने के बाद, ब्राउज़र से POST अनुरोध मिलेगा. इस अनुरोध में, Google से जारी किया गया कोड किया गया JWT शामिल है.

एक टैप वाले विकल्पों की पूरी सूची के लिए, एचटीएमएल कोड जनरेटर और एचटीएमएल एपीआई रेफ़रंस देखें.

साइन-इन करें

http://localhost:3000/static-page.html पर क्लिक करें.

आपको अपने ब्राउज़र में, 'एक टैप' प्रॉम्प्ट दिखेगा.

One Tap का यूज़र इंटरफ़ेस (यूआई)

साइन इन करने के लिए, इसी खाते से जारी रखें दबाएं.

साइन इन करने के बाद, Google आपके Python सर्वर के लॉगिन एंडपॉइंट पर एक POST अनुरोध भेजता है. अनुरोध में, कोड में बदला गया ऐसा JWT शामिल होता है जिस पर Google ने हस्ताक्षर किया हो.

इसके बाद, सर्वर Google की सार्वजनिक साइनिंग कुंजियों में से किसी एक का इस्तेमाल करके, इस बात की पुष्टि करता है कि Google ने JWT बनाया है और उस पर हस्ताक्षर किया है. इसके बाद, यह कोड को डिकोड करता है और पुष्टि करता है कि ऑडियंस आपके क्लाइंट आईडी से मेल खाती है या नहीं. इसके बाद, सीएसआरएफ़ की जांच की जाती है, ताकि यह पक्का किया जा सके कि पोस्ट बॉडी में कुकी वैल्यू और अनुरोध पैरामीटर की वैल्यू एक जैसी है. अगर ऐसा नहीं है, तो यह किसी समस्या का पक्का संकेत है.

आखिर में, लैंडिंग पेज पर पुष्टि किए गए JWT को JSON फ़ॉर्मैट वाले आईडी टोकन के उपयोगकर्ता क्रेडेंशियल के तौर पर दिखाया जाता है.

पुष्टि किया गया JWT

आम तौर पर होने वाली गड़बड़ियां

साइन इन फ़्लो के पूरा न होने की कई वजहें हो सकती हैं. इसकी कुछ आम वजहें ये हैं:

  • data-client_id मौजूद नहीं है या गलत है. इस मामले में, आपको DevTools कंसोल में गड़बड़ी दिखेगी और One Tap प्रॉम्प्ट काम नहीं करेगा.
  • data-login_uri उपलब्ध नहीं है, क्योंकि गलत यूआरआई डाला गया था, वेब सर्वर शुरू नहीं हुआ था या गलत पोर्ट पर सुन रहा था. ऐसा होने पर, One Tap प्रॉम्प्ट काम करता हुआ दिखता है. हालांकि, क्रेडेंशियल वापस मिलने पर, DevTools के नेटवर्क टैब में गड़बड़ी दिखेगी.
  • आपके वेब सर्वर का इस्तेमाल किया जा रहा होस्टनेम या पोर्ट, आपके OAuth क्लाइंट आईडी के लिए अनुमति वाले JavaScript ऑरिजिन की सूची में शामिल नहीं है. आपको कंसोल में यह मैसेज दिखेगा: "आईडी एश्योरेंस एंडपॉइंट को फ़ेच करते समय, 400 एचटीटीपी रिस्पॉन्स कोड मिला.". अगर आपको इस कोडलैब के दौरान यह दिखता है, तो देखें कि http://localhost/ और http://localhost:3000, दोनों सूची में शामिल हैं या नहीं.

5. डाइनैमिक पेज

अब हम JavaScript कॉल का इस्तेमाल करके, One Tap दिखाएंगे. इस उदाहरण में, हम पेज लोड होने पर हमेशा One Tap दिखाएंगे. हालांकि, आपके पास सिर्फ़ ज़रूरत पड़ने पर प्रॉम्प्ट दिखाने का विकल्प होता है. उदाहरण के लिए, यह देखा जा सकता है कि उपयोगकर्ता का सेशन 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 से बदलें जिसका इस्तेमाल आपने 'Google से साइन इन करें' बटन के पिछले कोडलैब में किया था.

यह कोड, एचटीएमएल और JavaScript का मिश्रण है. यह कई काम करता है:

  • google.accounts.id.initialize() को कॉल करके, Google Identity Services लाइब्रेरी (gsi/client) को कॉन्फ़िगर करता है,
  • किसी दूसरी साइट से किए गए फ़र्ज़ी अनुरोध (सीएसआरएफ़) की कुकी जनरेट करता है,
  • Google से एन्क्रिप्ट किया गया JWT पाने और उसे फ़ॉर्म पोस्ट का इस्तेमाल करके, हमारी Python स्क्रिप्ट /user-login एंडपॉइंट पर सबमिट करने के लिए, कॉलबैक हैंडलर जोड़ता है. साथ ही,
  • google.accounts.id.prompt() का इस्तेमाल करके, One Tap प्रॉम्प्ट दिखाता है.

One Tap की सेटिंग की पूरी सूची, JavaScript API के रेफ़रंस में देखी जा सकती है.

चलिए, साइन-इन करते हैं!

अपने ब्राउज़र में http://localhost:3000/dynamic-page.html खोलें.

One Tap प्रॉम्प्ट का व्यवहार, स्टैटिक एचटीएमएल के मामले जैसा ही होता है. हालांकि, इस पेज में एक JavaScript कॉलबैक हैंडलर तय किया जाता है, ताकि सीएसआरएफ कुकी बनाई जा सके, Google से JWT प्राप्त किया जा सके, और उसे Python सर्वर के user-login एंडपॉइंट पर पोस्ट किया जा सके. एचटीएमएल एपीआई, ये चरण आपके लिए अपने-आप पूरा करता है.

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 खोलें

पेज पर मिले क्लिक

एक टैप वाले प्रॉम्प्ट के बाहर कहीं भी क्लिक करके शुरू करें. आपको पेज और कंसोल, दोनों पर "अनुरोध रद्द कर दिया गया है." दिखेगा.

साइन इन करें

इसके बाद, सामान्य तरीके से साइन इन करें. आपको लॉगिंग और सूचना से जुड़े अपडेट दिखेंगे. इनका इस्तेमाल, उपयोगकर्ता सेशन को सेट अप करने या रीफ़्रेश करने जैसी किसी चीज़ को ट्रिगर करने के लिए किया जा सकता है.

प्रॉम्प्ट बंद करना

अब पेज को फिर से लोड करें. One Tap दिखने के बाद, उसके टाइटल बार में मौजूद ‘X' पर दबाएं. यह मैसेज कंसोल में लॉग किया जाना चाहिए:

  • "उपयोगकर्ता ने प्रॉम्प्ट को अस्वीकार या खारिज कर दिया. एपीआई एक्सपोनेंशियल कूल डाउन ट्रिगर हुआ."

टेस्टिंग के दौरान, कूल डाउन ट्रिगर हो जाएगा. कूल डाउन पीरियड के दौरान, 'एक टैप' प्रॉम्प्ट नहीं दिखता. टेस्टिंग के दौरान, हो सकता है कि आप इसे अपने-आप रीसेट होने का इंतज़ार करने के बजाय, खुद रीसेट करना चाहें. ऐसा तब तक नहीं करना चाहिए, जब तक आपको कॉफ़ी पीने या घर जाकर थोड़ी नींद लेने की ज़रूरत न हो. कूलडाउन रीसेट करने के लिए:

  • ब्राउज़र के पता बार की बाईं ओर मौजूद, "साइट की जानकारी" आइकॉन पर क्लिक करें,
  • "अनुमतियां रीसेट करें" बटन दबाएं और
  • पेज को फिर से लोड करें.

कूलडाउन रीसेट करने और पेज को फिर से लोड करने के बाद, आपको 'एक टैप' सुविधा का अनुरोध दिखेगा.

7. नतीजा

इस कोडलैब में, आपको कुछ चीज़ें पता चली हैं. जैसे, सिर्फ़ स्टैटिक एचटीएमएल का इस्तेमाल करके या JavaScript की मदद से डाइनैमिक तौर पर, One Tap को दिखाने का तरीका.

आपने स्थानीय टेस्टिंग के लिए, एक बहुत ही बुनियादी Python वेब सर्वर सेट अप किया है. साथ ही, आईडी टोकन को डिकोड करने और उनकी पुष्टि करने के लिए ज़रूरी चरणों के बारे में जाना है.

आपने उन सबसे सामान्य तरीकों को आज़मा लिया है जिनसे उपयोगकर्ता, One Tap प्रॉम्प्ट से इंटरैक्ट करते हैं और उसे खारिज करते हैं. साथ ही, आपके पास एक वेब पेज है जिसका इस्तेमाल, प्रॉम्प्ट के व्यवहार को डीबग करने के लिए किया जा सकता है.

बधाई हो!

ज़्यादा क्रेडिट पाने के लिए, पिछले पेज पर जाकर, उन अलग-अलग ब्राउज़र में One Tap का इस्तेमाल करें जिन पर यह काम करता है.

इन लिंक से आपको आगे की कार्रवाई करने में मदद मिल सकती है:

अक्सर पूछे जाने वाले सवाल