@wrnexus/oauth
OAuth 2.0, PKCE, provider presets, and profile mapping.
bun add @wrnexus/oauth@0.2.15Dependency-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
}
| Export | Default scopes | Notes |
|---|---|---|
google(creds) | openid, email, profile | Sets access_type: offline for refresh tokens. |
github(creds) | read:user, user:email | Maps name (falls back to login) and avatar_url. |
discord(creds) | identify, email | Builds 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
| Export | Signature | Purpose |
|---|---|---|
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?) | → string | Random 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
fetchand WebCrypto (crypto.getRandomValues, - The flow is stateless by design: you are responsible for storing
stateand - Pairs with [
@wrnexus/core](../core) — feed the normalizedOAuthProfileinto
crypto.subtle.digest) — no other runtime dependencies.
verifier between startAuth and completeAuth (session or signed cookie).
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/oauthExample 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
}