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।
uv add "pwdlib[argon2]" pyjwt# 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)>>> 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 এ পাঠায়।
#JWT এর ভেতরে কী আছে
JWT তিন অংশের, . দিয়ে আলাদা: header.payload.signature
// payload (base64 খুললেই পড়া যায়!)
{
"sub": "42", // user id
"role": "user",
"type": "access",
"exp": 1793214000, // মেয়াদ শেষের সময়
"iat": 1793213100
}Signature হলো server এর গোপন key দিয়ে বানানো একটা ছাপ। কেউ payload বদলালে ("role": "admin" করে দিলে) signature আর মিলবে না, server ধরে ফেলবে।
#Token বানানো আর যাচাই
# 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 payloadalgorithms=[ALGORITHM] অবশ্যই নির্দিষ্ট করে দেবে। না দিলে কিছু পুরনো library তে আক্রমণকারী "alg": "none" দিয়ে signature ছাড়াই token বানাতে পারতো।
#Login endpoint
# 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
# 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 ৩০ দিন চলে, চুরি হলে বিপদ। তাই:
- Database এ রাখো (hash করে): প্রতিটা refresh token এর একটা row,
user_id,expires_at,revoked_at, কোন device। - Rotation: refresh করলে পুরনো refresh token বাতিল, নতুন একটা দাও।
- চুরি ধরা: বাতিল হওয়া refresh token আবার কেউ ব্যবহার করতে এলে বুঝবে চুরি হয়েছে। সেই user এর সব refresh token বাতিল করো, আবার login করাও।
- Logout: সেই device এর refresh token বাতিল। "সব device থেকে logout": সব বাতিল।
@router.post("/refresh", response_model=TokenPair)
async def refresh(data: RefreshIn, svc: AuthServiceDep):
return await svc.rotate_refresh_token(data.refresh_token)#Authorization: অনুমতি
#Role ভিত্তিক
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 এর মালিকানা: সবচেয়ে বেশি ভুল হয় এখানে
# ❌ ভয়ানক: 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 ভরে যাবে।