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

@wocha/nextjs

v2.1.0

Published

Next.js App Router adapter for Wocha authentication

Readme

@wocha/nextjs

npm version npm downloads TypeScript License

Next.js App Router adapter for Wocha authentication. Implements a BFF (backend-for-frontend) pattern: OAuth runs server-side with a confidential client, and sessions are stored in encrypted httpOnly cookies.

Install

npm install @wocha/nextjs
# or: pnpm add / yarn add / bun add @wocha/nextjs

Peer dependencies: Next.js 14+, React 18+.

Environment variables

Set these in .env.local — all bundles (route handler, middleware, server helpers) read them automatically:

WOCHA_CLIENT_ID=your-client-id
WOCHA_CLIENT_SECRET=your-client-secret
WOCHA_ISSUER=https://tenant.auth.wocha.ai
WOCHA_API_URL=https://tenant.api.wocha.ai  # optional
WOCHA_COOKIE_SECRET=your-cookie-secret       # optional, defaults to client secret

Register the callback URI https://your-app.com/api/auth/callback in the Wocha Console.

Quick start

Three pieces of wiring: a route handler, middleware, and a client session provider.

1. Route handler — app/api/auth/[...wocha]/route.ts:

Important: The catch-all segment must be named wocha (i.e. [...wocha]). The SDK reads params.wocha to route login, callback, logout, and other auth actions.

import { createWochaHandler } from "@wocha/nextjs";

export const { GET, POST } = createWochaHandler({
  clientId: process.env.WOCHA_CLIENT_ID!,
  clientSecret: process.env.WOCHA_CLIENT_SECRET!,
  issuer: process.env.WOCHA_ISSUER!,
});

When env vars are set, you can omit the config object entirely — createWochaHandler() will resolve from env:

import { createWochaHandler, wochaAuthConfigFromEnv } from "@wocha/nextjs";

export const { GET, POST } = createWochaHandler(wochaAuthConfigFromEnv()!);

2. Middleware — middleware.ts:

When WOCHA_CLIENT_ID, WOCHA_CLIENT_SECRET, and WOCHA_ISSUER are set, middleware works without passing config:

import { withWochaAuth } from "@wocha/nextjs/middleware";

export default withWochaAuth(undefined, {
  publicPaths: ["/", "/about"],
});

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

To protect all application routes:

export default withWochaAuth(undefined, { publicPaths: [] });

Note: withWochaAuth() is public by default. Protect routes by calling auth.protect() in the middleware callback, or by explicitly supplying publicPaths (use [] to protect every route except /api/auth and its descendants).

Expired access tokens are refreshed silently using the refresh token before redirecting to login.

Overlapping refreshes for the same token, OAuth client and DPoP key share one token request within the same loaded SDK module. Results are discarded when the request finishes. Separate bundles, server instances and requests arriving after completion are not coordinated; rotating refresh tokens still require care in distributed deployments.

Persist refreshes in middleware or Route Handlers before rendering Server Components. Server Components cannot write cookies, so refreshing only inside a Server Component can leave the browser holding an old refresh token. authorizedFetch saves refreshed credentials in writable contexts before fetching the resource, including when that resource request fails.

OIDC discovery, signing-key retrieval and token requests each have a 15-second network deadline. A DPoP nonce challenge permits one additional token request.

3. Client provider — wrap your layout:

"use client";

import { WochaSessionProvider } from "@wocha/nextjs/client";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WochaSessionProvider refetchOnWindowFocus refetchInterval={0}>
      {children}
    </WochaSessionProvider>
  );
}

| Prop | Default | Description | |------|---------|-------------| | refetchOnWindowFocus | true | Refetch session when the browser window regains focus. | | refetchInterval | 0 | Poll interval in ms; 0 disables polling. |

Visit /api/auth/login to start the sign-in flow.

Subpath imports

| Import path | Purpose | |-------------|---------| | @wocha/nextjs | Route handler (createWochaHandler), middleware (withWochaAuth), server helpers | | @wocha/nextjs/middleware | Middleware only (for Edge runtime bundles) | | @wocha/nextjs/server | Server Components helpers (getSession, getUser, …) | | @wocha/nextjs/client | Client hooks and WochaSessionProvider |

Configuration reference

Config can be passed explicitly or resolved from environment variables (WOCHA_CLIENT_ID, WOCHA_CLIENT_SECRET, WOCHA_ISSUER). Each bundle entry (index, middleware, server) resolves config independently — there is no shared singleton across bundles.

Pass a WochaAuthConfig to createWochaHandler() or use wochaAuthConfigFromEnv():

| Field | Type | Required | Description | |-------|------|----------|-------------| | clientId | string | Yes | OAuth client ID. | | clientSecret | string | Yes | OAuth client secret (server-side only). | | issuer | string | Yes | OIDC issuer URL. | | baseUrl | string | No | Application base URL for redirect URIs. Defaults to the request origin. | | apiUrl | string | No | Platform API base URL for org switching. Defaults to the issuer origin. | | scopes | string[] | No | OIDC scopes. Default: openid, profile, email, org_id, offline_access. | | postLoginRedirect | string | No | Default path after login when no return_to is provided. Default: /. | | postLogoutRedirect | string | No | URL after logout. Defaults to the application base URL. | | authErrorRedirect | string | No | Path for OAuth error redirects (default: postLoginRedirect or /). Receives ?auth_error= query params. | | sessionCookieName | string | No | Encrypted session cookie name. Default: greet_session. | | sessionSecret | string | No | Separate secret for cookie encryption. Defaults to clientSecret. Set via WOCHA_COOKIE_SECRET. | | dpop | boolean \| { enabled, algorithm? } | No | Enable RFC 9449 DPoP sender-constrained tokens. Default: false. |

DPoP (Sender-Constrained Tokens)

When dpop: true is set, the SDK:

  1. Generates an ECDSA P-256 key pair at the callback step
  2. Includes DPoP proofs in all token requests
  3. Stores the private key in a separate encrypted cookie ({sessionCookieName}_dpop)
  4. Includes DPoP proofs when calling resource servers via buildResourceRequestHeaders()

The DPoP key is stored separately from the session cookie to keep both under the 4096-byte browser limit.

Pushed Authorization Requests (PAR)

When the OIDC discovery document advertises a pushed_authorization_request_endpoint, the SDK automatically uses it. This is required by Wocha production deployments. No configuration is needed — PAR support is transparent.

Route handler reference

Mount at app/api/auth/[...wocha]/route.ts. Routes are derived from the catch-all segment:

| Route | Method | Description | |-------|--------|-------------| | /api/auth/login | GET | Starts OAuth flow. Accepts ?return_to=/path to redirect after login (same-origin paths only, see below). ?prompt=none requests a silent re-authorise (no hosted UI). Sets an encrypted PKCE cookie. | | /api/auth/callback | GET | Exchanges the authorisation code, writes the session cookie, redirects to return_to (validated again). If the PKCE cookie is missing, restarts login once with prompt=none instead of returning missing_verifier. | | /api/auth/logout | GET, POST | Revokes tokens, clears session cookie, redirects to the OIDC end-session endpoint. | | /api/auth/session | GET | Returns the current session (see below), renewing it when it expires within 60 seconds. | | /api/auth/refresh | POST | Refreshes tokens server-side and updates the session cookie. 401 when the refresh token is refused (see Session renewal); 503 with Retry-After (cookies kept) when the identity provider is unavailable. | | /api/auth/switch-org | POST | Switches organisation. Body: { targetOrgId: string }. May return { redirect } for re-authorisation. |

Session response

GET /api/auth/session returns:

{
  "session": {
    "user": { "id": "...", "email": "...", "name": "...", "orgId": "..." },
    "accessToken": "...",
    "expiresAt": 1717000000,
    "orgId": "...",
    "orgIds": ["..."]
  }
}

When unauthenticated: { "session": null }, also when the refresh token was refused (see Session renewal).

When the session could not be renewed because the identity provider is unreachable or failing (network error, timeout, 5xx, 429), the route answers 503 with Retry-After and { "session": null, "error": "session_unavailable", "retryable": true }, and keeps the session cookie. WochaSessionProvider treats that (and any network error) as "retry later": a signed-in user stays signed in with error set.

return_to

return_to must be a same-origin relative path. The login, callback and BYO routes refuse anything else and fall back to postLoginRedirect (or /); logout falls back to postLogoutRedirect (or the app origin). Refused: absolute and protocol-relative URLs, backslashes (/\evil.com), tab, newline and other control characters (/\t/evil.com), paths that dot segments collapse into //host, and any of these percent-encoded (/%5Cevil.com, /%2F%2Fevil.com, double-encoded forms included).

Refresh tokens are not exposed to the browser — they remain encrypted in the httpOnly session cookie and are used only by the /refresh route.

Server helpers

Import from @wocha/nextjs/server (or the main package):

import { getSession, getUser, requireSession, getAccessToken } from "@wocha/nextjs/server";

| Function | Return type | Description | |----------|-------------|-------------| | getSession(config?) | Promise<GreetSession \| null> | Reads and decrypts the session cookie. Renews it in Route Handlers and Server Actions; null when there is no usable session. | | getSessionState(config?) | Promise<WochaSessionState> | Like getSession, but says why a session is not usable: active, needs_refresh, unavailable or signed_out. | | getUser(config?) | Promise<GreetUser \| null> | Returns session.user or null. | | requireSession(config?) | Promise<GreetSession> | Returns the session or redirects to /api/auth/login?return_to=.... In a Route Handler or Server Action, throws WochaAuthError("session_unavailable") when renewal failed transiently (for up to SESSION_UNAVAILABLE_MAX_SECONDS after expiry, then redirects). | | getAccessToken(config?) | Promise<string \| null> | Returns the access token from the session. |

Sign-in / sign-out URL helpers

import { signInUrl, signOutUrl } from "@wocha/nextjs/server";

signInUrl("/dashboard");   // → /api/auth/login?return_to=%2Fdashboard
signUpUrl("/welcome");     // → /api/auth/login?signup=1&return_to=%2Fwelcome
signOutUrl("/");           // → /api/auth/logout?return_to=%2F

// Custom auth base path (default: /api/auth)
signInUrl("/app", "/auth"); // → /auth/login?return_to=%2Fapp

Server helpers resolve config from env vars when not passed explicitly.

Session renewal

Access tokens are renewed with the refresh token 60 seconds before they expire (SESSION_RENEWAL_MARGIN_SECONDS). Identity providers rotate the refresh token on every renewal, so the renewed session must reach the browser, or its next request presents a spent token and is signed out.

  • Middleware (withWochaAuth) renews first. The new cookie goes to the browser and to Server Components and Route Handlers in the same request, so they never see an expired token. This needs Next.js 14.2.8 or later (x-middleware-set-cookie), the SDK's minimum.
  • Server Components cannot set cookies, so the SDK never renews there. An expired session is reported as needs_refresh by getSessionState(); getSession() returns null, requireSession() sends the user through sign-in (the identity provider skips the form while its own session is alive), and authorizedFetch() throws session_refresh_required.
  • Route Handlers and Server Actions renew themselves, at most once per request: getSession() followed by authorizedFetch() makes one token request.
  • A refused refresh token (invalid_grant) means the session is dead, but the refusing response does not expire the session cookies: another request may have rotated that same token a moment earlier, and its new cookie must survive. The refusing response marks the token instead (an httpOnly <cookie>_refused cookie holding a 64-bit SHA-256 fingerprint, never the token) and answers "sign in" (a redirect, a 401, { "session": null } or signed_out). The browser's next request settles it: if it still carries the refused token, the session cookies are cleared without asking the identity provider again; if it carries a newer token, the session carries on. Do not expire the session cookies yourself on signed_out.
  • Other failures that no retry can fix (an id_token that fails verification, a stored DPoP key that no longer signs) clear the session at once.
  • A transient failure keeps the session: a network error, timeout, 5xx, 408 or 429, and also a 4xx that refuses the client rather than the grant (invalid_client after a secret rotation, an HTML page from a WAF), so a misconfiguration does not sign every user out. A token still inside the margin keeps working. An expired one gets 503 + Retry-After from middleware and the session route, unavailable from getSessionState(), and session_unavailable from requireSession() and authorizedFetch(). The delay grows with the time since expiry (5 s up to 60 s, jittered). Once the token has been expired for SESSION_UNAVAILABLE_MAX_SECONDS (300 s), middleware and requireSession() send the user through sign-in instead, still keeping the cookies.

Overlapping renewals of the same refresh token within one server instance share a single token request. Parallel requests on different instances can still both present the same refresh token; configure a refresh-token rotation grace period on the identity provider so the second one is not refused (and, with reuse detection, the whole token family revoked).

Use in Server Components, Route Handlers, and Server Actions:

import { requireSession } from "@wocha/nextjs/server";

export default async function DashboardPage() {
  const session = await requireSession();
  return <h1>Hello, {session.user.email}</h1>;
}

Client hooks

Import from @wocha/nextjs/client. Requires WochaSessionProvider.

useSession()

const { data, status, error, refresh } = useSession();
// data: GreetSession | null
// status: "loading" | "authenticated" | "unauthenticated"
// error: Error | null
// refresh: () => Promise<void>

useUser()

const { user, status } = useUser();
// user: GreetUser | null
// status: SessionStatus

useOrg()

const { orgId, orgIds, switchOrg, isLoading } = useOrg();

switchOrg(targetOrgId, endpoint?)

Standalone function (does not require a hook):

import { switchOrg } from "@wocha/nextjs/client";

await switchOrg("org-abc123");

usePermission(check)

Checks a SpiceDB permission using the session access token. Requires apiUrl in handler config.

const { allowed, isLoading } = usePermission({
  resource: { type: "document", id: docId },
  permission: "edit",
});

Return type: { allowed: boolean; isLoading: boolean; error: Error | null }.

signIn() / signOut()

import { signIn, signOut, signInUrl, signOutUrl } from "@wocha/nextjs/client";

signIn("/dashboard");   // navigates to login with return_to
signOut("/");           // navigates to logout with return_to

Client Components

Import from @wocha/nextjs/client. All components require WochaSessionProvider.

SignInButton / SignUpButton / SignOutButton

Link-styled controls that navigate to the BFF login, signup, or logout routes. Shorter aliases SignIn, SignUp, and SignOut are also exported.

import { SignInButton, SignUpButton, SignOutButton } from "@wocha/nextjs/client";

<SignInButton returnTo="/dashboard" />
<SignUpButton returnTo="/welcome" />
<SignOutButton returnTo="/" />

SignUpButton appends signup=1 to the login URL; the route handler forwards screen_hint=signup to the authorisation server.

UserButton

Shows the signed-in user with a sign-out action. Renders nothing when unauthenticated.

import { UserButton } from "@wocha/nextjs/client";

// Default avatar menu
<UserButton />

// Custom render
<UserButton>
  {({ user, signOut }) => (
    <div>
      <span>{user.email}</span>
      <button type="button" onClick={() => signOut("/")}>Sign out</button>
    </div>
  )}
</UserButton>

OrgSwitcher

Organisation dropdown when the user belongs to multiple orgs. Renders nothing for a single org or when unauthenticated.

import { OrgSwitcher } from "@wocha/nextjs/client";

<OrgSwitcher />

<OrgSwitcher>
  {({ orgId, orgIds, switchOrg, isLoading }) => (
    <select
      value={orgId}
      disabled={isLoading}
      onChange={(e) => void switchOrg(e.target.value)}
    >
      {orgIds?.map((id) => (
        <option key={id} value={id}>{id}</option>
      ))}
    </select>
  )}
</OrgSwitcher>

Authenticated / Unauthenticated

Conditional rendering based on session status.

import { Authenticated, Unauthenticated, SignInButton } from "@wocha/nextjs/client";

<Authenticated fallback={<p>Loading…</p>}>
  <Dashboard />
</Authenticated>

<Unauthenticated>
  <SignInButton />
</Unauthenticated>

Protect

Permission-gated wrapper. Renders children only when the user is authenticated and passes the permission check. Renders fallback (or null) while loading or when denied. Matches @wocha/react's Protect component.

import { Protect } from "@wocha/nextjs/client";

<Protect
  permission={{ resource: { type: "document", id: "doc-123" }, permission: "edit" }}
  fallback={<p>You don't have access.</p>}
>
  <EditForm />
</Protect>

Requires apiUrl in handler config — same as usePermission.

Middleware

When env vars are set, middleware requires no config:

import { withWochaAuth } from "@wocha/nextjs/middleware";

export default withWochaAuth(undefined, {
  publicPaths: ["/", "/about", "/pricing"],
  loginPath: "/api/auth/login",
});

You can also pass config explicitly:

export default withWochaAuth(
  {
    clientId: process.env.WOCHA_CLIENT_ID!,
    clientSecret: process.env.WOCHA_CLIENT_SECRET!,
    issuer: process.env.WOCHA_ISSUER!,
  },
  {
    publicPaths: ["/", "/about", "/pricing"],
    loginPath: "/api/auth/login",
  },
);

| Option | Default | Description | |--------|---------|-------------| | publicPaths | [] | Paths that skip auth checks. Supports trailing * wildcards (e.g. /blog*). | | loginPath | /api/auth/login | Redirect target for unauthenticated requests. Appends ?return_to= automatically. |

Access tokens are renewed silently 60 seconds before they expire (see Session renewal). When the session cannot be renewed, a page navigation is redirected to login and any other request (RSC fetch, prefetch, fetch()) gets 401 { "error": "session_expired" }, which makes the Next.js router load the page for real. When the identity provider is unavailable, the session is kept and the request gets 503 with Retry-After (for page navigations, a page that retries after a growing delay and links to sign-in), for up to 300 seconds after the token expired. Routes under /api/auth are always public. Configure a custom matcher in middleware.ts to limit which routes the middleware runs on:

export const config = {
  matcher: ["/dashboard/:path*", "/settings/:path*"],
};

Security model

  • Confidential client: The client secret stays on the server. Token exchange and refresh happen in Route Handlers, never in the browser.
  • Encrypted httpOnly cookies: Sessions are serialised and encrypted with AES-256-GCM. The encryption key is derived from the client secret via PBKDF2 (100,000 iterations, SHA-256).
  • PKCE: The login flow uses PKCE with the verifier stored in a short-lived encrypted cookie (greet_pkce).
  • ID token verification: Callback exchanges verify the id_token signature against the issuer JWKS (RS256/ES256), and validate iss and aud.
  • No refresh token in the browser: The /session endpoint returns only user, accessToken, and expiresAt. Refresh tokens remain server-side.
  • No open redirects: return_to is accepted only as a same-origin relative path, checked at every decoding layer (see return_to).

Structured errors

Server-side auth flows throw WochaAuthError with a typed code (e.g. state_mismatch, token_exchange_failed). Import from the main package:

import { WochaAuthError } from "@wocha/nextjs";

try {
  // route handler logic
} catch (err) {
  if (err instanceof WochaAuthError && err.code === "state_mismatch") {
    // handle CSRF mismatch
  }
}

Self-hosted configuration

export const { GET, POST } = createWochaHandler({
  clientId: process.env.WOCHA_CLIENT_ID!,
  clientSecret: process.env.WOCHA_CLIENT_SECRET!,
  issuer: "https://auth.internal.example.com",
  apiUrl: "https://api.internal.example.com",
  baseUrl: "https://app.internal.example.com",
});

Or set the equivalent environment variables — see Environment variables above.

Related packages

| Package | Use when | |---------|----------| | @wocha/react | React SPA without a server-side BFF | | @wocha/sdk | Server-side user/org management via the Management API | | @wocha/cli | Scaffold auth integration with npx @wocha/cli init |

Migrating from @greet-auth/nextjs

If your project vendors the legacy @greet-auth/nextjs package, migrate to @wocha/nextjs:

  1. Replace the vendored package with npm install @wocha/nextjs
  2. Update imports: @greet-auth/nextjs → @wocha/nextjs
  3. Rename env vars: GREET_CLIENT_ID → WOCHA_CLIENT_ID, GREET_ISSUER → WOCHA_ISSUER, etc.

The SDK reads GREET_* env vars as a backward-compatible fallback (with a deprecation warning), so the migration can be done incrementally.

Troubleshooting

redirect_uri_mismatch

The callback URL in your app must exactly match a redirect URI registered in the Wocha Console (including scheme, host, port, and path). For this SDK the default is https://your-app.com/api/auth/callback.

CSRF / state_mismatch

The authorisation flow stores a signed state value in a cookie. If login fails with state_mismatch, clear site cookies for your app origin and retry. Avoid opening multiple login tabs in parallel.

Session cookie not set

Cookies require secure: true when NODE_ENV=production. Use HTTPS in production. In local development, http://localhost is allowed. Confirm WOCHA_CLIENT_SECRET (or WOCHA_COOKIE_SECRET) is set and stable across deploys — changing it invalidates existing sessions.

Missing environment variables

The route handler and middleware read WOCHA_CLIENT_ID, WOCHA_CLIENT_SECRET, and WOCHA_ISSUER from the environment. Missing values throw at startup or on the first auth request. Check .env.local is loaded (Next.js does this automatically for dev).

Callback URL not registered

Every environment (local, staging, production) needs its own redirect URI in the Console. A common mistake is registering production only while testing on http://localhost:3000.

Separate API server

If your architecture includes a separate API backend (e.g. NestJS, Go, Python), that server must validate access tokens independently. The @wocha/nextjs BFF handles login and session management; your API validates the forwarded access token via JWKS.

See the Resource server guide for JWT validation patterns in Express, NestJS, Go, Python, and other frameworks.