Skip to content

getAuthCode ​

Binds a loopback redirect URI, opens the authorization URL, and resolves with the authorization code from the first valid callback. The listener is always closed afterwards.

Signature ​

ts
function getAuthCode(
  authorization: string | URL | AuthorizationUrlBuilder,
  options?: GetAuthCodeOptions,
): Promise<AuthorizationCodeResult>;

type AuthorizationUrlBuilder = (ctx: {
  redirectUri: string; // bound redirect URI, exact value to send, e.g. http://127.0.0.1:53124/callback
  state: string; // 32 random bytes, base64url
  signal: AbortSignal; // aborted on timeout or cancellation
}) => string | URL | Promise<string | URL>;

Parameters ​

authorization ​

  • Builder (recommended): called after the listener binds. The returned URL gets redirect_uri and state appended when absent (except with request/request_uri, whose request object carries them); if present, they must equal the provided values exactly (redirect_uri as the same string, which is also the result's redirectUri). Required for PAR/JAR.
  • URL: the library listens on its redirect_uri (or on options.redirectUri when absent) and appends state when missing. A URL with request or request_uri is rejected; use the builder.

Either way the final URL must be https: (or loopback http:), with no fragment or credentials, no duplicated state, redirect_uri, response_type, response_mode, request or request_uri, not both request and request_uri, response_type absent or code, and response_mode absent or query.

options ​

OptionTypeDefaultDescription
redirectUristring | URLhttp://127.0.0.1:0/callback¹Loopback redirect URI to listen on
launch(url: URL) => unknownsystem browserOpens the final URL. Fulfillment is ignored; a throw or rejection fails the flow
timeoutnumber300000Milliseconds for the whole attempt; integer in [1, 2³¹−1]
signalAbortSignal—Cancels the flow
successHtmlstringneutral pageStatic HTML served after a successful callback
errorHtmlstringneutral pageStatic HTML served after an error callback

¹ Builder form. In URL form the default is the URL's redirect_uri; the option is required when the URL has none, and must match it when both are given.

redirectUri must be http: on 127.0.0.1, [::1] or localhost, with no fragment, credentials, duplicate query keys, or callback parameters (state, code, error, error_description, error_uri, iss) in its query. Port 0 (OS-assigned) is allowed only in builder form. localhost listens on 127.0.0.1.

Returns ​

ts
interface AuthorizationCodeResult {
  code: string;
  redirectUri: string;
  params: URLSearchParams;
}
  • code: the authorization code.
  • redirectUri: the exact redirect URI of this flow, never re-serialized. When the authorization request carried redirect_uri (always in builder form), send it verbatim as redirect_uri in the token request. In builder form with port 0 it contains the bound port.
  • params: the full callback query (state, iss, scope, provider extensions).

Throws ​

ErrorWhen
OAuthCallbackErrorThe callback carried error
DOMException named TimeoutErrorNo valid callback within timeout
signal.reasonsignal aborted (immediately if already aborted)
TypeErrorInvalid redirect URI, authorization URL or option, before the browser opens
RangeErrortimeout out of range
Launcher errorlaunch threw or rejected
Listener errorBind failure, e.g. EADDRINUSE on a fixed port

Examples ​

Builder with PKCE ​

ts
import { createHash, randomBytes } from "node:crypto";
import { getAuthCode } from "oauth-callback";

const verifier = randomBytes(32).toString("base64url");

const { code, redirectUri } = await getAuthCode(() => {
  const url = new URL("https://auth.example.com/authorize");
  url.searchParams.set("client_id", CLIENT_ID);
  url.searchParams.set("response_type", "code");
  url.searchParams.set("scope", "openid profile");
  url.searchParams.set(
    "code_challenge",
    createHash("sha256").update(verifier).digest("base64url"),
  );
  url.searchParams.set("code_challenge_method", "S256");
  return url;
});

const tokens = await fetch("https://auth.example.com/token", {
  method: "POST",
  body: new URLSearchParams({
    grant_type: "authorization_code",
    client_id: CLIENT_ID,
    code,
    redirect_uri: redirectUri,
    code_verifier: verifier,
  }),
}).then((res) => res.json());

Fixed redirect URI ​

For providers that require an exact redirect URI match:

ts
const { code } = await getAuthCode(build, {
  redirectUri: "http://127.0.0.1:8765/callback",
});

Prebuilt URL ​

ts
const url = new URL("https://auth.example.com/authorize");
url.searchParams.set("client_id", CLIENT_ID);
url.searchParams.set("response_type", "code");
url.searchParams.set("redirect_uri", "http://127.0.0.1:8765/callback");

const { code, params } = await getAuthCode(url); // state is appended

PAR ​

ts
const { code, redirectUri } = await getAuthCode(
  async ({ redirectUri, state, signal }) => {
    const res = await fetch(PAR_ENDPOINT, {
      method: "POST",
      body: new URLSearchParams({
        client_id: CLIENT_ID,
        response_type: "code",
        redirect_uri: redirectUri,
        state,
        code_challenge,
        code_challenge_method: "S256",
      }),
      signal,
    });
    const { request_uri } = await res.json();
    const url = new URL(AUTHORIZE_ENDPOINT);
    url.searchParams.set("client_id", CLIENT_ID);
    url.searchParams.set("request_uri", request_uri);
    return url;
  },
);

Headless or custom launch ​

ts
await getAuthCode(build, {
  launch: (url) => console.log(`Open this URL to sign in:\n${url}`),
});

Cancellation and timeout ​

ts
const controller = new AbortController();
process.once("SIGINT", () => controller.abort());

try {
  const { code } = await getAuthCode(build, {
    signal: controller.signal,
    timeout: 120_000,
  });
} catch (error) {
  if (error instanceof DOMException && error.name === "TimeoutError") {
    console.error("Timed out waiting for authorization");
  } else if (controller.signal.aborted) {
    console.error("Cancelled");
  } else {
    throw error;
  }
}

Custom pages ​

ts
await getAuthCode(build, {
  successHtml:
    "<!doctype html><title>Signed in</title><h1>Back to the terminal</h1>",
  errorHtml: "<!doctype html><title>Failed</title><h1>Sign-in failed</h1>",
});

Pages are served verbatim with Content-Security-Policy (no scripts), Cache-Control: no-store, Referrer-Policy: no-referrer and X-Content-Type-Options: nosniff. Callback data is never rendered; show error details in your app.

Testing ​

A launch that requests the URL plays the user against a mock authorization server:

ts
const { code } = await getAuthCode(
  () => new URL("/authorize", mockServer.url),
  {
    launch: (url) => fetch(url), // the mock redirects to the callback
    timeout: 10_000,
  },
);