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

১. ভূমিকা

এই কোডল্যাবটি ‘Sign in with Google button for Web’ কোডল্যাবটির উপর ভিত্তি করে তৈরি, তাই প্রথমে সেটি সম্পন্ন করে নিন।

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

এক ট্যাপে প্রম্পট

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

আপনি যা শিখবেন

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

আপনার যা যা লাগবে

  1. এইচটিএমএল, সিএসএস, জাভাস্ক্রিপ্ট এবং ক্রোম ডেভটুলস (বা সমতুল্য) সম্পর্কে প্রাথমিক জ্ঞান।
  2. এইচটিএমএল এবং জাভাস্ক্রিপ্ট ফাইল সম্পাদনা ও হোস্ট করার একটি স্থান।
  3. পূর্ববর্তী কোডল্যাবে প্রাপ্ত ক্লায়েন্ট আইডি
  4. একটি সাধারণ পাইথন অ্যাপ চালানোর উপযোগী পরিবেশ।

চলো যাই!

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

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

স্থানীয়ভাবে চালিত হয়ে, স্ক্রিপ্টটি ব্রাউজারে ল্যান্ডিং পেজ, স্ট্যাটিক এইচটিএমএল এবং ডাইনামিক ওয়ান ট্যাপ পেজ পরিবেশন করে। এটি POST রিকোয়েস্ট গ্রহণ করে, ক্রেডেনশিয়াল প্যারামিটারে থাকা JWT ডিকোড করে এবং সেটি গুগল আইডেন্টিটির 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()

যেহেতু আমরা আইডি টোকেনের অডিয়েন্স (aud) ফিল্ড ব্যবহার করে যাচাই করব যে JWT-টি আপনার ক্লায়েন্টের নামে ইস্যু করা হয়েছে, তাই আপনার পাইথন অ্যাপকে জানতে হবে কোন ক্লায়েন্ট আইডি ব্যবহার করা হচ্ছে। এটি করার জন্য, আগের 'Sign In With Google' বাটন কোডল্যাবে ব্যবহৃত ক্লায়েন্ট আইডি দিয়ে PUT_YOUR_WEB_CLIENT_ID_HERE প্রতিস্থাপন করুন।

পাইথন পরিবেশ

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

JWT যাচাইকরণ এবং ডিকোড করার জন্য আপনার পাইথন ৩.৮ বা তার পরবর্তী সংস্করণের সাথে কয়েকটি প্যাকেজ প্রয়োজন হবে।

$ python3 --version

আপনার পাইথন ৩-এর ভার্সন যদি ৩.৮-এর চেয়ে কম হয়, তবে প্রত্যাশিত ভার্সনটি খুঁজে পাওয়ার জন্য আপনাকে আপনার শেল 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/) ...

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

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

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

python3 ./simple-server.py

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

৩. একটি ল্যান্ডিং পেজ তৈরি করুন

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

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

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

বাটনটি চাপলে এন্ট্রিফিল্ডের বিষয়বস্তুসহ একটি POST রিকোয়েস্ট পাইথন স্ক্রিপ্টে পাঠানো হয়। স্ক্রিপ্টটি আশা করে যে এন্ট্রিফিল্ডে একটি এনকোডেড JWT থাকবে, তাই এটি পেলোডটি ডিকোড ও যাচাই করার চেষ্টা করে। এরপর ফলাফল দেখানোর জন্য এটি ল্যান্ডিং পেজে রিডাইরেক্ট করে দেয়।

কিন্তু দাঁড়ান, কোনো JWT ছিল না... এটা কি ব্যর্থ হয়নি? হ্যাঁ, তবে চমৎকারভাবে!

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

আপনি এন্ট্রিফিল্ডে গুগল কর্তৃক ইস্যুকৃত একটি এনকোডেড JWT আইডি টোকেন পেস্ট করতে পারেন এবং পাইথন স্ক্রিপ্টটি দিয়ে সেটি ডিকোড, ভেরিফাই ও প্রদর্শন করিয়ে নিতে পারেন... অথবা যেকোনো এনকোডেড JWT পরীক্ষা করার জন্য আপনি https://jwt.io ব্যবহার করতে পারেন।

৪. স্ট্যাটিক এইচটিএমএল পৃষ্ঠা

ঠিক আছে, এখন আমরা কোনো জাভাস্ক্রিপ্ট ব্যবহার না করে HTML পেজে কাজ করার জন্য ওয়ান ট্যাপ সেট আপ করব। এটি স্ট্যাটিক সাইট অথবা ক্যাশিং সিস্টেম এবং সিডিএন-এর জন্য উপযোগী হতে পারে।

প্রথমে 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 খুলুন এবং আগের 'Sign In With Google' বাটন কোডল্যাবে ব্যবহৃত ক্লায়েন্ট আইডি দিয়ে PUT_YOUR_WEB_CLIENT_ID_HERE প্রতিস্থাপন করুন।

তাহলে এটা কী করে?

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

data-login_uri অ্যাট্রিবিউটটি হলো সেই URI, যেখানে ব্যবহারকারী সাইন ইন করার পর ব্রাউজার থেকে একটি POST রিকোয়েস্ট আসবে। এই রিকোয়েস্টটিতে গুগল কর্তৃক ইস্যুকৃত এনকোডেড JWT থাকে।

ওয়ান ট্যাপ অপশনগুলোর সম্পূর্ণ তালিকার জন্য এইচটিএমএল কোড জেনারেটর এবং এইচটিএমএল এপিআই রেফারেন্স দেখুন।

সাইন-ইন

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

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

ওয়ান ট্যাপ UI

সাইন-ইন করতে 'চালিয়ে যান ' চাপুন।

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

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

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

যাচাইকৃত JWT

সাধারণ ভুল

সাইন-ইন প্রক্রিয়া ব্যর্থ হওয়ার বিভিন্ন কারণ রয়েছে। এর মধ্যে সবচেয়ে সাধারণ কয়েকটি কারণ হলো:

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

৫. ডাইনামিক পেজ

এখন আমরা একটি জাভাস্ক্রিপ্ট কল ব্যবহার করে ওয়ান ট্যাপ প্রদর্শন করব। এই উদাহরণে আমরা পৃষ্ঠা লোড হওয়ার সময় সর্বদা ওয়ান ট্যাপ প্রদর্শন করব, কিন্তু আপনি চাইলে শুধুমাত্র প্রয়োজনের সময় প্রম্পটটি প্রদর্শন করতে পারেন। উদাহরণস্বরূপ, আপনি পরীক্ষা করে দেখতে পারেন যে ব্যবহারকারীর সেশনটি ২৮ দিনের বেশি পুরানো কিনা এবং সেই অনুযায়ী সাইন-ইন প্রম্পটটি আবার প্রদর্শন করতে পারেন।

এই কোড নমুনাটি 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 খুলুন এবং আগের 'Sign In With Google' বাটন কোডল্যাবে ব্যবহৃত ক্লায়েন্ট আইডি দিয়ে PUT_YOUR_WEB_CLIENT_ID_HERE প্রতিস্থাপন করুন।

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

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

ওয়ান ট্যাপ সেটিংসের সম্পূর্ণ তালিকা জাভাস্ক্রিপ্ট এপিআই রেফারেন্সে পাওয়া যাবে।

চলুন সাইন-ইন করি!

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

ওয়ান ট্যাপ প্রম্পটের আচরণ স্ট্যাটিক এইচটিএমএল সিনারিওর মতোই, তবে এক্ষেত্রে একটি জাভাস্ক্রিপ্ট কলব্যাক হ্যান্ডলার সংজ্ঞায়িত করা থাকে যা একটি সিএসআরএফ কুকি তৈরি করে, গুগল থেকে জেডব্লিউটি (JWT) গ্রহণ করে এবং পাইথন সার্ভারের user-login এন্ডপয়েন্টে তা পোস্ট (POST) করে। এইচটিএমএল এপিআই আপনার জন্য এই ধাপগুলো স্বয়ংক্রিয়ভাবে সম্পন্ন করে।

ক্রোম নেটওয়ার্ক ট্যাব

৬. দ্রুত আচরণ

সুতরাং চলুন ওয়ান ট্যাপ দিয়ে কিছু পরীক্ষা করে দেখি, কারণ বাটনের মতো এর প্রম্পটটি সবসময় প্রদর্শিত হয় না। ব্রাউজার এবং ব্যবহারকারী এটিকে বাতিল, বন্ধ বা নিষ্ক্রিয় করতে পারে।

প্রথমে, এটি 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'-এ চাপ দিন। এই বার্তাটি কনসোলে লগ করা উচিত:

  • ব্যবহারকারী অনুরোধটি প্রত্যাখ্যান বা খারিজ করেছেন। এপিআই-এর এক্সপোনেনশিয়াল কুলডাউন শুরু হয়েছে।

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

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

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

৭. উপসংহার

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

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

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

অভিনন্দন!

অতিরিক্ত কৃতিত্বের জন্য, আবার ফিরে গিয়ে One Tap-কে এটি সমর্থন করে এমন বিভিন্ন ব্রাউজারে ব্যবহার করার চেষ্টা করুন।

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

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