Skip to content

필사 모드: The Complete Guide to Authentication and Authorization: Ten Misconceptions, Corrected From the Specs

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

Introduction

This blog already has OAuth 2.0 Mastery, JWT or Sessions, and When, and a per-framework SSO series. Those explain how the protocols work. This guide takes a different angle: it identifies exactly where the recurring misconceptions come from, and corrects each one against the sentence in the spec that settles it.

One thing to state before we start. This is not a recipe. Security advice only means something when its assumptions come with it. "Put it in a cookie", "use JWTs" — the moment you drop the assumptions, those sentences become wrong advice. So every recommendation here comes with the condition under which it holds. When the condition changes, the conclusion has to change too.


1. Authentication and authorization are different problems

  • Authentication: establishing who sent the request
  • Authorization: deciding whether that established party may perform this action

This distinction is not explanatory garnish. HTTP itself separates the two states into different status codes. RFC 9110 §15.5.2 defines 401 as "the request has not been applied because it lacks valid authentication credentials for the target resource", and §15.5.4 defines 403 as "the server understood the request but refuses to fulfill it". 401 is an authentication problem; 403 is an authorization problem.

The fact that 401 is named Unauthorized is close to a historical accident. The name looks like authorization; the meaning is authentication.

This matters in practice because client behaviour depends on it.

  • Return 403 to a user who is not logged in, and the client loses its basis for starting a re-login flow.
  • Return 401 to a user who lacks permission, and the client logs in again, gets another 401, and loops forever.

There is one deliberate exception. When the existence of a resource must itself be hidden, some systems return 404 instead of 403. If putting another user's document ID into the URL returns 403, that leaks the fact that the ID exists. This is a trade-off between conveying precise meaning and minimising information disclosure, and neither side generalises. Decide, and write the decision down.


2. OAuth 2.0 is an authorization framework — and what OIDC adds on top

This is the most frequently repeated misconception. If you start designing with OAuth 2.0 understood as a login protocol, every decision downstream is skewed.

RFC 6749 is titled The OAuth 2.0 Authorization Framework. The document defines itself as an authorization framework and does not describe itself as an authentication protocol. The four roles it defines are written in the language of delegated access.

RoleDefinition in RFC 6749 §1.1
Resource ownerAn entity capable of granting access to a protected resource; the end-user if human
ClientAn application making protected resource requests on behalf of the resource owner
Authorization serverThe server issuing access tokens after authenticating the owner and obtaining consent
Resource serverThe server hosting protected resources and accepting access tokens

An access token is a certificate saying "this client may access this resource to this extent". It is not a proof saying "this is who the person is". Those are two different sentences.

That is why using an access token directly as evidence of login is dangerous. An access token carries no standard mechanism for a client to confirm that the token was issued to it. A token minted for a different application can be presented to your server, and you may have no way to tell.

OIDC exists to fill exactly that gap. The first sentence of the OpenID Connect Core 1.0 abstract reads: "OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol." §2 defines the ID Token as "a security token that contains Claims about the Authentication of an End-User by an Authorization Server", formatted as a JWT.

The ID Token's required claims are iss, sub, aud, exp, and iat, and aud must contain that client's client_id. That aud check is the mechanism the access token does not have. An ID Token that does not carry your client_id was not issued for you, so you reject it.

To summarise:

  • OAuth 2.0 alone gives you delegated access, with no guarantees as an authentication protocol
  • OIDC gives you an ID Token — a verifiable authentication result
  • Most of what gets called "social login" is either OIDC or a non-standard convention each provider layered on top of OAuth

3. The flows that fell out of the recommendations, and what replaced them

OAuth 2.0's security guidance did not stop at RFC 6749 in 2012. RFC 9700 (January 2025, BCP 240) is the current security best current practice, and it updates RFC 6749, 6750, and 6819. Two grant types changed status there.

3-1. Implicit grant — SHOULD NOT

RFC 9700 §2.1.2 states: "Clients SHOULD NOT use the implicit grant (response type token) or other response types issuing access tokens in the authorization response, unless access token injection in the authorization response is prevented."

There are two reasons. Access tokens travelling through the authorization response (fragments, redirects, browser history, referrers) are exposed to leakage and replay, and there is no standardized method for sender-constraining tokens issued in the authorization response to a specific client.

3-2. Resource owner password credentials (ROPC) — MUST NOT

§2.4 is blunter: "The resource owner password credentials grant … MUST NOT be used."

The reasons are that it insecurely exposes the resource owner's credentials to the client, widening the attack surface, and that it is not designed to work with two-factor authentication or authentication processes requiring multiple user interaction steps.

Do not skim past the difference between SHOULD NOT and MUST NOT. The implicit grant is conditionally prohibited; ROPC is prohibited unconditionally. When you cite the basis in a document, that difference carries real weight.

3-3. What replaced them

The current recommendation is the authorization code grant plus PKCE. RFC 9700 §2.1.1 nails it down in three sentences.

  • "Public clients MUST use PKCE."
  • "For confidential clients, the use of PKCE is RECOMMENDED."
  • "Authorization servers MUST support PKCE."

The same section governs redirect URIs. Authorization servers must compare against pre-registered URIs using exact string matching, with the sole exception of variable port numbers for localhost redirects in native apps. Implementations that allow partial or prefix matching violate this requirement.

Refresh tokens are covered too. §2.2.2 requires that refresh tokens for public clients be sender-constrained or use refresh token rotation.


4. PKCE — what it stops and what it does not

What PKCE, defined in RFC 7636, stops is the authorization code interception attack: the authorization code is stolen not over the TLS channel between client and server but over an unprotected path, such as inter-application communication inside a device, and the attacker exchanges it for a token.

The mechanism is simple.

Client                                        Authorization server
   |                                              |
   |-- generate code_verifier (32 random octets)  |
   |-- code_challenge = BASE64URL(SHA256(verifier))|
   |                                              |
   |--- auth request + code_challenge + S256 ---->|
   |                                              |  (stores challenge)
   |<-- authorization code -----------------------|
   |                                              |
   |--- token request + code + code_verifier ---->|
   |                                              |  SHA256(verifier) == challenge ?
   |<-- access token -----------------------------|

The code_verifier is 43–128 characters, and the spec recommends generating 32 random octets and base64url-encoding them. code_challenge_method is either plain or S256; S256 is Mandatory To Implement on the server, and clients capable of S256 must use S256. plain survives only for constrained legacy environments.

What PKCE does not do must be equally clear.

  • PKCE is not CSRF protection. You still need the state parameter (or OIDC's nonce).
  • PKCE does not protect an access token that has already been issued and then leaked.
  • It is defeated if the authorization server permits a PKCE downgrade. RFC 9700 §2.1.1 requires (MUST) that a token request containing a code_verifier be accepted only if a code_challenge was present in the authorization request. Adding PKCE on the client alone does not finish the job.

5. Telling the three tokens apart

They are all called "tokens", so they get blurred together, but their consumers and lifetimes differ.

TokenWho validates itWhere it is sentMay the client read it?
Access tokenResource serverThe resource server (API)No. Opaque is perfectly normal
Refresh tokenAuthorization serverThe AS token endpointNo
ID tokenThe clientNowhereYes. That is what it exists for

Two common mistakes follow from this.

First, using the ID Token as the Bearer token on API calls. The ID Token exists so the client can confirm that the user was authenticated. It was not built to be presented to a resource server, and its aud points at the client rather than the API, so from the resource server's perspective the audience is simply wrong.

Second, parsing the access token to extract user information. Nothing guarantees an access token is a JWT. An opaque string is spec-conformant, and if the provider changes the format the client has no standing to object. When you need user information, the ID Token's claims or the UserInfo endpoint in OIDC Core §5.3 is what the contract actually offers.


6. JWT: decoding is not verifying

RFC 7519 defines a JWT as either a JWS (protected by a signature or MAC) or a JWE (encrypted). Starting there clears up most of the confusion.

6-1. base64url is encoding, not protection

Anyone can read a JWT payload, because it is encoding rather than encryption. Putting sensitive data in a JWT payload is equivalent to publishing it. Keep national ID numbers, internal system identifiers, and the details of your authorization policy out of it.

6-2. A decoder showing you the contents does not make the token valid

This genuinely happens during reviews and incident response: someone pastes the token into a decoder, sees a plausible sub and exp, and calls it valid. Decoding has nothing to do with signature verification. This blog's own JWT decoder states that it does not verify signatures. A decoder is a tool for reading contents, not for judging authenticity.

6-3. alg: none

RFC 7519 §6 defines the unsecured JWT, with alg set to none and an empty signature. The spec permits this only when security is provided by external means. The problem arises when the verifying side forgets that condition and follows whatever the token says. If an attacker strips the signature and sets alg to none, authentication disappears on that server.

6-4. Algorithm confusion

This one is subtler. Suppose the server issues tokens signed with RS256 (public-key signature), but the verification function reads the token's alg header and uses it. An attacker changes alg to HS256 and signs the token using the server's public key as the HMAC secret. The verifying code then computes an HMAC with that same public key and declares a match. Because the public key is public, anyone can forge tokens.

RFC 8725 (BCP 225) addresses this head-on.

  • §3.1 "Libraries MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms when performing cryptographic operations."
  • §3.1 "The library MUST ensure that the 'alg' or 'enc' header specifies the same algorithm that is used for the cryptographic operation."
  • §3.1 "Each key MUST be used with exactly one algorithm, and this MUST be checked when the cryptographic operation is performed."

Reduced to one sentence: do not trust the algorithm the token declares about itself. The verification algorithm must be pinned by the application, outside the token.

6-5. The spec does not decide what you must check

In RFC 7519, iss, sub, aud, exp, nbf, iat, and jti are all OPTIONAL. That fact is often read backwards. Optional does not mean "you need not check them"; it means the application, not the spec, must decide what has to be checked. This is where "surely the library validates it" breaks down. Library defaults that check only expiry, and never issuer or audience, are not rare.

At minimum, decide these explicitly and leave the decision in the code.

[ ] Is the verification algorithm pinned in code (not read from the token header)?
[ ] Is the source of the verification key fixed (JWKS URL, key rotation handled)?
[ ] Is iss the issuer you expect?
[ ] Does aud point at you?
[ ] Are exp / nbf checked, and what clock skew do you allow?
[ ] Where needed, does jti prevent reuse?

6-6. Revocation is still hard

If the signature verifies and the token has not expired, it is valid. That is both the benefit and the price of stateless verification. Reflecting logout, permission withdrawal, or account suspension immediately eventually requires server-side state — a deny list, a token version, a session lookup — and at that moment much of the "stateless" advantage is gone.

There is no right answer here. The axis is access token lifetime. Shorter lifetimes reduce revocation lag but increase authorization-server traffic and failure coupling. Longer lifetimes do the reverse. The choice is really a question of how many minutes of permission-withdrawal lag your organisation can tolerate, and picking that number and writing it down is the design work.


7. Where to put the token — and how to run the session

Be wary of any article here that says there is one right answer. Both sides carry cost; the costs are simply of different kinds.

7-1. Web storage (localStorage / sessionStorage)

  • Weakness: every piece of JavaScript running in the origin can read it. The OWASP Session Management Cheat Sheet explicitly says not to store "authentication tokens, session IDs, JWTs, refresh tokens, or any credential" in localStorage or sessionStorage, on the grounds that "a single XSS vulnerability discloses every token".
  • Strength: the browser does not attach it automatically, so the CSRF surface is structurally smaller. Attaching it as a header on cross-origin API calls is also straightforward.

7-2. Cookies (HttpOnly, Secure, SameSite)

  • Strength: with HttpOnly, JavaScript cannot read the value. OWASP calls Secure "mandatory to prevent the disclosure of the session ID through MitM attacks".
  • Weakness: the browser attaches it to requests automatically, which creates a CSRF surface. OWASP records this alongside: "if an XSS attack is combined with a CSRF attack, the requests sent to the web application will include the session cookie, as the browser always includes the cookies when sending requests." Session cookies must therefore set SameSite=Strict (preferred) or SameSite=Lax, and SameSite=None must never be used without Secure.

7-3. An honest summary of the trade

Cookies make token theft harder and request forgery easier. Web storage does the reverse. And decisively: if you have XSS, neither is safe. Even when the attacker cannot read the cookie, they can simply issue authenticated requests from that browser. Choosing a storage location does not substitute for fixing XSS.

Most authorities, OWASP included, recommend cookies, and the reasoning is sound. But that recommendation carries assumptions.

  • Is your site laid out so that the frontend and API can share cookies?
  • Are you implementing CSRF defences alongside (SameSite, CSRF tokens, Origin/Referer checks)?
  • Is this a third-party context or a native app, where cookies fit poorly?

When the assumptions differ, so must the conclusion. Security advice with its assumptions stripped out is dangerous to whoever reads it next.

7-4. What it means to operate a session

Whether it is a token or a session ID, the operating rules are similar. Per the OWASP Session Management Cheat Sheet, at minimum:

  • Entropy: session identifiers must have at least 64 bits of entropy (16 or more hexadecimal characters). If any part of the value is fixed or predictable, the effective entropy drops accordingly.
  • Idle timeout and absolute timeout: expiring on inactivity and enforcing a maximum lifetime from creation regardless of activity are two different mechanisms. You need both. The document's sense of scale is 2–5 minutes for high-value applications, 15–30 minutes for lower-risk ones.
  • Session fixation defence: "The session ID must be renewed or regenerated by the web application after any privilege level change within the associated user session." Immediately after a successful login is the canonical moment.

8. Where the authorization decision is made

Leading with the models makes the discussion drift. Let us reverse the order.

8-1. Three models are usually enough

  • RBAC: permissions attach to roles. Works when the number of roles stays manageable.
  • ABAC: decisions come from attributes (department, region, time, tier). Avoids role explosion but makes rule verification and debugging harder.
  • ReBAC: decisions come from relationships, such as "is this user on the same team as the document's owner". Fits sharing and collaboration models, and you pay for graph lookups.

The three are not exclusive. Most systems frame things with RBAC and mix in attributes or relationships at specific points.

8-2. The real problem is the decision point

A large share of authorization failures come not from choosing the wrong model but from choosing the wrong place for the decision.

Client -> [API gateway] -> [Service] -> [Data access layer] -> DB
                |               |                |
        authn + early reject  business rules  object ownership
  • Decide only at the gateway, and internal calls, batch jobs, and admin tools that bypass the gateway bypass the check.
  • Decide separately in every service, and the same rule scatters across many places, so one copy gets fixed and the others do not.

The working rule is this. The authorization decision must happen at least once as close to the data as possible. Decisions further forward exist for early rejection — latency, user experience, log noise — not as the basis for trust.

8-3. The check that gets skipped most often

Object-level authorization. Authentication passed and the role matches, but nobody verified whether this user has rights to this particular resource. Swapping an identifier in the path to read someone else's data comes from here. It is especially common to filter the list endpoint by owner and then forget the same condition on the single-item endpoint.

Decisions must also fail closed. If the default when a permission lookup times out is "allow", an outage in the authorization system becomes a blanket grant of access.

How much detail to give in a denial is also a decision. More detail makes debugging easier and leaks more. There is no reason for an internal admin tool and a public API to share one policy.


9. Should you build it yourself — and what that decision costs

Finally, the question that gets hedged most often.

Implementing authentication yourself is not forbidden. It is an expensive decision, and the cost is billed after the first release rather than before it. That sentence is worth writing into the meeting notes verbatim.

9-1. What you take on by building it

The login screen and the password check are only the first item on the list.

  • Password storage (an appropriate hash function and parameters, and periodic updates to those parameters)
  • Account recovery flows — statistically, this is breached more often than authentication itself
  • User enumeration defence: OWASP requires responding "in a generic manner", offering Login failed; Invalid user ID or password as the example. The HTTP status code must match too — the document notes that "the HTTP response code may differ which can leak information about whether the account is valid or not"
  • Lockout policy: the failure counter should be "associated with the account itself, rather than the source IP address", and exponential lockout starting at one second and doubling per failure is recommended. And password recovery must stay reachable while locked out, or lockout becomes a denial-of-service tool
  • MFA: OWASP writes that "multi-factor authentication (MFA) is by far the best defense against the majority of password-related attacks"
  • Session invalidation, concurrent-session policy, device management, audit logs, and mass logout on breach

9-2. Delegating does not remove cost — it changes its type

  • A provider outage becomes a login outage. That coupling has to appear in your SLOs
  • Account migration and provider replacement get harder. Bind your user identifier directly to the provider's sub and identity breaks at migration time
  • Per-user pricing, data residency, and audit requirements arrive as new constraints

9-3. The axes that actually decide it

  • Blast radius of a breach (is there payment, medical, or minors' data?)
  • Whether regulation mandates a particular approach
  • Whether the team has someone to maintain this area continuously — maintenance, not construction, is the criterion
  • The breadth of authentication methods required (enterprise SSO, SAML, passkeys, social)

Whichever you choose, write down the reason and the assumptions. What someone revisiting this decision in two years needs is not the conclusion but the assumptions that held at the time. That is the job of the design doc covered in part 1 of this series.


Quiz: check your understanding

Quiz 1: In a mobile app review you find code using response_type=token on the authorization request and receiving the access token in the redirect fragment. What is wrong, and what should replace it?

Answer: It is the implicit grant. RFC 9700 §2.1.2 marks it SHOULD NOT, and it should be replaced by the authorization code grant plus PKCE. A mobile app is a public client, so PKCE is a MUST.

Explanation: The problem is not that it is "old-fashioned". The access token travels through the authorization response and can leak via browser history, referrers, or logs, and there is no standard sender-constraining method to bind a token issued in the authorization response to a specific client. When you migrate, do not stop at adding PKCE on the client. Check that the authorization server blocks PKCE downgrade — that it rejects a token request carrying a code_verifier when the authorization request had no code_challenge.

Quiz 2: During an incident a colleague says "I pasted the token into a decoder, sub is right and exp has not passed, so this request is fine." What is wrong?

Answer: Decoding is not verifying. Without checking the signature, you cannot know whether your server issued that token.

Explanation: A JWT payload is base64url encoding, so anyone can construct one with whatever values they like. Most decoders, this blog's included, state that they do not verify signatures. What the judgement requires is not a plausible-looking payload but a passing signature check plus iss and aud matching your expectations. As an aside, pasting a production token into a decoder is itself exposing a credential to an external tool.

Quiz 3: The server issues JWTs with RS256. The verification code reads the token's alg header and verifies with that algorithm. What attack becomes possible?

Answer: Algorithm confusion. An attacker sets alg to HS256 and signs with the server's public key as the HMAC secret; the verification code then computes an HMAC with the same public key and declares a match.

Explanation: The public key is, by definition, public, so anyone can forge a valid token. RFC 8725 §3.1 requires that libraries let the caller specify a supported set of algorithms (MUST) and use no others (MUST NOT), and that each key be used with exactly one algorithm (MUST). The principle is single: the verification algorithm is chosen by the application, not by the token. For the same reason, check whether your configuration would accept alg: none.

Quiz 4: A team moved tokens from localStorage to HttpOnly cookies. Why is "XSS is solved now" wrong?

Answer: Two reasons. First, HttpOnly only prevents reading the token value; it does not stop XSS. The attacker can simply send authenticated requests from that browser. Second, moving to cookies introduced a new CSRF surface.

Explanation: Changing where you store it changes the type of risk, not its existence. Cookies make theft harder while opening a request-forgery path, because the browser attaches them automatically. OWASP records that when XSS and CSRF combine, the session cookie is included in the request. The move has to be accompanied by SameSite configuration, CSRF defences, and above all XSS mitigations such as output encoding and CSP.

Quiz 5: The API gateway verifies the JWT signature and expiry and forwards only passing requests to the order service. Putting another user's order ID into the order lookup API returns their data. What is missing?

Answer: Object-level authorization. The gateway confirmed only that the caller is an authenticated user; nobody confirmed that this user owns this particular order.

Explanation: This is the classic failure when authentication and authorization are assumed to be finished at the same point. The gateway's decision is for early rejection, not a basis for trust. Ownership must be decided as close to the data as possible — in the layer that actually loads the order. Because it is especially common to filter list queries by owner while forgetting the single-item path, audit the single-item path specifically.

Quiz 6: The frontend sends the ID Token it received after OIDC login as the Bearer token on API calls. Why is this a problem?

Answer: The ID Token exists for the client to confirm authentication; it is not a token to present to a resource server. Its aud points at the client's client_id rather than the API, so from the resource server's view the audience is wrong.

Explanation: Resource access should use the access token. If the resource server accepts this, it also means that server is not checking aud, which generalises to the broader problem of accepting tokens minted for other clients. Watch for the mirror-image mistake too: parsing an access token to extract user information is also a contract violation, because an access token is under no obligation to be a JWT.


Closing

Gathering the sentences this guide repeated:

  • OAuth 2.0 is an authorization framework; authentication is what OIDC supplies via the ID Token
  • The implicit grant is SHOULD NOT and ROPC is MUST NOT; the replacement is authorization code plus PKCE
  • Decoding is not verifying, and the algorithm a token declares is not evidence
  • Choosing a storage location changes the type of risk, not its existence. With XSS, neither option is safe
  • The authorization decision must happen at least once as close to the data as possible

And most importantly: do not write, or accept, security advice without its assumptions. "Do this and you are safe" almost always has "under these conditions" silently removed from the front. When the removed condition does not hold in your environment, what the sentence leaves behind is only the feeling of safety. The habit of writing the conditions down is the cheapest and most effective defence in this area.


References

  • RFC 6749 — The OAuth 2.0 Authorization Framework — the four roles (§1.1), the four grant types (§1.3), confidential vs public clients (§2.1), and the fact that the document defines itself as an authorization framework. Checked 2026-08-15.
  • RFC 9700 — Best Current Practice for OAuth 2.0 Security — implicit grant SHOULD NOT (§2.1.2), ROPC MUST NOT (§2.4), PKCE requirements and downgrade prevention (§2.1.1), exact redirect URI matching (§4.1.3), refresh token rotation and sender-constraining (§2.2.2). January 2025, BCP 240. Checked 2026-08-15.
  • RFC 7636 — Proof Key for Code Exchange by OAuth Public Clients — the authorization code interception attack, code_verifier length (43–128 characters) and generation guidance, S256 versus plain. Checked 2026-08-15.
  • RFC 7519 — JSON Web Token — the JWS/JWE distinction, unsecured JWTs and the condition on alg: none (§6), and the fact that every registered claim is OPTIONAL. Checked 2026-08-15.
  • RFC 8725 — JSON Web Token Best Current Practices — algorithm confusion, the requirement to specify a supported algorithm set, the requirement that the alg header match the actual operation, and one algorithm per key (§3.1). BCP 225. Checked 2026-08-15.
  • OpenID Connect Core 1.0 — the "simple identity layer on top of the OAuth 2.0 protocol" sentence from the abstract, the ID Token definition (§2) and required claims, and the UserInfo endpoint (§5.3). Checked 2026-08-15.
  • RFC 9110 — HTTP Semantics — the definitions of 401 and 403 and the authentication/authorization split (§15.5.2, §15.5.4). Checked 2026-08-15.
  • OWASP Authentication Cheat Sheet — generic failure responses including matching HTTP status codes, account-based failure counters and exponential lockout, keeping recovery reachable during lockout, and the MFA recommendation. Checked 2026-08-15.
  • OWASP Session Management Cheat Sheet — the web storage prohibition and its reasoning, what each cookie attribute defends against and the XSS/CSRF combination, 64-bit entropy, idle and absolute timeouts, and session regeneration after a privilege change. Checked 2026-08-15.
  • The placement principle for authorization decision points (gateway, service, data layer) and the axes for the build-versus-delegate decision are not items lifted from the documents above; they are organised in this article from those requirements.

Further reading

Complete guide series

현재 단락 (1/169)

This blog already has [OAuth 2.0 Mastery](/blog/architecture/2026-03-03-oauth-deep-dive), [JWT or Se...

작성 글자: 0원문 글자: 26,066작성 단락: 0/169