MindForge
MindForge
DashboardHelp & Support
Dashboard
MindForge

45-Day Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
API Security and Authentication
Course contents

45-Day Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
Browsing without an account — progress is saved in this browser only.Sign in to save it

API Security and Authentication

Reading70 min

Every API interview eventually asks some version of one question: how does the server know who's asking, and what stops that from being abused? This lesson covers JWTs, OAuth, and API keys, a real JWT login flow, refresh token rotation, session versus JWT, CSRF, SQL injection, secrets management, password hashing, and the OWASP API Security Top 10. It's the single most-asked backend topic in interviews; expect at least one system-design question and one "explain the difference between X and Y" question built from this exact material.

JWT, OAuth, and API keys: three different problems

These three get lumped together because they can all ride in the same Authorization header, but each one answers a different question.

JWT (JSON Web Token) is a signed, self-contained token that proves a claim, "this is user 42, expires at time T", without a database lookup. It has three base64 parts separated by dots: header.payload.signature.

Python
import jwt  # PyJWT
import datetime

SECRET = "use-an-env-var-in-real-code"

payload = {
    "sub": "42",
    "exp": datetime.datetime.now(datetime.UTC) + datetime.timedelta(minutes=15),
    "iat": datetime.datetime.now(datetime.UTC),
}
token = jwt.encode(payload, SECRET, algorithm="HS256")

decoded = jwt.decode(token, SECRET, algorithms=["HS256"])  # raises on bad sig or expiry

The server never stores this token. Anyone holding the secret (or the matching public key, for the RS256 variant) can verify it. That's the whole point, and also the whole risk: a leaked signing key, or a stolen token that hasn't expired yet, can't be revoked without extra machinery, which is exactly what refresh token rotation exists for.

OAuth 2.0 is a delegation protocol, not an authentication mechanism by itself. It lets a user grant a third-party app limited access to their data on another service, "let this app read your calendar", without ever handing over their password. The output is usually an access token (often a JWT) plus a refresh token. The Authorization Code flow, with PKCE for public clients, is the one to be able to draw on a whiteboard:

  1. The app redirects the user to the provider's /authorize endpoint with a client_id, redirect_uri, scope, and state.
  2. The user logs in and approves; the provider redirects back with a one-time code.
  3. The app's backend exchanges that code, plus a client_secret or a PKCE code_verifier, for an access token at /token.
  4. The app calls the resource API using the access token.

API keys are a static, long-lived secret that identifies a client, a service or app, not a user. No expiry, no claims, just "is this string in my allowed set." Good for server-to-server and third-party integrations, bad for representing a logged-in human, since a key can't carry identity claims or expire gracefully on its own.

Identifies Expires Carries claims Typical use
JWT A user, usually Yes, short-lived Yes API auth after login
OAuth token A user, via delegation Yes Yes (scopes) Third-party access
API key A client or service No, until rotated No Server-to-server

Remember: JWT proves a claim about a user, OAuth delegates limited access on a user's behalf, and an API key just identifies a client. Reach for the one that matches who or what you're actually identifying.

Sign in to try this knowledge check.

A real JWT login flow

The shape you'd write in an interview or a take-home: hash passwords, issue a short-lived token, verify it on every protected route.

Python
from datetime import datetime, timedelta, UTC
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from passlib.context import CryptContext
import jwt

SECRET_KEY = "read-from-env"  # os.environ["JWT_SECRET"] in real code
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 15

app = FastAPI()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# Stand-in for a DB lookup
FAKE_USERS = {
    "alice": {"username": "alice", "hashed_password": pwd_context.hash("secret123")}
}


def create_access_token(subject: str, expires_delta: timedelta) -> str:
    to_encode = {
        "sub": subject,
        "exp": datetime.now(UTC) + expires_delta,
        "iat": datetime.now(UTC),
    }
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


@app.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = FAKE_USERS.get(form_data.username)
    if not user or not pwd_context.verify(form_data.password, user["hashed_password"]):
        raise HTTPException(status_code=401, detail="Incorrect username or password")
    token = create_access_token(
        user["username"], timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    return {"access_token": token, "token_type": "bearer"}


def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username = payload.get("sub")
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")
    if username not in FAKE_USERS:
        raise HTTPException(status_code=401, detail="User not found")
    return username


@app.get("/me")
def read_current_user(username: str = Depends(get_current_user)):
    return {"username": username}

Three details interviewers check for: passwords are hashed with bcrypt, never stored or compared as plaintext; expiry is enforced server-side, jwt.decode raises automatically once exp has passed; and the signing secret never leaves the server (with the asymmetric RS256 scheme, only the private key signs, and the public key can be handed to other services so they can verify tokens without ever gaining the power to mint one).

Remember: hash with bcrypt, let exp expire tokens automatically, and never let a signing secret leave the server that issues tokens.

Sign in to try this knowledge check.

Refresh token rotation

Access tokens are short-lived, minutes, so a leaked one does limited damage. Refresh tokens are long-lived, days or weeks, and exist only to mint new access tokens, but a long-lived token sitting in storage is itself a target. Rotation means: every time a refresh token gets used, invalidate it and hand back a brand new one. If a stolen refresh token is ever used by an attacker and later by the real user, or the other way around, the reuse of an already-rotated token is a clear, detectable signal that something has been stolen.

Python
import secrets
from datetime import datetime, timedelta, UTC

# refresh_tokens table: token_hash, user_id, expires_at, revoked, replaced_by

def issue_refresh_token(db, user_id: str) -> str:
    raw_token = secrets.token_urlsafe(32)
    token_hash = hash_token(raw_token)  # sha256, never store raw
    db.refresh_tokens.insert(
        token_hash=token_hash,
        user_id=user_id,
        expires_at=datetime.now(UTC) + timedelta(days=14),
        revoked=False,
    )
    return raw_token


def rotate_refresh_token(db, raw_token: str) -> dict:
    token_hash = hash_token(raw_token)
    record = db.refresh_tokens.get(token_hash=token_hash)

    if record is None or record.expires_at < datetime.now(UTC):
        raise HTTPException(status_code=401, detail="Invalid refresh token")

    if record.revoked:
        # Reuse of a rotated-out token: someone has a copy of an old token.
        # Treat as compromise: revoke the whole chain for this user.
        db.refresh_tokens.revoke_all_for_user(record.user_id)
        raise HTTPException(status_code=401, detail="Token reuse detected, session revoked")

    # Rotate: revoke old, issue new, link them for audit
    new_raw = secrets.token_urlsafe(32)
    new_hash = hash_token(new_raw)
    db.refresh_tokens.insert(
        token_hash=new_hash,
        user_id=record.user_id,
        expires_at=datetime.now(UTC) + timedelta(days=14),
        revoked=False,
    )
    db.refresh_tokens.mark_revoked(token_hash=token_hash, replaced_by=new_hash)

    new_access = create_access_token(record.user_id, timedelta(minutes=15))
    return {"access_token": new_access, "refresh_token": new_raw}

Store only the hash of the refresh token, the same way you'd store a password. If the database leaks, the tokens inside it aren't directly usable. This is also the mechanism that makes an otherwise-unrevokable JWT practically revocable: you can't kill a live access token early, but you can kill its refresh chain so no new one ever gets minted.

Remember: rotate refresh tokens on every use, and store only their hash. Reuse of an already-rotated token is a compromise signal, not a bug to swallow silently.

Sign in to try this knowledge check.

JWT versus session: the honest answer

Session (cookie plus server-side store) JWT
Where the state lives Server-side, in Redis or a database Nowhere; the token is self-contained
Revoking it Instant: delete the session record Hard: wait for expiry, or check a blocklist
Scaling across servers Needs a shared session store Any server can verify it independently
Payload size Small, just a session ID Larger, since claims travel with every request
Typical use Traditional web apps, same-origin APIs, mobile apps, microservices, cross-domain

The honest interview answer: sessions are simpler to revoke and reason about. JWTs scale better across services because verifying one needs no shared store, at the cost of harder revocation, which is exactly why refresh rotation and short access-token lifetimes exist. Don't claim either one is unconditionally better; the right one depends on whether instant revocation or stateless scaling matters more for the system you're describing.

Remember: sessions revoke instantly but need a shared store. JWTs scale without a shared store but revoke slowly. Refresh rotation is the patch that makes JWT revocation practical.

Sign in to try this knowledge check.

Preventing CSRF

CSRF (Cross-Site Request Forgery) exploits a browser's habit of auto-attaching cookies to every request, including ones a malicious third-party site triggers without the user noticing. It only matters for cookie-based auth: a JWT sent by hand in an Authorization header isn't vulnerable, because a foreign page's form submission has no way to set that header.

Defenses, strongest first:

  1. SameSite=Lax or SameSite=Strict cookies. This blocks the cookie from being sent on cross-site requests at all. Lax still allows a top-level navigation GET through; Strict blocks everything cross-site. This alone stops most CSRF today.
  2. A CSRF token (the synchronizer token pattern). The server embeds a random token in the page; the client must echo it back in a header or hidden field on any state-changing request. A forged request from another site can't read that token, since the same-origin policy blocks it from doing so.
  3. Check the Origin or Referer header on state-changing requests as a secondary layer.
Python
from fastapi import Request, HTTPException
import secrets

def generate_csrf_token() -> str:
    return secrets.token_urlsafe(32)

def verify_csrf(request: Request, session_csrf_token: str):
    header_token = request.headers.get("X-CSRF-Token")
    if not header_token or not secrets.compare_digest(header_token, session_csrf_token):
        raise HTTPException(status_code=403, detail="CSRF token invalid")

Use secrets.compare_digest, a constant-time comparison, instead of ==. A plain == comparison exits as soon as it finds the first mismatched character, so how long the comparison takes leaks how many leading characters were correct. That's a small detail, but interviewers notice when it's skipped on anything security-adjacent.

Remember: CSRF only threatens cookie-based auth. SameSite=Lax/Strict stops most of it; a CSRF token plus constant-time comparison covers what's left.

Sign in to try this knowledge check.

Bearer token, token authentication, and RBAC: three different layers

These three get conflated because they all show up in the same Authorization header, but each answers a different question.

Bearer token is a transport scheme, not a credential type. It means "whoever holds this token is authorized," sent as Authorization: Bearer <token>. It says nothing about what's inside the token or how it was issued. A JWT, an opaque session ID, and a raw API key can all travel as bearer tokens; "bearer" only describes how the value moves in the header.

Token authentication is the authentication mechanism: proving who you are. Django REST Framework's TokenAuthentication is one concrete example. It stores an opaque token server-side per user and looks it up on each request, so the token itself carries no information, unlike a JWT, which is self-contained.

RBAC (Role-Based Access Control) is the authorization model: once the system knows who you are, what are you allowed to do. Permissions attach to roles, users get assigned one or more roles, and an endpoint checks role membership instead of checking individual users one by one.

The framing that ties these together: authentication answers "who is this," authorization answers "what can they do." A request can carry a perfectly valid bearer token naming a known user, and still get rejected by RBAC, because that user's role doesn't permit the action being requested. Don't let "bearer token" stand in for the whole story. It's the envelope the credential travels in, not the credential itself, and not the permission check that happens after.

Remember: bearer token is how a credential travels, token authentication is proving who you are, and RBAC is deciding what that identity is allowed to do. A valid credential can still fail the RBAC check.

Sign in to try this knowledge check.

SQL injection and parameterized queries

Injection happens when user input gets concatenated directly into a query string instead of passed as a bound parameter.

Python
# VULNERABLE — user input becomes part of the SQL itself
query = f"SELECT * FROM users WHERE email = '{email}'"
cursor.execute(query)
# email = "' OR '1'='1" returns every row

The vulnerable version fails because the string ' OR '1'='1 never stays data. Once it's spliced into the query text, the database parses it as SQL, and WHERE email = '' OR '1'='1' is always true, so the query returns every row in the table no matter what email was actually being searched for.

Python
# SAFE — parameterized: the driver sends value and query separately,
# the DB never interprets the value as SQL syntax
cursor.execute("SELECT * FROM users WHERE email = %s", [email])

In the safe version, the query text and the value travel to the database as two separate things. The database compiles the query shape once, with a placeholder, then substitutes the value in afterward, purely as data. No matter what characters email contains, the database never re-parses it as SQL grammar, so there's no way for it to change what the query does.

An ORM like Django's (Model.objects.filter(email=email)) parameterizes automatically. That's why raw .raw() queries, or SQL built with str.format(), are the actual risk surface in an ORM-based codebase, not the ORM itself. The interview framing: an ORM's safety comes from consistently doing parameterization for you, not from special magic, and the risk comes right back the moment someone drops to raw SQL and string-formats it instead of binding a parameter.

Remember: a parameterized query sends the SQL shape and the data as two separate things, so user input can never be re-parsed as SQL syntax. String-building a query is what reopens that door.

Sign in to try this knowledge check.

Secrets management

Never hardcode a secret in code, and never commit one in a .env file. A secret that reaches git history is compromised even if the commit is later reverted, since the history still exists and remains fetchable. The standard practice:

  • Use a managed secrets store, Vault, AWS Secrets Manager, GCP Secret Manager, rather than an environment file checked into a repo, so the app fetches secrets at startup or runtime instead of reading them from a committed file.
  • Rotate keys on a schedule, and immediately on a suspected breach. A secret that can't be rotated without a full deploy is a design flaw, not just an inconvenience.
  • Use .gitignore plus a pre-commit secret-scanning hook (tools like detect-secrets or gitleaks) to catch a leak before it's pushed, rather than after.
  • Mask sensitive fields in logs. Tokens, passwords, and personal data should never appear in plaintext log output, since logs are often kept and searched with less access control than the primary database.

Remember: a secret in git history is compromised the moment it's committed, revert or not. Fetch secrets from a managed store at runtime instead of a checked-in file.

Sign in to try this knowledge check.

The OWASP API Security Top 10 as a checklist

A named list worth reaching for whenever an interviewer asks "how would you secure this API." Two items on it, BOLA and Mass Assignment, are the most likely to show up as a live code-review exercise ("here's an endpoint, what's wrong with it"), because both fail from authorization being checked at the wrong granularity rather than being missing outright.

  1. Broken Object Level Authorization (BOLA). Not fixed by authentication alone. Every object-fetching endpoint must check that the authenticated user owns or can access this specific object ID, not just that they're logged in as someone. The classic bug: /orders/{id} returns any order to anyone who's logged in, regardless of whose order it actually is.
  2. Broken Authentication. Covered by correct JWT/OAuth/session handling and refresh token rotation, above.
  3. Excessive Data Exposure. A serializer returning a full model object instead of an explicit field allowlist. The fix: allowlist fields in the serializer, and don't rely on the client to politely ignore extra fields it receives.
  4. Lack of Resources and Rate Limiting. Covered by the Redis-backed rate limiter in the API Versioning and Error Handling lesson.
  5. Broken Function Level Authorization. A role check on the action being performed, not just a resource-level ownership check. An admin-only endpoint must reject a non-admin token even when the object-level check would otherwise pass, since the user might genuinely own the object they're trying to act on.
  6. Mass Assignment. Accepting a full JSON body into a model or serializer with no explicit allowed-fields list. A request body containing {"is_admin": true} can silently escalate privilege if the endpoint blindly deserializes the whole body into the model.
  7. Security Misconfiguration. Default credentials left in place, verbose error messages that leak a stack trace, or debug mode accidentally left on in production.
  8. Injection. SQL injection, as above, plus the general principle: never let unsanitized input reach an interpreter, whether that's SQL, a shell command, or a template engine.
  9. Improper Assets Management. An old or undocumented API version left running and unpatched. A /v1/ endpoint nobody remembers exists is still just as attackable as the current one.
  10. Insufficient Logging and Monitoring. Log auth failures, rate-limit hits, and unusual access patterns. A spike in 401/403 responses is often how a credential-stuffing attack actually gets caught in production.

Remember: BOLA and Mass Assignment are the two you're most likely to be asked to spot live in a code review. Both come from checking authorization at the wrong level, not from skipping it entirely.

Sign in to try this knowledge check.

Password hashing: why bcrypt, and what beats it

The login flow above hashes with bcrypt without explaining why bcrypt over the alternatives. Here's the comparison an interviewer expects if they push further.

Algorithm Type Defense mechanism
MD5 / SHA-256 (alone) Fast cryptographic hash None
PBKDF2 Key-derivation function A configurable iteration count (work factor)
bcrypt Password hash A configurable cost factor (2^cost rounds)
scrypt Password hash Configurable CPU cost plus memory cost
Argon2 Password hash Time cost, memory cost, and parallelism, tunable independently

Plain MD5 or SHA-256 must never be used for passwords. They're built to be fast, which is exactly the wrong property here: a modern GPU computes billions of hashes a second, making it trivial to brute-force every likely password against a stolen hash.

PBKDF2 fixes that by adding a configurable iteration count, so each guess costs more CPU time. It's widely supported and government-approved for regulated use, but it has no memory cost, so it's still crackable at scale by an attacker with enough parallel hardware.

Bcrypt has been the industry standard for over a decade. It's deliberately slow, with a cost factor you can raise as hardware gets faster, keeping the effective time to crack roughly constant.

Scrypt and Argon2 add memory hardness on top of bcrypt's approach. Bcrypt's cost factor only makes each guess slower on one CPU core, but an attacker with thousands of cheap GPU cores can still parallelize guesses cheaply. A memory-hard algorithm forces each guess to allocate a meaningful chunk of RAM, expensive to replicate thousands of times over, which blunts that parallelism advantage. Argon2, winner of the 2015 Password Hashing Competition, is the current best-practice pick for a new project; bcrypt remains an acceptable, battle-tested choice for existing systems already built on it.

Salting is automatic and built into bcrypt's own output format: one self-contained string shaped like $2b$<cost>$<22-char-salt><31-char-hash>. That's why you never see a separate "store the salt" step when using bcrypt; bcrypt.checkpw() pulls the salt out of the stored hash itself before checking a candidate password against it.

Remember: a fast hash like MD5/SHA-256 has no cost knob and must never be used for passwords. Bcrypt, scrypt, and Argon2 are all deliberately slow with a tunable cost, so you raise the cost as hardware gets faster.

Sign in to try this knowledge check.

API key lifecycle in production

Knowing what an API key is only goes so far; interviewers who push further want the operational side.

  • Never in source control. Load from environment variables or a secrets manager, never a hardcoded string, and never committed, even to a "private" repo.
  • Segregate per environment and per service. A dev key and a prod key should be different values, so a leaked dev key can't touch production data, and revoking one service's key doesn't break every other integration.
  • Rotate on a schedule, for example every 90 days, and immediately on a suspected leak. Rotation should be routine, not a fire drill, which means the system needs to accept two valid keys at once during the rotation window, so the old key keeps working for a grace period while callers migrate.
  • Transmit only over HTTPS, in a header (Authorization: Bearer <key> or a custom header), never as a URL query parameter. URLs get logged by proxies, browsers, and web server access logs, silently leaking the key into logs that outlive the request itself.
  • Monitor usage per key. Track request volume and source IPs per key, so an anomaly (a sudden spike, requests from an unexpected region) is detectable, and an unused key can be found and revoked instead of living forever "just in case."
  • Support instant revocation. A key must be disable-able immediately, via a database flag checked on every request, not a cache that takes minutes to propagate, the moment a leak is suspected.

Remember: never put a key in a URL, always support two valid keys during rotation, and make revocation a database check on every request, not something that waits for a cache to expire.

Sign in to try this knowledge check.
PreviousAPI Versioning and Error HandlingNextWebSockets and Real-Time Systems

Course progress

0%
0 of 177 modules complete

On this page

  • JWT, OAuth, and API keys: three different problems
  • A real JWT login flow
  • Refresh token rotation
  • JWT versus session: the honest answer
  • Preventing CSRF
  • Bearer token, token authentication, and RBAC: three different layers
  • SQL injection and parameterized queries
  • Secrets management
  • The OWASP API Security Top 10 as a checklist
  • Password hashing: why bcrypt, and what beats it
  • API key lifecycle in production