ওয়েবের জন্য Google One ট্যাপ প্রম্পট দিয়ে সাইন-ইন করুন

1. ভূমিকা

এই কোডল্যাবটি ওয়েব কোডল্যাবের জন্য Google এর সাথে সাইন ইন বোতামে তৈরি করে, তাই প্রথমে এটি সম্পূর্ণ করতে ভুলবেন না।

এই কোডল্যাবে, আপনি HTML এবং JavaScript API ব্যবহার করে স্ট্যাটিক এবং ডাইনামিক ওয়েব পৃষ্ঠাগুলিতে ব্যবহারকারী সাইন-ইন যোগ করতে Google পরিচয় পরিষেবা জাভাস্ক্রিপ্ট লাইব্রেরি এবং ওয়ান ট্যাপ প্রম্পট ব্যবহার করবেন।

একটি ওয়ান ট্যাপ প্রম্পট

আমরা JWT আইডি টোকেন যাচাই করার জন্য একটি সার্ভার-সাইড লগইন এন্ডপয়েন্টও সেটআপ করব।

আপনি কি শিখবেন

  • আইডি টোকেন যাচাই করতে সার্ভার-সাইড লগইন এন্ডপয়েন্ট কিভাবে সেটআপ করবেন
  • কীভাবে একটি ওয়েব পৃষ্ঠায় Google One Tap প্রম্পট যোগ করবেন
    • একটি স্ট্যাটিক HTML উপাদান হিসাবে, এবং
    • গতিশীলভাবে জাভাস্ক্রিপ্ট ব্যবহার করে।
  • ওয়ান ট্যাপ প্রম্পট কীভাবে আচরণ করে

আপনি কি প্রয়োজন হবে

  1. HTML, CSS, JavaScript এবং Chrome DevTools (বা সমতুল্য) এর প্রাথমিক জ্ঞান।
  2. HTML এবং JavaScript ফাইলগুলি সম্পাদনা এবং হোস্ট করার একটি জায়গা৷
  3. আগের কোডল্যাবে প্রাপ্ত ক্লায়েন্ট আইডি
  4. একটি মৌলিক পাইথন অ্যাপ চালাতে সক্ষম একটি পরিবেশ।

যেতে দাও!

2. একটি লগইন এন্ডপয়েন্ট সেটআপ করুন

প্রথমত, আমরা একটি পাইথন স্ক্রিপ্ট তৈরি করব যা একটি মৌলিক ওয়েব সার্ভার হিসাবে কাজ করে এবং এটি চালানোর জন্য প্রয়োজনীয় পাইথন পরিবেশ সেটআপ করে।

স্থানীয়ভাবে চলমান, স্ক্রিপ্টটি ব্রাউজারে ল্যান্ডিং পৃষ্ঠা এবং স্ট্যাটিক HTML এবং গতিশীল ওয়ান ট্যাপ পৃষ্ঠাগুলি পরিবেশন করে৷ এটি POST অনুরোধ গ্রহণ করে, শংসাপত্রের প্যারামিটারে থাকা JWT ডিকোড করে এবং যাচাই করে যে এটি Google Identity-এর OAuth প্ল্যাটফর্ম দ্বারা জারি করা হয়েছে।

একটি 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 জারি করা হয়েছে তা যাচাই করতে যাচ্ছি, তাই আপনার পাইথন অ্যাপটি জানতে হবে কোন ক্লায়েন্ট আইডি ব্যবহার করা হচ্ছে। এটি করার জন্য, PUT_YOUR_WEB_CLIENT_ID_HERE ক্লায়েন্ট আইডি দিয়ে প্রতিস্থাপন করুন যা আপনি Google বোতাম কোডল্যাবের সাথে আগের সাইন ইনে ব্যবহার করেছিলেন৷

পাইথন পরিবেশ

ওয়েব সার্ভার স্ক্রিপ্ট চালানোর জন্য পরিবেশ সেটআপ করা যাক।

JWT যাচাইকরণ এবং ডিকোড করতে সহায়তা করার জন্য আপনাকে Python 3.8 বা তার পরে কিছু প্যাকেজের প্রয়োজন হবে।

$ python3 --version

যদি আপনার python3 এর সংস্করণটি 3.8 এর কম হয় তাহলে আপনাকে আপনার শেল PATH পরিবর্তন করতে হতে পারে যাতে প্রত্যাশিত সংস্করণটি পাওয়া যায় বা আপনার সিস্টেমে পাইথনের একটি নতুন সংস্করণ ইনস্টল করুন।

এরপরে, 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/) ...

ক্লাউড ভিত্তিক IDEs

এই কোডল্যাবটি স্থানীয় টার্মিনাল এবং লোকালহোস্টে চালানোর জন্য ডিজাইন করা হয়েছিল, তবে কিছু পরিবর্তনের সাথে এটি রিপ্লিট বা গ্লিচের মতো প্ল্যাটফর্মে ব্যবহার করা যেতে পারে। প্রতিটি প্ল্যাটফর্মের নিজস্ব সেটআপ প্রয়োজনীয়তা এবং পাইথন পরিবেশের ডিফল্ট রয়েছে তাই আপনাকে সম্ভবত TARGET_HTML_PAGE_URL এবং পাইথন সেটআপের মতো কয়েকটি জিনিস পরিবর্তন করতে হবে।

উদাহরণস্বরূপ, গ্লিচে আপনি এখনও একটি requirements.txt যোগ করবেন কিন্তু স্বয়ংক্রিয়ভাবে পাইথন সার্ভার শুরু করতে start.sh নামে একটি ফাইল তৈরি করবেন:

python3 ./simple-server.py

পাইথন স্ক্রিপ্ট এবং এইচটিএমএল ফাইলগুলির দ্বারা ব্যবহৃত URLগুলিকেও আপনার ক্লাউড IDE-এর বাহ্যিক URL-এ আপডেট করা দরকার৷ তাই আমাদের কাছে এরকম কিছু থাকবে: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/" এবং যেহেতু এই কোডল্যাব জুড়ে এইচটিএমএল ফাইলগুলিও লোকালহোস্ট ব্যবহার করার জন্য ডিফল্ট, আপনাকে তাদের বাহ্যিক ক্লাউড IDE URL: data-login_uri="https://your-project-name.glitch.me/user-login" .

3. একটি ল্যান্ডিং পৃষ্ঠা তৈরি করুন

এরপরে, আমরা একটি ল্যান্ডিং পৃষ্ঠা তৈরি করব যা ওয়ান ট্যাপ দিয়ে সাইন ইন করার ফলাফল প্রদর্শন করে। পৃষ্ঠাটি ডিকোড করা JWT আইডি টোকেন বা একটি ত্রুটি প্রদর্শন করে। আমাদের পাইথন HTTP সার্ভারের লগইন এন্ডপয়েন্টে একটি JWT পাঠাতেও পৃষ্ঠার একটি ফর্ম ব্যবহার করা যেতে পারে যেখানে এটি ডিকোড এবং যাচাই করা হয়। এটি একটি CSRF ডবল-সাবমিট কুকি এবং POST অনুরোধ প্যারামিটার ব্যবহার করে যাতে এটি কোডল্যাবে gsi/client এইচটিএমএল এবং জাভাস্ক্রিপ্ট এপিআই উদাহরণগুলির মতো একই 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 ডিকোডিং পরীক্ষা করুন

ওয়ান ট্যাপ দিয়ে কাজ করার চেষ্টা করার আগে আমরা নিশ্চিত করব যে সার্ভারের এন্ডপয়েন্ট পরিবেশ সেটআপ এবং কাজ করছে।

ল্যান্ডিং পেজে ব্রাউজ করুন, http://localhost:3000/ এবং ভেরিফাই JWT বোতাম টিপুন।

আপনি এই দেখা উচিত

কোন বিষয়বস্তু ছাড়া JWT যাচাই করুন

বোতাম টিপলে পাইথন স্ক্রিপ্টে এন্ট্রিফিল্ডের বিষয়বস্তু সহ একটি পোস্ট পাঠানো হয়। স্ক্রিপ্টটি এন্ট্রিফিল্ডে একটি এনকোডেড JWT উপস্থিত থাকার আশা করছে, তাই এটি পেলোড ডিকোড এবং যাচাই করার চেষ্টা করে। তারপরে এটি ফলাফলগুলি প্রদর্শন করতে ল্যান্ডিং পৃষ্ঠায় পুনঃনির্দেশিত করে।

কিন্তু অপেক্ষা করুন, একটি JWT ছিল না... এটি কি ব্যর্থ হয়নি? হ্যাঁ, কিন্তু করুণাময়!

যেহেতু ক্ষেত্রটি খালি ছিল একটি ত্রুটি প্রদর্শিত হয়। এখন এন্ট্রিফিল্ডে কিছু পাঠ্য (যে কোনো পাঠ্য) প্রবেশ করার চেষ্টা করুন এবং আবার বোতাম টিপুন। এটি একটি ভিন্ন ডিকোড ত্রুটির সাথে ব্যর্থ হয়।

আপনি এন্ট্রিফিল্ডে একটি এনকোড করা Google জারি করা JWT আইডি টোকেন পেস্ট করতে পারেন এবং পাইথন স্ক্রিপ্ট ডিকোড করতে পারেন, যাচাই করতে এবং আপনার জন্য এটি প্রদর্শন করতে পারেন... অথবা আপনি যেকোনো এনকোড করা JWT পরিদর্শন করতে https://jwt.io ব্যবহার করতে পারেন।

4. স্ট্যাটিক HTML পৃষ্ঠা

ঠিক আছে, এখন আমরা কোনো জাভাস্ক্রিপ্ট ব্যবহার না করেই HTML পৃষ্ঠাগুলিতে কাজ করার জন্য One Tap সেট আপ করব। এটি স্ট্যাটিক সাইট বা ক্যাশিং সিস্টেম এবং 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 ক্লায়েন্ট আইডি দিয়ে প্রতিস্থাপন করুন যা আপনি Google বোতাম কোডল্যাবের সাথে পূর্ববর্তী সাইন ইনে ব্যবহার করেছিলেন।

তাহলে এই কি করে?

g_id_onload id সহ যেকোন এইচটিএমএল উপাদান এবং এর ডেটা অ্যাট্রিবিউটগুলি Google আইডেন্টিটি সার্ভিসেস লাইব্রেরি ( gsi/client ) কনফিগার করতে ব্যবহৃত হয়। ব্রাউজারে ডকুমেন্ট লোড হলে এটি ওয়ান ট্যাপ প্রম্পটও প্রদর্শন করে।

data-login_uri অ্যাট্রিবিউট হল ইউআরআই যা ব্যবহারকারী সাইন ইন করার পরে ব্রাউজার থেকে একটি POST অনুরোধ পাবে৷ এই অনুরোধটিতে Google দ্বারা জারি করা এনকোড করা JWT রয়েছে৷

ওয়ান ট্যাপ বিকল্পের সম্পূর্ণ তালিকার জন্য HTML কোড জেনারেটর এবং HTML API রেফারেন্স দেখুন।

সাইন ইন করুন

http://localhost:3000/static-page.html এ ক্লিক করুন।

আপনার ব্রাউজারে প্রদর্শিত ওয়ান ট্যাপ প্রম্পট দেখতে হবে।

ওয়ান ট্যাপ UI

সাইন-ইন করতে অবিরত প্রেস করুন।

সাইন ইন করার পর, Google আপনার Python সার্ভারের লগইন এন্ডপয়েন্টে একটি POST অনুরোধ পাঠায়। অনুরোধটিতে একটি এনকোড করা JWT রয়েছে যা Google দ্বারা স্বাক্ষরিত।

সেখান থেকে, সার্ভারটি Google এর পাবলিক সাইনিং কীগুলির একটি ব্যবহার করে যাচাই করে যে Google JWT তৈরি করেছে এবং স্বাক্ষর করেছে৷ তারপর এটি ডিকোড করে এবং যাচাই করে যে দর্শকরা আপনার ক্লায়েন্ট আইডির সাথে মেলে। এরপরে, POST বডিতে কুকির মান এবং অনুরোধের প্যারামিটার মান সমান কিনা তা নিশ্চিত করতে একটি CSRF চেক করা হয়। যদি তারা না হয়, এটি সমস্যার একটি নিশ্চিত চিহ্ন।

অবশেষে, ল্যান্ডিং পৃষ্ঠা সফলভাবে যাচাইকৃত JWT-কে JSON ফর্ম্যাট করা ID টোকেন ব্যবহারকারীর শংসাপত্র হিসাবে প্রদর্শন করে।

যাচাইকৃত JWT

সাধারণ ত্রুটি

সাইন-ইন প্রবাহ ব্যর্থ হওয়ার জন্য বিভিন্ন উপায় রয়েছে৷ সবচেয়ে সাধারণ কিছু কারণ হল:

  • data-client_id অনুপস্থিত বা ভুল, এই ক্ষেত্রে আপনি DevTools কনসোলে একটি ত্রুটি দেখতে পাবেন এবং ওয়ান ট্যাপ প্রম্পট কাজ করবে না।
  • data-login_uri উপলভ্য নয় কারণ একটি ভুল URI প্রবেশ করানো হয়েছে, ওয়েব সার্ভার শুরু হয়নি বা ভুল পোর্টে শুনছে। যদি এটি ঘটে তবে ওয়ান ট্যাপ প্রম্পটটি কাজ করছে বলে মনে হচ্ছে কিন্তু শংসাপত্রটি ফেরত দিলে DevTools নেটওয়ার্ক ট্যাবে একটি ত্রুটি দৃশ্যমান হবে।
  • আপনার ওয়েব সার্ভার যে হোস্টনাম বা পোর্ট ব্যবহার করছে সেটি আপনার OAuth ক্লায়েন্ট আইডির জন্য অনুমোদিত জাভাস্ক্রিপ্ট অরিজিনে তালিকাভুক্ত নয়। আপনি একটি কনসোল বার্তা দেখতে পাবেন: " আইডি অ্যাসারশন এন্ডপয়েন্ট আনার সময়, একটি 400 HTTP প্রতিক্রিয়া কোড পাওয়া গেছে। " আপনি যদি এই কোডল্যাবের সময় এটি দেখতে পান তবে দেখুন যে http://localhost/ এবং http://localhost:3000 উভয়ই তালিকাভুক্ত রয়েছে।

5. গতিশীল পৃষ্ঠা

এখন আমরা জাভাস্ক্রিপ্ট কল ব্যবহার করে ওয়ান ট্যাপ প্রদর্শন করব। এই উদাহরণে পৃষ্ঠা লোড হলে আমরা সর্বদা এক ট্যাপ প্রদর্শন করব, কিন্তু আপনি যখন প্রয়োজন তখনই প্রম্পট প্রদর্শন করতে বেছে নিতে পারেন। উদাহরণস্বরূপ, আপনি ব্যবহারকারীর সেশন 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 ক্লায়েন্ট আইডি দিয়ে প্রতিস্থাপন করুন যা আপনি Google বোতাম কোডল্যাবের সাথে আগের সাইন ইনে ব্যবহার করেছিলেন৷

এই কোডটি এইচটিএমএল এবং জাভাস্ক্রিপ্টের মিশ্রণ, এটি বেশ কিছু কাজ করে:

  • google.accounts.id.initialize() এ কল করে Google আইডেন্টিটি সার্ভিসেস লাইব্রেরি ( gsi/client ) কনফিগার করে,
  • একটি ক্রস-সাইট অনুরোধ জালিয়াতি ( CSRF ) কুকি তৈরি করে,
  • Google থেকে এনকোড করা JWT পাওয়ার জন্য একটি কলব্যাক হ্যান্ডলার যোগ করে এবং আমাদের Python স্ক্রিপ্ট /user-login endpoint-এ একটি ফর্ম POST ব্যবহার করে জমা দেয় এবং
  • google.accounts.id.prompt() ব্যবহার করে ওয়ান ট্যাপ প্রম্পট প্রদর্শন করে।

One Tap সেটিংসের একটি সম্পূর্ণ তালিকা JavaScript API রেফারেন্সে পাওয়া যাবে।

সাইন ইন করা যাক!

আপনার ব্রাউজারে http://localhost:3000/dynamic-page.html খুলুন।

ওয়ান ট্যাপ প্রম্পট আচরণটি স্ট্যাটিক এইচটিএমএল দৃশ্যের মতোই, এই পৃষ্ঠাটি একটি CSRF কুকি তৈরি করতে, Google থেকে JWT গ্রহণ করতে এবং পাইথন সার্ভারের user-login এন্ডপয়েন্টে পোস্ট করার জন্য একটি জাভাস্ক্রিপ্ট কলব্যাক হ্যান্ডলারকে সংজ্ঞায়িত করে। HTML API আপনার জন্য স্বয়ংক্রিয়ভাবে এই পদক্ষেপগুলি করে৷

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 খুলুন

পৃষ্ঠা ক্লিক

ওয়ান ট্যাপ প্রম্পটের বাইরে যেকোনো জায়গায় ক্লিক করে শুরু করুন। আপনার দেখতে হবে " অনুরোধটি বাতিল করা হয়েছে৷ " পৃষ্ঠা এবং কনসোল উভয়েই লগ করা হয়েছে৷

সাইন ইন

এরপরে, সাধারণভাবে সাইন-ইন করুন। আপনি লগিং এবং বিজ্ঞপ্তি আপডেটগুলি দেখতে পাবেন যা ব্যবহারকারীর সেশন প্রতিষ্ঠা বা রিফ্রেশ করার মতো কিছু ট্রিগার করতে ব্যবহার করা যেতে পারে।

প্রম্পট বন্ধ করুন

এখন পৃষ্ঠাটি পুনরায় লোড করুন এবং একটি ট্যাপ দেখানোর পরে, এর শিরোনাম বারে 'X' টিপুন। এই বার্তাটি কনসোলে লগ করা উচিত:

  • " ব্যবহারকারী প্রম্পট প্রত্যাখ্যান করেছে বা খারিজ করেছে। API সূচকীয় কুল ডাউন ট্রিগার হয়েছে। "

পরীক্ষার সময় আপনি কুল ডাউন ট্রিগার করবেন। কুল ডাউন সময়কালে ওয়ান ট্যাপ প্রম্পট প্রদর্শিত হয় না। পরীক্ষার সময় আপনি সম্ভবত এটি স্বয়ংক্রিয়ভাবে রিসেট হওয়ার জন্য অপেক্ষা করার পরিবর্তে এটিকে পুনরায় সেট করতে চাইবেন... যদি না আপনি সত্যিই কফি পান করতে বা বাড়িতে গিয়ে কিছুটা ঘুমাতে চান। কুলডাউন রিসেট করতে:

  • ব্রাউজারের ঠিকানা বারের বাম পাশে "সাইট তথ্য" আইকনে ক্লিক করুন,
  • "অনুমতি পুনরায় সেট করুন" বোতাম টিপুন, এবং
  • পৃষ্ঠাটি পুনরায় লোড করুন।

কুলডাউন রিসেট করার পরে এবং পৃষ্ঠাটি পুনরায় লোড করার পরে ওয়ান ট্যাপ প্রম্পটটি প্রদর্শিত হবে।

7. উপসংহার

তাই এই কোডল্যাবে আপনি কিছু জিনিস শিখেছেন, যেমন শুধু স্ট্যাটিক এইচটিএমএল ব্যবহার করে বা জাভাস্ক্রিপ্টের সাথে গতিশীলভাবে কীভাবে ওয়ান ট্যাপ প্রদর্শন করতে হয়।

আপনি স্থানীয় পরীক্ষার জন্য একটি খুব মৌলিক পাইথন ওয়েব সার্ভার সেট আপ করেছেন এবং আইডি টোকেনগুলি ডিকোড এবং যাচাই করার জন্য প্রয়োজনীয় পদক্ষেপগুলি শিখেছেন৷

আপনি ব্যবহারকারীদের সাথে ইন্টারঅ্যাক্ট এবং ওয়ান ট্যাপ প্রম্পট খারিজ করার সবচেয়ে সাধারণ উপায়গুলির সাথে খেলেছেন এবং একটি ওয়েব পৃষ্ঠা রয়েছে যা প্রম্পট আচরণ ডিবাগ করতে ব্যবহার করা যেতে পারে।

অভিনন্দন!

অতিরিক্ত ক্রেডিট পাওয়ার জন্য, এটি সমর্থন করে এমন বিভিন্ন ব্রাউজারে ওয়ান ট্যাপ ব্যবহার করে ফিরে যাওয়ার চেষ্টা করুন।

এই লিঙ্কগুলি আপনাকে পরবর্তী পদক্ষেপগুলিতে সাহায্য করতে পারে:

প্রায়শই জিজ্ঞাসিত প্রশ্ন