OAuth 2.0 Flow Attacks Against SaaS Authorization Servers
Attackers exploit OAuth's intended features to bypass MFA and steal account access.

OAuth 2.0 does not fail because it is broken. It fails because the features that make it useful, redirect delegation, device code flows, consent screens, and integration tokens, are the same features attackers turn against it. If you configure a system exactly as the specification intends, it can still hand an attacker everything needed for a full account takeover. That is the paradox running through every incident covered here: the attack surface and the value proposition are the same surface.
OAuth exists so one application can act on a user's behalf without that user ever handing over a password. That single design choice is why SaaS-to-SaaS ecosystems work at the scale they do today. A CRM can talk to a marketing tool, which can talk to a data enrichment service, which can talk to yet another vendor's app, and no human ever retypes a credential. Each handoff in that chain, resource owner to client to authorization server to resource server, is a trust boundary. Every one of those boundaries is also a place where a token, a code, or a redirect can be intercepted, because the whole mechanism runs through browsers and public APIs by design. A wide attack surface isn't a side effect of sloppy implementation: it's the cost of the thing OAuth was built to do.
A valid OAuth token can be as operationally useful to an attacker as a stolen password, and often more useful, because the token already carries the right scopes, the right application identity, and enough remaining lifetime to call APIs at machine speed. No password cracking, no exploit against a buffer or a memory bug. If an attacker can redirect a code, suppress a parameter, or manipulate a consent screen, they get the same outcome a credential thief gets, faster and with less noise.
Microsoft's research documents campaigns where threat actors abused trusted OAuth relationships connected to Salesforce environments for unauthorized access, data exfiltration, and long-term persistence. None of it depended on a flaw in Salesforce itself. The attackers exploited the trust OAuth grants between connected applications, which is exactly the relationship the protocol was designed to make frictionless.
How OAuth attacks bypass MFA
OAuth attacks are dangerous because they travel through legitimate authentication infrastructure, which makes them invisible to the controls most security teams depend on. MFA checks identity at the moment of sign-in. OAuth tokens get issued after that sign-in has already completed, which puts the two controls on different sides of the same door.
In a typical consent phishing attack, the victim completes MFA on a real login page, using a real account, with no password stolen and no MFA prompt tampered with. MFA answers one question: is this the user authenticating. OAuth consent asks a completely different question: should this application be allowed to act with these permissions. Those are two separate security decisions, made at two separate layers, and a system can get the first one right every single time while getting the second one wrong.
Once an attacker holds a valid token, every API call it makes can pass for normal integration traffic, because the token itself says the attacker is a legitimate application doing legitimate work. The same pattern that lets a marketing tool quietly sync contacts with a CRM all day without raising a flag is the pattern that lets a malicious OAuth app sit inside a tenant for months, querying records and exfiltrating data, with nothing in the logs that looks abnormal to a system built to watch for failed logins and suspicious IP addresses.
Microsoft observed this directly: intrusion paths that inherited user and application privileges let attackers enumerate and query CRM records while evading the authentication-layer detections most organizations rely on as their primary defense. That's the core problem with defending against OAuth abuse using MFA, conditional access, and login monitoring alone. Those controls watch the front door. OAuth attacks walk in through a door that was left open on purpose, because leaving it open is how the integration was supposed to work. The sections that follow break down the specific mechanisms behind that: how redirect URIs get hijacked, how missing state parameters turn into account takeover, how PKCE gets quietly downgraded, how JWTs get forged, and how refresh tokens turn a single breach into a persistent foothold.
Redirect URI manipulation and loose matching
Redirect URI manipulation is the highest-impact bug class in active OAuth exploitation today, a direct consequence of the redirect-delegation mechanism that makes OAuth work. After a user authenticates, the authorization server sends the authorization code back to the application by redirecting the browser to a registered URI. Any looseness in how that URI gets checked turns a routine redirect into an interception point.
The fix sounds simple: match the registered URI exactly, string for string, against an allowlist at the client level. Wildcards and pattern-based matching are what open the door, because a server that accepts a pattern instead of an exact string is trusting anything that fits the shape of a valid URI, not the URI itself.
Two bypass techniques show how that trust gets abused in practice. Path traversal takes advantage of validators that only check the suffix of a URI, letting an attacker append extra path segments that still pass the check while pointing somewhere the developer never intended. Open redirect chaining is more direct: if the registered redirect URI happens to be a page that itself contains an open-redirect parameter, an attacker can chain the two together, sending the authorization code first to the legitimate registered URI and then bouncing it straight to a server the attacker controls. A related failure comes from subdomain confusion, where a URI was registered against a subdomain that has since been abandoned and taken over by an attacker, making a URI that looks trustworthy on paper something the original owner no longer controls.
Any of these paths ends the same way. Once the attacker has the authorization code, they exchange it for an access token, and that's a full account takeover with no credentials stolen at any point in the process.
A related leak path runs through the older implicit flow, where access tokens get placed directly in the URL fragment instead of exchanged through a back-channel request. Tokens placed that way can leak through referrer headers and server logs. The implicit flow has been deprecated specifically to close this gap, but plenty of servers still allow it for backward compatibility, and an attacker can sometimes force that weaker flow simply by changing the response_type parameter in the authorization request, if the server hasn't disabled the option.
State parameter omission and login CSRF
The state parameter is OAuth's answer to cross-site request forgery, and it protects a different part of the flow than redirect URI validation does. Where redirect URI matching controls where a code can be sent, the state parameter controls whose authorization request a given code is allowed to complete. Omit it, or fail to validate it on the way back, and an attacker can force a victim's client to accept a code bound to the attacker's own identity, not the victim's.
The sequence runs in three steps. The attacker starts an OAuth flow against the target application and obtains an authorization URL carrying a code tied to the attacker's own account. The attacker sends that crafted link to the victim, often through something as ordinary as an email or a chat message, and when the victim clicks it, their browser completes the OAuth callback using the victim's own active session. The client application exchanges the code and logs the victim into the attacker's account. Anything the victim does from that point on, inside that session, happens inside an account the attacker controls and can review later.
Preventing this takes one concrete step: generate a cryptographically random state value for every authorization request, bind it to the user's session, and check that it matches on callback. A predictable or static state value offers no more protection than skipping the parameter, because an attacker who can guess or reuse it defeats the control just as completely as one who faces no check.
Redirect URI manipulation and state omission both target the same moment in the flow: how the authorization code gets delivered and accepted. PKCE closes a third angle on that same problem.
PKCE downgrade attacks and the enforcement gap
PKCE was built to stop authorization code interception, but it only works when the authorization server actually enforces it. If a server supports PKCE but still lets a flow complete without a code verifier, an attacker gets a clean downgrade path that strips the protection away entirely, even on a system that technically implements the standard.
When PKCE runs correctly, the client generates a random code_verifier, hashes it into a code_challenge, and sends that challenge with the initial authorization request. At token exchange, the client must present the original verifier, and a code intercepted anywhere along the way is worthless without it. RFC 7636 sets a minimum length for that verifier specifically because a short one can be brute-forced.
The downgrade attack is simple to describe and hard to catch without deliberate testing: an attacker strips the PKCE parameters out of the authorization request, and if the server falls back to completing the code exchange without checking for a verifier at all, PKCE has contributed nothing to that session's security. CVE-2026-41213, affecting @node-oauth/oauth2-server, shows the same failure from a different angle: the server accepted code_verifier values shorter than the RFC 7636 minimum, making those verifiers brute-forceable even when PKCE was technically in use.
Neither of these is a cryptographic failure. The hashing works fine, and the math behind PKCE is sound. Both are enforcement logic failures, and that's the pattern running through this entire class of bugs: the gap between a server that supports a protection and a server that actually requires it on every request, with no fallback that quietly removes it.
JWT algorithm confusion and the token forgery it enables
The attacks covered so far target the authorization flow itself, how a code or token gets issued. JWT algorithm confusion targets something downstream: how a token gets checked once it's already in hand. When an authorization server issues JWT access tokens without strictly pinning the signing algorithm it expects, attackers can forge tokens that sail through validation, carrying whatever tenant ID, user ID, and role claims they choose to write into them.
The clearest version of this is the RS256-to-HS256 confusion attack. RS256 is an asymmetric algorithm: a private key signs the token, and a public key verifies it, and the public key is meant to be public. HS256 is symmetric: the same secret both signs and verifies. Some implementations use the RS256 public key as the HS256 secret. An attacker who has access to that public key, which anyone can get, can sign a forged token using HS256, and the server accepts it as valid because it checks the signature with the same key the attacker just used to create it.
This stopped being a theoretical concern in at least one documented production case: a multi-tenant SaaS platform using JWTs for API authentication across multiple microservices accepted tokens where the algorithm field had been changed from RS256 to HS256 and the token signed with the platform's own publicly available RSA public key. That gap let any external user forge a token carrying any tenant ID, any user ID, and any role, up to and including admin access to another tenant's data, without ever touching the platform's real signing key.
This sits inside a broader family of validation-layer failures. The alg:none attack relies on older libraries that still accept unsigned tokens. Weak HMAC secrets let an attacker brute-force the signing key directly. JWKS injection manipulates the key set a server fetches to verify signatures. All three share the same root cause as the RS256/HS256 case: the failure happens at the validation step, not in the cryptography itself.
That risk compounds when access tokens stay valid for hours at a stretch. A stolen JWT with a multi-hour expiration gives an attacker a standing window of access, and there's usually no way to shut that window early, because a JWT remains valid until it expires unless the API has built an explicit revocation mechanism, which most have not. That absence of revocation is the thread connecting token-level failures to the next layer of risk: what happens when the credential that renews those tokens is just as long-lived.
Refresh token lifecycle failures and the persistence they grant attackers
If refresh tokens aren't rotated on use, an attacker holding a stolen one gets access with no real expiration, access that survives a password reset, an MFA change, or a session termination the victim organization thinks closed the door. The correct model rotates on every single use: each time a refresh token gets redeemed, the server invalidates it and issues a new one in its place. If an already-rotated token ever gets presented again, that's a signal the token family has been compromised, and the server should revoke the whole chain. If that rotation step is skipped, a stolen refresh token functions as a permanent credential for as long as nobody explicitly revokes the grant.
A second failure sits next to rotation: scope escalation during refresh. Some authorization servers honor a broader scope request made during a refresh-token exchange, so an attacker who presents a stolen refresh token can walk away with a higher-privilege access token than the original grant ever authorized. That's a logic failure in how the server validates refresh requests, not any defect in the token format itself.
Refresh tokens persist even after password resets, so attackers can hold durable access for months, and you can see this pattern directly in the SaaS supply chain breaches covered next, where stolen tokens kept working long after the organizations involved believed the original compromise had been contained.
The Salesloft, Gainsight, and Klue breaches and the SaaS supply chain risk
The Salesloft Drift, Gainsight, and Klue incidents share one structure across three different company names: attackers compromised a trusted SaaS integration vendor, obtained the OAuth tokens that vendor held on behalf of its customers, and used those tokens to reach into customer environments without ever needing a customer password.
Klue's own disclosure traces its breach to a legacy GitHub personal access token that had never been revoked, which let attackers inject code that harvested further tokens. Gainsight's case shows how one breach becomes the launchpad for another: Gainsight had itself been affected by the earlier Salesloft Drift breach, and the attackers behind the Gainsight compromise reportedly used secrets stolen in that first campaign to get at Gainsight's internal credentials and OAuth refresh tokens.
That chain shows every mechanism covered in this piece, each one appearing at a different point in the breach. A credential that should have been retired kept working because nobody rotated it out. A refresh token taken in one breach opened the door to a second, unrelated organization. None of it required breaking OAuth's cryptography or finding a flaw in the protocol's design. It required only what the protocol was built to allow: one trusted application acting on another's behalf, carried out by someone who was never supposed to hold the token.
Sources
- Defending SaaS-based applications against ShinyHunters OAuth abuse
- Consent Phishing: Bypassing MFA with OAuth
- OAuth 2.0 Redirect URI Validation Falls Short, Literally
- Mitigating CSRF attacks on OAuth 2.0 and OpenID Connect
- A Comprehensive Formal Security Analysis of OAuth 2.0
- OAuth 2.0 Protocol Cheatsheet
- Analysing the Security of Google's implementation of OpenID Connect


