W WRNexusJS
Security

@wrnexus/oauth

OAuth 2.0, PKCE, provider presets, and profile mapping.

bun add @wrnexus/oauth@0.2.15
Dependency-free OAuth 2.0 sign-in for any provider, with PKCE and presets for Google, GitHub, and Discord.

Part of the WRNexusJS framework — an SSR-first, Bun-native full-stack web framework.

Overview

@wrnexus/oauth implements the OAuth 2.0 Authorization Code flow (with PKCE) for server-side sign-in. It ships ready-made provider presets and a defineProvider helper for custom providers, then gives you two flow functions — startAuth (build the redirect) and completeAuth (exchange the code and fetch the user's profile). It has no runtime dependencies: it uses the platform fetch and WebCrypto only. Pairs naturally with @wrnexus/core's logIn to establish a session once you have a normalized profile.

Installation

bun add @wrnexus/oauth
Private package — the machine must be authenticated to the wrnexus npm org
(a read token in ~/.npmrc). Requires Bun (Node is not supported).

API

Providers

Each preset takes ProviderCredentials and returns an OAuthProvider.

interface ProviderCredentials {
  clientId: string;
  clientSecret: string;
  scopes?: string[]; // override the preset's default scopes
}
ExportDefault scopesNotes
google(creds)openid, email, profileSets access_type: offline for refresh tokens.
github(creds)read:user, user:emailMaps name (falls back to login) and avatar_url.
discord(creds)identify, emailBuilds the avatar CDN URL from the user id + hash.
defineProvider(config)Pass a full OAuthProvider to define a custom OAuth 2.0 provider.

An OAuthProvider describes the endpoints, scopes, credentials, optional extra authorize params, and a mapProfile normalizer:

interface OAuthProvider {
  name: string;
  authorizeUrl: string;
  tokenUrl: string;
  userInfoUrl: string;
  scopes: string[];
  clientId: string;
  clientSecret: string;
  authorizeParams?: Record<string, string>; // e.g. access_type, prompt
  mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
}

Flow

startAuth(provider, options): Promise<StartAuthResult>

Builds the authorize redirect URL with a generated PKCE challenge and CSRF state. Store the returned state and verifier (session/cookie), then 302 the user to url.

interface StartAuthOptions {
  redirectUri: string;
  state?: string; // reuse a state instead of generating one
  params?: Record<string, string>; // extra authorize params, merged last
}

interface StartAuthResult {
  url: string; // authorize URL to redirect to
  state: string; // CSRF state — verify on callback
  verifier: string; // PKCE code verifier — pass to completeAuth
}

completeAuth(provider, options): Promise<{ tokens, profile }>

On the callback: exchanges the authorization code for tokens, then fetches and normalizes the user profile. Convenience wrapper over exchangeCode + fetchProfile.

interface CompleteAuthOptions {
  code: string;
  redirectUri: string;
  verifier?: string; // the PKCE verifier from startAuth
  fetch?: typeof fetch; // inject a fetch implementation (tests)
}

Lower-level helpers

ExportSignaturePurpose
exchangeCode(provider, options)→ Promise<OAuthTokens>Exchange an authorization code for tokens.
fetchProfile(provider, tokens, fetch?)→ Promise<OAuthProfile>Fetch + normalize the user's profile.
randomToken(bytes?)→ stringRandom URL-safe token (default 32 bytes) for state/verifiers.

Types

interface OAuthTokens {
  access_token: string;
  token_type?: string;
  refresh_token?: string;
  expires_in?: number;
  id_token?: string;
  scope?: string;
}

interface OAuthProfile {
  id: string;
  email?: string;
  name?: string;
  avatar?: string;
  raw: Record<string, unknown>;
}

Usage

import { google, startAuth, completeAuth } from "@wrnexus/oauth";
import { logIn } from "@wrnexus/core";

const provider = google({
  clientId: process.env.GOOGLE_CLIENT_ID!,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
});

const redirectUri = "https://example.com/auth/callback";

// 1. Kick off sign-in: redirect the user to the provider.
async function beginLogin(ctx) {
  const { url, state, verifier } = await startAuth(provider, { redirectUri });
  // Persist state + verifier in the session, then redirect.
  ctx.session.set("oauth_state", state);
  ctx.session.set("oauth_verifier", verifier);
  return Response.redirect(url, 302);
}

// 2. Handle the callback.
async function handleCallback(ctx, code: string, state: string) {
  if (state !== ctx.session.get("oauth_state")) throw new Error("bad state");

  const { profile } = await completeAuth(provider, {
    code,
    redirectUri,
    verifier: ctx.session.get("oauth_verifier"),
  });

  logIn(ctx, { id: profile.id, email: profile.email });
}

Custom provider with defineProvider:

import { defineProvider, startAuth } from "@wrnexus/oauth";

const gitlab = defineProvider({
  name: "gitlab",
  authorizeUrl: "https://gitlab.com/oauth/authorize",
  tokenUrl: "https://gitlab.com/oauth/token",
  userInfoUrl: "https://gitlab.com/api/v4/user",
  scopes: ["read_user"],
  clientId: process.env.GITLAB_CLIENT_ID!,
  clientSecret: process.env.GITLAB_CLIENT_SECRET!,
  mapProfile: (raw) => ({
    id: String(raw.id),
    email: raw.email as string | undefined,
    name: raw.name as string | undefined,
    avatar: raw.avatar_url as string | undefined,
    raw,
  }),
});

Requirements / Notes

  • Bun-only. Relies on the global fetch and WebCrypto (crypto.getRandomValues,
  • crypto.subtle.digest) — no other runtime dependencies.

  • The flow is stateless by design: you are responsible for storing state and
  • verifier between startAuth and completeAuth (session or signed cookie).

  • Pairs with [@wrnexus/core](../core) — feed the normalized OAuthProfile into
  • logIn to establish a session.

Complete TypeScript API

This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.

/**
 * @wrnexus/oauth — OAuth 2.0 sign-in with any provider. Ships presets for Google,
 * GitHub, and Discord, and `defineProvider` for a custom one. Dependency-free
 * (uses `fetch` + WebCrypto for PKCE). Pairs with @wrnexus/core's `logIn`.
 *
 *   const provider = google({ clientId, clientSecret });
 *   // 1. send the user to the provider:
 *   const { url, state, verifier } = await startAuth(provider, { redirectUri });
 *   // (store `state` + `verifier` in the session, then 302 to `url`)
 *   // 2. on the callback:
 *   const { profile } = await completeAuth(provider, { code, redirectUri, verifier });
 *   logIn(ctx, { id: profile.id, email: profile.email });
 */
interface OAuthTokens {
    access_token: string;
    token_type?: string;
    refresh_token?: string;
    expires_in?: number;
    id_token?: string;
    scope?: string;
}
interface OAuthProfile {
    id: string;
    email?: string;
    name?: string;
    avatar?: string;
    raw: Record<string, unknown>;
}
interface OAuthProvider {
    name: string;
    authorizeUrl: string;
    tokenUrl: string;
    userInfoUrl: string;
    scopes: string[];
    clientId: string;
    clientSecret: string;
    /** Extra params for the authorize request (e.g. `access_type`, `prompt`). */
    authorizeParams?: Record<string, string>;
    /** Normalize the provider's raw userinfo into an OAuthProfile. */
    mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
}
interface ProviderCredentials {
    clientId: string;
    clientSecret: string;
    scopes?: string[];
}
type FetchLike = typeof fetch;
declare function google(creds: ProviderCredentials): OAuthProvider;
declare function github(creds: ProviderCredentials): OAuthProvider;
declare function discord(creds: ProviderCredentials): OAuthProvider;
/** Define a custom OAuth2 provider. */
declare function defineProvider(config: OAuthProvider): OAuthProvider;
/** A random URL-safe token (for `state` and the PKCE verifier). */
declare function randomToken(bytes?: number): string;
interface StartAuthOptions {
    redirectUri: string;
    /** Provide to reuse a state (else one is generated). */
    state?: string;
    /** Extra authorize params (merged over the provider's). */
    params?: Record<string, string>;
}
interface StartAuthResult {
    /** The full authorize URL to redirect the user to. */
    url: string;
    /** CSRF state — store it (session/cookie) and verify on callback. */
    state: string;
    /** PKCE code verifier — store it and pass to `completeAuth`. */
    verifier: string;
}
/** Build the authorize redirect (with PKCE + state). */
declare function startAuth(provider: OAuthProvider, options: StartAuthOptions): Promise<StartAuthResult>;
interface CompleteAuthOptions {
    code: string;
    redirectUri: string;
    /** The PKCE verifier from `startAuth`. */
    verifier?: string;
    /** Inject a fetch implementation (tests). */
    fetch?: FetchLike;
}
/** Exchange the authorization code for tokens, then fetch the user profile. */
declare function completeAuth(provider: OAuthProvider, options: CompleteAuthOptions): Promise<{
    tokens: OAuthTokens;
    profile: OAuthProfile;
}>;
/** Exchange an authorization code for tokens. */
declare function exchangeCode(provider: OAuthProvider, options: CompleteAuthOptions): Promise<OAuthTokens>;
/** Fetch + normalize the user's profile from the provider. */
declare function fetchProfile(provider: OAuthProvider, tokens: OAuthTokens, fetchImpl?: FetchLike): Promise<OAuthProfile>;

export { type CompleteAuthOptions, type OAuthProfile, type OAuthProvider, type OAuthTokens, type ProviderCredentials, type StartAuthOptions, type StartAuthResult, completeAuth, defineProvider, discord, exchangeCode, fetchProfile, github, google, randomToken, startAuth };

Examples

Copy-ready examples taken from this package's published documentation.

Example 1

bun add @wrnexus/oauth

Example 2

interface ProviderCredentials {
  clientId: string;
  clientSecret: string;
  scopes?: string[]; // override the preset's default scopes
}

Example 3

interface OAuthProvider {
  name: string;
  authorizeUrl: string;
  tokenUrl: string;
  userInfoUrl: string;
  scopes: string[];
  clientId: string;
  clientSecret: string;
  authorizeParams?: Record<string, string>; // e.g. access_type, prompt
  mapProfile: (raw: Record<string, unknown>) => OAuthProfile;
}

Example 4

interface StartAuthOptions {
  redirectUri: string;
  state?: string; // reuse a state instead of generating one
  params?: Record<string, string>; // extra authorize params, merged last
}

interface StartAuthResult {
  url: string; // authorize URL to redirect to
  state: string; // CSRF state — verify on callback
  verifier: string; // PKCE code verifier — pass to completeAuth
}