skip to content

Your app registers a callback URL with a partner authorization server; why must its redirect_uri match that registration exactly?

level: juniorimportance: must knowfreq 68%

answer

  1. the redirection endpoint belongs to the client
  2. the request names it, so anyone can
  3. compare, do not interpret
  4. no prefix, no substring, no wildcard
  5. one carve-out: a loopback port

basics

~20 s

Exact string matching is what ties an issued authorization code to the application that asked for it. Any looser rule - prefix, substring or wildcard - lets an attacker steer the response to a destination the client never registered.

solid answer

~40 s

The redirection endpoint belongs to the client, not to the authorization server, and the server learns where to send the authorization response from the `redirect_uri` in the request - a value anyone can write, because the authorization request is just a URL in a browser. The registered value is the only anchor, so the server compares the supplied string character for character against the registered ones and accepts nothing else. RFC 9700 states the rule that way and allows a single exception: port numbers in localhost redirection URIs of native apps. Loosen it to a prefix, a substring or a wildcard host and an attacker can name a destination that passes the check but is not yours, and the `code` arrives there instead.

code

http · 6 lines
http
Registered redirect_uri: https://timetable.example/callback

GET /authorize?response_type=code&client_id=s6BhdRkqt3
    &redirect_uri=https%3A%2F%2Ftimetable.example.attacker.example%2Fcallback
    &scope=timetable.read&state=af0ifjsldkj HTTP/1.1
Host: auth.partner-a.example

go deeper

for a junior

Remember that the callback URL is registered once and must then be sent back identically, and that a mismatch error is the check working rather than a defect to route around.

for a middle

Explain who performs the comparison and when: the authorization server, against the client record, before the user is ever asked to consent, because that comparison decides where the authorization code is delivered.

for a senior

Show what each loosening actually concedes - prefix, substring, wildcard host, host-only - and be able to name the single carve-out for a loopback port in a native app rather than reciting the rule flat.

for a principal

Frame registration strictness as inventory: every registered redirection URI is a destination you are promising to control for as long as the client record exists, including subdomains and hosts that outlive their owners.

## The redirection endpoint belongs to the client In OAuth 2.0 the **authorization endpoint** is a browser destination on the authorization server, while the **redirection endpoint** is a URL on the *client* that the user agent is sent back to carrying the authorization response. That response carries the `code` the client will later exchange at the **token endpoint**. Everything of value on the front-channel leg therefore arrives at whichever URL the authorization server chooses to redirect to. That choice is driven by the `redirect_uri` parameter of the authorization request. The request is an ordinary URL rendered in a browser, so an attacker can compose one just as easily as the client can, using the client's own `client_id`. The registration is the only thing standing between that and a redirect to a destination of the attacker's choosing: when the client record is created, one or more redirection URIs are recorded against its `client_id`, and the server's job at request time is to confirm the value in front of it is one of them. ## Compare the string, do not interpret the URI **Exact string matching** means the server compares the supplied `redirect_uri` character for character with each registered value and accepts only an identical one. It does not split the URI into scheme, authority and path and reason about the parts, and it does not accept *the same host* or *somewhere under the same directory* as close enough. RFC 9700, the security guidance for OAuth 2.0, states the requirement in exactly those terms, with one carve-out described below. | Matching rule | What it also accepts | What that costs | |---|---|---| | Exact string | Nothing but a registered value | Nothing; a wrong value fails visibly at request time | | Prefix, or `starts with` | Any deeper path appended to the registered value | The client no longer decides which of its own paths receives the `code` | | Substring, or `contains the registered host` | A host that merely embeds the registered name | A host nobody at the client owns receives the `code` | | Wildcard subdomain | Every sibling host under the domain | One weak or user-controlled subdomain becomes a collection point | | Scheme and host only | Every path on the registered host | Any endpoint on that host, including ones never meant as callbacks | ## A worked slip Suppose the registered value is `https://timetable.example/callback`. A server that merely checks the supplied value *contains* the registered host will accept `https://timetable.example.attacker.example/callback`. Read it carefully: the authority is `attacker.example`, and `timetable.example` is only a label inside it. The user consents on a page that looks entirely legitimate, the authorization server redirects as instructed, and the `code` lands on the attacker's host. Nothing else in the flow raised an objection, because nothing else in the flow was asked to. The same shape appears with a wildcard registration. A registered `https://*.timetable.example/callback` is a promise that every current and future host under that domain is trustworthy - including a subdomain serving user-supplied content, or one that outlived the service that owned it. ## The one carve-out RFC 9700 requires exact string matching **except for port numbers in localhost redirection URIs of native apps**. A native app listening on a loopback address cannot know in advance which port will be free, so the port is the single component allowed to vary at match time. Everything else in such a URI is still compared exactly, and the exception does not extend to web clients. ## What the rule does and does not buy - It fixes **where** the authorization response is delivered, to a destination the client declared in advance. - It is checked by the **authorization server**, at the moment the authorization request arrives, which is why a mismatch surfaces as an error before any user interaction. - It says nothing about **which** authorization server produced a response the client receives - a client trusting several providers needs a separate defence for that. - It is not a substitute for anything the client does after the `code` arrives. ## The mismatch errors are the rule working Almost every integrator meets this rule first as a rejected request: a trailing slash that was not registered, a different port during local development, `http` where `https` was registered. The instinct is to widen the registration until the error goes away. That instinct is what the requirement exists to resist - the narrow, literal registration is the guarantee, and the error is it holding.

  • Which part of a redirection URI may vary at match time, and for which kind of client?
    Only the port number, and only in localhost redirection URIs of native apps. Such an app cannot reserve a loopback port in advance, so RFC 9700 carves the port out of the exact comparison. Every other component is still matched character for character, and web clients get no such latitude.
  • If comparison is exact, how does a client legitimately use several callback URLs?
    By registering each of them. A client record may carry a set of redirection URIs, and the authorization request must name one of that set exactly; the server compares the supplied string against every registered value and accepts an identical match. Adding a URL is a registration change, not a looser rule.
  • The authorization request omits redirect_uri entirely - what should the authorization server do?
    Only a client with exactly one registered redirection URI can safely have the value defaulted for it. Where several are registered, or where the server cannot resolve one unambiguously, the request is rejected rather than guessed at, because the guess would decide where the authorization code is delivered.

saying these in an interview costs you the question

  • Thinks a wildcard subdomain registration is acceptable if the parent domain is yours
  • Says matching scheme and host is enough and the path may vary
  • Treats a trailing-slash mismatch as a server bug to work around
  • Widens the registration to a prefix to stop mismatch errors
  • Assumes exact matching also tells the client which server answered