OAuth Redirect URI Parser Confusion: Why PKCE Does Not Save You

Guilliano Molaire Guilliano Molaire 8 min read

PKCE does not stop an attacker from stealing an authorization code if the authorization server sends the code to the wrong origin and the attacker also chose the PKCE challenge. One way this happens is parser confusion: the server validates a redirect_uri with one URL parser, decides it points at a safe loopback address such as 127.0.0.1, and then hands the browser the original string, which the browser parses differently and treats as an attacker’s host. The defense is to validate and redirect using the same parsed value, to reject ambiguous characters, and to keep redirect URI allowlists exact.

This post explains why native apps and local agents use loopback redirects, how the parser mismatch works with a reproducible example, why PKCE alone does not cover this case, and what to check on a Keycloak realm. The bug is a general pattern that can appear in any OAuth server or proxy, and the example does not come from a specific product.

Key takeaways

  • The WHATWG URL Standard that browsers follow treats a backslash as a forward slash in http and https URLs, while many server-side parsers do not.
  • PKCE (RFC 7636) binds a code to the party holding the verifier. It assumes the code reaches the legitimate client, so it cannot help when the attacker started the flow.
  • Keycloak parses redirect URIs with java.net.URI, which rejects a raw backslash in the authority, so the specific example below fails there (checked against the Keycloak 26.8.1 source).
  • Exact-match redirect allowlists remove most of the room this bug class needs.

Why do native and agent apps use loopback redirect URIs?

Desktop apps, command-line tools and local AI agents cannot host a public web page to receive the OAuth callback. One of the three redirect options in RFC 8252 (IETF, “OAuth 2.0 for Native Apps”, 2017), and the usual one for desktop and command-line tools, is for the app to start a small web server on the loopback interface, send the user’s browser to the authorization server, and receive the code at a URL such as http://127.0.0.1:53682/callback.

Because the port is chosen at runtime, RFC 8252 tells authorization servers to allow any port for a registered loopback redirect. To support that, validation code often adds a special case for loopback hosts, and that special-case code is where two different URL parsers most often end up being used on the same value.

What is an OAuth redirect URI parser differential?

A parser differential means two components read the same string and arrive at different answers about what it means. Authorization is a particularly bad place for this because one component makes the security decision (the server checks that the redirect URI is allowed) and a different component acts on it (the browser follows the redirect).

Here is a reproducible example. Take this value, where the backslash is a literal character:

http://evil.example@127.0.0.1/cb

Python’s standard urllib.parse.urlparse does not treat a backslash as a separator. It sees evil.example as userinfo (the part before the @) and 127.0.0.1 as the host:

from urllib.parse import urlparse
p = urlparse("http://evil.example\@127.0.0.1/cb")
print(p.hostname)   # 127.0.0.1

A browser follows the WHATWG URL standard, which treats a backslash as a forward slash for special schemes like http. It reads the same string as host evil.example, with a path of /@127.0.0.1/cb:

const u = new URL("http://evil.example\@127.0.0.1/cb");
console.log(u.host);       // evil.example
console.log(u.href);       // http://evil.example/@127.0.0.1/cb

A server that checks hostname == "127.0.0.1" passes this value as loopback. If it then puts the original string in the Location header, the browser sends the user, and the authorization code in the query string, to evil.example. I ran both snippets on Python 3.13 and Node 22 to confirm the output.

Why doesn’t PKCE in OAuth save you here?

PKCE (RFC 7636) works by having the client create a random secret, the code verifier, and send only a hash of it, the code challenge, with the authorization request. When the client later redeems the code, it must present the verifier. A thief who steals the code in transit cannot redeem it without the verifier.

That protects against interception of the code on its way to the legitimate client. It does not protect against a flow the attacker started. In the scenario above the attacker crafts the authorization link, so the attacker chooses the verifier, includes the matching challenge, and sends the link to a victim. The victim logs in and the code lands on the attacker’s origin. Because a native loopback client is a public client with no secret, the attacker redeems the code with the verifier they already hold and ends up signed in to the client as the victim. PKCE did what it was designed to do here, because its design assumes the code reaches whoever started the flow, and in this case that was the attacker.

The OAuth security best current practice document (IETF, “OAuth 2.0 Security Best Current Practice”, RFC 9700, 2025) is explicit that redirect URI validation is the control that prevents this: authorization servers MUST compare redirect URIs by exact string matching, and the only exception is the port number of a loopback redirect used by a native app.

How does Keycloak handle redirect URIs?

Keycloak validates the redirect_uri against the URIs registered on the client, in RedirectUtils.verifyRedirectUri. Four behaviors matter for this bug class (checked against the Keycloak 26.8.1 source):

  • It parses with java.net.URI. A string that java.net.URI cannot parse is treated as invalid and rejected. I tested the example above with java.net.URI, and it throws “Illegal character in authority” for the backslash, so that exact payload never reaches the match step.
  • It matches against registered patterns. A registered URI ending in * matches by prefix, so a loose wildcard widens the set of acceptable values.
  • It has a loopback allowance. For http redirects whose host is localhost, 127.0.0.1 or [::1], Keycloak retries the match with the port removed, so a client registered as http://127.0.0.1/callback accepts that path on any port. This supports the variable-port native app pattern.
  • It turns off wildcard matching when the redirect URI contains userinfo (anything before an @ in the authority), and exact registrations are compared as plain strings, so a something@127.0.0.1 value cannot match an exact http://127.0.0.1/callback entry (a bare * entry is the exception: it matches regardless, which is another reason never to register it).

Keycloak also rejects redirect URIs that already carry OIDC response parameters like state or code, a related hardening step that we cover in the 26.8 redirect URI parameter pollution post.

None of this makes parser differentials impossible in your own code. If your application, a reverse proxy, a custom authenticator or a gateway also parses redirect URIs, each of those parsers is another place where two readings can diverge.

What should you check on your realm?

Work through this list for each client that uses a loopback or native redirect:

  1. Use exact redirect URIs. Register http://127.0.0.1/callback rather than http://127.0.0.1/*. Wildcards turn a precise check into a prefix check.
  2. *Never register a bare `.** In Keycloak a Valid Redirect URI ofaccepts any http or https URL, so it removes the check entirely. Keycloak supports only a trailing`, so keep any wildcard at the end of a specific path.
  3. Require PKCE with S256 on public clients. It remains the right control for the interception case. Our guide to creating a PKCE client in Keycloak and the React PKCE example show the setup.
  4. Treat dynamic client registration as a trust boundary. If clients can register themselves, the redirect URIs they register are attacker-controlled input. Restrict who can register, and use the client registration Trusted Hosts policy (applied to anonymous registrations by default) to limit which redirect hosts self-registered clients may use.
  5. Do not re-derive the redirect target. If you write an extension or a proxy, validate the string, then redirect to that exact validated string, using one parser for both steps. Never validate a parsed form and send the raw input.
  6. Reject unusual characters early. Backslashes, control characters, encoded slashes and whitespace have no place in a redirect URI, so reject them both when a client is registered and when a redirect_uri arrives on a request.

If you are looking at related failures, Python SDK issuer pinning covers another way a legitimate-looking flow ends up with credentials at the wrong place, and OAuth 2.1 changes explains why exact redirect matching and PKCE are becoming mandatory in the newer spec. For troubleshooting errors rather than attacks, use the redirect URI mismatch guide.

Checklist for B2B SaaS and AI agent OAuth clients

If you ship a desktop app, command-line tool or agent that signs users in against managed Keycloak, these checks cover your side:

  • Register one exact redirect URI per client type and keep a short, reviewed list.
  • Bind the local callback server to 127.0.0.1 only, not all interfaces.
  • Verify state on return, and check that it matches the value your app generated (this protects against CSRF and code injection into your app, not the attacker-initiated case above).
  • Do not follow redirect URIs supplied by a remote party (a link, a configuration file, a tool response) without comparing them to your registered set.
  • Log the host in the authorization request on the server so unexpected callback targets stand out during review.

Frequently asked questions

Does PKCE in OAuth prevent authorization code theft?

PKCE prevents a stolen code from being redeemed by someone who does not hold the code verifier, which covers interception between the authorization server and the legitimate client. It does not help when the attacker is the party who started the authorization request, because they hold the verifier.

What is a loopback redirect URI?

A loopback redirect URI points at the user’s own machine, for example http://127.0.0.1:53682/callback, so a native app running locally can receive the authorization code. RFC 8252 defines the pattern and lets authorization servers accept any port for it.

Why do browsers and server libraries parse URLs differently?

Browsers follow the WHATWG URL Standard, which was written to match what real web content depends on, including treating backslashes like slashes in http and https URLs. Many server libraries follow the older RFC 3986 grammar or their own rules, so unusual inputs can produce different results.

Is Keycloak vulnerable to backslash redirect URI confusion?

The example in this post is rejected by Keycloak’s java.net.URI parsing as of the 26.8.1 source I checked. That does not cover your own code or other components in front of Keycloak, so keep redirect URIs exact and review any custom code that handles them.

Should I use wildcards in redirect URIs?

Avoid them where you can. A wildcard turns an exact match into a prefix match, which widens what an attacker can send. If you need one, keep it at the end of a specific path on a host you control.

What is the difference between a redirect URI mismatch and this attack?

A mismatch error is a legitimate request whose redirect URI does not match what is registered, and the fix is to correct the configuration. The attack here is a request crafted to look acceptable to the server while pointing somewhere else after parsing.

Identity you do not have to patch yourself

Skycloak is a managed identity platform for SSO, MFA, SCIM and audit logs. When an advisory lands, the fix is ours to ship: Keycloak 26.7.2 closed an account takeover through password reset and customers had it the same day upstream published it.

Guilliano Molaire
Written by
Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman