# ❌ Bad: Weak secretSECRET_KEY="secret"SECRET_KEY="my-jwt-secret"# ✅ Good: Strong random secret (at least 256 bits)importsecretsSECRET_KEY=secrets.token_hex(32)# 64 hex chars = 256 bits# Or generate once and store in environment# openssl rand -hex 32
For HS256, the secret should be at least as long as the hash output (256 bits).
defcreate_token_pair(user_id:str)->dict:access_token=jwt.encode({"sub":user_id,"exp":datetime.utcnow()+timedelta(minutes=15),"type":"access"},SECRET_KEY,algorithm=ALGORITHM)refresh_token=jwt.encode({"sub":user_id,"exp":datetime.utcnow()+timedelta(days=7),"type":"refresh","jti":secrets.token_hex(16)# Unique ID for revocation},SECRET_KEY,algorithm=ALGORITHM)# Store refresh token ID for revocation checkingstore_refresh_token(user_id,refresh_token["jti"])return{"access_token":access_token,"refresh_token":refresh_token}defrefresh_access_token(refresh_token:str)->str:payload=jwt.decode(refresh_token,SECRET_KEY,algorithms=[ALGORITHM])ifpayload.get("type")!="refresh":raiseAuthError("Invalid token type")# Check if refresh token is revokedifis_token_revoked(payload["jti"]):raiseAuthError("Token revoked")# Issue new access tokenreturncreate_token(payload["sub"],expires_minutes=15)
# Store revoked token IDs (jti) in Redisdefrevoke_token(token:str):payload=jwt.decode(token,SECRET_KEY,algorithms=[ALGORITHM])ttl=payload["exp"]-int(datetime.utcnow().timestamp())redis.setex(f"revoked:{payload['jti']}",ttl,"1")defis_revoked(jti:str)->bool:returnredis.exists(f"revoked:{jti}")
# Store version in user record, increment on logout/password changedefcreate_token(user_id:str,token_version:int):returnjwt.encode({"sub":user_id,"ver":token_version,"exp":datetime.utcnow()+timedelta(minutes=15)},SECRET_KEY,algorithm=ALGORITHM)defverify_token(token:str):payload=jwt.decode(token,SECRET_KEY,algorithms=[ALGORITHM])user=get_user(payload["sub"])ifpayload["ver"]!=user.token_version:raiseAuthError("Token invalidated")returnpayload
// ❌ Bad: localStorage (accessible to XSS)
localStorage.setItem('token',accessToken);// ✅ Better: httpOnly cookie (not accessible to JavaScript)
// Set from server:
// Set-Cookie: access_token=xxx; HttpOnly; Secure; SameSite=Strict
// ✅ Also acceptable: In-memory only (lost on refresh)
letaccessToken=null;// Store refresh token in httpOnly cookie for persistence
HS256 (HMAC): Symmetric — same secret for signing and verifying.
1
2
3
# Good for single-service authtoken=jwt.encode(payload,SECRET_KEY,algorithm="HS256")payload=jwt.decode(token,SECRET_KEY,algorithms=["HS256"])
RS256 (RSA): Asymmetric — private key signs, public key verifies.
1
2
3
# Good for microservices — auth service has private key, others have publictoken=jwt.encode(payload,PRIVATE_KEY,algorithm="RS256")payload=jwt.decode(token,PUBLIC_KEY,algorithms=["RS256"])
RS256 is better when multiple services need to verify tokens but shouldn’t be able to create them.
# ❌ Library might not check exp by defaultpayload=jwt.decode(token,SECRET_KEY)# ✅ Ensure exp is validated (most libraries do this by default)payload=jwt.decode(token,SECRET_KEY,algorithms=["HS256"])
# ❌ Bad: Using claims without verificationuser_id=request.headers.get("X-User-ID")# ✅ Good: Always get user ID from verified tokenuser_id=verified_token["sub"]
# Support multiple secrets during rotationCURRENT_SECRET=os.environ["JWT_SECRET"]OLD_SECRET=os.environ.get("JWT_SECRET_OLD")defverify_token(token:str):forsecretin[CURRENT_SECRET,OLD_SECRET]:ifsecret:try:returnjwt.decode(token,secret,algorithms=["HS256"])exceptjwt.InvalidTokenError:continueraiseAuthError("Invalid token")
JWTs are a tool, not a security silver bullet. Use strong secrets. Keep tokens short-lived. Specify algorithms explicitly. Don’t store sensitive data in payloads. Plan for revocation.
The JWT that “just works” in development might be leaking data or accepting forged tokens in production. Test your implementation against known attacks.
📬 Get the Newsletter
Weekly insights on DevOps, automation, and CLI mastery. No spam, unsubscribe anytime.