npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

request-oauth2

v0.1.1

Published

Isomorphic TypeScript OIDC/OAuth2 authorization_code + PKCE client

Readme

request-oauth2

Isomorphic TypeScript client for the OIDC / OAuth2 authorization_code + PKCE flow. It reads the OIDC discovery document, builds the authorization URL, exchanges the authorization code for tokens, and turns the result into a plain session object.

  • Runs unchanged in Node 18+ and the browser — only global fetch and Web Crypto, zero runtime dependencies.
  • PKCE is always on (code_challenge_method=S256).
  • Never decodes or verifies a JWT itself. decodeIdToken POSTs the token to an endpoint you control.
  • Persists nothing. state, codeVerifier, and the Session are yours to store across the redirect.
  • Functional API — a client factory plus standalone functions, dual ESM/CJS.

Install

npm install request-oauth2

Three ways to exchange the code

The redirect and PKCE steps are identical everywhere. What differs is where the token exchange runs and who holds the client_secret:

| Pattern | Token exchange | client_secret | Use | | --- | --- | --- | --- | | Public client | exchangeCodeForToken in the browser, straight to the IdP | none (PKCE only) | SPA against an IdP that allows public clients | | Backend proxy | exchangeCodeForTokenViaBackend → your /token proxy → IdP | held by your proxy | SPA (or any client) that must authenticate as a confidential client without shipping the secret | | Server-side | exchangeCodeForToken on your server, with clientSecret | held by your server | classic web app / BFF |

A client_secret must never reach browser code. exchangeCodeForToken throws ConfidentialClientInBrowserError synchronously — before any network call — if clientSecret is set while typeof window !== 'undefined'.

Quick start — createOidcClient

createOidcClient(config) wires discovery, PKCE, exchange, decode, and session creation into one object. Discovery is fetched once per client and memoized.

import { createOidcClient } from 'request-oauth2';

const client = createOidcClient({
  wellKnownUrl: 'https://idp.example.com/.well-known/openid-configuration',
  clientId: process.env.OIDC_CLIENT_ID!,
  redirectUri: 'https://app.example.com/callback',
  scope: 'openid profile email',
  clientSecret: process.env.OIDC_CLIENT_SECRET, // server-side only; omit in a browser
  decodeEndpoint: 'https://app.example.com/api/decode', // for authenticate() / decodeIdToken()
});

// GET /login
app.get('/login', async (req, res) => {
  const { url, state, codeVerifier } = await client.startAuthorization();
  req.session.oauth = { state, codeVerifier }; // persist across the redirect
  res.redirect(url);
});

// GET /callback?code=...&state=...
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
  if (state !== req.session.oauth.state) return res.status(400).send('state mismatch');

  // authenticate() = exchangeCodeForToken -> decodeIdToken (if an id_token came back) -> createSession
  req.session.user = await client.authenticate(code, req.session.oauth.codeVerifier);
  res.redirect('/');
});

config.clientSecret and config.decodeEndpoint are optional. Set tokenProxyEndpoint instead to make the client use the backend-proxy exchange (below) via client.exchangeCodeForTokenViaBackend(code, codeVerifier).

Browser, public client (PKCE only)

No secret, no client factory needed — compose the standalone functions:

import {
  fetchOidcConfiguration,
  generatePkcePair,
  buildAuthorizationUrl,
  exchangeCodeForToken,
  decodeIdToken,
  createSession,
} from 'request-oauth2';

const CONFIG = {
  wellKnownUrl: 'https://idp.example.com/.well-known/openid-configuration',
  clientId: 'my-public-client',
  redirectUri: 'https://app.example.com/callback',
  scope: 'openid profile email',
  decodeEndpoint: 'https://app.example.com/api/decode',
};

async function login() {
  const discovery = await fetchOidcConfiguration(CONFIG.wellKnownUrl);
  const pkce = await generatePkcePair();

  const { url, state } = buildAuthorizationUrl({
    authorizationEndpoint: discovery.authorization_endpoint,
    clientId: CONFIG.clientId,
    redirectUri: CONFIG.redirectUri,
    scope: CONFIG.scope,
    codeChallenge: pkce.codeChallenge,
  });

  // Store { state, codeVerifier } somewhere that survives the redirect (e.g. sessionStorage).
  sessionStorage.setItem('oauth', JSON.stringify({ state, codeVerifier: pkce.codeVerifier }));
  window.location.href = url;
}

// On the /callback page:
async function handleCallback() {
  const params = new URLSearchParams(window.location.search);
  const { state, codeVerifier } = JSON.parse(sessionStorage.getItem('oauth')!);
  if (params.get('state') !== state) throw new Error('state mismatch');

  const discovery = await fetchOidcConfiguration(CONFIG.wellKnownUrl);
  const tokens = await exchangeCodeForToken({
    tokenEndpoint: discovery.token_endpoint,
    clientId: CONFIG.clientId,
    redirectUri: CONFIG.redirectUri,
    code: params.get('code')!,
    codeVerifier,
  });

  const claims = tokens.id_token
    ? await decodeIdToken({ decodeEndpoint: CONFIG.decodeEndpoint, idToken: tokens.id_token })
    : null;

  return createSession(tokens, claims);
}

Browser or server, confidential via a backend proxy

Drive a confidential client without the browser ever holding the secret. exchangeCodeForTokenViaBackend POSTs a credential-free JSON body — { code, code_verifier, redirect_uri } and nothing else — to a small /token proxy of yours. The proxy adds client_id + client_secret, forwards to the real token endpoint, and (optionally) decodes the id_token, returning the token response with a claims object attached:

import { exchangeCodeForTokenViaBackend, createSession } from 'request-oauth2';

const tokens = await exchangeCodeForTokenViaBackend({
  tokenProxyEndpoint: 'https://app.example.com/api/token',
  code,
  codeVerifier,
  redirectUri: 'https://app.example.com/callback',
});

// The proxy already decoded the id_token — no separate decodeIdToken round-trip.
const session = createSession(tokens, tokens.claims ?? null);

A non-2xx from the proxy rejects with ProxyTokenExchangeError (carrying status and the parsed body). The same call works from a server route — it sends no credentials and never triggers the browser guard.

The proxy is yours to build. A minimal one: accept the JSON body, add grant_type=authorization_code + your client_id + client_secret, POST it form-encoded to the IdP's token_endpoint, and return the JSON response (optionally verifying the id_token and adding claims). The decode-service + react-login + next-login demos under docs/ implement exactly this.

API reference

createOidcClient(config): OidcClient

config (OidcClientConfig): wellKnownUrl, clientId, redirectUri, scope (required); clientSecret, decodeEndpoint, tokenProxyEndpoint (optional).

| Method | Description | | --- | --- | | startAuthorization(overrides?) | Runs discovery + PKCE, returns { url, state, codeVerifier }. overrides: { state?, extraParams? }. | | exchangeCodeForToken(code, codeVerifier) | Exchanges against the discovered token_endpoint, using config.clientSecret if present. → TokenResponse | | exchangeCodeForTokenViaBackend(code, codeVerifier) | Exchanges via config.tokenProxyEndpoint (no discovery call). Throws ProxyTokenExchangeError if the endpoint is not configured. → ProxyTokenResponse | | decodeIdToken(idToken) | POSTs to config.decodeEndpoint. Throws DecodeError if it is not configured. → Claims | | createSession(tokenResponse, claims) | Builds a Session. | | authenticate(code, codeVerifier) | exchangeCodeForToken → decodeIdToken (only if an id_token came back) → createSession. → Session |

Standalone functions

| Export | Description | | --- | --- | | fetchOidcConfiguration(wellKnownUrl) | Fetches and parses the discovery document. → OidcConfiguration | | generateCodeVerifier(length?) / generateCodeChallenge(verifier) / generatePkcePair(length?) | PKCE helpers on Web Crypto. generatePkcePair → { codeVerifier, codeChallenge, codeChallengeMethod: 'S256' } | | buildAuthorizationUrl(params) | Builds the URL (response_type=code, S256). Generates state if you don't pass one. → { url, state } | | exchangeCodeForToken(params) | Form-urlencoded exchange against a real token endpoint. Params: { tokenEndpoint, clientId, redirectUri, code, codeVerifier, clientSecret? }. Throws ConfidentialClientInBrowserError if clientSecret is set in a browser. → TokenResponse | | exchangeCodeForTokenViaBackend(params) | JSON { code, code_verifier, redirect_uri } to your /token proxy. Params: { tokenProxyEndpoint, code, codeVerifier, redirectUri }. Throws ProxyTokenExchangeError on non-2xx. → ProxyTokenResponse | | decodeIdToken(params) | POSTs { id_token } to decodeEndpoint. Params: { decodeEndpoint, idToken }. Throws DecodeError on non-2xx. → Claims | | createSession(tokenResponse, claims) | → Session (accessToken, idToken?, refreshToken?, tokenType?, scope?, expiresAt: number \| null, claims). | | isAuthenticated(session) / isExpired(session, skewSeconds?) / getAccessToken(session) / getClaims(session) | Session helpers. |

Errors

All extend OAuth2Error (which carries name, message, and optional status / body). Discriminate with instanceof.

DiscoveryError · TokenExchangeError · ProxyTokenExchangeError · DecodeError · ConfidentialClientInBrowserError

Wire vs. native shapes

Types crossing the wire keep snake_case (TokenResponse.access_token, ProxyTokenResponse.claims, OidcConfiguration.token_endpoint). Library-native types use camelCase (Session.accessToken, PkcePair.codeVerifier).

What you own

state, codeVerifier, and the Session are not persisted by the library. Store state + codeVerifier (signed cookie, sessionStorage, …) before the redirect and pass them back on the callback. Token storage, refresh rotation, silent renew, and logout propagation are out of scope.

Development

npm test           # vitest run — full suite
npm run test:watch # vitest watch
npm run typecheck  # tsc --noEmit
npm run build      # tsup -> dist/{index.js,index.cjs,index.d.ts}

npm run typecheck + npm test are the full verification gate; there is no linter.

License

MIT