The most convincing broken login I have run into looked perfect from the outside. Click the button, bounce out to the identity provider, come back with an access token, and the user is in. It passes review. It sails through every demo. What it has actually shipped is a door that opens for anyone holding a valid token, because an access token is not proof of who is standing in front of it.

The trap is that the flow feels exactly like authentication. A login screen appeared and a token came back, so surely a real person signed in. That token, though, answers a narrower question than a login is supposed to ask: it says some app is allowed to reach an API on a user's behalf. Whether a human is on the other end at all, and which one, is nowhere in it. Treat it as proof of identity anyway and you have built pseudo-authentication, the seam that nearly every OAuth-login-gone-wrong story I have seen eventually tears along.

That gap is the whole reason OpenID Connect exists. The access token OAuth 2.1 issues lets a client authorize a call, and stops there. OIDC rides the same login flow and adds the piece those broken logins never had, a signed ID token that actually attests to who authenticated and when. Learn to keep those two tokens straight and the "OAuth 2.1 or OIDC" question mostly answers itself, which is most of what the rest of this covers.

None of that is hypothetical. In 2023, Salt Labs showed that Grammarly, Vidio, and Bukalapak had each wired up social login to accept an OAuth token without checking it had been issued for their own app, which is the whole ballgame: hand such a site a token minted somewhere else and you are logged in as someone else. Vidio alone reports around 100 million monthly users. The pattern shows up often enough that OWASP ranks Broken Authentication second on its 2023 API Security Top 10 (API2:2023), one rung below Broken Object Level Authorization, an authorization flaw, at the top. Getting there rarely means picking the wrong protocol. It means trusting an access token to carry a fact it never held, and to see why it cannot, you have to look at what each token actually is.

OIDC authorization-code-with-PKCE sequence: browser to app to authorization server, login and consent, code returned, app exchanges the code plus PKCE verifier, authorization server returns an ID token and an access token, app calls the resource server with the access token
Authorization code with PKCE: the user logs in at the authorization server, the app trades a one-time code plus its PKCE verifier for two different tokens, and only the access token goes to the API.

Access token and ID token: the one distinction that matters

Start with the access token, because it is the one you already have. OAuth 2.0 hands it to your app as a credential for reaching a protected resource, an API, on someone's behalf. It is scoped to particular permissions, it expires, and it rides along on every request the app makes to that API. What it deliberately leaves out is the user. The official OAuth site is blunt about the boundary: OAuth 2.0 is not an authentication protocol, and it "says absolutely nothing about the user." That is not a knock on it. OAuth was built as a delegated-authorization framework (RFC 6749), a way for a resource owner to grant an app scoped access without handing over a password, and it does that job well. Reporting who logged in was simply never part of the brief.

The ID token is the piece that reports it. OpenID Connect adds this second token, a signed JWT asserting that a specific user authenticated, who they are, and when it happened. The OIDC Core 1.0 spec calls itself "a simple identity layer on top of the OAuth 2.0 protocol," and the layer really is thin. Add the openid scope to an otherwise ordinary OAuth request, and the authorization server returns the ID token next to the access token, along with a UserInfo endpoint that will hand back claims like name, email, and subject identifier when your app presents the access token. Same flow, same server, one extra scope, and the guess about who is on the other end becomes something you can verify.

It helps to see the two decoded. A real OIDC login hands your client an ID token whose payload reads like this:

{
  "iss": "https://auth.example.com",
  "sub": "248289761001",
  "aud": "s6BhdRkqt3",
  "exp": 1721566860,
  "iat": 1721563260,
  "auth_time": 1721563200,
  "nonce": "n-0S6_WzA2Mj",
  "at_hash": "77QmUPtjPfzWtF2AnpK9RQ",
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}

The access token, when it is a JWT at all (many are opaque strings the client is not meant to read, and only the RFC 9068 profile makes them readable), carries a different set of claims:

{
  "iss": "https://auth.example.com",
  "sub": "248289761001",
  "aud": "https://api.example.com",
  "exp": 1721566860,
  "iat": 1721563260,
  "jti": "b3f1c2a4-9d8e-4f2b-a7c1-2e6d5f0a1b3c",
  "client_id": "s6BhdRkqt3",
  "scope": "openid profile email read:invoices"
}

The aud claim is the whole story. On the ID token it holds your app's own client_id, and the rest of the payload is authentication detail: auth_time records when the user logged in, nonce ties the token back to the exact login request your app started, at_hash binds it to the access token issued alongside it. The access token's aud is the API instead, and it trades all of that for authorization data, scope and client_id, which say what the caller may do rather than who they are. Hand your app the access token as proof of login and you have given it a credential addressed to someone else.

Getting that ID token into your app safely, though, depends on the flow that delivers it, and hardening that flow is most of what OAuth 2.1 set out to do.

OAuth 2.1 mostly deletes footguns

OAuth 2.1 is a consolidation of OAuth 2.0 plus the security best-current-practice guidance that accumulated after it. The IETF OAuth working group is folding both into a single baseline document. It is worth being precise about status: OAuth 2.1 is still an in-progress Internet-Draft (draft-ietf-oauth-v2-1), not a finalized RFC. Treat it as the settled direction of the ecosystem rather than a ratified standard, because the requirements are stable and the major authorization servers already enforce them.

The substantive changes all pull in the same direction, closing the footguns that OAuth 2.0 left loaded:

  • PKCE (Proof Key for Code Exchange) is required for every client using the authorization code flow. In OAuth 2.0, PKCE was effectively for public clients like mobile and single-page apps. OAuth 2.1 applies it to confidential server-side clients too, so the one-time code that comes back through the browser is useless to anyone who intercepts it without the matching verifier.
  • The Implicit grant is removed. It used to return the access token directly in the URL fragment, which meant tokens leaked into browser history, referrer headers, and server logs. The replacement for single-page apps is the authorization code flow with PKCE.
  • The Resource Owner Password Credentials grant is removed. Handing your username and password straight to a third-party app was always the flow that defeated the point of OAuth, so 2.1 drops it outright.
  • Redirect URIs must be compared with exact string matching. No prefix rules, no wildcards. Fuzzy matching is how open-redirect and token-stealing attacks slip through, so the draft requires an exact match against a registered value (with a narrow loopback exception). This is not a theoretical tightening. In 2023, Salt Labs hijacked Booking.com's "Sign in with Facebook" flow by chaining a loosely validated redirect path with an open redirect to capture a victim's authorization code, the exact class of attack that exact matching shuts down. Booking.com fixed it before any known abuse.
  • Bearer tokens in the query string are disallowed. Passing a token as a URL parameter, which RFC 6750 allowed but told clients they SHOULD NOT use, put credentials in exactly the places that get logged and cached.

If you are on OAuth 2.0 today, none of this is a rewrite. It is a migration checklist:

  1. Turn PKCE on for every client, confidential server-side ones included.
  2. Move any single-page app off Implicit onto the authorization code flow with PKCE.
  3. Delete any Resource Owner Password Credentials (ROPC) paths.
  4. Tighten redirect-URI registration to exact-match values.

If OAuth 2.1 still feels like an abstract draft, it is already load-bearing somewhere concrete: it is the authorization baseline the Model Context Protocol now requires for securing an MCP server, mandatory PKCE and audience-bound tokens included.

Hardening the flow is table stakes. The judgment call underneath it is knowing when you actually need the ID token that flow can carry, rather than the access token on its own.

Use OIDC the moment the requirement is about a person

The trigger is simple. When what you need is a person rather than an API call, you need authentication, and OIDC is the standard built for it: signing a user in, carrying that identity across a product suite, proving to your own backend which user is on the request. Those are the same need wearing different clothes.

Mechanically, running the authorization code flow with the openid scope gives your app both tokens at once. The access token stays opaque and continues on to your API, exactly as before. The ID token is the new part, a signed JWT meant for your client to open, verify, and read the identity claims out of. Need more than it carries, like a fuller profile, and you call the UserInfo endpoint with the access token. That signed ID token is what finally lets your app say, with proof rather than inference, that this specific user authenticated just now. Skip the verifying, lean on the access token instead, and you are back to the broken login this piece opened with.

The breach vector: treating an access token as proof of identity

You could almost forgive the assumption if a returned token at least meant a user had been there. It does not. An access token can be completely valid with no human behind it at all: it is opaque to your client, it was minted for the API rather than for you, and a refresh token can quietly mint a fresh one in the background hours after the person closed the tab.

That is what makes it the wrong instrument for proving a login. It cannot answer the questions authentication exists to answer, which are who authenticated, when, and how. An ID token was designed to answer exactly those, which is the whole reason the two are separate tokens. Trust an access token for identity and you are trusting something provable by possession alone and still valid long after its owner has walked away.

Two tokens, two jobs: one OIDC login issues an ID token and an access token; the ID token drives authentication (who the user is) while the access token drives authorization (what a caller may do); using the access token as proof of identity is pseudo-authentication, the breach vector
One login issues both tokens. The ID token exists to prove who logged in. The access token only opens the API, so treating it as identity is the pseudo-authentication that gets apps breached.

The fix is not to abandon OAuth but to layer OIDC on top of it, so the ID token arrives alongside the access token rather than replacing it. Your API carries on validating the access token exactly as before, and now your client also holds a signed ID token it can actually trust to answer who the user is. Keep those two jobs from blurring together and the whole class of pseudo-authentication bugs has nowhere left to take hold.

And the signed ID token only protects you if you actually verify it. In 2020, a researcher forged a valid "Sign in with Apple" identity token for any email address he named, so any app that accepted that signed token as proof of login could be walked straight into an account takeover. Apple paid a $100,000 bounty and found no accounts had been abused before the fix. On every ID token you accept, check the signature, confirm it came from the issuer you expect, confirm it was minted for your app and not another (the audience), and confirm it answers the login request you actually started (the nonce). A token you never verify is a token someone else can mint.

Which do you actually need?

First, what each one actually is:

OAuth 2.0 OAuth 2.1 OpenID Connect
What it is Authorization framework (RFC 6749) OAuth 2.0 plus its security best practices, consolidated Identity layer on top of OAuth 2.0
Primary token Access token Access token, same as 2.0 ID token, plus the access token
Answers What may this app access? The same, with safer defaults Who is the user, and when did they log in?
PKCE Optional Mandatory, every client Follows OAuth, effectively required now
Notable grants Auth code, implicit, ROPC, client credentials Implicit and ROPC removed Auth code with the openid scope
Spec status Final, 2012 IETF draft, not a finalized RFC Final, OIDC Core 1.0, 2014
Use it for Delegated API access New builds wanting hardened OAuth Login, SSO, knowing who the user is

For a specific screen, the call is usually quick. Here is the lookup.

What you are trying to do

  • Let users log in to your app
  • Single sign-on across several apps or a "log in with" button
  • Know who the user is inside your app, with proof
  • Let an app call an API on a user's behalf with scoped access
  • Give a third party delegated, limited access to a user's data
  • Let one backend service call another with no user involved
  • Build a new single-page or mobile app login

What you need

  • OIDC (authentication)
  • OIDC
  • OIDC ID token, then UserInfo for extra claims
  • OAuth 2.1 access token
  • OAuth 2.1
  • OAuth client credentials grant
  • OIDC over authorization code with PKCE (never Implicit)

Most real products land on several of these rows at once, which is why production systems almost always run both, each token carrying the job it was built for.

What this looks like when you ship it

The authentication and authorization service we built for a US nonprofit scientific standards-setting organization runs on Spring Boot and Spring Security, and it is the inverse of the broken login I opened with: the same two tokens, this time kept deliberately apart. It is where these distinctions stopped being theory for us.

Spring Security carries this cleanly, which is why we reached for it. Its OAuth 2.0 support splits along the exact seam this article is about.

On the resource-server side, Spring validates incoming access tokens on the protected APIs, verifying JWT signatures locally rather than phoning home on every request, which is what keeps authorization cheap on the hot path. On the login side, configuring oauth2Login() with the openid scope flips Spring Security over to its OIDC components, the OidcUserService among them, which consume the ID token to establish who the user is. The two halves share a framework but never share a token.

We ran auth as its own token-based microservice so it could scale horizontally behind a load balancer and act as the chokepoint every other service crosses. That is the same reason auth is often the first thing we extract when we modernize a monolith.

I am leaving out metrics on purpose. What matters is that every token in that system means exactly one thing and is checked for exactly that, so the OWASP Broken Authentication failure modes never find the ambiguity they feed on.

Questions

Frequently asked questions

Is OIDC the same as OAuth?

No. OAuth 2.0 is an authorization framework that decides what an app may access; OpenID Connect is an authentication layer on top of it that establishes who the user is. OIDC reuses the OAuth authorization code flow and adds an ID token, a UserInfo endpoint, and the openid scope. Every OIDC flow is an OAuth flow, but plain OAuth has no notion of login.

Can you use OAuth without OIDC?

Yes. OAuth 2.0 works on its own for pure authorization, like granting an app scoped access to an API without ever identifying the user to that app. You only reach for OIDC when the app has to log the user in and know who they are. Using bare OAuth for login is the common mistake: it returns an access token meant for an API, not proof that anyone authenticated.

Is OAuth 2.1 an official standard yet?

Not yet. OAuth 2.1 is an active IETF Internet-Draft (draft-ietf-oauth-v2-1) that folds OAuth 2.0 and its widely adopted security extensions into one document, and it has not shipped as a final RFC. Major identity providers already implement its profile because the changes are mostly non-controversial hardening. Until it lands as an RFC, OAuth 2.0 (RFC 6749) plus the Security Best Current Practice stays the formally published baseline.

What is the difference between an ID token and an access token?

An ID token is defined by OpenID Connect and is proof of authentication for the client: identity claims like subject, issuer, audience, and name, always as a signed JWT. An access token is defined by OAuth and is a credential for calling an API, describing scopes rather than identity, and often opaque to the client. They are not interchangeable, the ID token goes to your app and the access token goes to the API.

Does OpenID Connect use OAuth 2.1?

OIDC Core 1.0 is defined on top of OAuth 2.0, not OAuth 2.1, so the base spec predates the 2.1 draft. In practice they align: OAuth 2.1's mandates, like PKCE on the authorization code flow and exact redirect-URI matching, are the hardening most OIDC deployments already apply. You can run OIDC against an OAuth 2.1-conformant server, but OIDC does not formally require it.

Should you use OAuth or OIDC for login?

Use OIDC for login. OAuth alone was never designed to authenticate users, it authorizes access to resources, and treating its access token as a login signal is a known anti-pattern. OIDC adds the ID token as explicit, verifiable proof that the user authenticated, plus the UserInfo endpoint for profile data. Keep plain OAuth for the case where you only need delegated access to an API.

Is PKCE required in OAuth 2.1?

Yes. OAuth 2.1 requires PKCE for every client on the authorization code flow, including confidential ones, where OAuth 2.0 left it optional and aimed mainly at public clients. It sits alongside the other tightenings: implicit and password grants removed, exact redirect-URI matching, no bearer tokens in URLs. The goal is to close code-interception and downgrade attacks by default.

The takeaway

Stop asking "OAuth 2.1 or OIDC." Ask what each token in your system is meant to prove, and let that decide. A token that has to name the user is an ID token, so that is OIDC. If it only has to open an API, it is an access token, and that is OAuth 2.1 with PKCE on, Implicit off, and redirect URIs matched exactly. Settle what your tokens mean and the OAuth-versus-OIDC decision stops being one.

If you are untangling authentication and authorization in an existing codebase, or standing up token-based auth that has to hold up under real load, that is the kind of backend and API work we do. The protocols are the easy part. Getting authn and authz to mean the right things in your architecture is where the breaches actually come from.