Skip to main content

Getting started: Pages Router

This guide walks you through the steps to enable Kraken authentication in your Next.js app. When you're done, you'll know how the different parts of the package work together in order to provide a seamless authentication experience for your users.

What You'll Build​

By the end of this guide, you'll have:

  • Client providers and auth API routes for your chosen login flow
  • Protected routes using Next.js middleware
  • Session management with server-side authentication
  • Authenticated GraphQL queries
Learn more

This guide only covers the basic setup of the @krakentech/blueprint-auth package, check out our authentication guides to implement additional features.

Functions mentioned in this guide have more options, please refer to the API reference for more information. Functions are also annotated with JSDoc, providing in-editor documentation.

Requirements​

The @krakentech/blueprint-auth package requires:

  • next@^14.2.25 || ^15.0.0 || ^16.0.0,
  • react@^18.2.0 || ^19.1.0,
  • react-dom@^18.2.0 || ^19.1.0

If your project uses unsupported versions, make sure you upgrade them before proceeding.

Installation​

Install the @krakentech/blueprint-auth package and its peer dependencies.

Peer dependencies

The @krakentech/blueprint-auth package relies on:

  • @tanstack/react-query@^5.29.2,
  • graphql-request@^7.2.0,
  • @vercel/global-config@^1.5.0 (optional: required when you use the supplied shared cache adapter).
pnpm add @krakentech/blueprint-auth @tanstack/react-query @vercel/global-config graphql-request
Private packages

Packages of the @krakentech NPM organization are published privately to the NPM registry. To install these packages, you need to configure your package manager to use a Kraken issued NPM access token. Please get in touch if you need one.

Configuration​

Use the createAuthConfig factory to create a centralized configuration object that can be reused throughout your application. Check out the API Reference to learn more about the available configuration options.

Environment variables​

Create a .env.local file in the root of your project and define the following environment variables:

.env.local
GLOBAL_CONFIG="Vercel Global Config connection string"
VERCEL_AUTH_TOKEN="Vercel API token with Global Config write access"
VERCEL_TEAM_ID="Vercel team ID"
KRAKEN_AUTH_ENDPOINT="https://auth.xxxx-kraken.systems/"
KRAKEN_ACCESS_TOKEN_ISSUERS="https://api.xxxx-kraken.systems/v1/graphql/,https://auth.xxxx-kraken.systems/token/,https://support.xxxx-kraken.systems"
KRAKEN_GRAPHQL_ENDPOINT="Kraken GraphQL endpoint URL"
KRAKEN_X_CLIENT_IP_SECRET_KEY="Client IP secret key"
Environment variables

The use of environment variables is strongly recommended for supported options. When deploying to Vercel, configure these environment variables in your project settings for preview and production deployments.

Configuration object​

Create a configuration object that defines the API routes and app routes for your authentication flow. The paths in apiRoutes must match the actual API route files you'll create in the next section.

@/lib/auth/config.ts
import { createAuthConfig } from "@krakentech/blueprint-auth";

export const authConfig = createAuthConfig({
apiRoutes: {
login: "/api/auth/login",
logout: "/api/auth/logout",
graphql: { kraken: "/api/graphql/kraken" },
session: "/api/auth/session",
},
appRoutes: {
dashboard: { pathname: "/dashboard" },
home: { pathname: "/" },
login: { pathname: "/login" },
},
});

Trusted request origins​

The cookie-authenticated POST handlers require a list of trusted origins. This requirement applies to createLoginHandler, createLogoutHandler, and createGraphQLHandler. Add every origin that serves your app. An origin contains the scheme, host, and optional port. It does not contain a path, query, or fragment.

Each request must include an Origin header or a Referer header. The handler rejects a request that has neither header. The handler checks every source header that is present. If the request has both headers, both origins must be in the list.

Set the comma-separated ALLOWED_REQUEST_ORIGINS environment variable:

.env.local
ALLOWED_REQUEST_ORIGINS="https://www.example.com,http://localhost:3000"

You can set the list in the configuration instead:

lib/auth/config.ts
validation: {
allowedRequestOrigins: ["https://www.example.com", "http://localhost:3000"],
},

Use the same scheme that your server uses. Do not add a trailing path. createSessionHandler handles GET requests, so it does not use this check. See the createAuthConfig validation options for more information.

Token verification​

KRAKEN_ACCESS_TOKEN_ISSUERS is a comma-separated list. An issuer is the iss field in an access token. Use the exact issuers for your Kraken environment. Matching is case-sensitive. A trailing slash changes the issuer. For OAuth, add token/ to the exact Kraken auth endpoint. Replace the example domains above with the values for your Kraken environment.

KRAKEN_AUTH_ENDPOINT must use HTTPS. For local development, HTTP is accepted only for localhost, 127.0.0.1, and [::1]. Blueprint Auth gets the public JSON Web Key Set (JWKS) from .well-known/jwks.json at this endpoint. See Session management for an overview of the session lifecycle.

Server cache​

For organization auth, create one server-only cache adapter. This guide uses the supplied Vercel Global Config adapter.

For user and viewer auth, sessions, login, logout, and OAuth, skip this step. Omit the cacheAdapter import below and use createServerSideAuth(authConfig). You can also omit @vercel/global-config and the GLOBAL_CONFIG, VERCEL_AUTH_TOKEN, and VERCEL_TEAM_ID variables. Calling the factory's getAuth.org without an adapter fails with AuthMissingPropertiesError for cacheAdapter.

Pages Router does not support the server-only marker. Import this cache module only from getServerSideProps, API Routes, and other server modules. Do not import it from a component or client module.

lib/auth/cache.ts
import { createGlobalConfigCacheAdapter } from "@krakentech/blueprint-auth/cache/global-config";

export const cacheAdapter = createGlobalConfigCacheAdapter();

The adapter stores organization tokens. It checks that its required environment values are present. It also parses the store ID and creates the Global Config client when this module loads. The cache provider checks the Vercel settings and permissions during a read or write.

Middleware​

Create a Next.js middleware (version ≤15) or proxy (version ≥16) using the createAuthMiddleware factory.

The examples use the recommended catch-all matcher.

middleware.ts
import { createAuthMiddleware } from "@krakentech/blueprint-auth/middleware";
import { authConfig } from "@/lib/auth/config";

export const middleware = createAuthMiddleware(authConfig);

export const config = {
matcher: ["/", "/((?!api|_next|_vercel|.*\\..*).*)"],
};
Post-auth middleware mutations

If you modify headers after authMiddleware has run, wrap the final response with forwardHeaders to ensure they are visible in getServerSideProps via context.req.headers.

API routes​

You need to create 4 different API routes to handle the authentication flow. We recommend using the following structure:

  • pages/api/auth/login.ts
  • pages/api/auth/logout.ts
  • pages/api/auth/session.ts
  • pages/api/graphql/kraken.ts

Login API handler​

For email-and-password login, create the handler described in Email & password: login API handler. The route must match apiRoutes.login in your configuration. OAuth uses its own callback flow instead.

Logout API handler​

Create the logout API handler using createLogoutHandler.

pages/api/auth/logout.ts
import { createLogoutHandler } from "@krakentech/blueprint-auth/server";
import { authConfig } from "@/lib/auth/config";

export default createLogoutHandler(authConfig);

Session API handler​

Create the session API handler using createSessionHandler.

pages/api/auth/session.ts
import { createSessionHandler } from "@krakentech/blueprint-auth/server";
import { authConfig } from "@/lib/auth/config";

export default createSessionHandler(authConfig);

GraphQL API handler​

Create the GraphQL API handler using createGraphQLHandler.

pages/api/graphql/kraken.ts
import { createGraphQLHandler } from "@krakentech/blueprint-auth/server";
import { authConfig } from "@/lib/auth/config";

export default createGraphQLHandler(authConfig);

Client​

The package provides React hooks to handle the authentication flow, as well as a context provider in order to make configuration available across your app.

Context providers​

Context providers

The React hooks created using createClientSideAuth rely on the AuthProvider for configuration, and QueryClientProvider from @tanstack/react-query for data fetching.

Wrap your entire application with AuthProvider and QueryClientProvider.

pages/_app.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { AppProps } from "next/app";
import { useState } from "react";
import { AuthProvider } from "@/lib/auth/client";

export default function App({ Component, pageProps }: AppProps) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</QueryClientProvider>
);
}

Client Functions​

Create the client-side functions using createClientSideAuth.

lib/auth/client.ts
import { createClientSideAuth } from "@krakentech/blueprint-auth/client";
import { authConfig } from "./config";

export const {
AuthProvider,
useGraphQLClient,
useKrakenAuthErrorHandler,
useLogin,
useLogout,
useSession,
} = createClientSideAuth(authConfig, {
defaultTarget: "kraken",
router: "pages-router",
});

Server Side Rendering​

For pages that depend on authenticated GraphQL data, use the createServerSideAuth factory to bind configuration and an optional cache adapter. Keep the request context explicit.

lib/auth/server.ts
import { createServerSideAuth } from "@krakentech/blueprint-auth/server";
import { cacheAdapter } from "./cache";
import { authConfig } from "./config";

export const {
getAuth,
getGraphQLClient,
getSession,
prefetchSession,
redirectToLogin,
} = createServerSideAuth(authConfig, { cacheAdapter });

Recipes​

This section provides examples of how to use the authentication hooks in your application.

Login form​

Choose the authentication method your Kraken environment supports:

Logout button​

Create a logout button using the useLogout hook.

components/LogoutButton.tsx
import { useLogout } from "@/lib/auth/client";

export function LogoutButton() {
const logout = useLogout();

return (
<button onClick={() => logout.mutate()} disabled={logout.isPending}>
Logout
</button>
);
}

Supporting Internationalization (i18n)​

If your application supports multiple languages with localized URLs, configure the auth package to use your i18n library's pathname translation.

Next.js Native i18n vs Auth Config

The Pages Router has native i18n support for locale detection and prefixing (e.g., /fr/dashboard). However, the auth package's i18n config is needed when you use translated route segments (e.g., /fr/tableau-de-bord instead of /fr/dashboard).

Configuration with next-intl​

lib/auth/config.ts
import { createAuthConfig } from "@krakentech/blueprint-auth";
import { getPathname } from "@/i18n/navigation";
import { hasLocale } from "next-intl";
import { routing } from "@/i18n/routing";

export const authConfig = createAuthConfig({
appRoutes: {
/* ... */
},
i18n: {
localeCookie: "NEXT_LOCALE",
getLocalizedPathname({ locale, url, ...href }) {
const requestedLocale = locale ?? url.pathname.split("/")[1];
return getPathname({
href,
locale: hasLocale(routing.locales, requestedLocale)
? requestedLocale
: routing.defaultLocale,
});
},
},
// ... other config
});

See the next-intl documentation for setup instructions.

How It Works​

When configured:

  • Middleware: Matches routes using localized pathnames
  • Redirects: Sends users to localized destinations after login/logout
  • Error Handling: Redirects to localized login pages on auth errors
Comprehensive Guide

For detailed examples including URL-based locale detection, simple path prefix patterns, and how to use with Next.js native i18n, see the i18n guide.

Using session data​

Create a component rendering different UI based on the user's authentication status using the useSession hook.

components/Header.tsx
import { useSession } from "@/lib/auth/client";
import { Logo } from "@/components/Logo";
import { LogoutButton } from "@/components/LogoutButton";
import { MasqueradeIcon } from "@/icons/MasqueradeIcon";
import { NavigationMenu } from "@/components/NavigationMenu";
import { UserMenu } from "@/components/UserMenu";

export function Header() {
const { data: session, isError, isFetched } = useSession();

return (
<header>
<NavigationMenu />
<Logo />
{!isFetched ? (
<p role="status">Loading session...</p>
) : isError ? (
<p role="alert">Unable to load the session.</p>
) : (
<>
{session.authMethod === "masquerade" && <MasqueradeIcon />}
{session.isAuthenticated ? <LogoutButton /> : <UserMenu />}
</>
)}
</header>
);
}

Prefetching session data​

You can prefetch the session data on the server by using the prefetchSession function. This will ensure session data is available on the client-side as soon as the page loads.

Server-side rendering

This is only relevant in pages using both the useSession hook, and server-side rendering with getServerSideProps.

pages/_app.tsx
import type { AppProps } from "next/app";
import { useState } from "react";
import {
type DehydratedState,
HydrationBoundary,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import { AuthProvider } from "@/lib/auth/client";

export default function App({
Component,
pageProps,
}: AppProps<{ dehydratedState?: DehydratedState }>) {
const [queryClient] = useState(() => {
return new QueryClient({
defaultOptions: {
queries: { staleTime: 60 * 1000 },
},
});
});

return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary state={pageProps.dehydratedState}>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</HydrationBoundary>
</QueryClientProvider>
);
}
pages/subscribe.tsx
import type { GetServerSidePropsContext } from "next";
import { QueryClient, dehydrate } from "@tanstack/react-query";
import { prefetchSession } from "@/lib/auth/server";
import { useSession } from "@/lib/auth/client";

export default function SubscribePage() {
const { data: session, isError, isFetched } = useSession();

if (!isFetched) {
return <p role="status">Loading session...</p>;
}

if (isError) {
return <p role="alert">Unable to load the session.</p>;
}

if (!session.isAuthenticated) {
return <NewUserSubscribeForm />;
}

return <ExistingUserSubscribeForm />;
}

export async function getServerSideProps(context: GetServerSidePropsContext) {
const queryClient = new QueryClient();

await prefetchSession({
context,
queryClient,
});

return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}

GraphQL queries in getServerSideProps​

pages/dashboard/accounts/[accountNumber].tsx
import { BlueprintAuthErrorCode } from "@krakentech/blueprint-auth";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import * as z from "zod";
import { PropertyCard } from "@/components/PropertyCard";
import {
getAuth,
getGraphQLClient,
redirectToLogin,
} from "@/lib/auth/server";
import { graphql } from "@/lib/graphql";

export default function AccountPage({
account,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div>
<h1>Account #{account.number}</h1>
<h2>Properties</h2>
{account.properties.map((property) => (
<PropertyCard key={property.id} property={property} />
))}
</div>
);
}

export const AccountPageQuery = graphql(`
query AccountPageQuery($accountNumber: String!) {
account(accountNumber: $accountNumber) {
number
properties {
id
address
}
}
}
`);

const accountSchema = z.object({
number: z.string().min(1),
properties: z.array(
z.object({ address: z.string().min(1), id: z.string().min(1) }),
),
});

export async function getServerSideProps(
context: GetServerSidePropsContext<{ accountNumber: string }>,
) {
const accountNumber = context.params?.accountNumber;

if (!accountNumber) {
return { notFound: true };
}

const auth = await getAuth.user({ context });
if (!auth) {
return redirectToLogin({
context,
errorCode: BlueprintAuthErrorCode.AuthenticationRequired,
});
}

const graphqlClient = getGraphQLClient.user({ auth });
const { account } = await graphqlClient.request(AccountPageQuery, {
accountNumber,
});

const result = accountSchema.safeParse(account);

if (!result.success) {
return { notFound: true };
}

return {
props: {
account: result.data,
},
};
}
Data validation

GraphQL query fields are often nullable. Validate the response before you pass it to the page. This example returns a not found response when the account data is invalid.

Error handling

The example does not catch request errors. Handle expected errors in getServerSideProps. Use an error boundary in _app.tsx for unexpected render errors.

GraphQL queries with @tanstack/react-query​

This example uses gql.tada for type-safe GraphQL queries. The graphql function is exported from your project's GraphQL setup.

queries/account.ts
import type { BlueprintGraphQLClient } from "@krakentech/blueprint-auth";
import { queryOptions, skipToken, useQuery } from "@tanstack/react-query";
import * as z from "zod";
import { graphql } from "@/lib/graphql";
import { useGraphQLClient } from "@/lib/auth/client";

export const AccountQuery = graphql(`
query AccountPageQuery($accountNumber: String!) {
account(accountNumber: $accountNumber) {
number
properties {
id
address
}
}
}
`);

const accountSchema = z.object({
number: z.string().min(1),
properties: z.array(
z.object({ address: z.string().min(1), id: z.string().min(1) }),
),
});

export function accountQueryOptions(
graphQLClient: BlueprintGraphQLClient,
accountNumber?: string,
) {
return queryOptions({
queryKey: ["account", accountNumber],
queryFn: accountNumber
? async () => {
const { account } = await graphQLClient.request(AccountQuery, {
accountNumber,
});
return accountSchema.parse(account);
}
: skipToken,
});
}

export function useAccount(accountNumber?: string) {
const graphQLClient = useGraphQLClient();
return useQuery(accountQueryOptions(graphQLClient, accountNumber));
}
pages/accounts/[accountNumber].tsx
import { useRouter } from "next/router";
import { ErrorMessage } from "@/components/ErrorMessage";
import { PropertyCard } from "@/components/PropertyCard";
import { useAccount } from "@/queries/account";

export default function AccountPage() {
const router = useRouter();
const queryValue = router.query.accountNumber;
const accountNumber = router.isReady
? Array.isArray(queryValue)
? queryValue[0]
: queryValue
: undefined;
const { data: account, error, isError, isPending } =
useAccount(accountNumber);

if (!router.isReady) {
return <p role="status">Loading account...</p>;
}

if (!accountNumber) {
return <p role="alert">The account number is missing.</p>;
}

if (isPending) {
return <p role="status">Loading account...</p>;
}

if (isError) {
return <ErrorMessage error={error} />;
}

if (!account) {
return <p role="alert">Account not found.</p>;
}

return (
<div>
<h1>Account #{account.number}</h1>
<h2>Properties</h2>
{account.properties.map((property) => (
<PropertyCard key={property.id} property={property} />
))}
</div>
);
}
Prefetching with React Query

Queries made with @tanstack/react-query can be prefetched on the server. Check out our Prefetching session data recipe, or @tanstack/react-query's documentation to learn how to enable prefetching.

Learn more

Building on top of the example above, you can prefetch the account data by defining a getServerSideProps function like this:

pages/accounts/[accountNumber].tsx
import { BlueprintAuthErrorCode } from "@krakentech/blueprint-auth";
import { dehydrate, QueryClient } from "@tanstack/react-query";
import type { GetServerSidePropsContext } from "next";
import {
getAuth,
getGraphQLClient,
prefetchSession,
redirectToLogin,
} from "@/lib/auth/server";
import { accountQueryOptions } from "@/queries/account";

export default function AccountPage() {
// ...
}

export async function getServerSideProps(
context: GetServerSidePropsContext<{ accountNumber: string }>,
) {
const accountNumber = context.params?.accountNumber;

if (!accountNumber) {
return { notFound: true };
}

const queryClient = new QueryClient();
const auth = await getAuth.user({ context });
if (!auth) {
return redirectToLogin({
context,
errorCode: BlueprintAuthErrorCode.AuthenticationRequired,
});
}

const graphqlClient = getGraphQLClient.user({ auth });

await Promise.all([
queryClient.prefetchQuery(
accountQueryOptions(graphqlClient, accountNumber),
),
prefetchSession({
context,
queryClient,
}),
]);

return {
props: {
dehydratedState: dehydrate(queryClient),
},
};
}

Next Steps​

Explore these features after you complete the basic setup:

For detailed information about all available functions, types, and configuration options, see the API Reference.