ADR-007: One Redirect URI Concept and the Builder Form
Status: Accepted Date: 2026-09-26 Tags: api, oauth, security
Problem
- v2 described the callback address three ways (
port,hostname,callbackPath) that had to agree with theredirect_uriinside the authorization URL; a mismatch was a silent 5-minute timeout (#58). - Fixed ports collide; RFC 8252 recommends an OS-assigned loopback port, but the URL must be built after binding.
- PAR/JAR requests carry
stateandredirect_uriinside the request object, so the library can't read or add them.
Decision
getAuthCode(authorization, options?)takes a prebuilt URL or a builder({ redirectUri, state, signal }) => string | URL | Promise<…>called after the listener binds. The builder's URL getsredirect_uriandstateappended when absent and must match when present (redirect_urias the exact string, so the result'sredirectUrialways equals the builder's); it is the form for PAR/JAR (the library skips appending whenrequest/request_uriis present).- The redirect URI is the only address option:
http:on127.0.0.1,[::1]orlocalhost; no fragment, credentials or duplicate query keys; port 0 (ephemeral) only in builder form. Builder default:http://127.0.0.1:0/callback.localhostis accepted but never generated (RFC 8252 §8.3). - The redirect URI is kept as the original string (the OAuth value, handed to the builder and returned as
redirectUri) and a parsed URL (validation, listening), so e.g.:80is never canonicalized away and a pushed request carries the same value as the token request. Parameters are appended without re-serializing the existing query. - Every authorization URL is checked before any launcher runs:
https:(or loopbackhttp:), no fragment or credentials, interpreted parameters at most once, not bothrequestandrequest_uri,response_typeabsent orcode,response_modeabsent orquery. A prebuilt PAR/JAR URL is refused (outer parameters aren't authoritative, RFC 9126 §2.1, RFC 9101 §6.3). launchis a function receiving the final URL (default: system browser,openin a lazily loaded chunk); fulfillment is ignored and only a rejection fails the flow. There is nolaunch: false: the final URL only exists afterstateis appended.- One composed signal (caller
signal+timeout, default 5 minutes, integer ms in [1, 2³¹−1]) drives every stage; abort and timeout reject with itsreason. The first terminal event wins.
Alternatives (brief)
port/hostname/callbackPathoptions (v2) — three sources of truth for one value.launch: false— gives the caller no way to get the final URL; a logging launcher covers headless use.- Byte-exact URL preservation —
URLSearchParamscan't guarantee it; callers with signed URLs passstateor use the builder.
Impact
- Positive: no redirect mismatch timeouts, ephemeral ports by default, PAR/JAR supported, unsafe URLs never reach a browser.
- Negative/Risks: breaking API; a prebuilt URL without
redirect_urineeds theredirectUrioption.