→ From Coder to Shipper
From Coder to Shipper · Security

Backend Security

Your app now has real users and real data. This deck covers the three fundamentals every shipper must own — authentication, password storage, and SQL injection — and how they stack up into defense in depth.

← / → to navigate · EN/VI switch below · big picture first, code where it helps

Big picture

Your app now has real users — and real targets

Once you store users, orders, and money, three things become your job.

(a) Know who's asking

Every request must identify itself before touching anything. Sessions, tokens, API keys — authentication answers 'who is this?'

(b) Protect the secrets

Passwords must be stored so that even you — and even your own database — can't read them back.

(c) Stop injected commands

Attacker input must stay data. If it becomes part of your SQL, one text box can read or delete everything.

Defense in depth

Security is layers, not one wall. Each layer catches what the others miss — get the fundamentals right and you're already ahead.

01
Part 1

Authentication — knowing WHO

Sessions, JWT, API keys, OAuth — the four ways a request proves who it is, and when to use each.

The core distinction

Authentication vs authorization

Authentication — who are you?

Proving identity: logging in with a password, a token, or a key. The server answers 'yes, you are Alice.'

The question: WHO is asking?

Authorization — what may you do?

Deciding permissions: Alice can view her own orders, but not delete Bob's. Checked after identity is known.

The question: what are you ALLOWED to do?

The threat

Why authentication exists

The internet is full of scripts that try to act as anyone.

Anyone can claim

Without auth, a script can call your API with any user id. 'I am user 42' is not proof — it's a claim.

Accounts = identity

Once users have accounts, stealing one means stealing their data, orders, even money. Attackers hit logins first.

The server can't see the person

HTTP has no memory and no face. The only identity is something you can check: a session, a token, a key.

Auth is the front door

Everything else — permissions, rate limits, per-user data — only makes sense once 'who' is settled.

Method 1 · the most common

Cookie + session login, step by step

  1. 1

    User submits the form

    POST /login with email + password. The server hashes the password and compares it to the stored hash (more in Part 2).

  2. 2

    Server creates a session

    A random id like sid=abc123 is stored server-side, mapped to 'alice is logged in'. The server keeps the memory.

  3. 3

    Cookie is sent back

    The response carries Set-Cookie: sid=abc123. The browser stores it automatically.

  4. 4

    Browser sends it on every request

    From now on, the browser attaches Cookie: sid=abc123 to every request to your site.

  5. 5

    Server looks it up

    Each request: check the sid, find the session, know who's asking. Logout = delete the session row.

The mechanism

Every request after login

The cookie is the ticket; the session store is the guest list.

once, at login
Browsertypes email + password
→① POST /login
Your serverhashes password → matches
→② Set-Cookie:
sid=abc123
Session storesid=abc123 → "alice"
every request after
BrowserCookie: sid=abc123
→③ attached automatically
Your serverlooks up sid → 'alice'
→④ identity known
Protected pageher data, her actions
Method 2

JWT — the server forgets, the token remembers

A stateless token the client carries — no session store needed.

What it is

After login, the server signs a small JSON blob (user id + expiry) with a secret. The result — the JWT — goes to the client, who sends it in the Authorization header.

Why use it

The server stores nothing per user — stateless, scales across many servers, ideal for mobile apps and APIs.

The trade-off

Nothing stored means nothing revocable: a stolen JWT stays valid until it expires. Keep expiry short; for the web, sessions are usually simpler.

When to reach for it

APIs consumed by apps, microservices calling each other, mobile. For a classic browser site, start with sessions.

Method 3

API keys — for services, not people

What it is

A long random string a service shows in every request: X-Api-Key: sk_live_9f2c.... No login form — the key IS the identity.

Why use it

Machines don't have passwords. Stripe, GitHub, weather APIs — one key per service, easy to create, rotate, or kill.

The rule

Keys are powerful — treat them like passwords: never in committed code, never in the browser, least privilege per key.

Method 4

OAuth — 'login with Google' and delegated access

The idea

Your app asks Google: 'this user says they're alice@gmail.com — confirm?' Google logs the person in and says yes. Your app never sees their password.

Delegated access

OAuth also lets a user hand you a limited ticket: 'let this app read my calendar, but nothing else'. The scope is the ticket's size.

Where you've seen it

'Continue with Google/Apple' buttons, a CI bot pushing to GitHub, Google Drive sync. Same protocol, different scopes.

When to use it

When identity already lives elsewhere (Google), or when you want to share a resource with limited rights instead of sharing passwords.

Choose your tool

Which auth method, when

Sessions for web logins, JWT for stateless APIs, keys for machines, OAuth when someone else owns the identity.

MethodIdentity lives inState storedTypical use
Session + cookiethe cookie (sid)server (session store)classic browser login
JWTthe token (Authorization)nowhere — it's signedmobile, APIs, microservices
API keythe key (X-Api-Key)nowhere — just matchedservice-to-service
OAuththe user, at the IdPat the identity provider'login with Google', delegated
Common mistakes

Auth mistakes that get you pwned

'It's just an API'

An unauthenticated endpoint is a data breach with extra steps. Every endpoint that touches data needs identity + permission checks.

Trusting the client

The browser can change anything. 'I am user 42' from a client is a hint, not a fact — identity comes from a session/token you issued.

Tokens that never die

Sessions and JWTs need expiry. A forever-valid token leaked in a log is an open door, forever.

Cookies without flags

Session cookies should be HttpOnly (JS can't read them), Secure (HTTPS only), SameSite (adds CSRF friction).

02
Part 2

Passwords — never store them

Hashing, salts, bcrypt: how login works when even you can't read the password back.

The rule

Never store the password

Not plaintext, not 'encoded', not reversible. Ever.

Databases leak

SQL injection, misconfigured backups, stolen laptops, careless exports. Assume the password column WILL be read by someone who shouldn't.

Plaintext = instant takeover

One leaked row gives the attacker the real password — usable on your site and every other site it was reused on.

Even you shouldn't read it

Support staff must not be able to answer 'what's your password?'. The system should not be able to — that's the point.

Store a one-way fingerprint instead

Store a hash: a value computed from the password that cannot be turned back. The password itself never touches the database.

The mechanism

Hashing — a one-way fingerprint

What a hash is

A function that turns input into a fixed-size fingerprint: same input always gives the same hash; a 1-character change gives a completely different one.

hash('hunter2') → 9f86d081884c7d659a2feaa0c55ad015

One way only

Computing the hash is instant. Reversing it isn't done — you'd try passwords until one matches. That's why the hash is safe to store.

A stolen hash ≠ a stolen password. It's a lock with no key.

One more ingredient

Salt — one random pepper per user

The problem it solves

Without salt, 'password123' hashed by 10,000 users is the same string 10,000 times — an attacker cracks it once, logs in as all of them.

How salt works

Each user gets a random salt. The stored value is hash(password + salt). Two users with the same password now store two different strings.

Rainbow tables, dead

Precomputed crack lists assume no salt. With a per-user salt, every password in a leak needs its own slow brute-force attack.

You never see it

bcrypt/argon2 generate and embed the salt in the stored string automatically — a library handles it; you just call hashpw().

Which hash

bcrypt / argon2 — slow on purpose

Fast hashes are for attackers

MD5/SHA-256 were built for speed — a GPU tries billions per second. You want the opposite: a hash that is slow for everyone, especially them.

Slow by a dial

bcrypt/argon2 have a 'cost' setting; each bump makes hashing 2× slower. Your login still feels instant — the attacker's GPU just got poorer.

Just use the library

You never write this yourself: bcrypt/argon2 handle salt, cost, and safe comparison. Pick one — both are the right answer.

The whole design in one line

Make the right thing easy for you (one function call), and the wrong thing expensive for them (billions of slow tries).

The mechanism, in code

Register & login with a salted hash

auth.py
import bcrypt

def register(email, password):
    # bcrypt generates a fresh random salt for this user
    hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
    db.execute("INSERT INTO users(email, pw) VALUES(?, ?)", [email, hashed])

def login(email, password):
    row = db.execute("SELECT pw FROM users WHERE email = ?", [email]).fetchone()
    if row and bcrypt.checkpw(password.encode(), row[0]):
        return create_session(email)
    return None

The password exists in memory only briefly. What's stored — and what a leak exposes — is the hash.

The payoff

What a breach actually exposes

ops@vps — zsh
$ cat users_leak.csv # stolen database, 2026-05-12
1,alice@example.com,$2b$12$N9qo8uLOickgx2ZMRZoMyeIj4ZVxK...
2,bob@example.com,$2b$12$Xy3Qm...
3,carol@example.com,$2b$12$...
# attacker's verdict:
no plaintext → no instant logins; each hash needs a slow, loud crack
# with plaintext, this file would already be a login for everyone
03
Part 3

SQL injection — when data becomes code

The most common way web apps get breached — and one of the easiest to prevent.

The mechanism

What SQL injection is

Your app builds SQL from user input. The database has no idea which part is yours.

SQL is data to the DB

The database's job is to execute whatever SQL string it receives. It doesn't know half of it came from a text box.

You do the building

Your app composes the query: a fixed template + the user's values. If you build it by gluing strings, the user's text can change the whole sentence.

The attacker's trick

Type text that 'closes' the value and injects a new clause: ' OR 1=1 -- turns a lookup into 'return everything'.

Why it's a classic

Old (1998), everywhere (any app that builds SQL), and one line can dump or delete the whole database. The fix, though, is one habit.

The bug

The vulnerable login

login_vulnerable.py
def login(email, password):
    # email & password come from a text box — untrusted input
    q = "SELECT * FROM users WHERE email='" + email + "'"
    q = q + " AND password='" + password + "'"
    row = db.execute(q).fetchone()   # the DB runs whatever q says
    if row:
        return "logged in"
    return None

The email field is data — but Python glues it into a SQL sentence before the database ever sees it.

The attack

Now type this in the email field

what-the-database-receives.sql
-- the user types into the email field:
' OR 1=1 --

-- your string-building code produces:
SELECT * FROM users WHERE email='' OR 1=1 -- '
   AND password='whatever'

-- 1=1 is always true, and -- comments out the rest:
-- the password check is gone. every row comes back.

The payload is just a string to you — but to the database it's instructions: skip the password check.

The damage

What the attacker gets

ops@vps — zsh
$ sqlite3 shop.db "SELECT * FROM users WHERE email='' OR 1=1 -- "
1 | alice@example.com | $2b$12$N9qo8u...
2 | bob@example.com | $2b$12$Xy3Qm...
3 | carol@example.com | $2b$12$...
# the whole user table — every account can now be hijacked
# with a bit more work: DELETE, UPDATE, or exfiltrate anything
The fix

Parameterized queries — data stays data

login_fixed.py
def login(email, password):
    # parameters: the query and the data travel separately
    row = db.execute(
        "SELECT * FROM users WHERE email = ? AND password = ?",
        [email, password],   # data stays data — never parsed as SQL
    ).fetchone()
    if row and bcrypt.checkpw(password.encode(), row[0]):
        return create_session(email)
    return None

The database receives the query and the data as two separate things. The data can never alter the query.

Why it works

The rule behind the fix

Two channels, one boundary

The query template is decided by your code, before any input exists. The user's values are sent as parameters — the database stores them as data.

Typing ' OR 1=1 -- in a parameter does nothing: it's just a weird value no user has.

The habit

Never build SQL by concatenating or interpolating user input. No f-strings, no +, no .format() — only placeholders.

If you ever catch yourself gluing user input into a WHERE clause, stop. That is the bug.

Treat every piece of user input as hostile. It may be a name, a comment — or an attack. Your code decides whether it's data or a command.
The one habit that prevents SQL injection
04
Part 4

The other layers

HTTPS, secrets handling, least privilege — three more lines that close most gaps.

Layer 1

HTTPS everywhere — you already have it

What it does

Encrypts everything between browser and server: login forms, cookies, every request. A network eavesdropper sees only noise.

The threat it stops

Coffee-shop Wi-Fi, carrier snooping, man-in-the-middle. Without it, a 'hashed' password still ships across the wire in plaintext.

The standard

HTTPS only, everywhere — including API endpoints and redirects. Modern hosts give you certificates for free (Let's Encrypt).

Layer 2

Secrets in env vars, not in code

.env
# .env — on the server only, NEVER committed to git
DATABASE_URL=sqlite:////data/shop.db
SESSION_SECRET=9f2c41d8e7b3...8812
SMTP_PASSWORD=correct-horse-battery

# .gitignore
.env

Code goes to git (and maybe to a fork, a CI log, a recruiter). Secrets stay on the server. One line in .gitignore keeps them out.

Layer 3

Least privilege — everyone gets the minimum

The idea

Every user, key, and service account gets exactly the access it needs — nothing more. A blog commenter's session can't open the admin panel.

Why it matters

If one credential leaks, the blast radius is its permissions. Least privilege turns 'everything is gone' into 'one small thing is gone'.

And the two you'll meet later

CSRF (forged cross-site requests) and XSS (script injected into pages) are the next chapters — SameSite cookies and output escaping are their first lines of defense.

The big picture

Defense in depth — the full stack

No single layer is perfect. Together they make an attack expensive at every step.

0
HTTPS (TLS)the wire between browser and server
eavesdropping, tampering in transit
1
Authenticationsession · JWT · API key · OAuth
'I am user 42' — unproven identity
2
Salted password hashesbcrypt · argon2
a stolen database turning into logins
3
Parameterized queriesdata is never SQL
SQL injection — reading or deleting everything
4
Secrets in env + least privilegeminimum access per credential
one leaked credential opening the whole system
Shipper's checklist

Get these fundamentals right

Six habits that put you ahead of most real-world breaches.

Hash passwords

bcrypt or argon2, salted automatically. The plaintext never touches the database.

Parameterize every query

Placeholders only. No concatenation, no f-strings, no exceptions.

Sessions or tokens — properly

Auth on every endpoint, expiry everywhere, HttpOnly/Secure/SameSite cookies.

HTTPS only

Every page, every API, every redirect. No exceptions.

Secrets out of git

Env vars on the server, .env in .gitignore, rotate anything that was ever committed.

Least privilege

Minimum access per user, key, and service account.

Security isn't a feature you add at the end — it's a set of habits you build in: hash the passwords, parameterize the queries, verify every request.
From Coder to Shipper

Now go ship — with doors that lock

Next: deployment, CI/CD, and monitoring — the last mile of From Coder to Shipper.