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