Why must the redirect URI be pre-registered and compared exactly?
Because the authorization code (or token) is delivered to the redirect URI: if an attacker can make the server accept a URI they control, the server hands them the victim's code. Exact string matching closes the loopholes that pattern or prefix matching leaves open.
The redirect is the moment the secret result of the login (the authorization code) leaves the authorization server, travelling through the browser to whatever URI the request names. If the server accepted any URI, an attacker could send a victim a link with redirect_uri=https://evil.example/cb, the victim would log in normally, and the code would arrive at the attacker.
Pre-registration alone is not enough if the comparison is loose. Real-world attacks exploited:
- Wildcards and prefix matching:
https://app.example/*also matches a page with an open redirect or user-uploaded content on the same domain. - Subdomain matching: a forgotten or taken-over subdomain receives the codes.
That is why the OAuth security best practice (RFC 9700) and OAuth 2.1 require exact string comparison of redirect URIs (with a small exception for the port of localhost URIs used by native apps).
Go deeper:
RFC 9700, Section 4.1: Insufficient Redirection URI Validation — the attacks on loose redirect URI matching and the countermeasures.
RFC 6749, Section 3.1.2: Redirection Endpoint — the original rules for registering and validating redirect URIs.