Skip to main content

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​

Before you begin

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)​

lib/auth/config.ts
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)​

lib/auth/config.ts
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​

OptionTypeRequiredDescription
pathnamestring | 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
Child route access

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)
Public subroutes

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.

Route access

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​

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|.*\\..*).*)"],
};

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/anon/[preSignedKey]/[accountNumber]/feedback.tsx
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 } };
}

Session behavior​

After a pre-signed key establishes a scoped web session, its fields are:

FieldValueDescription
authMethod"scoped"The verified grant is PRE-SIGNED-TOKEN
authSource"web"The token came from the web accessToken cookie
isAuthenticatedtrueThe 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​

Security Notes
  1. Pre-signed keys are sensitive: Treat them like passwords
  2. Use HTTPS only: Never send keys over unencrypted connections
  3. Time-limit tokens: Set appropriate expiration times when generating keys
  4. Validate scope server-side: Always verify the token scope matches the accessed resource
  5. 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?
  1. The user opens a URL containing a pre-signed key generated by Kraken.
  2. Middleware extracts the key using getAnonParams and calls the obtainKrakenToken mutation.
  3. Blueprint Auth verifies the returned access token, including its PRE-SIGNED-TOKEN grant, before changing the session.
  4. 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 verified exp claim.
  5. 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:

API Reference​

Quick reference to relevant functions: