Skip to content

GitHub Sign-in Example ​

Sign a user in with a GitHub OAuth App and print their login. This is the core flow without MCP: getAuthCode() captures the code, your code exchanges it.

Prerequisites ​

GitHub accepts any port on a loopback callback URL (RFC 8252 §7.3), so the library can bind a free port and nothing has to be configured.

Code ​

ts
import { getAuthCode, OAuthCallbackError } from "oauth-callback";

const clientId = process.env.GITHUB_CLIENT_ID!;
const clientSecret = process.env.GITHUB_CLIENT_SECRET!;

try {
  // Binds 127.0.0.1 on a free port, then appends redirect_uri and state.
  const { code, redirectUri } = await getAuthCode(() => {
    const url = new URL("https://github.com/login/oauth/authorize");
    url.searchParams.set("client_id", clientId);
    url.searchParams.set("scope", "read:user");
    return url;
  });

  const response = await fetch("https://github.com/login/oauth/access_token", {
    method: "POST",
    headers: { Accept: "application/json" },
    body: new URLSearchParams({
      client_id: clientId,
      client_secret: clientSecret,
      code,
      redirect_uri: redirectUri, // the exact value the authorization request carried
    }),
  });
  const token = await response.json();
  if (!token.access_token)
    throw new Error(`Token exchange failed: ${token.error}`);

  const user = await fetch("https://api.github.com/user", {
    headers: { Authorization: `Bearer ${token.access_token}` },
  }).then((res) => res.json());
  console.log(`Signed in as ${user.login}`);
} catch (error) {
  if (error instanceof OAuthCallbackError) {
    console.error(`GitHub returned ${error.error}`); // e.g. access_denied
  } else {
    throw error;
  }
}

To run the version in the repository:

bash
git clone https://github.com/kriasoft/oauth-callback.git
cd oauth-callback && bun install
GITHUB_CLIENT_ID=… GITHUB_CLIENT_SECRET=… bun run example:github

How it works ​

  1. getAuthCode() binds http://127.0.0.1:<free port>/callback and calls the builder with that redirect URI and a fresh state.
  2. It appends both to the URL and opens the browser; the user approves on GitHub.
  3. GitHub redirects to the loopback listener. The callback with the matching state resolves the call, the browser shows a neutral "You can close this tab" page, and the listener closes.
  4. Your code exchanges the code, sending redirectUri verbatim.

Variations ​

Headless or SSH. Print the URL instead of opening a browser, and open it on the same machine:

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

Fixed port. If you'd rather register an exact callback URL, e.g. http://127.0.0.1:8765/callback, pass the same value as redirectUri.

Cancellation. Pass signal (for example, aborted on SIGINT) and timeout; see getAuthCode.

Security ​

  • A client secret embedded in a CLI you ship can be extracted by anyone. Where the provider supports public clients, use PKCE instead.
  • Keep the access token out of logs and store it in the OS keychain if you persist it.