@useauthio/nextjs
v0.4.4
Published
Next.js adapter for Authio: middleware, RSC helpers, and Provider.
Readme
@useauthio/nextjs
Part of Authio Lobby — Authio's drop-in passwordless authentication. Learn more at https://authio.com/products/lobby.
Next.js 14/15/16 adapter for Authio. Ships:
createAuthioMiddleware()— drop-in Edge middleware with silent session refresh.createAuthioSignInHandler()— drop-inapp/api/auth/sign-inhandler that mints the cookie-bound login-CSRF nonce (v0.3+).createAuthioCallbackHandler()/createAuthioRefreshHandler()/createAuthioSignOutHandler()— drop-inapp/api/auth/*route handlers.auth()— RSC helper that returns the verified session ({ userId, orgId, role }) inside Server Components and Route Handlers.authMiddleware()— the original v0.1 JWKS-verifying middleware. Still exported for backward compat.
Recent additions
- Embed
@useauthio/widgetsin App Router pages. Mint the widget JWT in a Server Component (or Server Action) using the existingauth()accessToken; render the widget inside a"use client"component. Full snippet on/sdks/nextjs. - Kind-aware route gating. Widget JWTs (
kind: "widget") are refused on every customer-session surface by the data plane, but if you ever embed your own JWKS verify in middleware (e.g. for a proxied admin tool), rejectkind === "widget"withwidget_token_not_allowed_here— the canonical refusal. - Roles + permissions on the session.
auth()exposessession.claims.roles(string in single-role mode, array in multi-role mode) andsession.claims.permissions(always an array). Both are reserved-claim names; the only override path is the Pattern 3 Actions hook (/actions/pattern-3-customer-roles). - MCP / DCR. Customers turning their product into an OAuth
provider can now flip
dcr_modeandcimd_enabledin the dashboard. The Next.js adapter is unchanged — the/.well-known/oauth-authorization-serveradvertisements are served byauth-coreand the new/oauth2/register//oauth2/cimd/resolveendpoints are reachable directly with any HTTP client. See/guides/mcp-integration.
Install
pnpm add @useauthio/nextjs @useauthio/reactFull session lifecycle in twenty lines
// src/middleware.ts
import { createAuthioMiddleware } from "@useauthio/nextjs";
export default createAuthioMiddleware();
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"] };// app/api/auth/sign-in/route.ts (new in v0.3 — closes the login-CSRF gap)
import { createAuthioSignInHandler } from "@useauthio/nextjs/server";
export const { GET, POST } = createAuthioSignInHandler();
export const dynamic = "force-dynamic";// app/api/auth/callback/route.ts
import { createAuthioCallbackHandler } from "@useauthio/nextjs/server";
export const GET = createAuthioCallbackHandler();// app/api/auth/refresh/route.ts
import { createAuthioRefreshHandler } from "@useauthio/nextjs/server";
export const GET = createAuthioRefreshHandler();// app/api/auth/sign-out/route.ts
import { createAuthioSignOutHandler } from "@useauthio/nextjs/server";
export const { GET, POST } = createAuthioSignOutHandler();Point your "Sign in" link at the new handler — <a href="/api/auth/sign-in">Sign in</a>
— instead of the hosted-UI URL directly. The handler mints a CSRF nonce, sets
the authio_callback_state cookie on your origin, and redirects to Authio with
the same value as ?client_state_nonce=…. The callback handler then verifies
cookie ⟷ URL match before persisting any session.
That's it. You now have sign-in, silent refresh, sign-out, and login-CSRF defense — and your users stay signed in for the full org-policy refresh window (default 30 days) even though the underlying access JWT rotates every 15 minutes.
Login-CSRF defense
The v0.3 release closes a login-CSRF gap: an attacker who legitimately obtains an Authio access token
could otherwise craft /api/auth/callback?access_token=<their JWT> and trick a
victim into silently signing in as the attacker on the victim's browser.
The defense is the OAuth-style cookie-bound state nonce. The sign-in handler mints a 32-byte random nonce, sets it as an HttpOnly cookie on your origin, and forwards the same value to Authio. Auth-core persists it on the magic-link / OAuth state row and echoes it back on the callback redirect. The callback handler refuses any callback where cookie and URL disagree.
Upgrading from 0.2.x
You're not required to change anything: v0.3 is backwards-compatible. Your
existing createAuthioCallbackHandler continues to work, but until you add
createAuthioSignInHandler and update your sign-in <a> to point at it, the
handler runs in legacy mode and writes a console.warn per callback. Migration
is opt-in but strongly recommended — until you adopt it, this vector
is still open against your app.
In a Server Component
import { auth } from "@useauthio/nextjs/server";
export default async function Page() {
const { userId, orgId } = await auth();
return <div>User {userId} in org {orgId ?? "(none selected)"}</div>;
}Environment ID and custom auth domains
AUTHIO_PROJECT_ID is your dashboard environment ID (proj_…). The env
var name is legacy; the API field is still project_id. Set it in server
env so createAuthioSignInHandler() can forward it to Lobby (via a signed
ctx token when auth-core supports it, or legacy query params as fallback).
The sign-in handler's default hosted UI URL is https://auth.authio.com/.
Customers on the platform Lobby or a branded auth host pass hostedUiUrl:
createAuthioSignInHandler({
hostedUiUrl: process.env.AUTHIO_HOSTED_UI_URL ?? "https://lobby.authio.com/",
});DNS for a vanity auth hostname (e.g. auth.acme.com) is dashboard-side: CNAME
to cname.authiodns.com. The SDK never references that CNAME at runtime —
only your hostedUiUrl / end-user sign-in URL changes. See
Custom domains.
Configuring cookie names
Multiple BFFs under the same parent domain need distinct cookie names so they don't collide. Pass the same names to every helper:
const opts = {
sessionCookieName: "myapp_session",
refreshCookieName: "myapp_refresh",
apiUrl: process.env.AUTHIO_API_URL,
};
export default createAuthioMiddleware(opts);
export const GET = createAuthioCallbackHandler(opts);License
MIT
