An OAuth authorization server eventually has to answer a deceptively simple question: where may it send the browser after authorization?

If that destination is validated too loosely, an authorization response intended for one client can be sent somewhere the client does not control. In an authorization-code flow, that can expose the authorization code to another endpoint. Other protections may still limit what can be done with a leaked code, but redirect validation should not create the leak in the first place.

The defensive rule for ordinary web clients is straightforward: register complete redirect URIs and require the redirect URI in an authorization request to match one of them exactly. This article explains why that rule matters, what “exact” means in practice, where developers commonly weaken it, and what the rule does not protect against.

Treat the redirect URI as a delivery address for a security response

OAuth separates two roles that are easy to blur together:

  • the authorization server authenticates the user and obtains authorization;
  • the client is the application that receives the resulting authorization response.

For a browser-based authorization-code flow, the browser moves between those systems. A simplified sequence looks like this:

client
  |
  | authorization request
  v
authorization server
  |
  | redirect with authorization code
  v
registered client callback

The redirect_uri tells the authorization server where that final browser navigation should go. That makes redirect validation part of the boundary around the authorization response.

Suppose a client has registered this callback:

https://app.example.com/oauth/callback

The safe mental model is not “does the requested URL look like part of app.example.com?” It is:

requested redirect URI
        |
        v
exactly equal to one URI registered for this client?
        |
      yes/no

If the answer is no, the authorization server should reject the authorization request rather than trying to guess whether the destination is probably acceptable.

This changes the attacker’s problem. A loosely matched rule may let an attacker influence the destination while still satisfying a prefix, suffix, hostname, or path test. Exact registration removes that flexibility: the authorization server sends the response only to a destination that was deliberately configured for that client.

Why flexible matching is harder than it looks

Developers often reach for partial matching because it appears convenient. A client may have several environments, callback paths, or query parameters, and a broad rule seems easier to maintain.

Consider a registered value such as:

https://app.example.com/oauth/callback

A custom validator might be tempted to accept any URI that starts with an expected string, contains an expected hostname, or matches a wildcard pattern. The problem is that URLs have structure. Scheme, authority, port, path, query, and fragment syntax do not obey the same boundaries as ordinary text prefixes.

For example, these are different URI authorities:

https://app.example.com/oauth/callback
https://app.example.com.evil.example/oauth/callback

A test that merely searches for app.example.com can confuse them. Similar mistakes can occur with user-information syntax, encoded characters, path normalization, wildcard subdomains, or custom parsing rules.

The lesson is broader than any one malformed URL: do not build a miniature URL-policy language when the protocol can use an exact allowlist instead. Every extra matching rule creates another boundary that must be understood, parsed, normalized, and tested consistently.

OAuth’s current security guidance therefore requires exact string matching against pre-registered redirect URIs, with a specific exception for the port of localhost loopback redirects used by native applications.

Exact matching means compare the registered and requested values directly

For an ordinary web client, configuration can be modeled as a set of complete allowed values:

client_id: billing-web
redirect_uris:
  - https://billing.example.com/oauth/callback
  - https://billing.example.com/oauth/admin-callback

An authorization request containing:

redirect_uri=https://billing.example.com/oauth/callback

matches the first registered value. A request containing a different path, host, scheme, port, or additional query text does not become acceptable merely because it is close to that value.

A simplified authorization-server decision is:

registered = redirect_uris_for(client_id)

if requested_redirect_uri not exactly in registered:
    reject authorization request

This pseudocode demonstrates the policy, not production parsing or storage. A real implementation should use the redirect validation supplied by a well-maintained OAuth or OpenID Connect server library when possible rather than inventing protocol logic around raw strings.

“Exact” also means avoiding helpful normalization that silently changes the comparison contract. Do not decide on your own that two redirect URIs are equivalent because their hosts differ only in case, a default port is explicit in one value, a trailing slash appears in one path, or percent-encoding could be decoded into similar text. Register the value the client will actually send and compare according to the protocol’s required matching rules.

This can feel strict during configuration. That strictness is useful because configuration becomes the explicit statement of where authorization responses may be delivered.

Register multiple real callbacks instead of one broad pattern

A client can legitimately need more than one callback. For example, an application might separate its normal sign-in callback from an administrative integration callback:

https://app.example.com/oauth/callback
https://app.example.com/admin/oauth/callback

Register both complete URIs. Do not replace them with a pattern such as:

https://app.example.com/*

The pattern gives every matching path security significance, including paths that were never reviewed as OAuth callbacks. It also makes future application changes dangerous: adding an unrelated redirecting endpoint under the allowed path space can unexpectedly affect the OAuth boundary.

The same reasoning applies to subdomains. If two deployed clients need:

https://eu.example.com/oauth/callback
https://us.example.com/oauth/callback

register those values explicitly rather than treating every *.example.com host as interchangeable. A wildcard makes the safety of the OAuth redirect depend on the security and lifecycle of every hostname that can satisfy the pattern.

Exact registration creates some operational work, but it keeps the trust boundary visible. When a new callback is needed, adding it is a deliberate security-relevant configuration change rather than an accidental consequence of a wildcard written months earlier.

Keep environment separation explicit

Development, staging, and production often need different redirect URIs. Avoid solving that problem by allowing one production client registration to redirect broadly across environments.

A clearer model is to give each environment its own client configuration where practical:

production client -> https://app.example.com/oauth/callback
staging client    -> https://staging.example.com/oauth/callback

This reduces coupling between environments. A staging hostname or callback does not automatically become a valid destination for a production authorization response.

Local development deserves particular care. The OAuth guidance for native applications has a defined loopback exception: localhost loopback redirect URIs may use a variable port because the operating system often assigns an ephemeral port to the application. That exception is specific; it is not a general reason to permit arbitrary hosts, paths, or ports for web clients.

For browser-based web applications, follow the requirements and development model of the authorization server and client type you actually use. Do not generalize a native-app exception into a universal wildcard policy.

Validate before asking the user to authorize

Redirect validation should happen before the authorization server relies on the destination for a response.

If the client_id and redirect_uri combination is missing, invalid, or mismatched, do not send an OAuth error by redirecting the browser to that untrusted URI. Doing so would use the very destination that failed validation.

Instead, reject the request at the authorization server and present an error through a destination the server itself controls. This matters even when no authorization code has been created. Automatically redirecting to an invalid URI can turn the authorization endpoint into a redirect service and can make a malicious URL appear to begin at a trusted authorization domain.

The validation order should therefore preserve a simple invariant:

unvalidated redirect URI -> never used as a redirect destination
validated redirect URI   -> may receive the protocol response

That invariant is easier to review than a collection of special cases for success and error paths.

Preserve the redirect URI through the code exchange

Redirect validation at the authorization endpoint is one part of the flow. When the authorization request includes a redirect_uri, the authorization-code exchange also needs the protocol’s corresponding redirect URI check.

A useful transaction model is:

authorization request
  client_id = billing-web
  redirect_uri = https://billing.example.com/oauth/callback
        |
        v
code is issued in that authorization context
        |
        v
token request uses the matching redirect_uri when required

Do not independently reconstruct a “close enough” redirect URI at the token endpoint. The checks at the authorization and token endpoints serve related purposes and should be implemented by protocol-aware code with consistent transaction state.

Modern OAuth deployments also use protections such as Proof Key for Code Exchange (PKCE) to bind an authorization code to a client instance that possesses the corresponding verifier. PKCE is important, especially for public clients, but it does not justify weak redirect URI validation. The controls address different failure conditions and work better together.

Know what exact redirect matching does not solve

Exact matching narrows where the authorization server will deliver an authorization response. It does not prove that the registered callback itself is well designed.

If https://app.example.com/oauth/callback accepts an authorization response and then forwards sensitive values to an attacker-controlled destination, exact matching at the authorization server cannot repair that client behavior. OAuth security guidance also warns clients against exposing open redirectors that can be used to leak authorization responses or support phishing.

Exact matching also does not replace other OAuth defenses. Depending on the flow and client type, you may still need controls for request/response correlation, authorization-code interception, cross-site request forgery, client authentication, token storage, and transport security. Use the security profile appropriate to the protocol and application rather than treating redirect validation as the whole OAuth security model.

Finally, exact matching cannot protect a callback host that an attacker has taken over. Domain ownership, DNS and hosting security, deployment controls, and removal of obsolete callback registrations remain part of the trust boundary.

Make redirect registrations part of security configuration

Because a redirect URI determines an allowed destination for authorization responses, changing the registered set should be treated as a security-relevant configuration change.

For managed clients, record which redirect URIs belong to each client and environment. Restrict who can add or replace them according to the sensitivity of the application. Remove obsolete values rather than leaving them registered indefinitely “just in case.”

Operational verification can be simple and valuable. Test that every intended callback succeeds, then test representative near-misses: an unregistered path, a different host, an unexpected scheme, an extra query component, and an unregistered port. The expected result is rejection before authorization is completed and without redirecting the browser to the invalid destination.

Also test alternate paths through the authorization server. If one endpoint, legacy client type, dynamic-registration path, or administrative override applies weaker matching, the stronger primary path does not define the real boundary.

Choose explicit configuration over clever matching

The central security decision is small: an OAuth authorization response should go only to a destination deliberately registered for that client.

For ordinary web clients, represent those destinations as complete redirect URIs and require exact matching. Register multiple concrete callbacks when the application genuinely needs them. Keep environments separate, reject invalid destinations without redirecting to them, preserve the redirect context through the code flow, and remove stale registrations.

This control reduces the risk that flexible URL matching sends an authorization response to an unintended endpoint. It does not replace PKCE, request correlation, strong client and callback security, or the rest of the OAuth threat model. Its value comes from making one important trust decision simple enough to reason about: this client asked for this exact destination, and that exact destination was approved in advance.