Critical OAuth 2.0 redirect and token handling flaws (SSO account takeover)
At a glance
- Vulnerability class
- OAuth 2.0 and OpenID Connect redirect and token handling flaws, including authorisation code injection and identity token audience confusion
- Severity
- Critical for the redirect flaw chained to authorisation code injection, which is reachable unauthenticated and affects every federated account on that client. High for the account-linking variants, which need either an authenticated victim session or a provider willing to issue an identity for an unverified address
- Also searched as
- Account takeover via OAuth, redirect_uri bypass, SSO login bypass, Sign in with Google account takeover, OAuth login CSRF, authorisation code interception, id_token audience confusion, account pre-hijacking
- OWASP
- A07:2021 Identification and Authentication Failures for the authentication bypass, A01:2021 Broken Access Control for the open redirect component, API2:2023 Broken Authentication for the token exchange
- CWE
- CWE-287 improper authentication as the parent, CWE-294 authentication bypass by capture-replay for code and authorisation-response replay, CWE-347 improper verification of cryptographic signature for identity tokens, CWE-601 open redirect for the forwarding component, CWE-290 authentication bypass by spoofing for the email-claim merge, CWE-352 cross-site request forgery for account linking
- Normative baseline
- RFC 6749 and RFC 6819, hardened by RFC 9700 Best Current Practice for OAuth 2.0 Security and by the OAuth 2.1 draft, with RFC 7636 for PKCE, RFC 9207 for issuer identification and RFC 8252 for native applications
- Reachable from
- The unauthenticated authorisation flow, from any browser, with one link the victim clicks, for the redirect and token handling paths. An authenticated victim session plus an attacker-held federated identity for the account-linking paths
- Who is affected
- Every user and every staff member whose account is linked to a federated login, and also local accounts that carry no link yet and addresses that have not registered at all, which are the targets of account pre-hijacking
What it is
An OAuth account takeover, also searched as an SSO login bypass, is a login hijack. The application accepts an authorisation code or an identity token that reached the wrong party, so an attacker holds the victim's session without ever knowing a password. The flaw sits on one of two sides. On the provider side, the authorisation server compares the submitted redirect_uri against the registered one too loosely and sends the code to an address an attacker can reach. On the application side, the code or identity token that arrives is accepted without the checks that bind it to the browser which started the flow and to this client, so a response captured or minted elsewhere can be redeemed or injected. The base OAuth 2.0 and OpenID Connect specifications left both decisions to implementers. The OAuth 2.0 security best current practice now removes that latitude, requiring exact redirect matching and PKCE and advising against the implicit grant, which is why upgrading the authorisation server does not remove the class while a client still runs the old pattern, and why a login rewrite can reintroduce it.
The commercial problem is the shape of the exposure. Where the defect is in redirect handling or token validation on the login route, it applies to every account linked to that provider on that client rather than to one victim at a time, and the account-linking variants behave differently because each one needs a specific victim. There is no credential to guess, so password policy and lockout contribute nothing, and any multi-factor step has already been satisfied at the provider by the time the code moves. The session that results is a genuine federated authentication, so unless token issuance at the provider is correlated with session creation in the application, nothing in the application's own records separates it from a legitimate login and the customer report arrives before any alert does. For an operator holding spendable balances and identity documents, as travel and hospitality platforms do, that combination can turn a parsing mistake in a callback comparison into both a fraud loss and a notifiable personal data breach. Testing for it belongs to web application penetration testing and API penetration testing together, because the flow spans a browser redirect and a back-channel token exchange.
How the attack unfolds
Mechanism, not a recipe. Reproduction detail stays in client reports, because a page that hands a reader a working attack is a liability.
Loose redirect_uri validation is where an OAuth takeover starts
Testing starts by capturing a normal authorisation request and reading the parameters the application sends, which include the submitted redirect_uri, the client identifier, the scopes requested and whether state, nonce and a code challenge are present. The registered set of callbacks is server-side configuration at the authorisation server and never appears in the request, so it is inferred by probing how the server responds to variations. The submitted value is resubmitted with appended path segments, a sibling subdomain, an extra or duplicated parameter, a fragment, a changed scheme and a changed port.
0 security best current practice hardens that to exact matching, so any variation that still returns a code marks the validation as loose.
A wildcard or a forwarding page sends the authorisation code to the attacker
Loose comparison gives several ways to land the callback somewhere reachable. A wildcard or subdomain registration can be satisfied by a dangling DNS record, a user-content host or a subdomain carrying its own cross-site scripting flaw. A prefix comparison can be satisfied by an appended path.
Where the server tolerates added query components, a registered page that forwards an attacker-supplied destination onward and carries the query string with it delivers the response off-origin even though the host matched. All three are symptoms of the same loose comparison, because the attacker's destination has to ride in the submitted value to get there. Three routes survive comparison that is genuinely exact.
The registered callback may itself forward unconditionally to a destination read from a cookie, a stored session value or a path suffix rather than from the submitted parameter. The response may leak out of the registered page through the Referer header or browser history. Or script an attacker controls may already run on the registered origin, in which case the code is read where it was meant to land.
One click from the victim delivers a valid authorisation code off-site
The attacker needs only that the victim begins the flow, which one link achieves because the authorisation request is legitimate and the consent screen names the real application. Completion with no interaction beyond the click needs two conditions together, a live session at the provider and consent already granted, which also covers a first-party client the provider approves automatically and a request that suppresses the prompt. Where consent has not been granted, the victim sees the provider's own screen for the real application and the attack costs one approval click instead.
Any multi-factor step runs at the provider before the redirect, so it has already been cleared by the time the response moves.
Without PKCE, a captured code becomes somebody else's session
0 security best current practice. It has two outcomes and they are not equivalent. A public client holds no secret, so an attacker who has the code can present it at the token endpoint directly, using the client identifier published in the application's own front-end.
That yields provider tokens for the victim's identity, bounded by the scopes in the original request, which is often no more than profile and email. The takeover comes from the second outcome. The attacker starts their own flow in their own browser, substitutes the captured response at the application's own callback, and the application exchanges it and mints a session for the victim's identity.
State does not stop this. State is the cross-site request forgery control and it defends against an unsolicited response forced into a victim's browser, whereas the attacker here drives their own session and supplies their own perfectly valid state. What closes the route is binding the code to the party that started the flow.
With PKCE the authorisation server checks the code against the verifier held by whoever redeems it, and where the relying party keeps that verifier server-side against the initiating browser session, the attacker's session holds no matching verifier and the exchange fails at the authorisation server. In OpenID Connect the nonce carried inside the identity token and checked against the initiating record does the same work for that half of the response.
Audience confusion takes over an account with no authorisation code at all
This route carries a precondition the code flow does not, that the application exposes a sign-in endpoint which accepts an identity token supplied by the client. That is normal for mobile applications and single-page-application backends and absent from the server-side code flow mapped in the earlier steps, so it is tested only where such an endpoint exists. An attacker registers a client of their own with the same provider and has the victim sign in to it, which produces an identity token for the victim's identity issued to the attacker's client.
The signature is genuine and the issuer is correct, because the provider really did mint it, and only the audience is wrong. Where the application reads the subject or email claim without comparing the audience with its own client identifier, the token is accepted and a session for the victim is created. The same endpoint is where key resolution matters, because an implementation that fetches verification keys from a location named inside the token lets the attacker supply both the token and the key that validates it.
A code can also leak out of the URL, or be aimed at the wrong provider
Two further members of this class need naming because their controls differ. The first is leakage from the URL itself. An authorisation response sits in the query string, or in the fragment where the implicit grant is still in use, so it can reach a third party through the Referer header of an outbound request from the callback page, through shared or synchronised browser history, or through anything that records full URLs.
The controls are a restrictive referrer policy on the callback page, no credentials in URLs, and the code flow with PKCE in place of the implicit grant. The second is the mix-up attack, which applies once an application accepts more than one identity provider and cannot tell from the response which one answered. A code issued by an honest provider can then be redeemed at a token endpoint the attacker controls, which discloses the code and any client credential sent with it, or an identity asserted by the wrong issuer can be accepted as if it came from the expected one.
The controls are issuer identification in the authorisation response and a distinct callback for each provider.
Email-claim account linking hands over the account with no redirect flaw at all
Redirect and token handling are only half of this class. An application that merges a federated identity into an existing local account on the strength of an email claim, without reading whether the provider marked that address verified, can be made to attach an attacker-held identity to a victim's account. That variant needs a provider which will issue an identity for an address the attacker has not proven they own, and the major consumer and enterprise providers refuse, so in practice it reaches applications that accept a self-hosted or customer-supplied issuer.
The same effect arrives by a different road where state is not checked during linking, because a response for the attacker's own provider account can be completed inside the victim's authenticated session, after which the attacker signs in with the ordinary login button. A third variant inverts the order. An attacker claims an identity for an address that has no account yet, and the real owner's later registration merges into the record the attacker already holds, which is account pre-hijacking.
The session is spent, and a password reset alone does not close it
What the session yields on its own is the stored profile, including identity documents, and control of the contact address, which is already enough for a notifiable personal data breach and enough to stop notifications reaching the owner. Movement of value depends on what else guards it. A loyalty transfer to an external destination, a change of payment instrument and a booking against a stored card commonly sit behind a step-up such as re-authentication, a card security code or an out-of-band confirmation, none of which the federated login satisfied.
Where no step-up guards those actions they follow immediately, and where one does it becomes the next target. Persistence is the part that is routinely underestimated. Because the entry point is a linked identity rather than a password, a reset rotates a credential the attacker never used, and a refresh token obtained during the takeover keeps minting access until it is revoked as well.
Closing the route means revoking sessions and refresh tokens and re-verifying the linked login, so where that step is skipped the account can be retaken after the incident is believed closed.
Business impact
A travel profile gathers an unusually wide set of personal data into a single record. It typically carries passport number, expiry and nationality, date of birth, residency or visa details, next of kin, frequent traveller number, billing address and the last four digits of stored cards, plus any assistance or dietary notes the traveller entered, which can amount to health information. It also carries forward itineraries, and an itinerary is a physical-safety disclosure as much as a privacy one. It names where a specific person will be, at what hour, in which seat or room. Identity document numbers cannot be rotated the way a password can, so a passport number read out of a hijacked profile follows the traveller until the document expires. Corporate travel accounts widen the radius again, since a single takeover can expose an organisation's whole travel pattern and the names of the people moving.
Loyalty balances are the part that converts into loss, because points spend like currency. Redemption into bookings, upgrades, partner airline or hotel transfers, gift cards and third-party exchange sites gives an attacker several routes to move value outside the operator's reach, and transferred points are far harder to recall than a reversed card payment. Each of those routes depends on whether a step-up control guards it, which is why the first question after a takeover is which value-moving actions accept a session alone. Where they do, the attacker can also change the payment instrument on file, add a redemption beneficiary or place one-click bookings against a stored card. The costs land on the operator rather than on the identity provider, in points reinstated as goodwill, card-not-present chargebacks and scheme fees, fraud review and contact centre load, and a write-down against the deferred revenue the loyalty programme carries on the balance sheet. A programme priced on the assumption that points are only redeemable by their owner misprices quickly once that assumption fails.
Regulatory exposure follows from the data rather than from the technique. Under the UK and EU GDPR, unauthorised access to passport details, date of birth and payment-adjacent data engages Article 32 on security of processing, the 72-hour notification to the supervisory authority under Article 33 and, where the risk to travellers is high, notification to the travellers themselves under Article 34. Penalties under Article 83(5) reach the higher of 4 per cent of worldwide annual turnover or EUR 20 million under the EU GDPR and GBP 17.5 million under the UK GDPR. Where cards are stored or tokenised, acquirer questions and PCI DSS expectations on authentication and secure development enter the same conversation. Operationally the hardest part is scoping the incident, because every exploited login is a genuine assertion from a real provider, so there are no failed attempts to count and no obvious anomaly to pivot from. If token issuance at the provider was never correlated with session creation in the application, the only truthful answer to a regulator and to affected customers is that the number of accounts touched is unknown, and recovery then means revoking every session and refresh token and re-verifying every linked identity rather than forcing a password reset and treating the matter as closed.
How it is found
| Signal | How it is confirmed |
|---|---|
| Redirect validation that is not a whole-string match | The registered callback is resubmitted with appended path segments, a sibling subdomain, a changed scheme or port, an added fragment and duplicated parameters, including the case where one copy of a parameter is validated and another is used. A code issued to a destination that was never registered is a lead rather than a confirmation, because an appended path on the application's own host is a destination an attacker cannot read. Confirming the class takes three stages, a code issued to a non-registered destination, that destination shown to be readable by an attacker, and the captured response converted into a live application session. Only the third stage is reported as account takeover. |
| A forwarding path or attacker-writable content on an allowed origin | Every origin and path the registration permits is enumerated, then each is checked for onward redirection that preserves the response, for unclaimed DNS pointing at it, for script execution under it and for user-generated content served from it. The readable-destination stage is met when the response arrives somewhere an attacker observes with the application's own callback earlier in the hop chain. Claimability of any dangling name is proven against the hosting provider before it is treated as readable, because a vendor 404 signature on its own does not establish that a name can be claimed. |
| Code leakage from the callback page itself | The callback page is reviewed for outbound requests made while the authorisation response is still in the URL, for third-party script on that origin and for the referrer policy actually sent. The question answered is whether the code or identity token can reach a party other than the application with no redirect flaw involved at all, and whether the flow places credentials in a URL fragment in the first place. |
| State that is not bound to the initiating browser session | An authorisation response is presented to a browser session that never began a flow. A session created from it shows that state is either absent or not stored against the initiating session. The impact of this one is read carefully, because the identity landing in the victim's browser is the attacker's, which is login cross-site request forgery and a route into account linking abuse rather than victim account takeover. |
| No code challenge, or a code redeemed without the matching verifier | An authorisation request is sent with the code challenge removed, to see whether the authorisation server still issues a code, and a captured response is offered at the application's callback from a browser session that holds no verifier. Either acceptance confirms that authorisation code injection is open, which is the control the takeover chain turns on. |
| Authorisation codes that are not single use | A code already exchanged is presented a second time at the token endpoint, and a second successful issuance is the observable. This behaviour belongs to the authorisation server rather than to the application, so on a managed identity platform the finding and its fix sit with that platform, and the application-side question becomes whether reuse is detected and acted on when the provider reports it. |
| A nonce that is sent but never validated | An identity token whose nonce does not match the value recorded for the initiating request is presented, and acceptance shows the nonce is decorative. This is tested separately from state, because the nonce is checked inside the token by the application while state is checked against a server-side record of the request. |
| Signature and algorithm enforcement on identity tokens | Tokens the tester has re-signed with their own key, stripped of a signature or marked with an unexpected algorithm are offered, together with an expired token. Every one should be refused before any session exists. This test says nothing about audience handling, because a token whose claims have been altered no longer carries the provider's signature, so acceptance proves only that the signature is unchecked. |
| Audience enforcement on identity tokens | The clean instrument for this is a token the same provider genuinely minted for a different client, with signature and issuer intact and the audience pointing elsewhere. Acceptance confirms that the audience is never compared with the application's own client identifier. The related check is where verification keys come from, and whether a key set location or issuer value named inside the token is ever resolved. |
| Issuer identification where several providers are accepted | Where more than one identity provider is allowed, the checks are whether each provider has its own callback, whether the application records which provider a given flow was started with, and whether it reads the issuer identifier from the authorisation response. Absence of all three leaves the mix-up attack open. |
| Account linking that treats an email address as the identifier | The first question is whether the verified-email claim is read at all and whether a stored link is keyed on the provider's issuer and subject rather than on an address, which is answerable from how the application behaves when an address changes at the provider. Exercising the merge needs an accepted provider that will issue an identity for an unverified or self-asserted address, so where no accepted provider permits that, the row is recorded as not applicable with the provider policy as the evidence rather than as a negative result. |
How it is fixed
| Control | What makes it hold |
|---|---|
| Exact-match redirect registration, necessary and not sufficient (authorisation server) | Compare the submitted redirect_uri with the registered value as one complete string, with no wildcards, no prefix or subdomain patterns and no dynamic path or query segments. Register one callback per client, keep separate clients per environment, and remove localhost and staging entries from production. The standing exception is a native application using a loopback redirect, where RFC 8252 requires the port to vary and the rest of the URI is matched exactly instead. Exact matching removes the parsing step that every redirect bypass depends on, and it does nothing about authorisation code injection, identity token validation, account linking or leakage from the callback page, so it is never the whole fix. |
| A registered callback origin that stays trustworthy (application) | Exact matching only helps while the registered origin is one an attacker cannot reach. Implement no onward forwarding at the callback, including forwarding driven by a cookie, a stored value or a path suffix. Keep user-generated content and third-party script off that origin. Send a restrictive referrer policy from the callback page so the response cannot ride a Referer header outbound. Prove that every name in the registration resolves to infrastructure you control and that no retired name remains claimable. Keep credentials out of URL fragments by using the code flow rather than the implicit grant. |
| PKCE with S256, enforced and held server-side (provider registration, with an application fallback) | Bind every authorisation code to a verifier held by the party that started the flow, as defined in RFC 7636. Require code_challenge_method S256 and reject the plain method, which gives no protection once the challenge is observable in the authorisation request. Keep the verifier server-side against the initiating browser session and never in storage the browser can read, because a verifier an attacker's own browser can supply reopens the injection this control exists to close. Mandatory enforcement is a client registration setting at the authorisation server. Where the provider cannot be made to require it, the application must refuse any callback whose own session holds no verifier, which produces the same refusal one step later. |
| State and nonce kept as a server-side record (application) | Generate state for each authorisation request, store it against the initiating browser session, and refuse any response whose state is missing, unrecognised or already consumed. Validate the OpenID Connect nonce inside the identity token against the same record. The protection comes from the server-side record rather than from the value appearing in the URL. State is the cross-site request forgery control, so it is not a substitute for PKCE and does not address code injection. |
| Identity token validation with keys resolved from configuration (application) | Verify the signature against keys fetched from a pre-configured, TLS-validated discovery or key set location for the expected issuer, and never from a key set URL or an issuer value taken from the token itself. Pin the expected algorithm rather than reading it from the token. Check the issuer, an audience equal to your own client identifier, the authorised party claim where more than one audience is present, expiry within a bounded clock skew, and the nonce. Where a sign-in endpoint accepts an identity token supplied by a client, apply every one of those checks there, because that endpoint is the whole attack surface for audience confusion. |
| Single-use codes, reuse revocation and issuer identification (authorisation server) | Treat authorisation codes as single use, and revoke everything derived from a code that is presented twice so a replay becomes a detected event instead of a second session. Where several providers are accepted, return the issuer identifier in the authorisation response as RFC 9207 defines, and give each provider its own callback so the application always knows which one answered. These are provider-side settings, so on a managed platform the work is a registration and tenant configuration review rather than application code, and the application's part is to act on the reuse signals the provider emits. |
| Federated links keyed on issuer and subject, never on an email address (application) | Key every link on the provider's stable subject identifier together with the issuer, and treat the email claim as metadata that can gate a convenience merge rather than as the identifier. A change of email address at the provider must not alter an existing link, and an address that changes hands must not inherit one. Where a merge into an existing local account is offered at all, require the verified-email claim from the provider plus a step-up on the account being joined. Treat reservations of identities for addresses that have never registered with the same care, since that is the path account pre-hijacking takes. |
| Severance that actually removes the attacker (application and provider together) | Make a credential reset or any security event revoke local sessions and refresh tokens and force re-verification of every linked identity, because a password reset on its own rotates a credential the attacker never used. Honour provider-side revocation so an identity whose access was withdrawn at the provider cannot keep a live application session. Correlate token issuance at the provider with session creation in the application, since without that pairing an incident cannot be scoped and an exploited login cannot be distinguished from a legitimate one. |
Questions
Can an attacker take over an account through Sign in with Google?
Yes. An OAuth or SSO login can be hijacked when either the authorisation server or the application validates the callback loosely, or when the application accepts an identity token without checking it properly. The identity provider is rarely the weak point. The gap is usually in how the application registers its callback and what it checks on the way back, and a redirect target an attacker can influence, or an identity token accepted without verifying its audience and signature, is enough to turn a genuine login into somebody else's session.
What is a redirect_uri bypass in OAuth?
A redirect_uri bypass is when an authorisation server accepts a callback address that is not the registered one, because the comparison is a prefix, a pattern or a parsed host rather than the whole string. Appended paths, sibling subdomains, extra parameters and duplicated parameters are the usual routes. The authorisation code then arrives somewhere an attacker can read, which is the first half of a takeover. The second half is whether that code can be redeemed or injected into a session, which is what PKCE prevents.
Does multi-factor authentication stop an OAuth account takeover?
No. The multi-factor step runs at the identity provider before the redirect, so an authorisation code captured after it has already cleared MFA. Multi-factor authentication raises the cost of guessing a password and does nothing about an authorisation response delivered to the wrong destination.
Why does a password reset not remove the attacker after an SSO takeover?
Because the attacker never used a password. A reset rotates the local credential and may end current browser sessions, while the federated identity linked to the account still authenticates and any refresh token issued during the takeover keeps working. Recovery has to revoke sessions and refresh tokens and re-verify every linked login, which is the step most incident runbooks leave out.
Do Okta, Auth0 or Firebase Authentication stop a redirect_uri bypass by default?
No hosted identity platform can decide for you what your callback registration permits. It enforces the registration you configure, so a wildcard, a domain-level pattern or a leftover staging entry is honoured exactly as entered, and whole-string matching is only in force when every entry is one complete URI. Validating the identity token, storing state and the code verifier, and deciding how federated identities link to local accounts stay in your own code either way. When we test this, we work with identities we create at the provider and probes aimed at the authorisation flow rather than at customer accounts, so live sessions are not disturbed, and anything that would need a real user's consent is agreed in writing beforehand.
How many accounts does one redirect_uri flaw expose?
Every account that can sign in through that provider on that client. The flaw sits in the login route rather than in any one account, so there is no per-victim condition beyond a single click, and an account with a long password and multi-factor authentication is affected identically to one without. The account-linking variants of the same class behave differently, because each one needs a specific victim who is already signed in, or an address the attacker can claim at the provider.
What does an OAuth account takeover look like in our authentication logs?
An OAuth account takeover appears in authentication logs as a successful federated login with nothing failing before it. There is no brute force, no password error and often no new-device prompt, only a session created from an assertion the provider genuinely issued. Visibility comes from correlating token issuance at the provider against session creation in the application, and from alerting on changes to the contact address and to the set of linked identities rather than on failed logins.