Home / Notebooks / Security
Security
intermediate

Auth Principles

Core principles of authentication and authorization in modern systems

April 25, 2026
Updated regularly

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)
ConceptQuestionExample
AuthenticationWho are you?Login with username + password
AuthorizationWhat 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

PrincipleRule
Password storageHash with bcrypt/argon2, never plaintext or MD5/SHA1
Access tokensShort-lived (≤1h), signed JWTs
Refresh tokensLong-lived, HttpOnly cookie, rotatable
PermissionsLeast privilege, deny by default
Access controlAlways enforce on server, never trust client
MFARequire for sensitive actions or privileged roles
Rate limitingLimit login attempts per IP and account
IDORAlways verify resource ownership server-side

Resources

  • OWASP Authentication Cheat Sheet
  • OWASP Authorization Cheat Sheet
  • JWT Best Practices (RFC 8725)
  • NIST Digital Identity Guidelines
  • OAuth 2.0 Security Best Practices
  • Topics

    AuthenticationAuthorizationSecurityBest Practices

    Found This Helpful?

    If you have questions or suggestions for improving these notes, I'd love to hear from you.