Anonymous authentication
Overviewβ
Anonymous authentication grants temporary, scoped access to specific resources
without requiring a normal login. An expiring URL is the entry point to this
flow, not a separate authentication method: it carries a pre-signed key generated
by Kraken, for example /anon/{preSignedKey}/{accountNumber}/feedback.
Blueprint Auth exchanges the key for a scoped access token, verifies it, and
stores it in the accessToken cookie. The verified PRE-SIGNED-TOKEN grant
identifies the session as authMethod: "scoped".
The customer feedback form is one use case: a customer can follow a link to submit feedback without signing in through the normal login screen.
The pre-signed key and access token have separate expiration times. The key
controls whether Kraken can issue a token; the token's verified exp claim
controls how long that session can be used. Scoped tokens are not automatically
refreshed. See the expiration FAQ for details.
Getting startedβ
Complete the Getting Started: Pages Router or Getting Started: App Router guide. The App Router examples require Next.js 15 or later and React 19.
Configurationβ
Basic Configuration (single anon path)β
import { createAuthConfig } from "@krakentech/blueprint-auth";
export const authConfig = createAuthConfig({
appRoutes: {
anon: {
// Extract pre-signed key from URL path
getAnonParams({ url }) {
const [route, preSignedKey] = url.pathname.split("/").filter(Boolean);
if (route === "anon" && preSignedKey) {
return { preSignedKey };
}
},
// Single pathname
pathname: "/anon",
},
dashboard: { pathname: "/dashboard" },
home: { pathname: "/" },
login: { pathname: "/login" },
},
});
Advanced Configuration (multiple paths with custom redirects)β
import { createAuthConfig } from "@krakentech/blueprint-auth";
export const authConfig = createAuthConfig({
appRoutes: {
anon: {
getAnonParams({ url }) {
// Support /anon/{key}/... paths.
const [route, preSignedKey] = url.pathname.split("/").filter(Boolean);
if (route === "anon" && preSignedKey) {
return { preSignedKey };
}
// Support a feedback path with a ?key={key} search parameter.
if (url.pathname.startsWith("/feedback")) {
return { preSignedKey: url.searchParams.get("key") };
}
},
// Multiple pathnames
pathname: ["/anon", "/feedback"],
// Optional: Custom redirect on success
customSuccessResponse({ url }, { redirect }) {
// Example: redirect to clean URL without /anon prefix
const cleanPath = url.pathname.replace("/anon", "/feedback");
return redirect(new URL(cleanPath, url.origin));
},
// Optional: Custom error handling
customErrorResponse({ url, errorCode }, { redirect }) {
return redirect(new URL(`/error?code=${errorCode}`, url.origin));
},
},
dashboard: { pathname: "/dashboard" },
home: { pathname: "/" },
login: { pathname: "/login" },
},
});
Configuration Optionsβ
| Option | Type | Required | Description |
|---|---|---|---|
pathname | string | string[] | β | Routes where anonymous auth is enabled. Supports glob patterns and dynamic segments. |
getAnonParams | (options: { url: NextURL }) => { preSignedKey: string | null | undefined } | undefined | β | Extracts preSignedKey from URL |
customSuccessResponse | (options: { url: NextURL }, helpers: { redirect, rewrite }) => NextResponse | undefined | Promise<NextResponse | undefined> | β | Override redirect after successful authentication |
customErrorResponse | (options: { url: NextURL; errorCode: ErrorCode }, helpers: { redirect, rewrite }) => NextResponse | undefined | Promise<NextResponse | undefined> | β | Override redirect on authentication failure |
Providing a parent route in pathname automatically grants access to all child
routes. For example:
pathname: "/anon"grants access to/anon/{key},/anon/{key}/account/123,/anon/{key}/account/123/feedback, etc.- This is useful for multi-step forms where the scoped token persists across
multiple pages (e.g.,
/anon/{key}/feedback/step-1,/anon/{key}/feedback/step-2,/anon/{key}/feedback/step-3)
Use allowList to make specific anon subroutes publicly accessible without a
pre-signed key. Both glob patterns and Next.js dynamic segment syntax are
supported:
anon: {
pathname: "/anon",
getAnonParams({ url }) { /* ... */ },
allowList: ["/anon/public/**"], // No pre-signed key required
}
See the API reference for all supported formats, and the i18n guide for information about localized dynamic routes.
A scoped session can access an anonymous route. It can also access a route that matches both the anonymous and dashboard settings. It cannot access a standard dashboard route.
Middlewareβ
- 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|.*\\..*).*)"],
};
See matcher coverage for the recommended pattern and exclusions.
Anonymous routeβ
Create a page that accepts a pre-signed key and account number in the URL path.
The middleware gets a scoped access token when the user opens the URL. It
verifies the token and stores it in accessToken. Call getAuth.user in the
page and deny access if it returns null; do not rely on middleware alone.
Only then pass the resolved auth context to getGraphQLClient.user.
- Pages Router
- App Router
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import { FeedbackForm } from "@/components/FeedbackForm";
import { getAuth, getGraphQLClient } from "@/lib/auth/server";
import { graphql } from "@/lib/graphql";
export default function FeedbackPage({
account,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<>
<h1>Customer Feedback</h1>
<FeedbackForm account={account} />
</>
);
}
const FeedbackFormQuery = graphql(`
query FeedbackForm($accountNumber: String!) {
account(accountNumber: $accountNumber) {
id
number
balance
}
}
`);
export async function getServerSideProps(
context: GetServerSidePropsContext<{
preSignedKey: string;
accountNumber: string;
}>,
) {
const accountNumber = context.params?.accountNumber;
if (!accountNumber) return { notFound: true };
// Middleware already set the scoped accessToken cookie.
const auth = await getAuth.user({ context });
if (!auth) return { notFound: true };
const graphQLClient = getGraphQLClient.user({ auth });
// Fetch data for the page
const { account } = await graphQLClient.request(FeedbackFormQuery, {
accountNumber,
});
if (!account) return { notFound: true };
return { props: { account } };
}
import { getAuth, getGraphQLClient } from "@/lib/auth/server";
import { FeedbackForm } from "@/components/FeedbackForm";
import { graphql } from "@/lib/graphql";
import { notFound } from "next/navigation";
const FeedbackFormQuery = graphql(`
query FeedbackForm($accountNumber: String!) {
account(accountNumber: $accountNumber) {
id
number
balance
}
}
`);
export default async function FeedbackPage({
params,
}: PageProps<"/anon/[preSignedKey]/[accountNumber]/feedback">) {
const { accountNumber } = await params;
// Resolve the scoped token that middleware stored.
const auth = await getAuth.user();
if (!auth) notFound();
const graphQLClient = getGraphQLClient.user({ auth });
const { account } = await graphQLClient.request(FeedbackFormQuery, {
accountNumber,
});
if (!account) notFound();
return (
<>
<h1>Customer Feedback</h1>
<FeedbackForm account={account} />
</>
);
}
Session behaviorβ
After a pre-signed key establishes a scoped web session, its fields are:
| Field | Value | Description |
|---|---|---|
authMethod | "scoped" | The verified grant is PRE-SIGNED-TOKEN |
authSource | "web" | The token came from the web accessToken cookie |
isAuthenticated | true | The request has a verified user token |
βAnonymousβ means the user did not go through the normal login flow. It does not
mean isAuthenticated is false or that the token grants full account access.
Require authentication on the serverβ
Middleware is not a substitute for checking auth where access is required. Call
getAuth.user in the page, handler, or server action and deny access if it returns
null. Only then create getGraphQLClient.user({ auth }) with the resolved auth
context, as in the route examples above. Continue to enforce resource-specific
authorization on the server.
Existing sessionsβ
When a pre-signed key is exchanged successfully, Blueprint Auth keeps an existing
non-scoped session if both tokens identify the same issuer (iss) and user
(sub). Otherwise, it replaces the session with the verified scoped token.
Tailor the UIβ
Use authMethod to distinguish scoped access from a normal login. For example,
an App Router Server Component can show a different access notice:
import { getSession } from "@/lib/auth/server";
export async function AccessNotice() {
const session = await getSession();
if (!session.isAuthenticated) return null;
return (
<p>
{session.authMethod === "scoped"
? "You have limited access through this link."
: "You are signed in."}
</p>
);
}
This example only changes presentation; it does not protect access. See Session management for client-side usage, verification, and the general session lifecycle.
Security Considerationsβ
- Pre-signed keys are sensitive: Treat them like passwords
- Use HTTPS only: Never send keys over unencrypted connections
- Time-limit tokens: Set appropriate expiration times when generating keys
- Validate scope server-side: Always verify the token scope matches the accessed resource
- No automatic refresh: The auth package does not automatically refresh expired tokens (users can re-authenticate using the same pre-signed key)
FAQβ
How does an expiring URL authenticate a user?
- The user opens a URL containing a pre-signed key generated by Kraken.
- Middleware extracts the key using
getAnonParamsand calls theobtainKrakenTokenmutation. - Blueprint Auth verifies the returned access token, including its
PRE-SIGNED-TOKENgrant, before changing the session. - Unless it preserves an existing non-scoped login for the same identity, it
stores the scoped token in
accessToken. The cookie expiry comes from the token's verifiedexpclaim. - The user can access the configured anonymous route within the token's scope.
Can I use a custom URL path or query parameter?
Yes. Set appRoutes.anon.pathname to the path or paths you want to support, and
use getAnonParams to extract the key. The key does not have to be the first path
segment after /anon; it can appear elsewhere in the path or in a query
parameter.
See the multiple-path configuration
for an example using both /anon/{key} and /feedback?key={key}. Create the
corresponding page in your router and check
matcher coverage
for custom paths.
How long does an expiring URL remain valid?
The pre-signed key's validity is controlled by Kraken. While the key remains valid, it can be reused to obtain scoped access tokens. Its expiration is separate from the expiration of any token it has already issued.
Each scoped access token has its own exp claim. Blueprint Auth uses that
verified value for the access-token cookie expiry; it does not set one universal
lifetime for all expiring URLs.
Scoped tokens are not automatically refreshed. When a token expires, the user can revisit the URL to obtain another token only if the pre-signed key is still valid. Once the key expires, the user needs a new valid link.
What happens if the key is expired or invalid?
If the key exchange or token verification fails, Blueprint Auth does not create
a scoped session. By default, middleware redirects to the login page with an
error code. Use appRoutes.anon.customErrorResponse to return a custom response,
such as an expired-link page.
Blueprint Auth keeps an existing non-scoped session on failure. If the existing session is scoped, it removes the selected managed scoped-token cookies. If verification keys are unavailable, middleware returns HTTP 503 rather than redirecting to login.
Next Stepsβ
Now that you have anonymous authentication configured, explore these related guides:
- Kraken OAuth: Enable OAuth-based authentication flows
- Masquerade: Implement staff impersonation for support scenarios
- Organization-Scoped Auth: Restrict authentication to specific organizations
API Referenceβ
Quick reference to relevant functions:
createAuthConfig: Configure anonymous auth routescreateAuthMiddleware: Enable middleware handlinggetAuth.user: Resolve the scoped tokengetGraphQLClient.user: Make authenticated GraphQL requests with the resolved context