devto 2026-07-02 원문 보기 ↗
Every authentication mechanism in use today emerged to address a specific set of constraints the previous one wasn't designed for. This article walks through that chain — not as a list of definitions, but as a sequence of problems and the constraints that shaped each solution.
The earliest widely used approach, HTTP Basic Authentication, is also the simplest to understand. The client sends the username and password, base64-encoded, on every single request:
GET /api/data
Authorization: Basic dXNlcjpwYXNzd29yZA==
Base64 is not encryption — it's just a reversible encoding. Anyone who intercepts this header has the raw credentials.
This approach has three structural problems.
The next evolution moved the credential check to a single moment: login. After verifying the password once, the server creates a session, stores it (in memory or a database), and gives the client a session ID, usually via a cookie. Every subsequent request just sends that ID, not the password.
The user logs in by submitting their username and password.
Browser ───────────────▶ Server
Login Request
The server validates the credentials.
Server ── checks username/password ──▶ Database
If the credentials are valid, the server creates a new session, stores it in the session store, and generates a unique session ID.
Server ───────────────▶ Session Store
Create Session
The server returns the session ID to the browser in a Set-Cookie header.
Server ───────────────▶ Browser
Set-Cookie: session_id=abc123
For every subsequent request, the browser automatically includes the session cookie.
Browser ───────────────▶ Server
Request + session_id cookie
The server uses the session ID to retrieve the corresponding session from the session store.
Server ───────────────▶ Session Store
Lookup session_id
Session Store ─────────▶ Server
Session data
If the session exists and is still valid, the server processes the request and returns the requested resource.
Server ───────────────▶ Browser
Response
This solves the repeated-password-exposure problem cleanly. But it trades that problem for a different set of issues. The server now has to store and look up a session on every request, which is a form of state that has to live somewhere and be kept in sync if you're running more than one server. Cookies are also sent automatically by the browser, which is exactly what makes them vulnerable to CSRF — a malicious site can trigger a request that carries the user's cookie without their knowledge. And sessions are fundamentally a browser-native concept; they don't map cleanly onto mobile apps, CLI tools, or server-to-server calls, all of which have no natural concept of a cookie jar. For those contexts specifically, sessions weren't a broken solution — they were a solution built for a different problem.
The fix here was to stop relying on server-side storage altogether and instead issue a self-contained token — most commonly a JWT (JSON Web Token) — that carries its own proof of validity.
The token is called a "bearer" token because possession is the only requirement — whoever holds it can use it, no further proof needed. Crucially, the server doesn't need to look anything up in a database to validate it; it just verifies the signature.
Session-based check: Request → DB/store lookup → Response (stateful)
Bearer token check: Request → verify signature → Response (stateless)
This single change resolves several of the earlier problems at once. There's no session store to maintain or synchronize across servers. The token can carry scopes, limiting exactly what it's allowed to do, rather than granting blanket access. And because it's just a string in a header, it works identically whether the caller is a browser, a mobile app, a CLI tool, or another backend service — there's no cookie-jar assumption baked in.
Solving the statelessness problem exposes a different question that sessions never really had to answer cleanly: how long should a token live?
A short-lived token limits the damage if it's ever stolen — an attacker only has a small window before it expires. But it also means the user gets logged out frequently, since there's no way to silently extend access once the token has expired. A long-lived token solves the convenience problem, but now a single stolen token grants extended, possibly indefinite access, which is a much worse outcome if it leaks.
Short-lived token: safer if stolen, but logs the user out constantly
Long-lived token: convenient, but dangerous if it's ever compromised
Picking one or the other means accepting one of these two problems outright. The fix wasn't to pick a side — it was to stop treating this as a single-token problem.
Instead of one token trying to be both safe and convenient, the solution introduces two tokens, each optimized for a different job. The access token is short-lived and is the one actually sent with every API call. The refresh token is long-lived, but it's used for exactly one purpose: silently obtaining a new access token when the old one expires. It's never sent to the API directly.
`1. The user logs in by submitting their credentials.
Client ───────────────▶ Auth Server
Login Request`
The authentication server verifies the credentials.
Auth Server ── validates username/password ──▶ User Database
If authentication succeeds, the server issues two tokens:
A short-lived access token (e.g., 15 minutes)
A long-lived refresh token (e.g., 30 days)
Auth Server ───────────────▶ Client
access_token
refresh_token
The client calls protected APIs using the access token.
Client ───────────────▶ API
Authorization: Bearer access_token
The API validates the access token. If it's valid, the request
is processed and the response is returned.
API ── verifies token signature/claims ──▶
API ───────────────▶ Client
Response
After the access token expires, API requests are rejected
with an authentication error (typically 401 Unauthorized).
Client ───────────────▶ API
Expired access_token
◀─────────────── 401 Unauthorized
The client sends the refresh token to the authentication server
to obtain a new access token.
Client ───────────────▶ Auth Server
refresh_token
The authentication server validates the refresh token and,
if it's still valid, issues a new access token
(and optionally a new refresh token).
Auth Server ───────────────▶ Client
new access_token
The client retries the original request using the new
access token and continues working without requiring
the user to log in again.
Client ───────────────▶ API
Authorization: Bearer new_access_token
◀─────────────── Response`
This refresh exchange happens silently in the background, invisible to the user — there's no re-login screen, no interruption. The user only has to log in again once the refresh token itself eventually expires, which can be set to a much longer window precisely because the refresh token isn't exposed to the same attack surface as the access token; it's used rarely, against a single dedicated endpoint, rather than being sent on every API call where it would have more chances to leak.
This two-token model is part of the OAuth 2.0 specification, which formalized exactly this pattern: access tokens for calling resources, refresh tokens for renewing access, and a clear separation of purpose between the two.
Looking back at the chain, the access token plus refresh token pair addresses each prior constraint in turn. The password itself is only ever sent once, at login — not on every request, which closes the exposure problem Basic Auth had. There's no server-side session store to maintain, since the access token is self-contained and stateless, removing the scaling constraint that sessions introduce for multi-server deployments. The user isn't forced to log in every few minutes, since the refresh token silently renews access in the background. And if an access token does leak, the damage is bounded by its short expiry, while the refresh token — the more sensitive, longer-lived credential — is never exposed on routine API calls in the first place, limiting its attack surface.
Each step in this chain didn't make the previous mechanism wrong — it made it insufficient for a different set of requirements: scale, statelessness, portability across client types, and convenience without sacrificing security. The access token and refresh token pair is where that chain currently settles in stateless, API-driven systems, and it's the foundation that more specialized mechanisms later build directly on top of it.
Bearer tokens and refresh tokens address the constraints of stateless, multi-client APIs well — but that doesn't make sessions obsolete. They remain widely used, and in several contexts they're genuinely the better fit.
In practice, many large systems use both approaches for different layers: session-based auth for first-party browser interfaces where the server needs tight control, and token-based auth for APIs and third-party integrations where statelessness and portability matter more. The two aren't mutually exclusive — they answer different questions.