Organization Auth
Introduction​
Organization-scoped authentication enables server-side requests authenticated at the organization level rather than the user level. This authentication method is essential for operations that require authentication but cannot use user-scoped auth (typically when users are unauthenticated or don't have accounts yet).
Unlike user-scoped authentication which uses individual user credentials, organization-scoped auth uses a shared organization access token to perform operations on behalf of the organization. This prevents sensitive data from being publicly accessible while still allowing necessary operations for unauthenticated users.
- Server-side only: Strictly for SSR, API handlers, and Server Actions. Never exposed client-side
- Token storage: Encrypted tokens stored through a shared server cache adapter
- Scheduled refresh: A protected cron job refreshes cached tokens
- Request-time recovery: Server requests recover when no usable cached token is available
Getting started​
This is an advanced authentication feature. Complete the Getting Started: Pages Router or Getting Started: App Router guide first.
Configuration​
Organization-scoped authentication requires a shared server cache and token encryption in addition to the basic auth setup. This guide uses the supplied Vercel Global Config adapter.
See Vercel's Global Config documentation for setup instructions.
Environment variables​
Add these environment variables to your .env.local file and configure them in
your Vercel project settings:
# Token verification
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"
# Organization authentication (Kraken > API Tools > API organisations)
KRAKEN_ORGANIZATION_KEY="your-organization-secret-key"
# Vercel Global Config (automatically configured by Vercel)
GLOBAL_CONFIG="https://global-config.vercel.com/..."
# Vercel API token with access to the owning team and store
# Create one at https://vercel.com/account/tokens
VERCEL_AUTH_TOKEN="your-vercel-api-token"
# Vercel team ID
VERCEL_TEAM_ID="your-vercel-team-id"
# Token encryption
AUTH_ENCRYPTION_KEY="your-encryption-key"
# Cron job authentication (Vercel recommends at least 16 characters)
CRON_SECRET="your-cron-secret"
Generate strong random values for the encryption key and cron secret:
# Encryption key
openssl rand -base64 32
# Cron secret
openssl rand -base64 16
Blueprint Auth creates a new 96-bit initialization vector for each encrypted organization token. It stores this value with the ciphertext. The initialization vector is not a secret.
Treat the encryption key and cron secret as sensitive credentials. Do not commit them to version control.
Auth configuration​
createAuthConfig reads the encryption and Kraken settings from environment
variables. createGlobalConfigCacheAdapter reads the three Vercel settings when
it creates the adapter.
Server cache adapter​
Create one adapter at module scope in a file that browser code cannot import.
For App Router, add import "server-only"; as the first line. Pages Router does
not support this marker, so import the module only from server code.
import { createGlobalConfigCacheAdapter } from "@krakentech/blueprint-auth/cache/global-config";
export const cacheAdapter = createGlobalConfigCacheAdapter();
Adapter creation checks that its required environment values are present. It also parses the store ID and creates the Global Config client. Remote credentials and access are verified during cache operations. See the custom adapter contract when using another shared store.
Server functions​
Resolve organization auth with getAuth.org. The access token must have a
verified API-KEY grant. Then create a client with getGraphQLClient.org.
Use one of the following setups.
- Direct
- Server-side
- App Router
import type { ServerActionContext } from "@krakentech/blueprint-auth";
import { getAuth, getGraphQLClient } from "@krakentech/blueprint-auth/server";
import { cacheAdapter } from "@/lib/auth/cache";
import { authConfig } from "@/lib/auth/config";
import { ProductsQuery } from "./products-query";
export async function getProducts(context: ServerActionContext) {
const auth = await getAuth.org(authConfig, { cacheAdapter, context });
if (!auth) throw new Error("Organization authentication is unavailable.");
const graphQLClient = getGraphQLClient.org(authConfig, { auth });
return graphQLClient.request(ProductsQuery);
}
import { createServerSideAuth } from "@krakentech/blueprint-auth/server";
import { cacheAdapter } from "./cache";
import { authConfig } from "./config";
export const { getAuth, getGraphQLClient } = createServerSideAuth(authConfig, {
cacheAdapter,
});
import { createAppRouterAuth } from "@krakentech/blueprint-auth/server";
import { cookies, headers } from "next/headers";
import { cache } from "react";
import { cacheAdapter } from "./cache";
import { authConfig } from "./config";
export const { getAuth, getGraphQLClient } = createAppRouterAuth(authConfig, {
cache,
cacheAdapter,
cookies,
headers,
});
Token refresh cron job​
Organization tokens expire after 60 minutes. Schedule a token refresh every 30 minutes. This interval reduces the risk that a missed run leaves an expired token.
API handler​
- Pages Router
- App Router
import { createUpdateOrgTokenHandler } from "@krakentech/blueprint-auth/server";
import { cacheAdapter } from "@/lib/auth/cache";
import { authConfig } from "@/lib/auth/config";
export default createUpdateOrgTokenHandler(authConfig, { cacheAdapter });
import { createUpdateOrgTokenHandler } from "@krakentech/blueprint-auth/server";
import { cacheAdapter } from "@/lib/auth/cache";
import { authConfig } from "@/lib/auth/config";
export const GET = createUpdateOrgTokenHandler(authConfig, { cacheAdapter });
Vercel cron configuration​
Create or update vercel.json at the root of your project:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"crons": [
{
"path": "/api/auth/update-org-token",
"schedule": "*/30 * * * *"
}
]
}
This schedule runs at minute 0 and minute 30 of each hour. See Vercel's cron expression documentation for the supported syntax and current limitations.
Add CRON_SECRET to the production environment. Vercel sends this value in the
Authorization header when it invokes the cron job. Treat the value as a
sensitive credential. Never put it in a URL or log. See
Vercel's guidance for securing cron jobs.
Usage​
Organization-scoped authentication is for server-side use only.
Queries​
Use organization-scoped auth when fetching data that isn't publicly accessible for unauthenticated users.
- Pages Router
- App Router
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import { getAuth, getGraphQLClient } from "@/lib/auth/server";
import { graphql } from "@/lib/graphql";
import { ProductCard } from "@/components/ProductCard";
export default function ProductsPage({
products,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div>
<h1>Products</h1>
{products.map((product) => (
<ProductCard key={product.code} product={product} />
))}
</div>
);
}
const ProductsQuery = graphql(`
query SignupProducts {
products(availability: AVAILABLE) {
code
fullName
description
tariffs {
id
standingCharge
unitRate
}
}
}
`);
export async function getServerSideProps(context: GetServerSidePropsContext) {
const auth = await getAuth.org({ context });
if (!auth) throw new Error("Organization authentication is unavailable.");
const graphQLClient = getGraphQLClient.org({ auth });
const { products } = await graphQLClient.request(ProductsQuery);
if (!products?.length) {
return { notFound: true };
}
return {
props: { products },
};
}
import { Suspense } from "react";
import { getAuth, getGraphQLClient } from "@/lib/auth/server";
import { graphql } from "@/lib/graphql";
import { notFound } from "next/navigation";
import { ProductCard } from "@/components/ProductCard";
const ProductsQuery = graphql(`
query SignupProducts {
products(availability: AVAILABLE) {
code
fullName
description
tariffs {
id
standingCharge
unitRate
}
}
}
`);
async function Products() {
const auth = await getAuth.org();
if (!auth) throw new Error("Organization authentication is unavailable.");
const graphQLClient = getGraphQLClient.org({ auth });
const { products } = await graphQLClient.request(ProductsQuery);
if (!products?.length) {
notFound();
}
return products.map((product) => (
<ProductCard key={product.code} product={product} />
));
}
export default function ProductsPage() {
return (
<div>
<h1>Products</h1>
<Suspense fallback={<div>Loading products...</div>}>
<Products />
</Suspense>
</div>
);
}
Mutations​
Use organization-scoped auth for mutations that create or modify data for users who aren't authenticated yet. A common example is account creation.
- Pages Router
- App Router
import type { NextApiRequest, NextApiResponse } from "next";
import { getAuth, getGraphQLClient } from "@/lib/auth/server";
import { graphql } from "@/lib/graphql";
import { z } from "zod";
const CreateAccountMutation = graphql(`
mutation CreateAccount($input: CreateAccountInput!) {
createAccount(input: $input) {
account {
id
number
}
}
}
`);
const createAccountSchema = z.object({
email: z.string().email(),
// ... other fields
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
const result = createAccountSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: "Invalid request data" });
}
const {
email,
// ... other fields
} = result.data;
try {
// The user has no account yet, so use organization auth.
const auth = await getAuth.org({ context: { req, res } });
if (!auth) {
return res.status(503).json({ error: "Authentication unavailable" });
}
const graphQLClient = getGraphQLClient.org({ auth });
const { createAccount } = await graphQLClient.request(
CreateAccountMutation,
{
input: {
email,
// ... other fields
},
},
);
return res.redirect(
303,
`/dashboard/accounts/${createAccount.account.number}`,
);
} catch (error) {
console.error("Account creation failed:", error);
return res.status(500).json({ error: "Account creation failed" });
}
}
"use server";
import { getAuth, getGraphQLClient } from "@/lib/auth/server";
import { graphql } from "@/lib/graphql";
import { redirect } from "next/navigation";
import { z } from "zod";
const CreateAccountMutation = graphql(`
mutation CreateAccount($input: CreateAccountInput!) {
createAccount(input: $input) {
account {
id
number
}
}
}
`);
const createAccountSchema = z.object({
email: z.string().email(),
// ... other fields
});
type CreateAccountState = {
error?: string;
};
export async function createAccount(
prevState: CreateAccountState | null,
formData: FormData,
): Promise<CreateAccountState> {
const rawData = {
email: formData.get("email"),
// ... other fields
};
// Validate form data
const result = createAccountSchema.safeParse(rawData);
if (!result.success) {
return {
error: "Invalid form data",
};
}
const {
email,
// ... other fields
} = result.data;
let accountNumber: string;
try {
const auth = await getAuth.org();
if (!auth) return { error: "Authentication unavailable" };
const graphQLClient = getGraphQLClient.org({ auth });
const { createAccount } = await graphQLClient.request(
CreateAccountMutation,
{
input: {
email,
// ... other fields
},
},
);
accountNumber = createAccount.account.number;
} catch (error) {
console.error("Account creation failed:", error);
return {
error: "Account creation failed",
};
}
redirect(`/dashboard/accounts/${accountNumber}`);
}
How it works​
Blueprint Auth uses this process for organization tokens:
- It reads an encrypted token through the adapter's normal read path.
- It requires a versioned value that contains a unique initialization vector and the ciphertext.
- If the cached value is absent or invalid, it requests another adapter read
with
bypassCache: true. Adapters with another cache layer can use this hint to bypass it. - If no valid value is available, it requests a replacement token from Kraken.
- It encrypts the replacement and tries to store it for later requests.
If Kraken cannot provide a replacement token, organization authentication fails.
Security considerations​
- Environment variables: Treat
KRAKEN_ORGANIZATION_KEY,AUTH_ENCRYPTION_KEY, andCRON_SECRETas highly sensitive credentials - Cron endpoint protection: The token refresh endpoint is protected by
CRON_SECRET. Never expose this value - Token encryption: All cached tokens are encrypted before storage in the shared cache
- Minimal scope: Organization tokens grant broader permissions than user tokens. Use user-scoped auth whenever possible
FAQ​
When should I use organization-scoped vs user-scoped auth?
Use organization-scoped auth when:
- Users are unauthenticated but need to perform authenticated operations (e.g., account creation, signup flows)
- Users don't have accounts yet but need access to organization data (e.g., product catalogs, pricing)
- Operations require organization-level permissions rather than user-level permissions
Use user-scoped auth when:
- User is authenticated and performing actions on their own account
- Accessing user-specific data (account details, usage, billing, payment methods)
- Standard logged-in user workflows (dashboard, settings, account management)
How does token caching work?
Blueprint Auth encrypts organization tokens with AES-256-GCM before it stores them through the configured adapter. Each stored value contains a format version, a new random initialization vector, and the ciphertext.
Version 44 uses a new cache key. It does not read, change, or delete the item that earlier versions use. This isolation prevents a preview deployment from writing a value that an earlier production deployment cannot read.
The cron job requests a new token, encrypts it, and writes it to the version 44 item.
Can I use organization-scoped auth in client components?
No. Organization-scoped auth is strictly server-side only. Exposing the organization token to client-side code would be a critical security vulnerability, granting malicious actors organization-level permissions.
Use it only in:
- Server-Side Rendering (
getServerSideProps,getStaticPropsin Pages Router) - Server Components (App Router)
- API Route Handlers (
pages/api/...in Pages Router,app/.../route.tsin App Router) - Server Actions (App Router)
Next steps​
Now that you have organization-scoped authentication configured, explore these related guides:
- Masquerade Auth: Enable staff impersonation for customer support
- Anonymous Auth: Pre-signed key authentication for temporary access
- Kraken OAuth: Enable OAuth-based authentication flows
API Reference​
Quick reference to relevant functions:
createAuthConfig: Configure Kraken organization credentials and token encryptiongetAuth.org: Resolve organization authenticationgetGraphQLClient.org: Make organization-scoped GraphQL requestscreateUpdateOrgTokenHandler: Cron job handler for automatic token refreshgetSession: Check user authentication status to choose appropriate auth scope