পাঠ ৩৪ · ৫৮-এর মধ্যে · মডিউল ৮
Home / Courses / Full-Stack Web Frameworks / টোকেন-ভিত্তিক অথেন্টিকেশন ও JWT

টোকেন-ভিত্তিক অথেন্টিকেশন ও JWT

Token-based authentication & JSON Web Tokens
১২ মিনিট পড়া মধ্যবর্তী · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • JWT-র তিনটি অংশ (header, payload, signature) ও তাদের base64url এনকোডিং
  • HMAC-SHA256 দিয়ে সিগনেচার গণনা করে একটি সম্পূর্ণ, সত্যিকারের JWT তৈরি করা
  • constant-time তুলনা (hmac.compare_digest) ব্যবহার করে একটি বাস্তব verify_jwt() ফাংশন লেখা
  • একটি টেম্পার-করা টোকেন কেন ও কীভাবে ভেরিফিকেশনে ব্যর্থ হয় তা হাতে-কলমে প্রমাণ করা

১ · JWT কেন — সেশনের বিকল্প স্টেটলেস পদ্ধতি

L33-এ দেখা সেশন-ভিত্তিক অথেন্টিকেশনে সার্ভারকে প্রতিটি সক্রিয় ইউজারের জন্য একটি এন্ট্রি মনে রাখতে হয় (স্টেটফুল)। JWTJSON Web Tokenএকটি স্ব-অন্তর্ভুক্ত (self-contained), ডিজিটালি-সাইনড টোকেন যা নিজের ভেতরেই দাবি (claims) বহন করে, ফলে সার্ভারকে আলাদা করে কোনো সেশন মনে রাখতে হয় না।-এর মূল ধারণা হলো — টোকেনটি নিজেই সব প্রয়োজনীয় তথ্য (যেমন user_id) বহন করে, এবং একটি ক্রিপ্টোগ্রাফিক সিগনেচার দিয়ে "সিল" করা থাকে। সার্ভার শুধু সিগনেচার যাচাই করে নিশ্চিত হয় টোকেনটি সত্যিই তার নিজের ইস্যু করা এবং কেউ এটি বদলায়নি — কোনো session_store লুকআপ প্রয়োজন হয় না, তাই একাধিক সার্ভার ইনস্ট্যান্স নির্বিঘ্নে যেকোনো একটি এই টোকেন যাচাই করতে পারে।

Header
অ্যালগরিদম ও টোকেনের ধরন জানায় — {"alg":"HS256","typ":"JWT"}।
Payload
প্রকৃত দাবি (claims) — যেমন user_id, exp (মেয়াদ শেষের সময়)।
Signature
Header ও Payload-এর উপর গোপন কী দিয়ে গণনা করা HMAC-SHA256 — টেম্পারিং ধরার একমাত্র রক্ষাকবচ।

২ · একটি সত্যিকারের JWT তৈরি ও যাচাই — encode → sign → verify → tamper

নিচের কোড সেলে চারটি সত্যিকারের ধাপ আছে — (১) header ও payload ডিকশনারিকে base64url এনকোড করা, (২) দুটো অংশ . দিয়ে জোড়া দিয়ে "signing input" তৈরি করা, (৩) সেই ইনপুটকে গোপন কী দিয়ে HMAC-SHA256 সাইন করে সিগনেচার বানানো, (৪) তিনটি অংশ জোড়া দিয়ে চূড়ান্ত টোকেন তৈরি করা। তারপর একটি সত্যিকারের verify_jwt() ফাংশন সিগনেচার পুনরায় গণনা করে hmac.compare_digest() দিয়ে constant-time তুলনা করে যাচাই করে।

Python
import hashlib, hmac, base64, json, time

def b64url_encode(data: bytes) -> str:
    """স্ট্যান্ডার্ড base64url এনকোড করে, JWT কনভেনশন অনুযায়ী শেষের '=' প্যাডিং বাদ দিয়ে"""
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")

def b64url_decode(s: str) -> bytes:
    """বাদ দেওয়া প্যাডিং ফিরিয়ে দিয়ে base64url ডিকোড করে"""
    padding = "=" * (-len(s) % 4)
    return base64.urlsafe_b64decode(s + padding)

def create_jwt(payload: dict, secret: str) -> str:
    header = {"alg": "HS256", "typ": "JWT"}
    header_b64 = b64url_encode(json.dumps(header, separators=(",", ":")).encode())
    payload_b64 = b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
    signing_input = f"{header_b64}.{payload_b64}"
    signature = hmac.new(secret.encode(), signing_input.encode(), hashlib.sha256).digest()
    signature_b64 = b64url_encode(signature)
    return f"{signing_input}.{signature_b64}"

def verify_jwt(token: str, secret: str):
    """টোকেন ভেঙে সিগনেচার পুনরায় গণনা করে constant-time তুলনা করে --
    মিললে ডিকোড করা পেলোড, না মিললে None রিটার্ন করে"""
    parts = token.split(".")
    if len(parts) != 3:
        return None
    header_b64, payload_b64, signature_b64 = parts
    signing_input = f"{header_b64}.{payload_b64}"
    expected_sig = hmac.new(secret.encode(), signing_input.encode(), hashlib.sha256).digest()
    expected_sig_b64 = b64url_encode(expected_sig)
    if not hmac.compare_digest(expected_sig_b64, signature_b64):
        return None
    return json.loads(b64url_decode(payload_b64))

SECRET_KEY = "abcltech-super-secret-key"

payload = {"user_id": 1, "username": "sohel", "exp": int(time.time()) + 3600}
token = create_jwt(payload, SECRET_KEY)
print("তৈরি হওয়া JWT (header.payload.signature):")
print(token)

decoded = verify_jwt(token, SECRET_KEY)
print("\n--- আসল টোকেন ভেরিফাই করা ---")
print("ভেরিফিকেশন পাস করেছে:", decoded is not None)
print("ফেরত পাওয়া পেলোড:", decoded)
print("আসল পেলোডের সাথে হুবহু মিলেছে:", decoded == payload)

# --- এখন টোকেন টেম্পার করা যাক -- পেলোড বদলে সিগনেচার পুনরায় সাইন করা হয়নি ---
header_b64, payload_b64, signature_b64 = token.split(".")
tampered_payload = json.loads(b64url_decode(payload_b64))
tampered_payload["user_id"] = 999
tampered_payload_b64 = b64url_encode(json.dumps(tampered_payload, separators=(",", ":")).encode())
tampered_token = f"{header_b64}.{tampered_payload_b64}.{signature_b64}"

print("\n--- টেম্পার-করা টোকেন ভেরিফাই করা ---")
tampered_result = verify_jwt(tampered_token, SECRET_KEY)
print("ভেরিফিকেশন পাস করেছে:", tampered_result is not None)
print("রিটার্ন মান:", tampered_result)

    
টেম্পার-করা টোকেনে শুধু payload_b64 অংশ বদলানো হয়েছে, signature_b64 অংশ পুরনোটাই রাখা হয়েছে (আক্রমণকারীর কাছে SECRET_KEY নেই, তাই সে নতুন সঠিক সিগনেচার বানাতে পারবে না)। verify_jwt() যখন নতুন (বদলে যাওয়া) payload_b64 দিয়ে সিগনেচার পুনরায় গণনা করে, ফলাফলটি টোকেনে থাকা পুরনো সিগনেচারের সাথে মিলবে না — তাই hmac.compare_digest() False রিটার্ন করে এবং পুরো ফাংশন None রিটার্ন করে। এটাই দেখায় সিগনেচার আসলে ডেটার অখণ্ডতা (integrity) রক্ষা করে, শুধু ডেটাকে অস্পষ্ট (obscure) করে না।
কেন constant-time তুলনা (hmac.compare_digest)

সাধারণ == দিয়ে দুটো স্ট্রিং তুলনা করলে Python প্রথম অমিল অক্ষরেই তুলনা থামিয়ে দেয় — এর মানে তুলনার সময় (কত মাইক্রোসেকেন্ড লাগলো) কতগুলো অক্ষর মিলেছে তার উপর নির্ভর করে। একজন আক্রমণকারী বহুবার চেষ্টা করে এই সময়ের পার্থক্য পরিমাপ করে ধীরে ধীরে সঠিক সিগনেচার অনুমান করতে পারে (একে "টাইমিং অ্যাটাক" বলা হয়)। hmac.compare_digest() সবসময় একই সময় নেয়, তুলনা যত অক্ষরই মিলুক না কেন — তাই এই আক্রমণ ঠেকায়।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ JWT-র পেলোড এনকোডেড, কিন্তু এনক্রিপ্টেড নয় — এর ব্যবহারিক অর্থ কী?

base64url এনকোডিং কোনো গোপনীয়তা (secrecy) দেয় না — এটি শুধু bytes-কে টেক্সট-নিরাপদ ফরম্যাটে রূপান্তর করে, এবং যেকেউ b64url_decode() দিয়ে পেলোড পড়ে ফেলতে পারে (এই কোডের মতোই)। তাই JWT-র পেলোডে কখনো পাসওয়ার্ড, ক্রেডিট কার্ড নম্বরের মতো গোপন তথ্য রাখা উচিত নয় — সিগনেচার শুধু নিশ্চিত করে ডেটা বদলানো হয়নি, কিন্তু গোপন রাখে না।

প্র ০২ SECRET_KEY যদি কোনোভাবে ফাঁস হয়ে যায়, তাহলে কী ঝুঁকি তৈরি হয়?

SECRET_KEY ফাঁস হয়ে গেলে যেকেউ create_jwt()-এর মতো নিজেই যেকোনো পেলোড দিয়ে একটি সঠিকভাবে-সাইনড টোকেন তৈরি করতে পারবে — যেমন নিজেকে user_id: 1 (অ্যাডমিন) দাবি করে একটি বৈধ-দেখতে টোকেন বানানো। যেহেতু সার্ভার শুধু সিগনেচার মেলে কি না তা যাচাই করে (এবং সিগনেচার সঠিকভাবেই তৈরি হবে, কারণ কী-টি এখন আক্রমণকারীর কাছেও আছে), সার্ভার এটিকে বৈধ হিসেবে গ্রহণ করবে। তাই SECRET_KEY কখনো সোর্স কোডে হার্ডকোড করে পাবলিক রিপোজিটরিতে রাখা উচিত নয় (L51-এ পরিবেশ ভেরিয়েবল ও সিক্রেট ম্যানেজমেন্ট নিয়ে বিস্তারিত)।

প্র ০৩ উপরের কোডে b64url_decode() ফাংশনে "=" * (-len(s) % 4) লাইনটি কী কাজ করছে?

create_jwt()-এ এনকোড করার সময় rstrip(b"=") দিয়ে শেষের প্যাডিং ক্যারেক্টার (=) বাদ দেওয়া হয়েছিল, কারণ JWT কনভেনশনে প্যাডিং রাখা হয় না। কিন্তু Python-এর base64.urlsafe_b64decode() ফাংশনের সঠিকভাবে কাজ করতে ইনপুটের দৈর্ঘ্য ৪-এর গুণিতক হওয়া প্রয়োজন। -len(s) % 4 হিসাব করে ঠিক কতগুলো = লাগবে দৈর্ঘ্যকে ৪-এর গুণিতক করতে (যেমন দৈর্ঘ্য ৪-এর গুণিতক হলে ০টি, ২ কম হলে ২টি, ৩ কম হলে ১টি) — এভাবে বাদ-দেওয়া প্যাডিং সঠিকভাবে ফিরিয়ে দিয়ে ডিকোড সম্ভব করা হয়।

অনুশীলন

  1. চিন্তা করুন: সেশন-ভিত্তিক অথেন্টিকেশনে (L33) logout() করলে সার্ভার সেশন স্টোর থেকে এন্ট্রি মুছে দিতে পারে, তাই সেই আইডি তৎক্ষণাৎ অকার্যকর হয়ে যায়। JWT-তে "লগআউট" করা কেন সহজাতভাবে বেশি জটিল?

    যেহেতু JWT স্টেটলেস এবং সার্ভার কোনো "ইস্যু-করা টোকেনের তালিকা" রাখে না, তাই কোনো নির্দিষ্ট টোকেনকে তাৎক্ষণিকভাবে "বাতিল" করার সরাসরি উপায় নেই — verify_jwt() শুধু সিগনেচার ও (থাকলে) মেয়াদ যাচাই করে, টোকেনটি "এখনো বৈধ কি না" এমন কোনো তালিকা চেক করে না। বাস্তব সিস্টেমে এটি সমাধান করা হয় ছোট মেয়াদ (exp) রেখে, অথবা একটি আলাদা "ব্ল্যাকলিস্ট" স্টোর রেখে বাতিল-করা টোকেনের আইডি মনে রাখার মাধ্যমে — যা আংশিকভাবে স্টেটলেসনেসের সুবিধা কমিয়ে দেয়।

  2. পরীক্ষা করুন: উপরের কোড সেলে verify_jwt(token, "wrong-secret-key") কল করে দেখুন — অর্থাৎ সঠিক টোকেন কিন্তু ভুল secret দিয়ে যাচাই করলে কী হয়, এবং Run চেপে ফলাফল দেখুন।

    ভুল secret দিয়ে hmac.new() ভিন্ন একটি সিগনেচার তৈরি করবে (কারণ HMAC-এর ফলাফল সরাসরি কী-এর উপর নির্ভরশীল), যা টোকেনে থাকা আসল সিগনেচারের সাথে মিলবে না। ফলে hmac.compare_digest() False রিটার্ন করবে এবং verify_jwt() None রিটার্ন করবে — অর্থাৎ ভুল কী দিয়ে চেষ্টা করা এবং টেম্পার-করা টোকেন — দুটোই একই কারণে ব্যর্থ হয়: সিগনেচার প্রত্যাশিত মানের সাথে মেলে না।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
  • Cybersecurity কোর্স সহোদর কোর্স ক্রিপ্টোগ্রাফিক হ্যাশিং, কী ম্যানেজমেন্ট, ও টাইমিং অ্যাটাকের মতো গভীর নিরাপত্তা বিষয়গুলো সেই কোর্সে বিস্তারিত আলোচিত।
  • Python Programming কোর্স সহোদর কোর্স hashlib, hmac, base64, json মডিউলসহ Python-এর স্ট্যান্ডার্ড লাইব্রেরির ভিত্তি সেই কোর্সে তৈরি হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics ও Full-Stack Web Frameworks — সব এক জায়গায়।
আগের পাঠ
সেশন-ভিত্তিক অথেন্টিকেশন