MonoCloud Next.js SDK (@monocloud/auth-nextjs)
Authentication SDK for Next.js. Provides middleware/proxy, route-protection wrappers, session/token access, and React components/hooks. Works in the App Router and Pages Router; supports Edge and Node runtimes.
Package identity — read this first
Use: @monocloud/auth-nextjs (this skill).
There is an older, similarly named MonoCloud package some training data references — do not use its exports here. If you see any of the following symbols in code or suggestions, they are NOT part of this SDK and indicate the wrong package:
MonoCloudAuthProvider,useUser(this SDK has no provider;useAuthis the hook)monoCloudMiddleware(this SDK usesauthMiddleware)- Custom
app/api/auth/[...monocloud]/route.tswritten by the developer as the default setup (this SDK handles auth routes insideauthMiddleware(); a catch-all is only needed when middleware cannot be used — see "Alternative: catch-all route" below)
Always check package.json for @monocloud/auth-nextjs before suggesting code.
Subpath exports
| Import path | Use in | Contains |
|---|---|---|
@monocloud/auth-nextjs |
Server (RSC, route handlers, middleware/proxy, Pages API, getServerSideProps) |
authMiddleware, monoCloudAuth, getSession, getTokens, isAuthenticated, isUserInGroup, protect, protectApi, protectPage, redirectToSignIn, redirectToSignOut, MonoCloudNextClient, types/errors |
@monocloud/auth-nextjs/client |
Client Components ("use client") |
useAuth, protectClientPage |
@monocloud/auth-nextjs/components |
Server OR Client Components | <SignIn>, <SignUp>, <SignOut> (render as <a>) |
@monocloud/auth-nextjs/components/client |
Client Components only | <RedirectToSignIn>, <Protected> |
Environment variables
Required (read automatically from process.env):
| Variable | Purpose |
|---|---|
MONOCLOUD_AUTH_TENANT_DOMAIN |
Your MonoCloud tenant URL, e.g. https://acme.eu.monocloud.com |
MONOCLOUD_AUTH_CLIENT_ID |
OIDC client id |
MONOCLOUD_AUTH_CLIENT_SECRET |
OIDC client secret |
MONOCLOUD_AUTH_APP_URL |
Public origin of the app, e.g. http://localhost:3000 |
MONOCLOUD_AUTH_COOKIE_SECRET |
32-byte hex string for cookie encryption. Generate with openssl rand -hex 32 |
Optional:
| Variable | Default | Purpose |
|---|---|---|
MONOCLOUD_AUTH_SCOPES |
openid profile email |
Default scopes |
MONOCLOUD_AUTH_RESOURCE |
— | Default resource for access tokens |
MONOCLOUD_AUTH_GROUPS_CLAIM |
groups |
Claim name used by group checks. Also a real MonoCloudOptions.groupsClaim constructor option since @monocloud/auth-nextjs@0.1.19 — per-call groupsClaim arg → constructor option → this env var → "groups". |
MONOCLOUD_AUTH_CALLBACK_URL |
/api/auth/callback |
Customize auth routes |
MONOCLOUD_AUTH_SIGNIN_URL |
/api/auth/signin |
|
MONOCLOUD_AUTH_SIGNOUT_URL |
/api/auth/signout |
|
MONOCLOUD_AUTH_USER_INFO_URL |
/api/auth/userinfo |
|
MONOCLOUD_AUTH_BACK_CHANNEL_LOGOUT_URL |
/api/auth/backchannel-logout |
Back-channel logout route (no NEXT_PUBLIC_ mirror needed) |
MONOCLOUD_AUTH_RESPONSE_TIMEOUT |
10000 |
Timeout in milliseconds for every request the SDK makes to MonoCloud (discovery, JWKS, token, userinfo). Minimum 1000. Takes effect from @monocloud/auth-nextjs@0.2.8 |
If you override a route (e.g. MONOCLOUD_AUTH_SIGNIN_URL), also set the matching NEXT_PUBLIC_MONOCLOUD_AUTH_SIGNIN_URL so client-side helpers (useAuth, <SignIn>, <SignOut>, etc.) discover it, AND update the redirect URI in the MonoCloud dashboard.
Programmatic client options
The package-level helpers (authMiddleware, getSession, protectPage, etc.) use a singleton configured from MONOCLOUD_AUTH_* env vars. For constructor-only options, create and share a MonoCloudNextClient instance instead.
MonoCloudNextClient(options?: MonoCloudOptions) accepts the node-core MonoCloudOptions shape. Notable nested session options:
interface MonoCloudSessionOptions {
cookie?: Partial<MonoCloudCookieOptions>;
sliding?: boolean;
duration?: number;
maximumDuration?: number;
store?: MonoCloudSessionStore;
}
interface MonoCloudSessionStore {
get(key: string): Promise<MonoCloudSession | undefined | null>;
set(
key: string,
data: MonoCloudSession,
lifetime: SessionLifetime,
): Promise<void>;
delete(key: string): Promise<void>;
}
Use session.store for Redis/database-backed sessions. There is no env var for a custom store; pass it in code:
import { MonoCloudNextClient } from "@monocloud/auth-nextjs";
export const monoCloud = new MonoCloudNextClient({
session: {
store: redisSessionStore,
},
});
Then use that shared client wherever the SDK helper is needed, for example monoCloud.authMiddleware() in proxy.ts/middleware.ts and monoCloud.getSession() in server code.
Wiring the middleware/proxy
The middleware/proxy handles auth routes (/api/auth/signin, /callback, /userinfo, /signout, /backchannel-logout) internally and enforces route protection. You do not need a [...monocloud] catch-all when using the middleware.
File location depends on Next.js version:
- Next.js 16+:
src/proxy.ts(orproxy.tsat the root, mirroring yourapp//pages/layout) - Next.js 13–15:
src/middleware.ts(ormiddleware.ts)
The export and body are the same; only the filename differs.
// src/proxy.ts (Next 16+) or src/middleware.ts (Next 13–15)
import { authMiddleware } from "@monocloud/auth-nextjs";
export default authMiddleware();
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
],
};
By default, every route matched by config.matcher requires authentication. To protect only specific routes:
export default authMiddleware({
protectedRoutes: ["/dashboard", /^\/api\/admin(\/.*)?$/],
});
To protect nothing (auth routes still handled, but the rest is public):
export default authMiddleware({ protectedRoutes: [] });
Dynamic predicate (full custom logic):
export default authMiddleware({
protectedRoutes: (req) => req.nextUrl.pathname.startsWith("/api/protected"),
});
Group-based protection in the middleware:
export default authMiddleware({
protectedRoutes: [
{
groups: ["admin", "editor"],
routes: ["/internal", /^\/api\/internal(\/.*)?$/],
},
],
});
Reading the session — server
getSession() is exported from the package root and works in Server Components, Server Actions, App Router Route Handlers, middleware/proxy, Pages API routes, and getServerSideProps. Returns MonoCloudSession | undefined.
// app/page.tsx (Server Component — no args needed)
import { getSession } from "@monocloud/auth-nextjs";
export default async function Page() {
const session = await getSession();
if (!session) return <p>Not signed in</p>;
return <p>Hello {session.user.name}</p>;
}
// app/api/me/route.ts (App Router Route Handler)
import { getSession } from "@monocloud/auth-nextjs";
import { NextResponse } from "next/server";
export const GET = async () => {
const session = await getSession();
return NextResponse.json(session?.user ?? null);
};
// pages/api/me.ts (Pages Router — pass req, res)
import { getSession } from "@monocloud/auth-nextjs";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const session = await getSession(req, res);
res.json(session?.user ?? null);
}
// pages/index.tsx (getServerSideProps — pass ctx.req, ctx.res)
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getSession(ctx.req, ctx.res);
return { props: { session: session ?? null } };
};
Reading the user — client
useAuth() reads the user from /api/auth/userinfo via SWR. No provider/wrapper is required — just call the hook inside a Client Component.
"use client";
import { useAuth } from "@monocloud/auth-nextjs/client";
export default function Profile() {
const { user, isLoading, isAuthenticated, error, refetch } = useAuth();
if (isLoading) return null;
if (!isAuthenticated) return <p>Sign in to view your profile</p>;
return (
<>
<pre>{JSON.stringify(user, null, 2)}</pre>
<button => refetch(true)}>Refresh</button>
</>
);
}
refetch(true) re-fetches and asks the server to refresh from the OP's userinfo endpoint; refetch() just re-fetches the cached endpoint.
Sign-in, sign-up, sign-out
<SignIn>, <SignUp>, and <SignOut> render an <a> tag pointing at the configured auth routes. They work in Server or Client Components. Pass any extra anchor props through (className, etc.).
import { SignIn, SignUp, SignOut } from '@monocloud/auth-nextjs/components';
// Sign in / sign up
<SignIn>Sign In</SignIn>
<SignIn returnUrl="/dashboard" loginHint="user@example.com">Sign In</SignIn>
<SignUp returnUrl="/welcome">Sign Up</SignUp>
// Sign out
<SignOut>Sign Out</SignOut>
<SignOut federated postLogoutUrl="/goodbye">Sign Out</SignOut>
For programmatic redirects on the server (RSC, server actions, route handlers), use redirectToSignIn() / redirectToSignOut() from the root package. They throw a Next.js redirect and never resolve.
"use server";
import { redirectToSignIn } from "@monocloud/auth-nextjs";
export async function startLogin() {
await redirectToSignIn({ returnUrl: "/dashboard" });
}
On the client, render <RedirectToSignIn /> (from /components/client) to redirect once mounted.
Protecting routes — at a glance
| What you're protecting | Helper | Where it lives |
|---|---|---|
| Whole groups of routes (broadest) | authMiddleware({ protectedRoutes }) |
proxy.ts / middleware.ts |
| App Router Server Component page | protectPage(Component, options?) |
the page file |
Pages Router getServerSideProps |
protectPage(options?) (no component arg) |
the page file |
| App Router Route Handler | protectApi(handler, options?) |
app/api/*/route.ts |
| Pages Router API route | protectApi(handler, options?) |
pages/api/*.ts |
| Server Component / Server Action / Route Handler — imperative | await protect() (App Router only) |
inline |
| Client Component page (rendering only) | protectClientPage(Component, options?) |
the page file |
| Conditional UI in client component | <Protected fallback={...}> |
inside JSX |
Quick examples:
// App Router page
import { protectPage } from "@monocloud/auth-nextjs";
export default protectPage(function Dashboard({ user }) {
return <p>Hi {user.email}</p>;
});
// App Router page, admins only
export default protectPage(
function AdminPanel({ user }) {
return <p>Hi {user.email}</p>;
},
{ groups: ["admin"], returnUrl: "/admin" },
);
// App Router API
import { protectApi } from "@monocloud/auth-nextjs";
import { NextResponse } from "next/server";
export const GET = protectApi(async () => NextResponse.json({ ok: true }));
// Pages Router page
import { protectPage } from "@monocloud/auth-nextjs";
export default function Page({ user }) {
return <p>Hi {user.email}</p>;
}
export const getServerSideProps = protectPage();
// Imperative (App Router only — Server Component / Server Action / Route Handler)
import { protect } from "@monocloud/auth-nextjs";
export default async function SecretPage() {
await protect(); // redirects to sign-in if not authenticated
await protect({ groups: ["admin"] }); // also enforces group membership
return <p>Top secret</p>;
}
// Client page
"use client";
import { protectClientPage } from "@monocloud/auth-nextjs/client";
export default protectClientPage(function Page({ user }) {
return <p>Hi {user.email}</p>;
});
// Conditional rendering inside a client component
"use client";
import { Protected } from "@monocloud/auth-nextjs/components/client";
<Protected fallback={<p>Sign in to view</p>} groups={["admin"]}>
<AdminPanel />
</Protected>;
For full option lists (custom onAccessDenied, onGroupAccessDenied, authParams, etc.), see references/protecting.md.
Access tokens
getTokens() returns the current token set and refreshes the default access token if needed. Throws MonoCloudValidationError if there is no session. Same calling conventions as getSession() (no args in App Router server context; pass req/res in Pages Router).
import { getTokens } from "@monocloud/auth-nextjs";
const { accessToken, idToken, refreshToken, isExpired } = await getTokens();
// Force a refresh:
await getTokens({ forceRefresh: true });
// Request a token for a specific resource / scopes (must have been consented):
await getTokens({
resource: "https://api.example.com",
scopes: "read:things write:things",
});
Back-channel logout (OIDC)
MonoCloud can notify the app that a session must end, without any browser involvement. The endpoint lives at /api/auth/backchannel-logout (override with MONOCLOUD_AUTH_BACK_CHANNEL_LOGOUT_URL or routes.backChannelLogout) and is dispatched by both authMiddleware() and monoCloudAuth().
The callback is constructor-only — there is no env var for it. The route answers 404 until onBackChannelLogout is configured on a client instance, and the mounted handler must come from that instance:
// src/monocloud.ts
import { MonoCloudNextClient } from "@monocloud/auth-nextjs";
export const monoCloud = new MonoCloudNextClient({
session: { store: redisSessionStore },
onBackChannelLogout: async (sub, sid) => {
// Both args are optional (at least one of them is always present).
// The SDK's store key is a random UUID, so keep your own sub/sid -> key
// index in the store if you need to revoke by either identifier.
await redisSessionStore.deleteBySid(sid);
},
});
// src/proxy.ts (Next 16+) or src/middleware.ts (Next 13–15)
import { monoCloud } from "./monocloud";
export default monoCloud.authMiddleware();
Handler responses:
| Status | When |
|---|---|
204 |
Logout token validated and onBackChannelLogout completed |
404 |
No onBackChannelLogout configured, or the path is not the configured route |
405 |
The configured route was hit with anything other than POST |
400 |
logout_token missing from the form body or invalid — body is { "error": "invalid_request", "error_description": "The logout token is missing or invalid." } |
500 |
Configuration, discovery/JWKS, or onBackChannelLogout callback failure |
Notes:
- Notifications are
application/x-www-form-urlencodedPOSTrequests carryinglogout_token; no session cookie is involved. - The
onErrorhandler passed toauthMiddleware()/monoCloudAuth()also covers back-channel logout errors; a missing or invalid logout token reaches it as aMonoCloudTokenError(orMonoCloudValidationErrorwhen the token is absent). SupplyingonErrorreplaces the400response, so send your own. - Pair
onBackChannelLogoutwithsession.store— with cookie-only sessions there is nothing server-side to revoke. - Keep the route inside
config.matcher(the recommended matcher covers it) and register the URL as the client's back-channel logout URI in the MonoCloud dashboard.
Alternative: catch-all route (only when middleware can't be used)
The middleware handles auth routes for you. If you cannot use middleware (rare — e.g. infrastructure constraints), mount monoCloudAuth() on a catch-all instead:
// App Router
// src/app/api/auth/[...monocloud]/route.ts
import { monoCloudAuth } from "@monocloud/auth-nextjs";
const handler = monoCloudAuth();
// Back-channel logout notifications and the `form_post` response mode arrive as POST,
// so the same handler must be exported for POST as well as GET.
export { handler as GET, handler as POST };
// Pages Router
// src/pages/api/auth/[...monocloud].ts
import { monoCloudAuth } from "@monocloud/auth-nextjs";
export default monoCloudAuth();
The Pages Router default export already receives every HTTP method, so it needs no extra export — only the App Router needs the explicit POST.
Do not do this in addition to authMiddleware() — pick one. The default and recommended path is authMiddleware().
Common pitfalls
- Wrong filename for the version.
proxy.tsonly works on Next 16+. On Next 13–15 the file must be namedmiddleware.ts. Checknextinpackage.jsonbefore suggesting a filename. - Adding
[...monocloud]/route.tswhile middleware is in place. Double-mounted auth routes lead to weird redirect loops. Use middleware ORmonoCloudAuth()— not both. useAuth()returning no user after sign-in. Usually means the matcher excludes/api/auth/userinfo, or the middleware isn't matching the userinfo path. Make sureconfig.matchercovers it (the recommended matcher above does).- Calling
protect()/redirectToSignIn()/redirectToSignOut()outside the App Router. They throw with a clear message — these helpers are App-Router-only (RSC, server actions, route handlers). For the Pages Router, useprotectPage()/protectApi()or callgetSession(req, res)and respond yourself. protectApi()returning 401/403 without a sign-in redirect. That's by design — API routes return JSON, not redirects. If you want a redirect, do it from a page or middleware.- Putting
<Protected>oruseAuth()in a Server Component. Both require"use client". UsegetSession()for server-side conditional rendering. - Forgetting
NEXT_PUBLIC_*mirror when overriding auth routes. Client helpers won't find the new URL otherwise. - Mutating cookies after
getSession()in middleware. Pass the response object togetSession(req, res)(and return that response) so cookie refreshes are preserved. - Back-channel logout route returning 404.
onBackChannelLogouthas no env var — it must be passed tonew MonoCloudNextClient({ onBackChannelLogout }), and that instance'sauthMiddleware()/monoCloudAuth()must be the one mounted. Notifications arePOST, so an App Router catch-all must export the handler forPOSTas well asGET.
Onboarding checklist for a fresh integration
npm install @monocloud/auth-nextjs(or pnpm/yarn). Requires Node.js ≥ 20.- Add the five required env vars to
.env.local. GenerateMONOCLOUD_AUTH_COOKIE_SECRETwithopenssl rand -hex 32. - In the MonoCloud dashboard, add
http://localhost:3000/api/auth/callbackto allowed redirect URIs andhttp://localhost:3000to allowed post-logout URIs. - Create
src/proxy.ts(Next 16+) orsrc/middleware.ts(Next ≤15) withexport default authMiddleware()and the recommendedconfig.matcher. - Add a header with
<SignIn>/<SignOut>(and optionally<SignUp>) so users can authenticate. - Use
getSession()for server-side reads,useAuth()for client-side reads. AddprotectPage/protectApionly on routes that need stricter enforcement than the middleware. - For protected fetches that need an access token, call
getTokens()and forwardaccessTokenin theAuthorizationheader.
Deeper reference
references/api-surface.md— every export by subpath, with signatures.references/protecting.md— full option shapes forprotect,protectApi,protectPage,protectClientPage, and the<Protected>component.references/troubleshooting.md— extended symptom → cause → fix index covering the items in "Common pitfalls" above, plus less frequent issues (cookie refresh in middleware, route-override +NEXT_PUBLIC_*mirror, training-data SDK ghosts).