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
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.
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
- npm
- yarn
- bun
pnpm add @krakentech/blueprint-auth @tanstack/react-query @vercel/global-config graphql-request
npm install @krakentech/blueprint-auth @tanstack/react-query @vercel/global-config graphql-request
yarn add @krakentech/blueprint-auth @tanstack/react-query @vercel/global-config graphql-request
bun add @krakentech/blueprint-auth @tanstack/react-query @vercel/global-config graphql-request
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:
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"
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.
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:
ALLOWED_REQUEST_ORIGINS="https://www.example.com,http://localhost:3000"
You can set the list in the configuration instead:
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.
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.
- Next.js ≤15
- Next.js ≥16
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|.*\\..*).*)"],
};
import { createAuthMiddleware } from "@krakentech/blueprint-auth/middleware";
import { authConfig } from "@/lib/auth/config";
export const proxy = createAuthMiddleware(authConfig);
export const config = {
matcher: ["/", "/((?!api|_next|_vercel|.*\\..*).*)"],
};
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.tspages/api/auth/logout.tspages/api/auth/session.tspages/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.
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.
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.
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​
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.
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.
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.
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:
- Kraken OAuth is recommended for new integrations.
- Email & password: Pages Router covers the login API handler, form, redirects, and error handling.
Logout button​
Create a logout button using the useLogout hook.
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.
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​
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
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.
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.
This is only relevant in pages using both the useSession hook, and server-side
rendering with getServerSideProps.
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>
);
}
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​
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,
},
};
}
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.
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.
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));
}
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>
);
}
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:
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:
- Organization-scoped authentication: restrict authentication to an organization.
- Masquerade: let support staff impersonate users.
- Anonymous authentication: provide access with scoped tokens.
- Kraken OAuth: enable OAuth authentication.
For detailed information about all available functions, types, and configuration options, see the API Reference.