Skip to content

Next.js

masksNext from @masks/server/next covers the App Router. It reads the settings every integration shares.

  1. Create one Masks for the app:

    lib/masks.ts
    import { masksNext } from "@masks/server/next";
    export const { handlers, auth, requireAuth, withMasks, proxy, masks } =
    masksNext({
    name: "Catalog",
    scope: ["openid", "profile", "email", "offline_access", "organization"],
    privileges: { admin: ["catalog:admin"] },
    protect: ["/dashboard"],
    });
  2. Answer every route under /auth with an optional catch-all route:

    app/auth/[[...masks]]/route.ts
    import { handlers } from "@/lib/masks";
    export const { GET, POST, DELETE } = handlers;
  3. Add the proxy, which refreshes a session before a page renders:

    proxy.ts
    export { proxy } from "@/lib/masks";
    export const config = {
    matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
    };
  4. Set MASKS_ISSUER, MASKS_RESOURCE, and MASKS_SECRET, start the app, and open /auth. The first visit starts the handshake.

requireAuth guards a page:

app/dashboard/page.tsx
import { requireAuth } from "@/lib/masks";
export default async function Dashboard() {
const masked = await requireAuth();
return <h1>Hello, {masked.name}</h1>;
}

withMasks guards a route handler, by session or by access token:

app/api/items/route.ts
import { withMasks } from "@/lib/masks";
export const GET = withMasks(async (request, { masked }) =>
Response.json({ subject: masked.subject }),
);

A server component cannot set cookies, and masks revokes a refresh token that is used twice, so auth and requireAuth never refresh. The proxy refreshes the session first and hands the fresh cookie to the page. Without the proxy, requireAuth sends a person whose token expired through /auth/refresh.

requireAuth sends a signed-out person to sign in, and asks masks once for a privilege’s missing scopes. It throws a ForbiddenError for a privilege the person does not hold, which the nearest error.tsx shows.

After a sign-in, requireAuth sends the person back to the path the proxy recorded. Without the proxy, name the page first, as in requireAuth({ returnTo: "/billing" }, "admin"). Otherwise the sign-in comes back to /.

masksNext(options) takes the options every integration shares, and two more:

Option
protect Paths, or regular expressions, whose signed-out visitors the proxy sends to sign in. A path also covers every path under it.
masks A Masks the app already built, used in place of a new one.

It returns:

handlers GET, POST, and DELETE for app/auth/[[...masks]]/route.ts.
auth() The person, or null when nobody is signed in. It works in a server component, a server action, or a route handler.
requireAuth({ returnTo }, ...privileges) The person, after sending a signed-out visitor to sign in and checking each privilege. { returnTo } is optional.
withMasks(handler, ...privileges) Guards a route handler by session or by access token, and passes { masked } to it.
proxy Refreshes the session before a page renders, and sends a signed-out visitor to a path under protect to sign in.
masks The Masks underneath.