Modern Backend Engineering

Lesson 1 of 15

Authentication & Authorization

"Amateurs hack systems, professionals hack people — but both walk in through the login screen." — Bruce Schneier (paraphrased)

Every backend you will ever build has a front door, and behind it, a set of locked rooms. Authentication is the guard who checks that you are who you claim to be at the door. Authorization is the set of keys that decides which rooms you may enter once you are inside. Confuse the two — or trust the wrong one to the browser — and you have not built a login system, you have built the illusion of one. This chapter is about getting the front door right, because everything else in modern backend engineering sits behind it.

The question this chapter answers

A user types a password and clicks "Log in." Between that click and them seeing their dashboard, what must your server actually prove — and what must it never, ever store to prove it?

Core Concepts

Authentication vs Authorization — two questions, never one

These two words are so often said in the same breath that engineers start treating them as one step. They are not. They answer two entirely different questions, and the security of your system depends on keeping them apart.

  • Authentication (often shortened to authn) answers "Who are you?" It is the act of proving identity — matching a presented credential (a password, a fingerprint, a signed token) against what the system knows.
  • Authorization (authz) answers "What are you allowed to do?" It happens after identity is established, and decides whether this known user may perform this specific action on this specific resource.

Here is the distinction in a table, because it is worth burning in:

Authentication Authorization
Question Who are you? What may you do?
Happens First, at login After identity is known, on every request
Fails with 401 Unauthorized 403 Forbidden
Example "This is really Chika." "Chika may edit courses but not delete users."
Answer lives in A credential you present A policy the server enforces

Why does conflating them create a hole? Because the most common real-world bug is a system that authenticates well and then assumes that a logged-in user is an allowed user. Picture an admin dashboard where the login is airtight — bcrypt, MFA, the works — but the endpoint DELETE /api/users/42 only checks that you are logged in, not that you are an admin. Any authenticated student can now delete accounts. The door was strong; the internal locks were missing. Authentication without authorization is a guard who checks IDs at the entrance and then lets everyone into the vault.

Storing passwords: never the password itself

The single most important rule of authentication: you never store the password. Not encrypted, not "encoded," not "just for now." If your database is ever leaked — and databases leak — plaintext passwords hand the attacker not only your site but every other site where your users reused that password.

Instead you store a hash. Recall from the Hashing chapter that a cryptographic hash is a one-way function: easy to compute forward, computationally infeasible to reverse. You store hash(password). When the user logs in, you hash what they typed and compare. The real password never lives on disk.

But a plain sha256(password) is nowhere near enough, for two reasons that every good scheme must defeat:

  • Rainbow tables. Attackers precompute the hashes of billions of common passwords. If everyone's password123 hashes to the same value, one lookup cracks millions of accounts. The defence is a salt: a unique random value generated per user, mixed into the hash. Now password123 produces a different hash for every user, and the attacker's precomputed table is worthless — they would need a separate table per salt.
  • Speed. A fast hash is a bad hash for passwords. If your algorithm can compute a billion hashes per second, so can the attacker's GPU, and short passwords fall in minutes by brute force. The defence is deliberate slowness: algorithms designed to be expensive — bcrypt, scrypt, argon2, or PBKDF2 — that take a tuneable number of iterations. Slow enough that verifying one login costs a fraction of a second (invisible to your user), but brute-forcing millions of guesses becomes economically hopeless.

So the rule in full: hash with a slow, salted, purpose-built algorithm. Salt defeats precomputation; slowness defeats brute force. A general-purpose hash like SHA-256 was built to be fast — exactly the wrong property here. (In production you would reach for bcrypt or argon2; the runnable example below uses PBKDF2 because it ships in Python's standard library and is a legitimate, widely-deployed choice.)

A password is one kind of credential

A credential is any proof of identity. A password is the most common, but it is only one of three fundamental factors:

  • Something you know — a password, a PIN.
  • Something you have — a phone receiving a code, a hardware key, an authenticator app.
  • Something you are — a fingerprint, a face, a retina.

Multi-factor authentication (MFA) requires two or more of these from different categories. Why "different categories" matters: two passwords are still just one factor — an attacker who phishes one likely phishes both. But a password (know) plus a phone code (have) means a remote attacker with your leaked password still cannot log in without physically holding your phone. That is the entire point of MFA: it makes a single stolen secret insufficient.

Sessions vs tokens: how identity survives the next request

HTTP is stateless — the server forgets you the instant it answers. So once a user authenticates, how does request number two know it is still them? You met both answers in Part III (Cookies, Sessions & JWT), so we will cross-reference rather than repeat:

  • Session-based auth — the server creates a session record, stores it (in memory, Redis, a DB), and hands the browser a session ID in a cookie. Each request sends the cookie; the server looks up the session. State lives on the server; the cookie is just a claim ticket. Easy to revoke (delete the record), but requires shared session storage to scale across servers.
  • Token-based auth (e.g. JWT) — the server issues a signed token the client stores and sends on each request. The server verifies the signature and trusts the claims inside the token without a lookup. Stateless and scales horizontally with ease, but hard to revoke before expiry, so tokens are kept short-lived.

Neither is "better" — they trade revocability against statelessness. The one rule that binds both: the credential travels on every request, and the server re-verifies it every time. Authentication is not a one-time event; it is re-proven on each interaction.

Authorization models: RBAC vs ABAC, and least privilege

Once you know who, you decide what. Two models dominate:

  • Role-Based Access Control (RBAC) — assign users to roles (student, instructor, admin), attach permissions to roles, and check the user's role. Simple, auditable, and enough for most systems.
  • Attribute-Based Access Control (ABAC) — decide from attributes of the user, the resource, and the context: "an instructor may edit a course only if they own it and it is not yet published." More expressive, more complex.
RBAC ABAC
Decides from The user's role Attributes of user + resource + context
Example rule "Admins may delete users." "Owners may edit their own draft courses."
Strength Simple, auditable Fine-grained, dynamic
Cost Coarse; role explosion at scale Complex policies, harder to reason about

Both serve one overriding principle: the principle of least privilege — every user, service, and token gets the minimum access needed to do its job, and nothing more. The student role cannot reach admin endpoints not because we trust students, but because the blast radius of any compromised student account is capped by design.

Authorization lives on the server — always

The most expensive lesson in this chapter: never trust the client to enforce authorization. Hiding the "Delete" button in the UI for non-admins is good UX; it is not security. The browser is fully under the user's control — they can call your API directly with curl, forge any header, and replay any request. If the only thing stopping a student from calling DELETE /api/users/42 is that the button is hidden, you have no access control at all.

Authorization must be checked server-side, on every request, at the point the action happens. The client-side check is a convenience; the server-side check is the truth.

A few attacks, framed briefly so you recognise them:

  • Credential stuffing — attackers take username/password pairs leaked from other breaches and try them against your login at scale, betting on password reuse. Rate limiting and MFA are the countermeasures.
  • Same error for every failure — when login fails, return one generic message ("Invalid email or password") whether the email was wrong or the password was wrong. If you say "no such user," you have just told an attacker which emails are registered — a free way to enumerate your entire user base. Leak nothing about why it failed.

The runnable version

Below: register a password the right way — random per-user salt, slow salted PBKDF2 hash — then verify a correct and an incorrect attempt, and prove that the same password with different salts produces different hashes. Run it, then change a password and run it again:

Python — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

That last False is the whole idea of salting in one line: identical passwords, unrecognisable hashes. An attacker staring at your leaked database cannot even tell which users share a password, let alone what it is. Notice too the hmac.compare_digest in verify — a plain == can leak, through tiny timing differences, how many bytes matched before it bailed. Constant-time comparison closes that side channel. A small detail guarding against a real class of attack — make it a habit.

Everyday analogy

A nightclub. Authentication is the bouncer checking your ID against your face at the door — proving you are who you say. Authorization is the wristband colour that decides whether you get into the main floor, the balcony, or the VIP room. The bouncer proving your identity says nothing about which rooms you may enter — that is a separate decision, made after you are inside, enforced by staff at every door. And crucially: the wristband check happens at each room, not once at the entrance. That is server-side authorization on every request.

History changed here

In 1978 Robert Morris and Ken Thompson, building early Unix, faced a problem: how do you let the system check a password without storing it where an attacker can read it? Their answer became foundational — hash the password, store only the hash, and mix in a small random salt so identical passwords look different on disk. Unix salts were a mere 12 bits, enough for the era. The idea outlived the number: every modern scheme — bcrypt (1999), scrypt, argon2 (winner of the 2015 Password Hashing Competition) — is a direct descendant of that 1978 insight. We are still, decades later, hashing and salting for exactly the reasons Morris and Thompson identified.

The engineer's notebook

What experienced engineers actually do with this, day to day:

• They never roll their own crypto. They reach for a vetted library — bcrypt, argon2, or a framework's built-in — because a subtle mistake in hand-written auth is invisible until it is catastrophic.
• They check authz at the boundary, every time. A middleware or decorator on the route, not a scattered `if user.is_admin` buried three functions deep where someone will forget it.
• They assume the database will leak. Design so that a full dump of your users table is embarrassing but not fatal — salted slow hashes, secrets in a vault, no plaintext anything. Defence in depth means the last wall still standing is the one that matters.

Real-world example — chika.ng Academy

When a student logs into the Academy, their password is verified against a salted, slow hash — the plaintext is never stored, exactly as in the code above. Once authenticated, a session identifies them on every subsequent request. But logging in only makes them a student. The admin panel at /admin sits behind a second, independent gate: a server-side role check on every admin request. A logged-in student who manually types /admin into the URL bar is authenticated — the system knows exactly who they are — and still receives a 403 Forbidden, because knowing who they are is not the same as granting what they may do. Two questions, two gates, both enforced on the server.

Common Mistakes

  • Treating login as the whole story. Authenticating a user and then assuming they are authorized. Every sensitive action needs its own server-side authz check, not just a logged-in session.
  • Storing recoverable passwords. If you can email a user their forgotten password, you stored it wrong. You should only ever be able to reset it, never retrieve it.
  • Using a fast hash. md5, sha1, or plain sha256 for passwords. Fast is a virtue for checksums and a fatal flaw for password storage.
  • Forgetting the salt — or reusing one salt for everyone. A shared salt defeats rainbow tables globally but still lets an attacker crack the whole set at once. Salt must be unique per user.
  • Enforcing permissions in the UI only. Hiding a button is UX, not security. If the server does not re-check, the button never mattered.
  • Leaking which part of a login failed. "No such user" versus "wrong password" hands attackers a user-enumeration oracle. Return one generic error.
Thought experiment

Your database is stolen tomorrow — assume it, don't hope against it. Two versions of your users table exist. In version A, passwords are stored as plain sha256(password). In version B, they are stored as per-user salted PBKDF2 with 200,000 iterations (illustrative — current OWASP guidance is higher still, and bcrypt/scrypt/argon2 are the production choices). The attacker has both a GPU farm and a list of the ten million most common passwords. Against version A, they precompute once and match millions of accounts in an afternoon. Against version B, they must run 200,000 iterations against each user's unique salt separately — the precomputation is worthless and the per-guess cost is thousands of times higher. Same breach, same passwords. One is a headline; the other is a bad week. That gap is what "slow and salted" buys — and why the choice is made long before the breach, not after.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
"What's the difference between authentication and authorization?"
Model answer — "Authentication proves who you are — like checking a password at login. Authorization decides what you're allowed to do once you're known — like whether you can access the admin panel. Authentication comes first; authorization is checked on every request after. They fail with different status codes: 401 for authn, 403 for authz."
Follow-up they'll throw: "How should you store a password?" → Never plaintext. Hash it with a slow, salted, purpose-built algorithm like bcrypt or argon2. Say why: salt defeats rainbow tables, slowness defeats brute force.
Intermediate
"Why is a per-user salt necessary if you're already hashing, and why does the hash need to be slow?"
Model answer — "Without a salt, identical passwords produce identical hashes, so an attacker precomputes a rainbow table once and cracks millions of accounts. A unique per-user salt makes every hash different, forcing them to attack each user separately. Slowness matters independently: a fast hash lets a GPU try billions of guesses per second. A deliberately slow algorithm — bcrypt, argon2, PBKDF2 with high iterations — makes each guess expensive enough that brute force stops being economical, while a single legitimate login stays imperceptibly fast."
Follow-up: "Session-based or token-based auth — which and why?" → Sessions are easy to revoke but need shared storage; JWTs are stateless and scale but are hard to revoke, so keep them short-lived. Naming the trade-off is the signal.
Senior
"Design the authorization layer for a multi-tenant SaaS where users, roles, and resource ownership all matter. RBAC, ABAC, or both?"
Model answer — "I'd start with RBAC for the coarse layer — roles like owner, admin, member map cleanly to org-wide permissions and are auditable. But multi-tenancy and ownership are attribute questions: 'can this member edit this document?' depends on tenant match and ownership, which RBAC alone can't express without role explosion. So I'd layer ABAC policies on top — decisions from user, resource, and context attributes — and enforce them in one central place: a policy check at the request boundary, server-side, on every call. Least privilege throughout: default deny, grant explicitly. And I'd never let tenant isolation depend on the client sending the right tenant ID — the server derives it from the authenticated identity."
This is a system-design answer wearing a security costume. The senior move: name the models, then reach past them to central enforcement, default-deny, and tenant isolation that can't be forged client-side.
Company lens — how the same topic gets asked
micro1Live and conversational. The AI interviewer wants you to reason out loud — define authn vs authz crisply, then justify your password-storage choice. Don't just name bcrypt; explain what salt and slowness each defend against.
GoogleWants precision and the threat model. Expect "what attack does that prevent?" until you name rainbow tables, brute force, and timing side channels by name.
MicrosoftValues structured, defensive reasoning. Walk the request lifecycle: authenticate, establish identity, authorize each action server-side. Narrate the boundaries.
Amazon / AWSOperational and least-privilege obsessed (it's the IAM company). "What's the blast radius if this credential leaks?" Tie every access grant to the minimum privilege and the cost of getting it wrong.
Hands-on exercise
  1. In the editor above, change the iterations from 200,000 to 2,000,000 and observe the delay. That delay is your defence against brute force — now imagine it multiplied across an attacker's billion guesses.
  2. Modify verify to use plain == instead of hmac.compare_digest. It still returns the right answer — explain in one sentence what security property you just quietly gave up, and why it wouldn't show in any test.
  3. Sketch (in words or pseudocode) a can_access(user, resource, action) function that combines an RBAC role check with an ABAC ownership check, defaulting to deny. State where in the request lifecycle it must run — and why the client can never be trusted to run it.

Chapter Summary

  • Authentication proves who you are; authorization decides what you may do. They are separate steps, fail with different codes (401 vs 403), and conflating them is a security hole.
  • Never store passwords. Store a salted, slow, purpose-built hash (bcrypt / scrypt / argon2 / PBKDF2). Salt defeats rainbow tables; slowness defeats brute force.
  • A password is one factor (something you know). MFA combines factors from different categories so a single stolen secret isn't enough.
  • Sessions (server-side state, easy revoke) and tokens (stateless, scalable, short-lived) are two ways to carry identity across stateless HTTP — re-verified on every request.
  • RBAC decides from roles, ABAC from attributes; both serve least privilege. Enforce authorization server-side, on every request — never trust the client.
  • Return one generic error on login failure, and rate-limit to blunt credential stuffing. Leak nothing about why auth failed.
Narrative bridge

We've secured the front door for your own users, with your own passwords. But the modern web runs on a harder question: how do you let a user prove their identity to your app using an account they hold somewhere else — "Log in with Google" — without ever seeing their Google password? How does one service grant another limited, revocable access on a user's behalf, without handing over the keys? That is the problem OAuth2 was built to solve, and it is where we turn next.

Editorial reflection — most engineers learn auth as a checklist: hash the password, add a salt, check the role. But auth is really a discipline of humility — it is the code you write assuming you will be breached, assuming the client is hostile, assuming the database will leak. Every good decision here is a bet you make before the attack, paid out during it. The engineers who sleep well are the ones who assumed the worst on a quiet Tuesday, and built for it anyway.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.