Authentication: JWT, Password, Role

Login, token, refresh token, permission

রোডম্যাপের মাস ৪পড়তে প্রায় ১৭ মিনিট

"তুমি কে?" (authentication) আর "তোমার কী করার অনুমতি আছে?" (authorization)। প্রায় প্রতিটা API এর এই দুইটা প্রশ্নের উত্তর দিতে হয়, আর এখানে ভুল মানে অন্যের data ফাঁস। এই অধ্যায়ে password নিরাপদে রাখা, JWT দিয়ে login, refresh token, আর role ভিত্তিক permission বানাবো, তোমার Flutter app এর জন্য উপযুক্ত করে।

#Password: কখনো আসল password রাখবে না

Database এ রাখবে password এর hash: একমুখী রূপান্তর, যেখান থেকে আসল password ফেরত বের করা যায় না। Login এর সময় দেওয়া password আবার hash করে মেলাও।

কিন্তু যেকোনো hash না। MD5 বা SHA-256 খুব দ্রুত, ফলে চোর প্রতি সেকেন্ডে কোটি কোটি password আন্দাজ করে দেখতে পারে। দরকার ইচ্ছা করে ধীর hash: Argon2 বা bcrypt।

Terminal
uv add "pwdlib[argon2]" pyjwt
Python
# core/security.py
from pwdlib import PasswordHash

password_hasher = PasswordHash.recommended()     # Argon2


def hash_password(plain: str) -> str:
    return password_hasher.hash(plain)


def verify_password(plain: str, hashed: str) -> bool:
    return password_hasher.verify(plain, hashed)
Python
>>> hash_password("rahim123")
'$argon2id$v=19$m=65536,t=3,p=4$Xc8q...$9vG1...'

Hash এর ভেতরেই salt (প্রতিটা password এর জন্য আলাদা random অংশ) থাকে। তাই দুইজনের একই password হলেও hash আলাদা, আর আগে থেকে বানানো "hash এর তালিকা" দিয়ে ভাঙা যায় না।

#Token ভিত্তিক login: JWT

Mobile app এর জন্য সবচেয়ে প্রচলিত উপায়: login এ server একটা token দেয়, app সেটা রেখে দেয় আর প্রতিটা request এ পাঠায়।

Flutter app FastAPI ১. POST /auth/login (email, password) ২. access token (১৫ মিনিট) + refresh token (৩০ দিন) token নিরাপদে রাখো (secure storage) ৩. GET /notes Authorization: Bearer <access> ৪. 200 OK (token যাচাই হলো) ৫. ১৫ মিনিট পর: 401 (মেয়াদ শেষ) ৬. POST /auth/refresh (refresh token) → নতুন access token
Access token ছোট মেয়াদের, প্রতিটা request এ যায়। Refresh token লম্বা মেয়াদের, শুধু নতুন access token আনতে যায়।

#JWT এর ভেতরে কী আছে

JWT তিন অংশের, . দিয়ে আলাদা: header.payload.signature

JSON
// payload (base64 খুললেই পড়া যায়!)
{
  "sub": "42",              // user id
  "role": "user",
  "type": "access",
  "exp": 1793214000,        // মেয়াদ শেষের সময়
  "iat": 1793213100
}

Signature হলো server এর গোপন key দিয়ে বানানো একটা ছাপ। কেউ payload বদলালে ("role": "admin" করে দিলে) signature আর মিলবে না, server ধরে ফেলবে।

#Token বানানো আর যাচাই

Python
# core/security.py (চলবে)
from datetime import datetime, timedelta, timezone
import jwt
from app.core.config import get_settings

ALGORITHM = "HS256"


def create_token(user_id: int, token_type: str, minutes: int, extra: dict | None = None) -> str:
    now = datetime.now(timezone.utc)
    payload = {
        "sub": str(user_id),
        "type": token_type,
        "iat": now,
        "exp": now + timedelta(minutes=minutes),
        **(extra or {}),
    }
    return jwt.encode(payload, get_settings().jwt_secret, algorithm=ALGORITHM)


def decode_token(token: str, expected_type: str) -> dict:
    payload = jwt.decode(token, get_settings().jwt_secret, algorithms=[ALGORITHM])  # মেয়াদ নিজে check করে
    if payload.get("type") != expected_type:
        raise jwt.InvalidTokenError("ভুল ধরনের token")
    return payload

algorithms=[ALGORITHM] অবশ্যই নির্দিষ্ট করে দেবে। না দিলে কিছু পুরনো library তে আক্রমণকারী "alg": "none" দিয়ে signature ছাড়াই token বানাতে পারতো।

#Login endpoint

Python
# api/v1/auth.py
from fastapi import APIRouter
from pydantic import BaseModel, EmailStr

router = APIRouter(prefix="/auth", tags=["auth"])


class LoginIn(BaseModel):
    email: EmailStr
    password: str


class TokenPair(BaseModel):
    access_token: str
    refresh_token: str
    token_type: str = "bearer"


@router.post("/login", response_model=TokenPair)
async def login(data: LoginIn, db: DbDep):
    user = await db.scalar(select(User).where(User.email == data.email.lower()))
    # user না থাকলেও একই message আর প্রায় একই সময়: কোন email এ account আছে সেটা ফাঁস না করা
    if user is None or not verify_password(data.password, user.password_hash) or not user.is_active:
        raise UnauthorizedError("Email বা password ভুল")
    s = get_settings()
    return TokenPair(
        access_token=create_token(user.id, "access", s.jwt_access_minutes, {"role": user.role}),
        refresh_token=create_token(user.id, "refresh", s.jwt_refresh_days * 24 * 60),
    )

#Current user dependency

Python
# api/deps.py
from typing import Annotated
from fastapi import Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt

bearer = HTTPBearer(auto_error=False)


async def get_current_user(
    db: DbDep,
    creds: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)],
) -> User:
    if creds is None:
        raise UnauthorizedError("Login করতে হবে")
    try:
        payload = decode_token(creds.credentials, expected_type="access")
    except jwt.ExpiredSignatureError:
        raise UnauthorizedError("Token এর মেয়াদ শেষ", code="TOKEN_EXPIRED")
    except jwt.InvalidTokenError:
        raise UnauthorizedError("Token সঠিক না")
    user = await db.get(User, int(payload["sub"]))
    if user is None or not user.is_active:
        raise UnauthorizedError("Account পাওয়া যায়নি বা বন্ধ")
    return user


CurrentUser = Annotated[User, Depends(get_current_user)]

এখন যেকোনো route এ user: CurrentUser লিখলেই সেটা login ছাড়া চলবে না। TOKEN_EXPIRED code দেখে Flutter app বুঝবে refresh করতে হবে।

#Refresh token: নিরাপদভাবে

Refresh token ৩০ দিন চলে, চুরি হলে বিপদ। তাই:

  1. Database এ রাখো (hash করে): প্রতিটা refresh token এর একটা row, user_id, expires_at, revoked_at, কোন device।
  2. Rotation: refresh করলে পুরনো refresh token বাতিল, নতুন একটা দাও।
  3. চুরি ধরা: বাতিল হওয়া refresh token আবার কেউ ব্যবহার করতে এলে বুঝবে চুরি হয়েছে। সেই user এর সব refresh token বাতিল করো, আবার login করাও।
  4. Logout: সেই device এর refresh token বাতিল। "সব device থেকে logout": সব বাতিল।
Python
@router.post("/refresh", response_model=TokenPair)
async def refresh(data: RefreshIn, svc: AuthServiceDep):
    return await svc.rotate_refresh_token(data.refresh_token)

#Authorization: অনুমতি

#Role ভিত্তিক

Python
from enum import StrEnum

class Role(StrEnum):
    USER = "user"
    STAFF = "staff"
    ADMIN = "admin"


def require_role(*allowed: Role):
    async def checker(user: CurrentUser) -> User:
        if user.role not in allowed:
            raise PermissionDeniedError()
        return user
    return checker


@router.get("/admin/users", dependencies=[Depends(require_role(Role.ADMIN))])
async def list_all_users(...): ...

#Object এর মালিকানা: সবচেয়ে বেশি ভুল হয় এখানে

Python
# ❌ ভয়ানক: login আছে কিনা দেখলো, কিন্তু note কার সেটা দেখলো না
@router.get("/notes/{note_id}")
async def get_note(note_id: int, user: CurrentUser, db: DbDep):
    return await db.get(Note, note_id)      # যে কেউ ১, ২, ৩... দিয়ে সবার note পড়তে পারবে

# ✓ service এ মালিকানা যাচাই (আগের অধ্যায়ের get_owned)

এই ভুলের নাম IDOR (Insecure Direct Object Reference), আর এটা API security এর এক নম্বর সমস্যা হিসেবে বারবার তালিকায় আসে। AI এর লেখা code এ এটা খুব সাধারণ: login check দেয়, মালিকানা check ভুলে যায়।

#Flutter দিকে

  • Token রাখো flutter_secure_storage এ (Android Keystore / iOS Keychain), SharedPreferences এ না
  • Dio interceptor এ প্রতিটা request এ Authorization: Bearer ... যোগ করো
  • 401 আর TOKEN_EXPIRED পেলে refresh করে আসল request আবার পাঠাও। একসাথে ৫টা request 401 পেলে যেন ৫ বার refresh না হয়, একবারই হয় (lock/queue)
  • Refresh ও ব্যর্থ হলে login screen এ পাঠাও

বিস্তারিত code Flutter ও FastAPI সংযোগ অধ্যায়ে।

#আরও কিছু নিরাপত্তা

  • Login এ rate limit: একটা email এ ৫ বার ভুল হলে কিছুক্ষণ বন্ধ (Redis অধ্যায়ে বানাবো)
  • Password এর নিয়ম: কমপক্ষে ৮ অক্ষর; বহুল ব্যবহৃত password (12345678) বাতিল
  • OTP login: বাংলাদেশে phone + OTP খুব প্রচলিত। OTP ৬ অঙ্কের, ২-৫ মিনিট মেয়াদ, একবার ব্যবহার, hash করে রাখা, আর ভুল চেষ্টার কঠোর সীমা (না হলে ১০ লাখ সম্ভাবনা অল্প সময়ে আন্দাজ করা যায়)
  • Google/Apple login: নিজের password না রেখে OAuth (Security অধ্যায়)

#অনুশীলন

#নিজেকে যাচাই করো

Password hash এর জন্য SHA-256 কেন যথেষ্ট না?

এটা খুব দ্রুত, ফলে চোর database পেলে প্রতি সেকেন্ডে কোটি কোটি password আন্দাজ করে মেলাতে পারে। Argon2 বা bcrypt ইচ্ছা করে ধীর আর বেশি memory নেয়, তাই আন্দাজ করা অনেক কঠিন।

JWT এর payload এ phone নম্বর রাখা কেন ভুল?

JWT শুধু sign করা, encrypt না। যে কেউ token থেকে payload পড়তে পারে। তাই শুধু id আর role এর মতো তথ্য রাখা উচিত।

Access token ছোট মেয়াদের আর refresh token লম্বা মেয়াদের কেন?

Access token প্রতিটা request এ যায়, চুরির সুযোগ বেশি, আর server এটা বাতিল করতে পারে না, তাই ছোট মেয়াদ। Refresh token কম ব্যবহার হয়, database এ রাখা হয় বলে বাতিল করা যায়, তাই লম্বা মেয়াদ দিলেও নিয়ন্ত্রণ থাকে।

IDOR কী? উদাহরণ দাও।

Login যাচাই করা হলো কিন্তু চাওয়া object টা এই user এর কিনা যাচাই হলো না। যেমন /notes/57 তে যেকোনো login করা user অন্যের note পড়তে পারে। সমাধান: প্রতিটা object এ মালিকানা বা অনুমতি যাচাই।

Login এ "এই email এ account নেই" আর "password ভুল" আলাদা message দেওয়া কেন উচিত না?

তাহলে আক্রমণকারী জেনে যায় কোন email এ account আছে, তারপর সেগুলোতে password আন্দাজ বা phishing চালায়। একই message দেওয়া নিরাপদ।

চিহ্ন দিলে sidebar আর রোডম্যাপে এই অধ্যায়ের station ভরে যাবে।