# `Attesto.RedirectURI`
[🔗](https://github.com/XukuLLC/attesto/blob/v1.15.0/lib/attesto/redirect_uri.ex#L1)

Redirect-URI matching for the authorization endpoint (RFC 6749 §3.1.2.3,
RFC 8252 §7.3).

RFC 6749 §3.1.2.3 matches a request `redirect_uri` against the client's
registered set by **simple string comparison**: byte-for-byte, no
normalization and no prefix matching. That is the default here, and it is what
the OpenID Connect and FAPI profiles assume.

RFC 8252 (BCP 212) defines one narrow exception. A native application that
cannot use a private-use URI scheme (§7.1) instead binds an ephemeral port on
the loopback interface (§7.3) and only learns that port at runtime, so the
port cannot be registered ahead of time. §7.3 therefore requires the
authorization server to "allow any port to be specified at the time of the
request for loopback IP redirect URIs", while comparing the rest of the URI
exactly.

## Matching modes

  * `:exact` (default) - RFC 6749 §3.1.2.3 simple string comparison, and
    nothing else.

  * `:exact_allow_loopback_port` - exact comparison first; failing that, the
    RFC 8252 §7.3 loopback exception. The exception applies only when BOTH the
    request URI and the registered URI are loopback redirect URIs, meaning
    each one:

      * begins with the byte-exact scheme `http://` (an `https` URI, a
        private-use scheme, and an upper-case `HTTP://` are all outside the
        exception and stay exact-match);
      * has an authority of exactly `127.0.0.1` or `[::1]`, optionally
        followed by `:<port>` and nothing else. Any userinfo, any other host,
        and any other spelling of the loopback address are outside the
        exception.

        `localhost` is deliberately excluded. RFC 8252 §8.3 makes the literal
        IP preferable precisely because `localhost` is a *name*, resolved by
        the device's host-name configuration, and so is not guaranteed to be
        the loopback interface. Extending port flexibility to a name whose
        resolution the server cannot reason about would widen the exception
        past what §7.3 asks for, so `http://localhost:PORT/...` never matches
        under it.

        RFC 9700 §2.1 and §4.1.3 phrase the same exception as applying to
        "`localhost` redirection URIs of native apps", which reads as a wider
        allowance. Both, however, define it by reference - "as described in
        Section 7.3 of [RFC8252]" - and §7.3 constructs the URI from the
        loopback IP literal, not the name. `localhost` there is shorthand for
        "the loopback interface", so the normative content is §7.3's and this
        module follows it. A client that has registered a `localhost` URI is
        unaffected in the ordinary case: it still matches exactly, it just
        gets no port flexibility.

        This scopes the exception, not registration: a client that has
        registered a `localhost` redirect URI still reaches it by exact
        match, since that URI is one the host deliberately registered and
        §8.3's "NOT RECOMMENDED" is guidance to the client about which URI to
        choose, not a requirement that the server refuse it. Such a client
        simply gets no port flexibility;
      * carries no fragment (a redirect URI must not, RFC 6749 §3.1.2).

    The two sides differ in one respect. The **request** URI names an
    endpoint the server is about to redirect a browser to, so a port it
    carries must be decimal `1..65535` or absent; an empty
    (`http://127.0.0.1:/cb`) or out-of-range port is not a reachable endpoint
    and falls back to exact comparison. The **registered** URI is a pattern
    whose port is discarded, so any port stands there - including the
    conventional `:0` placeholder for "an ephemeral port chosen at runtime".

    Two loopback URIs match when their scheme, host literal, path, and query
    are all identical; only the port is ignored. IPv4 and IPv6 loopback are
    distinct hosts and never match each other.

  * `:exact_allow_loopback_port_including_localhost` - everything
    `:exact_allow_loopback_port` does, and additionally treats the bare
    hostname `localhost` as a loopback authority, so
    `http://localhost:<ephemeral>/cb` matches a registered
    `http://localhost/cb`.

    This exists for interoperability, not because §7.3 requires it. §7.3's
    MUST is scoped to "loopback IP redirect URIs" and the paragraphs above
    are the right default. But nothing forbids a server allowing the name,
    and §8.3's case against `localhost` is stated entirely in terms of what
    the *client* does - it "avoids inadvertently listening on network
    interfaces other than the loopback interface" and is "less susceptible to
    client-side firewalls and misconfigured host name resolution on the
    user's device". Those are reasons for an app author to choose the IP
    literal; refusing port flexibility at the authorization server does not
    make any of them true, it only fails the request. Real native clients
    exist that register a portless `localhost` callback and then bind an
    ephemeral port, and they cannot authorize at all under the stricter rule.

    The residual risk is bounded by two allowances this module already makes
    independently: a registered `localhost` URI is already reachable by exact
    match, so redirecting to a name the server cannot resolve is already
    accepted; and §7.3 already mandates port flexibility, so a code landing
    on an arbitrary local port is already accepted for `127.0.0.1`. This mode
    permits their combination and nothing else, which is why it is a separate
    opt-in rather than a relaxation of `:exact_allow_loopback_port`.

    `localhost` is its own host identity, never folded onto `127.0.0.1`: the
    name and the IP literals do not cross-match, exactly as IPv4 and IPv6
    loopback do not. Every other constraint above is unchanged - byte-exact
    `http://` scheme, anchored authority (so `localhost.evil.example`,
    `sub.localhost`, `evil-localhost`, `localhost.` and any userinfo stay
    outside), no fragment, exact path and query, and the same asymmetric port
    rule between the request and registered sides.

Enabling either exception is a deliberate deployment decision: a profile that
mandates exact redirect-URI matching forbids them. Both are off by default so
the matching behavior is unchanged unless a host asks for it.

## Registration convention

A client registers its loopback redirect URI with whatever port it likes
(`http://127.0.0.1/cb`, `http://127.0.0.1:0/cb`, or a fixed port) and any
usable request port then matches. Only the port is variable; a request that
differs in path or query is still rejected.

## Failure is never a redirect

This module answers a boolean. A request URI that matches nothing is not a
trusted redirect target, and the caller MUST report the failure directly to
the user agent rather than redirecting to the supplied URI (OIDC Core
§3.1.2.6) - otherwise the endpoint is an open redirect.

## Parser agreement

Matching is only as sound as the agreement between the parser that *decides*
and the parser that *navigates*. Elixir's `URI` follows RFC 3986; the browser
that receives the `Location` follows the WHATWG URL Standard, and the two
disagree about some authorities. In `https://evil.example\@client.example/cb`,
RFC 3986 reads `evil.example\` as userinfo and `client.example` as the host,
while WHATWG treats the backslash as a path separator and navigates to
`evil.example`.

Byte-exact matching is immune - it compares strings, never origins - and the
§7.3 loopback exception is immune because it anchors on the whole authority
rather than the parsed host. Any check phrased in terms of a *host* or an
*origin* is not, so `unambiguous?/1` exists to keep such a URI out of a
registered set in the first place.

# `matching`

```elixir
@type matching() ::
  :exact
  | :exact_allow_loopback_port
  | :exact_allow_loopback_port_including_localhost
```

The redirect-URI matching mode (see the moduledoc).

# `matching!`

```elixir
@spec matching!(term()) :: matching()
```

Normalize a caller-supplied matching mode, raising `ArgumentError` on an
unrecognized value.

A misspelled mode must never silently degrade into a different matching
policy, in either direction: quietly falling back to `:exact` would hide a
host's deliberate opt-in, and quietly enabling anything else would relax
matching nobody asked for. Raising makes the misconfiguration visible.

# `matching_modes`

```elixir
@spec matching_modes() :: [matching()]
```

The supported matching modes. Exposed so a caller can validate host
configuration against the same list this module enforces.

# `registered?`

```elixir
@spec registered?(String.t(), [String.t()], matching()) :: boolean()
```

Whether `uri` matches one of the client's `registered` redirect URIs under
`matching` (RFC 6749 §3.1.2.3, RFC 8252 §7.3).

A non-binary entry in `registered` is ignored rather than raising: the
registered set comes from the host, and one malformed entry must not make an
otherwise valid request crash the endpoint.

# `unambiguous?`

```elixir
@spec unambiguous?(term()) :: boolean()
```

Whether every URL parser agrees which origin `uri` names (see "Parser
agreement" in the moduledoc).

Answers `false` for a URI carrying anything that makes RFC 3986 and the WHATWG
URL Standard read a different authority out of the same bytes:

  * a **backslash** anywhere. WHATWG maps `\` to `/` in a special scheme, so
    it can terminate an authority that RFC 3986 reads as continuing.
  * **userinfo**. `https://a@b/` is unambiguous today, but userinfo is the
    component every authority-confusion trick is built out of, and a redirect
    URI has no legitimate use for credentials (RFC 6749 §3.1.2 wants a plain
    absolute URI). Refusing it removes the whole class rather than the one
    spelling known to differ.
  * a **C0 control, space, tab, CR, or LF**. WHATWG strips tab/CR/LF before
    parsing and percent-encodes the rest; RFC 3986 does neither.

A URI that does not parse at all is likewise `false` - it is not a target the
server can reason about.

This is a check on what may be *registered*, not a matching mode. It says
nothing about whether a URI is a good redirect target, only that the answer
will not depend on which parser is asked.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
