# @wrnexus/oauth > Dependency-free OAuth 2.0 sign-in for any provider, with PKCE and presets for Google, GitHub, and Discord. Part of the **WrNexus** 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 ```bash 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`. ```ts 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: ```ts interface OAuthProvider { name: string; authorizeUrl: string; tokenUrl: string; userInfoUrl: string; scopes: string[]; clientId: string; clientSecret: string; authorizeParams?: Record; // e.g. access_type, prompt mapProfile: (raw: Record) => OAuthProfile; } ``` ### Flow #### `startAuth(provider, options): Promise` 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`. ```ts interface StartAuthOptions { redirectUri: string; state?: string; // reuse a state instead of generating one params?: Record; // 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`. ```ts 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` | Exchange an authorization code for tokens. | | `fetchProfile(provider, tokens, fetch?)` | `→ Promise` | Fetch + normalize the user's profile. | | `randomToken(bytes?)` | `→ string` | Random URL-safe token (default 32 bytes) for `state`/verifiers. | ### Types ```ts 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; } ``` ## Usage ```ts 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`: ```ts 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.