You've built "Login with Google" more times than you can count. You've drawn the OAuth2 authorization code flow on a whiteboard in at least three interviews, complete with the redirect arrows and the little padlock icon for the token. You know what a scope is. You've read the RFC, or at least skimmed it once during a slow sprint. By every reasonable measure, you understand OAuth2.
Then you ship it to production, and six months later a pentest report lands in your inbox with findings that make you want to disappear into your keyboard.
That gap has nothing to do with OAuth2 being complicated. The spec is almost insultingly simple once you see the diagram: redirect, consent, code, token exchange, done. The gap exists because OAuth2 is a framework, not a recipe. It leaves dozens of decisions up to whoever implements it, and most teams make those decisions once, under deadline pressure, and never revisit them. The result is production systems that pass every code review and still leak tokens, still confuse who a user is with what a user is allowed to do, and still trust scopes nobody has audited in two years.
Here is where that actually happens, and why "we used a standard library" is not the alibi most engineers think it is.
OAuth2 was never built to answer "who is this user"
This is the confusion that causes the most downstream damage, because it feels harmless at first. OAuth2 is an authorization protocol. A successful token exchange proves that an application has permission to call an API on someone's behalf. It does not prove who that person is. Somewhere along the way, a huge number of teams started treating "the user completed the OAuth2 flow" as equivalent to "the user is authenticated," and built session logic on top of that assumption.
It works fine until it doesn't. The access token you get back is opaque or, if it's a JWT, it might happen to carry a `sub` claim, and someone on the team decides that's good enough to identify the user. Nobody validates the token's audience. Nobody checks that it was actually issued for your application and not some other client sharing the same authorization server. OpenID Connect exists as a layer on top of OAuth2 specifically to close this gap, with a proper ID token and identity claims you can actually trust. Teams that skip it and improvise their own "identity from the access token" logic are the ones that end up with account takeover bugs that never show up in a demo, only in an incident review.
Where access tokens actually go to die
There's also a version of this mistake that looks more sophisticated and isn't. Teams that store tokens correctly on the client side but then forward them, unmodified, to every downstream microservice as a de facto internal auth mechanism. One compromised service now holds a token scoped for the entire user session, not just its own slice of work. Token storage is not just a client-side problem. It's an architectural one, and it doesn't stop mattering once the token leaves the browser or the app.
None of this requires an exotic fix. Short-lived access tokens, httpOnly cookies where the architecture allows it, platform-native secure storage on mobile, and log scrubbing that strips auth headers before anything gets persisted. The fix is boring. That's exactly why it gets skipped: it doesn't show up in a demo, and it's not the part of OAuth2 anyone puts on a slide.
Refresh token rotation is the part nobody finishes building
Everyone implements the access token exchange, because you can't ship without it. Almost nobody finishes refresh token rotation properly, because you can ship without it and the shortcuts don't show up until something goes wrong. Rotation means that every time a refresh token gets used, the old one is invalidated and a new one is issued. If that invalidated token ever shows up again, that's a signal someone else has a copy, and the entire token family should be killed immediately.
Most implementations skip this. A refresh token lives for thirty or ninety days, gets reused without any tracking, and if it leaks, the exfiltration is invisible until someone notices unusual activity weeks later, if they notice at all. Even teams that do implement rotation often skip reuse detection, which defeats the entire point of rotating in the first place. And revocation, the part where logging out, changing a password, or an admin flagging a compromised account actually kills the relevant tokens server-side, gets treated as a nice-to-have right up until an incident makes it urgent. By then it's a hotfix under pressure instead of a design decision made calmly in advance.
The scope you added "temporarily" two years ago is still there
Scopes exist to limit blast radius. In practice, teams request broad scopes early because the integration works, they tell themselves they'll narrow it down after launch, and after launch never comes. Two years later, a third-party integration still holds read-write access to a user's entire calendar because scoping it down to "read events on this one calendar" would have meant extra work during a sprint that already had too much in it.
Then that integration's OAuth2 client credentials leak, or the third party gets breached, and the blast radius is the entire calendar instead of the narrow slice of data the integration actually needed. Least privilege is not a compliance checkbox you tick to get through a security review. It is the single factor that determines whether a breach is a contained incident or a full-blown disaster, and it costs nothing to get right except the discipline to say no to the wider scope when the narrower one would do.
A standard OAuth2 library will not save you from yourself
This is the myth that does the most damage, because it sounds completely reasonable. "We used Auth0, Okta, Passport, whatever, so we're covered." A good library handles the protocol mechanics correctly. It builds the right redirect URLs, validates signatures, exchanges codes for tokens according to spec, and saves you from a category of mistakes that used to be common when everyone rolled their own OAuth2 client by hand.
What it does not do is choose your scopes for you. It does not decide where the token it hands back gets stored. It does not set your session length, your rotation policy, or how strict your redirect URI validation needs to be to block an open redirect. Every actual security decision in an OAuth2 implementation lives in the configuration and the application code wrapped around the library, which is precisely the part the library has no visibility into. Using a well-maintained OAuth2 library rules out an entire category of mistakes. It does not rule out the ones your team is currently making, and treating it as a security guarantee is how teams stop looking for the mistakes that are actually theirs to fix.
OAuth2 isn't the vulnerability, neglect is
None of this means OAuth2 is a bad spec, and it definitely doesn't mean you should roll something homegrown instead. That road is worse, every time. What it means is that the hard part of OAuth2 was never the flow diagram you drew in an interview. The hard part is the unglamorous maintenance work almost nobody wants to own: reviewing which scopes are actually still needed, rotating tokens the way the spec actually intends instead of the way that was fastest to ship, deciding where tokens live before a security review forces the question.
OAuth2 in production is not a protocol you implement once and move on from. It's a set of decisions you have to keep making correctly for as long as the system stays alive. Most teams stop making them right after launch, when the diagram is still fresh and the demo still works. That's not a flaw in the spec. That's the actual vulnerability, and it's sitting in your codebase right now, whether or not anyone has looked for it yet.



