Auth Principles
Authentication (AuthN) and Authorization (AuthZ) are two distinct but related security concepts. Understanding the difference and applying both correctly is fundamental to building secure systems.
Authentication vs Authorization
Authentication → Who are you? (identity)
Authorization → What can you do? (permissions)
| Concept | Question | Example |
|---|---|---|
| Authentication | Who are you? | Login with username + password |
| Authorization | What are you allowed to do? | Admin can delete users, viewer cannot |
Authentication Principles
1. Never Store Plaintext Passwords
Always hash passwords with a slow, purpose-built algorithm:
# ❌ WRONG: Plaintext or fast hashes
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest() # Crackable!
password_hash = hashlib.sha256(password.encode()).hexdigest() # Still too fast!
# ✅ CORRECT: Use bcrypt, argon2, or scrypt
import bcrypt
def hash_password(password: str) -> str:
salt = bcrypt.gensalt(rounds=12)
return bcrypt.hashpw(password.encode(), salt).decode()
def verify_password(password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode(), hashed.encode())
2. Use Secure Token Standards
Prefer JWT for stateless auth; always sign and optionally encrypt:
import jwt
from datetime import datetime, timedelta
SECRET_KEY = "your-secret-key" # Use a strong, random key in production
def create_token(user_id: int) -> str:
payload = {
"sub": str(user_id),
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(hours=1),
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
def decode_token(token: str) -> dict:
return jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
// JavaScript — using jose
import { SignJWT, jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
async function createToken(userId: string) {
return new SignJWT({ sub: userId })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('1h')
.sign(secret);
}
async function verifyToken(token: string) {
const { payload } = await jwtVerify(token, secret);
return payload;
}
3. Short-Lived Access Tokens + Refresh Tokens
Access tokens expire quickly; refresh tokens enable seamless re-authentication:
Access Token → short-lived (15 min – 1 hour) → sent with every request
Refresh Token → long-lived (7 – 30 days) → stored securely, used only to get new access token
def create_token_pair(user_id: int) -> dict:
access_token = create_token(user_id, expires_in=timedelta(minutes=15))
refresh_token = create_token(user_id, expires_in=timedelta(days=30))
return {
"access_token": access_token,
"refresh_token": refresh_token,
}
def refresh_access_token(refresh_token: str) -> str:
payload = decode_token(refresh_token)
# Verify refresh token is not revoked (check DB/cache)
return create_token(payload["sub"], expires_in=timedelta(minutes=15))
4. Multi-Factor Authentication (MFA)
Add a second factor beyond the password:
Something you know → password, PIN
Something you have → TOTP app, hardware key
Something you are → fingerprint, face ID
import pyotp
def generate_totp_secret() -> str:
return pyotp.random_base32()
def verify_totp(secret: str, code: str) -> bool:
totp = pyotp.TOTP(secret)
return totp.verify(code, valid_window=1) # Allow 30s clock drift
5. Protect Against Brute Force
Rate-limit login attempts and lock accounts after repeated failures:
import time
from collections import defaultdict
login_attempts = defaultdict(list)
MAX_ATTEMPTS = 5
WINDOW_SECONDS = 300 # 5 minutes
def is_rate_limited(ip: str) -> bool:
now = time.time()
attempts = login_attempts[ip]
# Remove attempts outside the window
login_attempts[ip] = [t for t in attempts if now - t < WINDOW_SECONDS]
return len(login_attempts[ip]) >= MAX_ATTEMPTS
def record_attempt(ip: str):
login_attempts[ip].append(time.time())
---
Authorization Principles
1. Principle of Least Privilege
Grant the minimum permissions required to perform a task:
# ❌ WRONG: Single admin role for everything
class User:
is_admin: bool # Either full access or nothing
# ✅ CORRECT: Granular permissions
class User:
permissions: list[str] # ["read:posts", "write:posts", "delete:comments"]
def can(user: User, action: str) -> bool:
return action in user.permissions
2. Role-Based Access Control (RBAC)
Assign permissions to roles, then roles to users:
ROLES = {
"viewer": ["read:posts", "read:comments"],
"editor": ["read:posts", "write:posts", "read:comments", "write:comments"],
"moderator": ["read:posts", "read:comments", "delete:comments", "ban:users"],
"admin": ["*"], # All permissions
}
def get_permissions(role: str) -> set[str]:
return set(ROLES.get(role, []))
def has_permission(user_role: str, action: str) -> bool:
perms = get_permissions(user_role)
return "*" in perms or action in perms
3. Attribute-Based Access Control (ABAC)
Make decisions based on user attributes, resource attributes, and environment:
def can_edit_post(user: User, post: Post) -> bool:
# Owner can always edit their own post
if post.author_id == user.id:
return True
# Admins and editors can edit any post
if user.role in ("admin", "editor"):
return True
return False
def can_view_document(user: User, doc: Document) -> bool:
# Public documents visible to all
if doc.visibility == "public":
return True
# Private documents only for owner or admins
if doc.visibility == "private":
return doc.owner_id == user.id or user.role == "admin"
# Internal documents for authenticated users in the same org
if doc.visibility == "internal":
return user.org_id == doc.org_id
return False
4. Always Authorize on the Server
Never trust the client to enforce permissions:
# ❌ WRONG: Frontend hides the button, but no server-side check
@app.get("/admin/users")
def list_users():
return db.query(User).all() # Anyone can call this directly!
# ✅ CORRECT: Enforce authorization on every endpoint
@app.get("/admin/users")
def list_users(current_user: User = Depends(get_current_user)):
if not has_permission(current_user.role, "read:users"):
raise HTTPException(status_code=403, detail="Forbidden")
return db.query(User).all()
5. Deny by Default
Start with no access; explicitly grant what is needed:
def check_permission(user: User, resource: str, action: str) -> bool:
# Default: deny everything
allowed = False
# Check explicit grants
for rule in user.access_rules:
if rule.matches(resource, action):
if rule.effect == "allow":
allowed = True
elif rule.effect == "deny":
return False # Explicit deny always wins
return allowed
---
Secure Storage of Tokens
Access Token → Memory (JavaScript) or short-lived cookie with HttpOnly + Secure + SameSite=Strict
Refresh Token → HttpOnly cookie (not accessible to JS)
Never → localStorage for sensitive tokens (XSS risk)
// ✅ Set refresh token as HttpOnly cookie (server-side)
res.cookie('refresh_token', refreshToken, {
httpOnly: true, // Not accessible via document.cookie
secure: true, // HTTPS only
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
});
---
Common Vulnerabilities
Broken Authentication
# ❌ Predictable session IDs
session_id = str(user.id) # Easily guessable!
# ✅ Cryptographically random session IDs
import secrets
session_id = secrets.token_urlsafe(32)
Insecure Direct Object Reference (IDOR)
# ❌ WRONG: Trusts user-supplied ID without ownership check
@app.get("/orders/{order_id}")
def get_order(order_id: int):
return db.query(Order).filter(Order.id == order_id).first()
# ✅ CORRECT: Verify the resource belongs to the requester
@app.get("/orders/{order_id}")
def get_order(order_id: int, current_user: User = Depends(get_current_user)):
order = db.query(Order).filter(
Order.id == order_id,
Order.user_id == current_user.id # Ownership check
).first()
if not order:
raise HTTPException(status_code=404)
return order
Token Leakage
❌ Tokens in URLs → appear in server logs, browser history
❌ Tokens in localStorage → exposed to XSS attacks
✅ Tokens in Authorization header or HttpOnly cookies
---
Quick Reference
| Principle | Rule |
|---|---|
| Password storage | Hash with bcrypt/argon2, never plaintext or MD5/SHA1 |
| Access tokens | Short-lived (≤1h), signed JWTs |
| Refresh tokens | Long-lived, HttpOnly cookie, rotatable |
| Permissions | Least privilege, deny by default |
| Access control | Always enforce on server, never trust client |
| MFA | Require for sensitive actions or privileged roles |
| Rate limiting | Limit login attempts per IP and account |
| IDOR | Always verify resource ownership server-side |