Lesson 2.3 · 13 min read
Every API request we've looked at so far has been naive. It arrives, and the server just answers it. But think for a moment about what that would actually mean in a real product. If /api/orders/42 simply returns order number 42 to whoever asks, then anyone who changes the number in the address can read every order in your entire system, including everyone else's. If /api/users returns all users to any request, your whole user list walks out the door. Without something standing at the gate, an API is a door with no lock, and everything behind it is public whether you meant it to be or not.
That something is authentication, and it's the layer of every real product that answers two related questions on every single request: who are you, and are you allowed to do this? Getting this right is the difference between a product and a liability. And it's an area where "just install a library and don't think about it" genuinely fails people, because you cannot judge whether your authentication is correct if you don't understand what it's supposed to be doing.
So we're going to actually understand it. Not memorize acronyms, but build the mental model that lets you look at any authentication setup, including one an AI wrote for you, and reason about whether it's right for what you're building.
First, a distinction that matters enormously and gets blurred all the time, including by experienced developers.
Authentication is proving who you are. It's showing your ID at the door. The system checks and concludes: yes, this is genuinely Sarah.
Authorization is what you're allowed to do once you're in. Sarah is authenticated, certainly Sarah, but is Sarah allowed to delete this project? View this order? Access the admin dashboard? Authentication establishes identity. Authorization establishes permissions.
Think of an office building. Authentication is the badge reader at the entrance confirming you're an employee. Authorization is which doors your badge opens once inside. You can be perfectly authenticated (definitely an employee) and completely unauthorized for a particular room (the server room stays locked to you). Two separate systems, two separate questions.
This matters because a shocking number of real security holes come from a system that authenticates carefully and then forgets to authorize. It correctly confirms who you are, then hands you data belonging to someone else because nobody checked whether you should have it. Remember our /api/orders/42 example. A system might confirm you're a valid logged-in user, then happily hand you order 42 without ever checking whether order 42 belongs to you. That's authentication working and authorization missing, and it's one of the most common real-world vulnerabilities there is. Hold both questions in your head, always: who are you, and should you have this?
Before the types, understand the underlying mechanic, because all the types are variations on it.
Remember that requests are stateless and independent. Each one arrives at the server on its own, with no inherent memory of anything that came before. The server doesn't recognize you from your last request. It's a counter clerk with total amnesia between customers. So every single request that needs to be authenticated has to carry its own proof of identity, attached to it, every time.
Where does that proof ride? In the headers, that part of a request from the last post that carries extra information alongside the main content. Nearly every authenticated request in the world attaches a credential to a header, and the server reads that header to decide who's asking. The differences between authentication types are largely differences in what that credential is, how it's obtained, and what it proves.
So the shape is always the same. Client attaches a credential to the request. Server checks the credential. If valid, the server knows who's asking and proceeds. If not, it returns a 401 Unauthorized, that status code you now recognize, meaning "I don't know who you are, or your proof isn't good." Every type below is a different answer to "what's the credential and where did it come from?"
The simplest credential is an API key, a long random string that acts like a password for a program rather than a person. You get one from a service, you attach it to your requests, and the service recognizes it as yours.
An API key answers "which account is making this request," and that's mostly all it answers. It's a single unchanging secret, like a physical key to a building. Whoever holds it, gets in. The key doesn't know who's holding it, it just works.
That property tells you exactly where API keys belong and where they're dangerous. They're excellent for server-to-server communication, one of your own backends calling some external service's API. The key lives on your server, hidden from users, identifying your application to that service. Payments, email sending, AI providers, most services you call from your backend authenticate with a key. It's simple, it works, and the key stays safely out of sight.
They're terrible in the frontend, and this is the most common serious mistake that AI-assisted builders make. Anything in your frontend code is visible to the user; that code runs on their machine and they can read all of it. Put your API key in the frontend and you have published your key to the world. Someone will find it, use your account, and run up your bill or steal your quota. This is not a hypothetical. People scan public code for exposed keys constantly and automatically. So the rule is absolute: API keys go on the server, never in frontend code. When you're building something that needs to call a service with a key, the frontend calls your backend, and your backend, holding the key privately, calls the service. That indirection is not bureaucracy; it's the entire point.
Because a key is a permanent secret that grants access to whoever holds it, keys have real weaknesses. They don't expire on their own. They don't identify a specific person, just an account. If one leaks, it stays valid until you notice and revoke it. Which is exactly why they're right for backend-to-service calls and wrong for identifying your actual human users.
Now we move to authenticating people, not programs, and the oldest and still very common approach is the session cookie.
Here's how it works. A user logs in with their email and password. The server checks the password, confirms it's really them, and then creates a session: a record on the server saying "session number abc123 belongs to Sarah, logged in just now." The server sends back a cookie, a small piece of data the browser stores automatically, containing that session identifier. From then on, the browser automatically attaches that cookie to every request to that site. The server sees the session id, looks it up in its own records, finds "that's Sarah," and knows who's asking.
The essential thing to notice: the cookie itself is meaningless, just an id. The real information lives on the server, in the session record. The cookie is a coat check ticket. The ticket has no coat on it, just a number. The coat is in the back room, and the number tells the attendant which one is yours.
That design has genuine strengths. Because the server holds the session record, the server can destroy it at any moment. Log the user out everywhere, instantly. Revoke access to a suspicious session immediately. The server retains full control over who is logged in, because the truth is on the server. This is real and valuable, and it's why sessions remain the right choice for many traditional web applications.
The cost is that the server must store and look up a session for every logged-in user on every request. That's fine at moderate scale and gets heavier as you grow, especially if you have many servers, since they all need access to the same session records. Notice that this is a version of the trade-off from our architecture post: keeping the truth central gives you control but creates a coordination cost. Every real engineering decision has this shape.
The alternative to storing sessions on the server is to make the credential itself carry the information. This is a token, and the most common form you'll meet is a JWT (JSON Web Token, pronounced "jot").
Here's the shift in thinking. When the user logs in, instead of storing a session and handing back a ticket, the server creates a token that contains the information: this is Sarah, user id 42, she's an admin, and this token expires in one hour. Then the server signs it, a cryptographic operation that produces a signature proving the server created this token and nobody has altered it. The client stores the token and attaches it to every request in a header.
Now the server, receiving a token, doesn't need to look anything up. It verifies the signature, which proves the token is genuine and untampered, then simply reads the information right out of the token: this is Sarah, id 42, admin, not yet expired. No database lookup, no stored session. The credential carries its own truth, and the signature makes that truth trustworthy.
Go back to the coat check. A session cookie is a numbered ticket; the coat's details are in the back room. A JWT is a ticket that has all the details printed right on it, along with a tamper-proof seal. The attendant doesn't check any records. They read the ticket and verify the seal is authentic. If someone tried to change what's printed on it, the seal breaks and it's rejected.
The strength is obvious and is why tokens are everywhere in modern systems, especially with the microservices architecture we discussed. No lookup means less work per request, and any server, anywhere, can verify a token independently as long as it can check the signature. That scales beautifully across many services.
The weakness is the exact mirror of that strength, and it's the thing people forget. Because the server stores nothing, the server has no easy way to un-issue a token. A session can be destroyed on the server instantly. A signed token, once handed out, stays valid until it expires, no matter what. You logged someone out, but their token is still floating around, valid, until it times out. This is why tokens usually have short lifetimes, minutes or an hour, paired with a longer-lived refresh token used solely to get a fresh one. The short life limits the damage window. When you see this refresh-token dance in code, that's what it's for: buying back some of the control that stateless tokens gave away.
So the honest comparison isn't "sessions are old, tokens are modern." It's: sessions keep the truth on the server, giving you control at the cost of lookups. Tokens put the truth in the credential, giving you scale at the cost of control. Choose according to which of those you need, which is exactly the kind of engineering judgment this series exists to build.
Now the one that seems most complicated and is actually the most elegant once you see what problem it solves.
Imagine you're building an app that needs to read a user's calendar. The naive approach: ask the user for their calendar account's email and password, then log in as them. This is a catastrophe. You now hold their actual password, you have complete access to their entire account forever, and if you're breached, so are they. Nobody should ever hand their password to a third-party app.
OAuth exists to solve exactly this: giving one application limited access to your account on another service, without ever sharing your password with it.
Here's the flow, and you've experienced it many times. You click "Connect your calendar" in some app. You get sent to the calendar provider's own site, their real page, where you log in directly with them. (Crucially, the app never sees this. You're typing your password into the provider, not the app.) The provider shows you exactly what the app is asking permission for: "this app wants to read your calendar events." You approve. The provider then hands the app a token that grants precisely that access, reading your calendar, nothing more, and revocable by you at any time from your account settings.
Think of a hotel keycard. When you check in, you don't get the manager's master key. You get a card that opens your room and the gym, expires at checkout, and can be deactivated instantly. OAuth gives applications a keycard to a specific part of your account, rather than the master key to all of it. The scoping (only the calendar) and the revocability (turn it off anytime) are the whole point.
"Log in with Google" is a close relative of this, using the same machinery for identity rather than data access: instead of your app managing passwords, a provider vouches for who the user is. The benefit is enormous and often underrated. You never store passwords, so you can never leak them. You inherit the provider's serious security. Users don't create another account. For most products, this is a genuinely excellent default, and choosing it is a mature engineering decision, not a shortcut.
The cost is real dependency: your login now relies on that provider, and users who don't have or want that account need another path. But the trade is usually worth it, and understanding why it's worth it, rather than just clicking the easy option, is the difference we keep returning to.
The types are useless as trivia. What matters is the judgment. So here's how to actually think about it when you're building.
If your backend is calling someone else's service, use an API key, and keep it strictly on the server, never in frontend code. This is the most common case in AI-assisted building, calling an AI provider, a payments service, an email sender, and the exposed-key mistake is the one you must never make.
If you're building a traditional web application, users logging into your own product, and you value straightforward control over who's logged in, session cookies are a solid, sometimes underrated choice. The server keeps the truth and you can revoke instantly.
If your system spans multiple services, or you need to authenticate requests without a lookup on every one, tokens are the natural fit, and you accept the revocation problem by keeping their lifetime short and pairing them with refresh tokens.
If your app needs access to a user's data on another service, or you'd rather not manage passwords at all, OAuth is the answer, and choosing it is usually a mark of good judgment, not laziness.
And through every one of these, hold the two questions from the beginning. Authentication tells you who is asking. Then, separately and always, authorization must ask whether this particular person should have this particular thing. When an AI builds you an endpoint that fetches an order by its id, your job, your actual engineering job, is to look at it and ask: does this check that the order belongs to the person asking? That single question, asked consistently, will catch more real vulnerabilities in AI-generated code than any tool. It's the question that separates a product from a liability, and you can now ask it with full understanding of what it means.
Do this before the next post
Take something you've built, or ask an AI to build a small backend with a route like /api/orders/:id that returns an order. Then read it with your new eyes and ask the two questions.
First, does anything check who is making this request? Look for a header being read, a token being verified, a session being looked up. If nothing does, this endpoint will serve anyone, and you've found an authentication gap.
Second, and this is the one people miss, even if it does verify who you are, does it check that this specific order belongs to this specific user before returning it? Look for a comparison between the order's owner and the authenticated user. If it just fetches the order by id and returns it, you've found an authorization gap, and anyone logged in can read anyone's orders by changing a number in the address.
Do this on real code, ideally code an AI wrote for you. Finding one of these gaps yourself, in something you were about to ship, is a genuinely formative moment. It's when authentication stops being a chapter you read and becomes a lens you look through, which is exactly what it needs to be for anyone building things real people will use.