diff --git a/server/public/dashboard-api-keys.html b/server/public/dashboard-api-keys.html
index bc487e67b03..6e9a686c1bc 100644
--- a/server/public/dashboard-api-keys.html
+++ b/server/public/dashboard-api-keys.html
@@ -566,16 +566,23 @@
${escapeHtml(key.name)}
}
const data = await response.json();
- const { org, needsSelection } = DashboardNav.resolveOrg(data.organizations);
+ // Key management requires a selection in this request, including for
+ // single-org users. Do not substitute a remembered or primary org.
+ const selectedOrgIds = new URLSearchParams(window.location.search).getAll('org');
+ const organizations = data.organizations || [];
- if (needsSelection) {
+ if (selectedOrgIds.length === 0 && organizations.length > 0) {
DashboardNav.init({ currentOrgName: 'Select organization' });
- DashboardNav.renderOrgPicker(data.organizations, document.getElementById('loading'));
+ DashboardNav.renderOrgPicker(organizations, document.getElementById('loading'));
return;
}
+ const org = selectedOrgIds.length === 1
+ ? organizations.find(candidate => candidate.id === selectedOrgIds[0])
+ : null;
if (!org) {
- document.getElementById('loading').innerHTML = 'No organization found. Go to dashboard';
+ DashboardNav.init({ currentOrgName: 'Select organization' });
+ document.getElementById('loading').textContent = 'The selected organization is unavailable. Choose an organization from the dashboard.';
return;
}
diff --git a/server/src/auth/organization-principal.ts b/server/src/auth/organization-principal.ts
index dccf139df37..5f4ef47cb06 100644
--- a/server/src/auth/organization-principal.ts
+++ b/server/src/auth/organization-principal.ts
@@ -2,7 +2,7 @@ import type { WorkOSUser } from "../types.js";
export type OrgAuthorizationPrincipal = Pick<
WorkOSUser,
- "id" | "authWorkosUserId"
+ "id" | "authWorkosUserId" | "authorizationSnapshot"
>;
/**
diff --git a/server/src/auth/organization-selection.ts b/server/src/auth/organization-selection.ts
new file mode 100644
index 00000000000..7c8b08124d6
--- /dev/null
+++ b/server/src/auth/organization-selection.ts
@@ -0,0 +1,24 @@
+import type { Request } from 'express';
+
+export class ConflictingOrganizationSelectionError extends Error {}
+
+/** Every explicit selector must agree with the authenticated provider selection.
+ * Request fields cannot switch an organization-bound credential to another org.
+ * Neither memberships nor caches pick an organization. */
+export function selectedOrganizationForAuthentication(
+ req: Partial>,
+ providerOrg?: string,
+): string | null {
+ const selectors = [
+ providerOrg,
+ req.headers?.['x-organization-id'], req.query?.org, req.query?.org_id, req.query?.organization_id, req.query?.organizationId,
+ req.body?.org_id, req.body?.organization_id, req.body?.organizationId,
+ req.params?.org_id, req.params?.orgId, req.params?.organizationId,
+ ].filter((value) => value !== undefined);
+ if (selectors.some((value) => typeof value !== 'string' || !value.trim())) {
+ throw new ConflictingOrganizationSelectionError();
+ }
+ const ids = new Set(selectors.map((value) => (value as string).trim()));
+ if (ids.size > 1) throw new ConflictingOrganizationSelectionError();
+ return ids.values().next().value ?? null;
+}
diff --git a/server/src/auth/workos-jwt.ts b/server/src/auth/workos-jwt.ts
index e8c81cd04a7..8b672736a84 100644
--- a/server/src/auth/workos-jwt.ts
+++ b/server/src/auth/workos-jwt.ts
@@ -7,11 +7,46 @@
* token issued by the MCP OAuth flow works across both surfaces.
*/
-import { createRemoteJWKSet, jwtVerify, type JWTPayload } from 'jose';
+import { createRemoteJWKSet, errors, jwtVerify, type JWTPayload, type JWTVerifyGetKey } from 'jose';
import { createLogger } from '../logger.js';
const logger = createLogger('workos-jwt');
+/** Only token/claim failures prove invalidity; key-service and unknown failures do not. */
+export function isInvalidWorkOSJWTError(error: unknown): boolean {
+ if (!(error instanceof Error)) return false;
+ const code = (error as { code?: unknown }).code;
+ return typeof code === 'string' && [
+ 'ERR_JWT_EXPIRED', 'ERR_JWT_INVALID', 'ERR_JWT_CLAIM_VALIDATION_FAILED',
+ 'ERR_JWS_INVALID', 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED',
+ 'ERR_JWKS_NO_MATCHING_KEY',
+ 'ERR_JOSE_ALG_NOT_ALLOWED', 'ERR_JOSE_NOT_SUPPORTED',
+ ].includes(code);
+}
+
+export class WorkOSJWTUnavailableError extends Error {
+ readonly status = 503;
+
+ constructor() {
+ super('Bearer authentication temporarily unavailable');
+ this.name = 'WorkOSJWTUnavailableError';
+ }
+}
+
+/** Key-service failures can share JOSE codes with malformed token failures. */
+export function unavailableJWTKeyService(resolver: JWTVerifyGetKey): JWTVerifyGetKey {
+ return async (header, token) => {
+ try {
+ return await resolver(header, token);
+ } catch (error) {
+ // A healthy JWKS that does not contain the selected kid definitively
+ // rejects this credential; only key-service failures are unavailable.
+ if (error instanceof errors.JWKSNoMatchingKey) throw error;
+ throw new WorkOSJWTUnavailableError();
+ }
+ };
+}
+
/**
* Read `WORKOS_CLIENT_ID` at call time rather than at module import.
* Vitest can evaluate `workos-jwt.ts` before a test file has had a
@@ -92,15 +127,21 @@ export function looksLikeJWT(token: string): boolean {
export async function verifyWorkOSJWT(token: string): Promise {
const jwksInstance = getJWKS();
- const { payload } = await jwtVerify(token, jwksInstance);
+ const { payload } = await jwtVerify(token,
+ typeof jwksInstance === 'function' ? unavailableJWTKeyService(jwksInstance) : jwksInstance,
+ // WorkOS user-management tokens use RS256. Reject unsupported token
+ // algorithms before consulting a key service whose failure means 503.
+ { algorithms: ['RS256'] },
+ );
const azp = typeof payload.azp === 'string' ? payload.azp : undefined;
const clientIdClaim =
typeof payload.client_id === 'string' ? payload.client_id : undefined;
const applicationId = azp ?? clientIdClaim;
if (!applicationId || applicationId !== workosClientId()) {
- throw new Error(
+ throw new errors.JWTClaimValidationFailed(
`Token application id ("${applicationId ?? 'missing'}") does not match this application`,
+ payload, azp ? 'azp' : 'client_id', applicationId ? 'mismatch' : 'missing',
);
}
diff --git a/server/src/conformance/token-route.ts b/server/src/conformance/token-route.ts
index 1fb6f208c5e..e1f2000b4b7 100644
--- a/server/src/conformance/token-route.ts
+++ b/server/src/conformance/token-route.ts
@@ -10,9 +10,9 @@
* Removed in production builds via NODE_ENV gate.
*/
-import { Router, type Request, type Response } from 'express';
+import { Router, type Request, type Response, type NextFunction } from 'express';
import { requireAuth } from '../middleware/auth.js';
-import { resolveCallerOrgId } from '../routes/helpers/resolve-caller-org.js';
+import { resolveCallerOrgId, sendCallerOrganizationAuthError } from '../routes/helpers/resolve-caller-org.js';
import { createLogger } from '../logger.js';
import { issueConformanceToken } from './token.js';
import { conformanceSessions } from './session-store.js';
@@ -86,7 +86,7 @@ export function buildConformanceTokenRouter(): Router {
router.post('/_debug/run-storyboard', requireAuth, async (req: Request, res: Response) => {
const isStaticAdmin = (req as Request & { isStaticAdminApiKey?: boolean })
.isStaticAdminApiKey === true;
- const callerOrgId = await resolveCallerOrgId(req);
+ const callerOrgId = isStaticAdmin ? null : await resolveCallerOrgId(req);
const bodyOrgId = typeof req.body?.org_id === 'string' ? req.body.org_id : null;
let targetOrgId: string;
@@ -124,5 +124,11 @@ export function buildConformanceTokenRouter(): Router {
});
}
+ // Preserve typed credential failures before the application's generic 5xx
+ // handler can turn unavailable organization verification into a 500.
+ router.use((error: unknown, _req: Request, res: Response, next: NextFunction) => {
+ if (!sendCallerOrganizationAuthError(error, res)) next(error);
+ });
+
return router;
}
diff --git a/server/src/db/client.ts b/server/src/db/client.ts
index cfc6dfb77f9..172688bccfa 100644
--- a/server/src/db/client.ts
+++ b/server/src/db/client.ts
@@ -147,8 +147,65 @@ export function withDatabaseDeadline(
return queryDeadline.run({ deadlineMs, readOnly: options.readOnly ?? true }, work);
}
+function databaseDeadlineExceededError(): Error & { code: '57014' } {
+ return Object.assign(new Error('Database query deadline exceeded'), { code: '57014' as const });
+}
+
+/**
+ * Check out a pooled client without allowing pool saturation to outlive the
+ * caller's absolute deadline. A client delivered after the timer wins is
+ * released immediately so a timed-out request cannot leak a pool slot.
+ */
+async function checkoutClientBeforeDeadline(deadlineMs: number): Promise {
+ const remainingMs = deadlineMs - Date.now();
+ if (remainingMs <= 0) throw databaseDeadlineExceededError();
+
+ const checkout = getPool().connect();
+ let timeout: ReturnType | undefined;
+ let timedOut = false;
+ const deadline = new Promise((_resolve, reject) => {
+ timeout = setTimeout(() => {
+ timedOut = true;
+ reject(databaseDeadlineExceededError());
+ }, remainingMs);
+ });
+
+ try {
+ return await Promise.race([checkout, deadline]);
+ } catch (error) {
+ if (timedOut) {
+ void checkout.then(
+ (lateClient) => lateClient.release(),
+ () => undefined,
+ );
+ }
+ throw error;
+ } finally {
+ if (timeout !== undefined) clearTimeout(timeout);
+ }
+}
+
+async function getClientBeforeDeadline(
+ deadlineMs: number,
+ retryTransientCheckout: boolean,
+): Promise {
+ const attempts = retryTransientCheckout ? 2 : 1;
+ for (let attempt = 0; attempt < attempts; attempt++) {
+ try {
+ return await checkoutClientBeforeDeadline(deadlineMs);
+ } catch (error) {
+ if (attempt === attempts - 1 || !isTransientConnectionError(error) || Date.now() >= deadlineMs) {
+ throw error;
+ }
+ console.warn('Transient DB connection error, retrying client checkout:', (error as Error).message);
+ }
+ }
+ throw databaseDeadlineExceededError();
+}
+
/**
- * Execute one query with server-enforced statement and lock deadlines.
+ * Execute one query with deadline-bounded pool checkout plus server-enforced
+ * statement and lock deadlines.
*
* Use this for public read paths whose inputs can select unusually large
* registry fan-outs. The transaction-local settings ensure PostgreSQL stops
@@ -159,18 +216,22 @@ export async function queryWithTimeout(
text: string,
params: any[] | undefined,
timeoutMs: number,
+ options: { retryTransientCheckout?: boolean } = {},
): Promise> {
const inheritedDeadline = queryDeadline.getStore();
const deadlineMs = Math.min(
Date.now() + timeoutMs,
inheritedDeadline?.deadlineMs ?? Number.POSITIVE_INFINITY,
);
- const client = await getClient();
+ const client = await getClientBeforeDeadline(
+ deadlineMs,
+ options.retryTransientCheckout ?? true,
+ );
let transactionStarted = false;
try {
const effectiveTimeoutMs = deadlineMs - Date.now();
if (effectiveTimeoutMs <= 0) {
- throw Object.assign(new Error('Database query deadline exceeded'), { code: '57014' });
+ throw databaseDeadlineExceededError();
}
await client.query(inheritedDeadline?.readOnly === false ? 'BEGIN' : 'BEGIN READ ONLY');
transactionStarted = true;
diff --git a/server/src/db/user-authorization-snapshot-db.ts b/server/src/db/user-authorization-snapshot-db.ts
new file mode 100644
index 00000000000..263982e1281
--- /dev/null
+++ b/server/src/db/user-authorization-snapshot-db.ts
@@ -0,0 +1,190 @@
+import { isTransientConnectionError, queryWithTimeout } from './client.js';
+
+const SNAPSHOT_TIMEOUT_MS = 2_000;
+
+/** The exact credential and its authority, observed in one primary-database statement. */
+export interface AuthorizationSnapshot {
+ readonly authenticatedUserId: string;
+ readonly canonicalUserId: string;
+ readonly identityId: string | null;
+ readonly selectedOrganizationId: string | null;
+ /** PostgreSQL bigint text: converting to a number would lose revocations above 2^53. */
+ readonly authorizationEpoch: string;
+ readonly credential: Readonly<{
+ email: string | null;
+ emailVerified: boolean;
+ firstName: string | null;
+ lastName: string | null;
+ }>;
+ readonly credentialGrant: Readonly<{
+ id: string;
+ organizationId: string;
+ role: 'owner' | 'admin' | 'member';
+ effectiveFrom: string;
+ effectiveUntil: string | null;
+ }> | null;
+}
+
+/** Distinct from a missing credential or denied organization access. */
+export class AuthorizationSnapshotUnavailableError extends Error {
+ constructor(message = 'Authorization snapshot unavailable') {
+ super(message);
+ this.name = 'AuthorizationSnapshotUnavailableError';
+ }
+}
+
+interface SnapshotRow {
+ in_recovery: boolean;
+ authenticated_user_id: string | null;
+ canonical_user_id: string | null;
+ identity_id: string | null;
+ authorization_epoch: string;
+ email: string | null;
+ email_verified: boolean;
+ first_name: string | null;
+ last_name: string | null;
+ grant_id: string | null;
+ grant_organization_id: string | null;
+ grant_role: 'owner' | 'admin' | 'member' | null;
+ grant_effective_from: string | null;
+ grant_effective_until: string | null;
+}
+
+/** A retry may replace a broken connection, never renew the statement budget. */
+async function querySnapshot(statement: string, parameters: [string, string | null]) {
+ const deadlineMs = Date.now() + SNAPSHOT_TIMEOUT_MS;
+ for (let attempt = 0; attempt < 2; attempt++) {
+ const remainingMs = deadlineMs - Date.now();
+ if (remainingMs <= 0) throw new AuthorizationSnapshotUnavailableError();
+ try {
+ const result = await queryWithTimeout(statement, parameters, remainingMs, {
+ // querySnapshot owns the one retry so checkout and statement failures
+ // share this request's single absolute authorization budget.
+ retryTransientCheckout: false,
+ });
+ if (Date.now() >= deadlineMs) throw new AuthorizationSnapshotUnavailableError();
+ return result;
+ } catch (error) {
+ if (attempt === 1 || !isTransientConnectionError(error) || Date.now() >= deadlineMs) throw error;
+ }
+ }
+ throw new AuthorizationSnapshotUnavailableError();
+}
+
+/**
+ * Never hydrate identity and epoch separately: a binding change can commit
+ * between the reads and stamp old attribution with the new epoch. One SELECT
+ * gives every field the same PostgreSQL MVCC snapshot. The bounded query wrapper
+ * uses the primary pool with transaction-local statement and lock timeouts;
+ * a recovering replica fails closed in this statement.
+ *
+ * Organization selection must be explicit. Neither linked credentials nor the
+ * organization_memberships cache supplies authority on a cold start/cache miss.
+ */
+export async function loadAuthorizationSnapshot(
+ authenticatedUserId: string,
+ selectedOrganizationId: string | null,
+): Promise {
+ const organizationId = selectedOrganizationId || null;
+ try {
+ const result = await querySnapshot(
+ `SELECT pg_catalog.pg_is_in_recovery() AS in_recovery,
+ credential.workos_user_id AS authenticated_user_id,
+ primary_binding.workos_user_id AS canonical_user_id,
+ binding.identity_id,
+ COALESCE(epoch.epoch, 0)::text AS authorization_epoch,
+ credential.email, credential.email_verified, credential.first_name, credential.last_name,
+ credential_grant.id AS grant_id,
+ credential_grant.workos_organization_id AS grant_organization_id,
+ credential_grant.role AS grant_role,
+ to_char(credential_grant.effective_from AT TIME ZONE 'UTC',
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS grant_effective_from,
+ to_char(credential_grant.effective_until AT TIME ZONE 'UTC',
+ 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS grant_effective_until
+ FROM (VALUES (1)) AS anchor(value)
+ LEFT JOIN users credential ON credential.workos_user_id = $1
+ LEFT JOIN identity_workos_users binding
+ ON binding.workos_user_id = credential.workos_user_id
+ LEFT JOIN identity_workos_users primary_binding
+ ON primary_binding.identity_id = binding.identity_id AND primary_binding.is_primary
+ LEFT JOIN authorization_epochs epoch
+ ON epoch.workos_user_id = credential.workos_user_id
+ LEFT JOIN organization_credential_grants credential_grant
+ ON credential_grant.workos_user_id = credential.workos_user_id
+ AND credential_grant.workos_organization_id = $2
+ AND credential_grant.revoked_at IS NULL
+ AND credential_grant.effective_from <= statement_timestamp()
+ AND (credential_grant.effective_until IS NULL
+ OR credential_grant.effective_until > statement_timestamp())`,
+ [authenticatedUserId, organizationId],
+ );
+ const row = result.rows[0];
+ if (!row || row.in_recovery) {
+ throw new AuthorizationSnapshotUnavailableError();
+ }
+ if (!row.authenticated_user_id) return null;
+ // Migration 460 backfills bindings and creates them on every user insert.
+ // Missing attribution is an integrity failure, not a singleton fallback.
+ if (!row.identity_id || !row.canonical_user_id) {
+ throw new AuthorizationSnapshotUnavailableError();
+ }
+
+ const credentialGrant = row.grant_id
+ ? Object.freeze({
+ id: row.grant_id,
+ organizationId: row.grant_organization_id!,
+ role: row.grant_role!,
+ effectiveFrom: row.grant_effective_from!,
+ effectiveUntil: row.grant_effective_until,
+ })
+ : null;
+ return Object.freeze({
+ authenticatedUserId: row.authenticated_user_id,
+ canonicalUserId: row.canonical_user_id,
+ identityId: row.identity_id,
+ selectedOrganizationId: organizationId,
+ authorizationEpoch: row.authorization_epoch,
+ credential: Object.freeze({
+ email: row.email,
+ emailVerified: row.email_verified,
+ firstName: row.first_name,
+ lastName: row.last_name,
+ }),
+ credentialGrant,
+ });
+ } catch (error) {
+ if (error instanceof AuthorizationSnapshotUnavailableError) throw error;
+ // Callers must distinguish unavailable from denied without exposing raw DB
+ // errors or reusing stale identity, organization, or grant cache entries.
+ throw new AuthorizationSnapshotUnavailableError();
+ }
+}
+
+export function sameAuthorizationIdentity(
+ previous: AuthorizationSnapshot,
+ current: AuthorizationSnapshot,
+): boolean {
+ return previous.authenticatedUserId === current.authenticatedUserId
+ && previous.canonicalUserId === current.canonicalUserId
+ && previous.identityId === current.identityId
+ && previous.authorizationEpoch === current.authorizationEpoch
+ && previous.credential.email === current.credential.email
+ && previous.credential.emailVerified === current.credential.emailVerified;
+}
+
+/** Compare equality, not epoch ordering: deletions and replay can move backwards. */
+export function sameAuthorizationSnapshot(
+ previous: AuthorizationSnapshot,
+ current: AuthorizationSnapshot,
+): boolean {
+ if (!sameAuthorizationIdentity(previous, current)
+ || previous.selectedOrganizationId !== current.selectedOrganizationId) return false;
+ const before = previous.credentialGrant;
+ const after = current.credentialGrant;
+ if (!before || !after) return before === after;
+ return before.id === after.id
+ && before.organizationId === after.organizationId
+ && before.role === after.role
+ && before.effectiveFrom === after.effectiveFrom
+ && before.effectiveUntil === after.effectiveUntil;
+}
diff --git a/server/src/middleware/api-key-format.ts b/server/src/middleware/api-key-format.ts
index bc8a94dc4b9..57c282fc457 100644
--- a/server/src/middleware/api-key-format.ts
+++ b/server/src/middleware/api-key-format.ts
@@ -5,3 +5,23 @@
export function isWorkOSApiKeyFormat(token: string): boolean {
return token.startsWith('wos_api_key_') || token.startsWith('sk_');
}
+
+/**
+ * Recognize an explicitly selected bearer credential before considering cookies.
+ * HTTP authentication schemes are case-insensitive; accept separator spaces
+ * and tabs so alternate header formatting cannot cause credential fallback.
+ * An empty string is a presented, invalid bearer; null means no bearer scheme.
+ */
+export function getBearerToken(authorization: string | undefined): string | null {
+ if (authorization === undefined) return null;
+ let separator = 0;
+ while (separator < authorization.length && authorization[separator] !== ' ' && authorization[separator] !== '\t') separator++;
+ if (authorization.slice(0, separator).toLowerCase() !== 'bearer') return null;
+ while (authorization[separator] === ' ' || authorization[separator] === '\t') separator++;
+ return authorization.slice(separator);
+}
+
+export function getWorkOSApiKeyToken(authorization: string | undefined): string | null {
+ const token = getBearerToken(authorization);
+ return token !== null && isWorkOSApiKeyFormat(token) ? token : null;
+}
diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts
index b1e3c756f70..aed1d64c0cd 100644
--- a/server/src/middleware/auth.ts
+++ b/server/src/middleware/auth.ts
@@ -10,11 +10,11 @@ import {
} from '../auth/admin-access.js';
import { isWebUserAAOAdmin } from '../addie/mcp/admin-tools.js';
import { bansDb } from '../db/bans-db.js';
-import { isWorkOSApiKeyFormat } from './api-key-format.js';
-import { verifyWorkOSJWT, looksLikeJWT } from '../auth/workos-jwt.js';
+import { getBearerToken, getWorkOSApiKeyToken, isWorkOSApiKeyFormat } from './api-key-format.js';
+import { verifyWorkOSJWT, looksLikeJWT, isInvalidWorkOSJWTError, WorkOSJWTUnavailableError } from '../auth/workos-jwt.js';
import { storeRefreshedSession, getRefreshedSession, cleanExpiredRefreshes } from '../db/session-refresh-db.js';
-import { getPool } from '../db/client.js';
-import { getAuthorizationFingerprint } from '../db/authorization-epoch-db.js';
+import { loadAuthorizationSnapshot, AuthorizationSnapshotUnavailableError, type AuthorizationSnapshot } from '../db/user-authorization-snapshot-db.js';
+import { ConflictingOrganizationSelectionError, selectedOrganizationForAuthentication } from '../auth/organization-selection.js';
import { getOrganizationAuthorizationUserId } from '../auth/organization-principal.js';
import { constantTimeEqual } from '../utils/constant-time-equal.js';
import { resolveEffectiveMembership } from '../db/org-filters.js';
@@ -25,6 +25,13 @@ const logger = createLogger('auth-middleware');
const workos = new WorkOS(process.env.WORKOS_API_KEY!, {
clientId: process.env.WORKOS_CLIENT_ID!,
});
+// Credential validation must finish within the request budget. Keep session
+// refresh's client policy separate; never retry an API-key authorization check.
+const apiKeyWorkos = new WorkOS(process.env.WORKOS_API_KEY!, {
+ clientId: process.env.WORKOS_CLIENT_ID!,
+ timeout: 5_000,
+ maxRetries: 0,
+});
const WORKOS_CLIENT_ID = process.env.WORKOS_CLIENT_ID!;
const WORKOS_COOKIE_PASSWORD = process.env.WORKOS_COOKIE_PASSWORD!;
@@ -35,8 +42,8 @@ interface CachedSession {
accessToken: string;
expiresAt: number;
newSealedSession?: string; // Set if session was refreshed
- /** Persisted authorization epoch observed when this entry was stored (#6827). */
- authorizationFingerprint?: string;
+ /** Explicit organization from the authenticated provider session, never inferred. */
+ orgId?: string;
}
const sessionCache = new Map();
@@ -183,6 +190,14 @@ function isTransientAuthError(err: unknown): boolean {
if (!(err instanceof Error)) return false;
const name = err.name;
const code = (err as { code?: unknown }).code;
+ const status = (err as { status?: unknown; statusCode?: unknown }).status
+ ?? (err as { statusCode?: unknown }).statusCode;
+ if (typeof status === 'number' && (status === 408 || status === 429 || (status >= 500 && status < 600))) return true;
+ // jose does not retain the HTTP status when the remote JWKS endpoint fails.
+ // This exact error denotes key-service failure, not token signature failure.
+ if (name === 'JOSEError' && code === 'ERR_JOSE_GENERIC'
+ && (err.message === 'Expected 200 OK from the JSON Web Key Set HTTP response'
+ || err.message === 'Failed to parse the JSON Web Key Set HTTP response as JSON')) return true;
if (name === 'JWKSTimeout' || name === 'TimeoutError' || name === 'AbortError') return true;
if (typeof code === 'string') {
if (code === 'ERR_JWKS_TIMEOUT') return true;
@@ -195,6 +210,16 @@ function isTransientAuthError(err: unknown): boolean {
return false;
}
+/** Keep provider-key failures distinct from malformed sealed credentials. */
+function isInvalidSealedBearerError(error: unknown): boolean {
+ // The SDK verifies the sealed JWT with its own unwrapped key resolver.
+ // Unsupported provider JWKs can throw this code even for a healthy token.
+ if (error instanceof Error && (error as { code?: unknown }).code === 'ERR_JOSE_NOT_SUPPORTED') return false;
+ return isInvalidWorkOSJWTError(error) || (error instanceof Error && (
+ error.name === 'InvalidCharacterError' || error.message === 'Invalid expiration'
+ ));
+}
+
/**
* Invalidate session cache for a specific cookie (e.g., on logout)
*/
@@ -218,6 +243,11 @@ declare global {
interface Request {
user?: WorkOSUser;
accessToken?: string;
+ /** Set only after sealed bearer authentication and fresh snapshot hydration. */
+ authenticatedSealedBearer?: Readonly<{
+ tokenHash: string;
+ authorizationSnapshot: AuthorizationSnapshot;
+ }>;
company?: Company;
companyUser?: CompanyUser;
/** Authority path used by requireAdmin, retained for audited mutations. */
@@ -243,33 +273,57 @@ export interface ValidatedApiKey {
type ApiKeySyntheticUser = WorkOSUser & { isMember?: boolean };
+class ApiKeyAuthorizationUnavailableError extends Error {
+ readonly status = 503;
+
+ constructor() {
+ super('API key authorization unavailable');
+ this.name = 'ApiKeyAuthorizationUnavailableError';
+ }
+}
+
/**
* Validate a WorkOS API key from the Authorization header
- * Returns the validated API key info or null if invalid
+ * Returns an organization-owned key or null if invalid/unsupported.
+ * Provider failures throw: unavailable validation is not evidence of invalidity.
+ * Deliberately uncached so revoke/rotation is checked for every request.
*/
export async function validateWorkOSApiKey(req: Request): Promise {
- const authHeader = req.headers.authorization;
- if (!authHeader?.startsWith('Bearer ')) return null;
-
- const token = authHeader.slice(7); // Remove 'Bearer ' prefix
-
- // WorkOS API keys use 'wos_api_key_' (legacy) or 'sk_' (current) prefix
- if (!isWorkOSApiKeyFormat(token)) return null;
+ const token = getWorkOSApiKeyToken(req.headers.authorization);
+ if (!token) return null;
+ let result: Awaited>;
try {
- const result = await workos.apiKeys.createValidation({ value: token });
- if (!result.apiKey) return null;
-
- return {
- id: result.apiKey.id,
- organizationId: result.apiKey.owner.id,
- name: result.apiKey.name,
- permissions: result.apiKey.permissions,
- };
- } catch (error) {
- logger.debug({ err: error }, 'API key validation failed');
- return null;
+ result = await apiKeyWorkos.apiKeys.createValidation({ value: token });
+ } catch {
+ // The provider authenticates this call with the server's WorkOS secret.
+ // Even a provider 401 may concern that secret, not the presented key.
+ // Do not log upstream errors that may contain the submitted key value.
+ throw new ApiKeyAuthorizationUnavailableError();
+ }
+ if (result.apiKey === null) return null;
+
+ const apiKey = result.apiKey;
+ // Only organization-owned keys are issued by this application. User-owned
+ // keys require their own exact-credential authorization policy.
+ if (apiKey?.owner?.type === 'user') return null;
+ if (
+ !apiKey?.owner || apiKey.owner.type !== 'organization' ||
+ typeof apiKey.id !== 'string' || !apiKey.id ||
+ typeof apiKey.owner.id !== 'string' || !apiKey.owner.id ||
+ typeof apiKey.name !== 'string' ||
+ !Array.isArray(apiKey.permissions) ||
+ !apiKey.permissions.every((permission) => typeof permission === 'string')
+ ) {
+ throw new ApiKeyAuthorizationUnavailableError();
}
+
+ return {
+ id: apiKey.id,
+ organizationId: apiKey.owner.id,
+ name: apiKey.name,
+ permissions: [...apiKey.permissions],
+ };
}
/**
@@ -280,13 +334,7 @@ function apiKeyHasPermission(apiKey: ValidatedApiKey, permission: string): boole
}
async function buildApiKeyUser(apiKey: ValidatedApiKey): Promise {
- let isMember = false;
- try {
- const membership = await resolveEffectiveMembership(apiKey.organizationId);
- isMember = membership.is_member;
- } catch (err) {
- logger.warn({ err, apiKeyId: apiKey.id, organizationId: apiKey.organizationId }, 'Failed to resolve API key owner membership');
- }
+ const membership = await resolveEffectiveMembership(apiKey.organizationId);
return {
id: `api_key_${apiKey.id}`,
@@ -296,10 +344,48 @@ async function buildApiKeyUser(apiKey: ValidatedApiKey): Promise {
+ if (!getWorkOSApiKeyToken(req.headers.authorization)) return false;
+
+ try {
+ const apiKey = await validateWorkOSApiKey(req);
+ if (!apiKey) {
+ res.status(401).json({ error: 'invalid_api_key' });
+ return true;
+ }
+
+ selectedOrganizationForAuthentication(req, apiKey.organizationId);
+
+ // Key and ban authorization are fresh on every use, including replays of
+ // a key that was previously accepted on this process.
+ const banCheck = await bansDb.checkPlatformBanForApiKey(apiKey.id, apiKey.organizationId);
+ if (typeof banCheck.banned !== 'boolean') throw new ApiKeyAuthorizationUnavailableError();
+ if (banCheck.banned) {
+ if (banCheck.ban) sendBanResponse(res, banCheck.ban);
+ else res.status(403).json({ error: 'Account suspended' });
+ return true;
+ }
+
+ const user = await buildApiKeyUser(apiKey);
+ req.user = user;
+ req.accessToken = 'workos-api-key';
+ (req as Request & { apiKey?: ValidatedApiKey }).apiKey = apiKey;
+ } catch (error) {
+ if (sendAuthorizationStateError(error, res)) return true;
+ logger.warn({ path: req.path }, 'API key authorization unavailable');
+ res.status(503).json({ error: 'authorization_unavailable' });
+ return true;
+ }
+
+ next();
+ return true;
+}
+
/**
* Validated OAuth bearer token — a WorkOS-signed JWT issued via the
* OAuth 2.1 flow brokered by mcpAuthRouter. Callers end up here when
@@ -313,19 +399,15 @@ export interface ValidatedBearerJWT {
/**
* Short-lived positive cache for successfully-validated bearer JWTs.
- * Keyed on SHA-256 of the raw token; value includes the synthesized user
+ * Keyed on SHA-256 of the raw token; value includes verified provider claims
* and an expiry that is the minimum of (token exp, now + BEARER_JWT_CACHE_TTL_MS).
*
- * Without this, every REST request with a Bearer JWT incurs a DB round-trip
- * for the local-user lookup — the cookie path has an equivalent cache
- * (`sessionCache`) and its absence here creates a DoS amplifier.
+ * Only signature-verified provider claims live here. Request authority is
+ * hydrated afresh from the primary database; no canonical user or grant is cached.
*/
interface CachedBearerJWT {
- user: WorkOSUser;
- orgId?: string;
+ verified: Awaited>;
expiresAt: number;
- /** Persisted authorization epoch observed when this entry was stored (#6827). */
- authorizationFingerprint?: string;
}
const bearerJwtCache = new Map();
const BEARER_JWT_CACHE_TTL_MS = 60 * 1000;
@@ -351,9 +433,8 @@ function hashBearerToken(token: string): string {
* `enrichUserWithMembership` resolves real membership via the DB.
*/
export async function validateWorkOSBearerJWT(req: Request): Promise {
- const authHeader = req.headers.authorization;
- if (!authHeader?.startsWith('Bearer ')) return null;
- const token = authHeader.slice(7);
+ const token = getBearerToken(req.headers.authorization);
+ if (!token) return null;
if (isWorkOSApiKeyFormat(token)) return null; // handled by validateWorkOSApiKey
if (ADMIN_API_KEY && token === ADMIN_API_KEY) return null; // handled by hasValidAdminApiKey
@@ -362,66 +443,37 @@ export async function validateWorkOSBearerJWT(req: Request): Promise>;
if (cached && cached.expiresAt > now) {
- if (await isAuthorizationFingerprintCurrent(cached.user, cached.authorizationFingerprint)) {
- return { user: cached.user, rawToken: token, orgId: cached.orgId };
+ verified = cached.verified;
+ } else {
+ try {
+ verified = await verifyWorkOSJWT(token);
+ } catch (err) {
+ if (isInvalidWorkOSJWTError(err)) return null;
+ throw new WorkOSJWTUnavailableError();
}
- // Identity bindings changed since this entry was stored — re-verify.
- bearerJwtCache.delete(cacheKey);
- }
-
- let verified: Awaited>;
- try {
- verified = await verifyWorkOSJWT(token);
- } catch (err) {
- logger.debug({ err }, 'Bearer JWT verification failed');
- return null;
}
-
if (verified.isM2M || !verified.sub) return null;
- // Confirm the subject corresponds to a real local user. This catches
- // tokens from WorkOS accounts that have been deleted or never synced,
- // and gives us names for the synthesized WorkOSUser.
- const pool = getPool();
- const localUser = await pool.query<{
- first_name: string | null;
- last_name: string | null;
- email: string | null;
- }>(
- `SELECT first_name, last_name, email FROM users WHERE workos_user_id = $1`,
- [verified.sub],
- );
- if (localUser.rowCount === 0) {
- logger.warn({ sub: verified.sub }, 'Bearer JWT verified but user not found in local DB');
- return null;
- }
-
- const row = localUser.rows[0];
- const email = verified.email ?? row.email ?? '';
- const user: WorkOSUser = {
+ // Only provider authentication is cached. Identity, grants and the epoch
+ // always come from the same fresh primary-database snapshot, even on hits.
+ const user = await hydrateAuthenticatedUser({
id: verified.sub,
- email,
- firstName: row.first_name?.trim() || undefined,
- lastName: row.last_name?.trim() || undefined,
+ email: verified.email ?? '',
emailVerified: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
- };
+ }, selectedOrganizationForAuthentication(req, verified.orgId));
const tokenExpMs = verified.expiresAt ? verified.expiresAt * 1000 : Infinity;
const cacheUntil = Math.min(now + BEARER_JWT_CACHE_TTL_MS, tokenExpMs);
- if (cacheUntil > now) {
+ if ((!cached || cached.expiresAt <= now) && cacheUntil > now) {
if (bearerJwtCache.size >= BEARER_JWT_CACHE_MAX_SIZE) {
const oldest = bearerJwtCache.keys().next().value;
if (oldest) bearerJwtCache.delete(oldest);
}
- bearerJwtCache.set(cacheKey, {
- user,
- orgId: verified.orgId,
- expiresAt: cacheUntil,
- authorizationFingerprint: await authorizationFingerprintFor(user),
- });
+ bearerJwtCache.set(cacheKey, { verified, expiresAt: cacheUntil });
}
return { user, rawToken: token, orgId: verified.orgId };
@@ -703,136 +755,98 @@ if (ADMIN_API_KEY) {
*/
function hasValidAdminApiKey(req: Request): boolean {
if (!ADMIN_API_KEY) return false;
- const authHeader = req.headers.authorization;
- if (!authHeader?.startsWith('Bearer ')) return false;
- const token = authHeader.slice(7);
+ const token = getBearerToken(req.headers.authorization);
+ if (!token) return false;
// Don't match WorkOS API keys - those are handled separately
if (isWorkOSApiKeyFormat(token)) return false;
return constantTimeEqual(token, ADMIN_API_KEY);
}
-/**
- * True if `id` is a synthetic user (static admin API key or per-org WorkOS
- * API key). Synthetic users don't represent a person, so they have no
- * identity binding.
- */
-function isSyntheticUser(id: string): boolean {
- return id === 'admin_api_key' || id.startsWith('api_key_');
-}
-
/**
* Drop any cached sessions whose WorkOS user id matches one of the given
- * ids, on either auth credential (post-swap canonical or actual auth).
- * Called when an identity binding changes (mergeUsers, admin rebind) so
- * subsequent requests re-resolve identity instead of serving a stale swap.
+ * ids. This saves a provider round trip after local mutations; correctness
+ * comes from the fresh database snapshot on every request, on every instance.
*/
export function invalidateSessionsForUsers(workosUserIds: string[]): void {
if (workosUserIds.length === 0) return;
const ids = new Set(workosUserIds);
for (const [key, value] of sessionCache.entries()) {
- if (ids.has(value.user.id) || (value.user.authWorkosUserId && ids.has(value.user.authWorkosUserId))) {
+ if (ids.has(value.user.id)) {
sessionCache.delete(key);
}
}
for (const [key, value] of bearerJwtCache.entries()) {
- if (ids.has(value.user.id) || (value.user.authWorkosUserId && ids.has(value.user.authWorkosUserId))) {
+ if (ids.has(value.verified.sub)) {
bearerJwtCache.delete(key);
}
}
}
-/**
- * Resolve the identity for a WorkOS user. Sets `user.identityId` and, when
- * the authenticated user is a non-primary binding, swaps `user.id` to the
- * identity's primary workos_user_id so app-state reads land on the right
- * person. The original authenticated id is preserved on `user.authWorkosUserId`.
- *
- * Skipped for synthetic users (admin API key, WorkOS API key) — they don't
- * represent a person. Failures are swallowed: identity resolution must
- * never block an authenticated request, and a degraded request that sees
- * only the auth user's slice of data is still better than a 500.
- */
-async function attachIdentityId(user: WorkOSUser): Promise {
- if (isSyntheticUser(user.id)) return;
- try {
- const result = await getPool().query<{
- identity_id: string;
- primary_workos_user_id: string | null;
- }>(
- `SELECT iwu.identity_id, primary_iwu.workos_user_id AS primary_workos_user_id
- FROM identity_workos_users iwu
- LEFT JOIN identity_workos_users primary_iwu
- ON primary_iwu.identity_id = iwu.identity_id
- AND primary_iwu.is_primary = TRUE
- WHERE iwu.workos_user_id = $1`,
- [user.id]
- );
- const row = result.rows[0];
- if (!row) return;
-
- user.identityId = row.identity_id;
+class InvalidAuthorizationCredentialError extends Error {}
+export { ConflictingOrganizationSelectionError, selectedOrganizationForAuthentication } from '../auth/organization-selection.js';
+
+/** Hydrate a new request object; never mutate the provider object in a cache. */
+async function hydrateAuthenticatedUser(user: WorkOSUser, organizationId: string | null): Promise {
+ const authenticatedUserId = getOrganizationAuthorizationUserId(user);
+ const snapshot = await loadAuthorizationSnapshot(authenticatedUserId, organizationId);
+ if (!snapshot) throw new InvalidAuthorizationCredentialError();
+ const hydrated: WorkOSUser = {
+ ...user,
+ id: snapshot.canonicalUserId,
+ authWorkosUserId: snapshot.canonicalUserId !== authenticatedUserId ? authenticatedUserId : undefined,
+ identityId: snapshot.identityId ?? undefined,
+ email: snapshot.credential.email ?? '',
+ emailVerified: snapshot.credential.emailVerified,
+ firstName: snapshot.credential.firstName?.trim() || user.firstName?.trim() || undefined,
+ lastName: snapshot.credential.lastName?.trim() || user.lastName?.trim() || undefined,
+ };
+ // Server-only authority must not leak through JSON responses or user spreads.
+ Object.defineProperty(hydrated, 'authorizationSnapshot', { value: snapshot });
+ return hydrated;
+}
- if (row.primary_workos_user_id && row.primary_workos_user_id !== user.id) {
- // Non-primary binding signed in. Swap id so app-state reads see the
- // canonical person; preserve the actual auth user on authWorkosUserId.
- logger.debug(
- { authWorkosUserId: user.id, canonicalUserId: row.primary_workos_user_id, identityId: row.identity_id },
- 'Identity id-swap: routing non-primary binding to canonical user'
- );
- user.authWorkosUserId = user.id;
- user.id = row.primary_workos_user_id;
- }
- } catch (err) {
- logger.warn({ err, userId: user.id }, 'Failed to resolve identity_id');
- }
+/** Request-local provenance, never retained in the shared cookie/session cache. */
+function markAuthenticatedSealedBearer(req: Request, bearerToken: string | null): void {
+ if (!bearerToken || getBearerToken(req.headers.authorization) !== bearerToken || !req.user?.authorizationSnapshot) return;
+ Object.defineProperty(req, 'authenticatedSealedBearer', {
+ value: Object.freeze({
+ tokenHash: hashSessionCookie(bearerToken),
+ authorizationSnapshot: req.user.authorizationSnapshot,
+ }),
+ configurable: true,
+ });
}
-/**
- * Authorization fingerprint for the credential that actually authenticated
- * this session. `attachIdentityId` may swap `user.id` to the identity's
- * canonical credential, so the authenticated credential is read through
- * `getOrganizationAuthorizationUserId` — that value is stable across the
- * swap, and every identity-binding mutation bumps the epoch for each
- * credential bound to the identity, so a change always moves this value.
- *
- * Synthetic principals (admin API key, WorkOS API key) have no credential
- * row and no identity binding; they return '' and never mismatch.
- */
-async function authorizationFingerprintFor(
- user: WorkOSUser,
-): Promise {
- if (isSyntheticUser(user.id)) return '';
+/** Local dev login is an explicit synthetic-auth bypass. Keep attribution useful
+ * against seeded databases without making a database mandatory for dev fixtures. */
+async function hydrateDevUser(user: WorkOSUser): Promise {
try {
- return await getAuthorizationFingerprint([getOrganizationAuthorizationUserId(user)]);
- } catch (err) {
- // Never fail authentication over the stamp: an unstamped entry reads as
- // stale on its next hit, so the session is re-validated rather than
- // served from a fingerprint we could not confirm.
- logger.warn({ err }, 'Authorization epoch stamp failed — session will not be served from cache');
- return undefined;
+ return await hydrateAuthenticatedUser(user, null);
+ } catch {
+ return user;
}
}
-/**
- * True when a cached entry still reflects persisted authorization state.
- *
- * Entries cached before an identity-binding change carry the pre-change
- * fingerprint and must not be served — on any instance, not just the one
- * that ran the mutation. A lookup failure returns false so the request
- * falls through to full re-validation rather than serving state we could
- * not confirm; that degrades the cache, it does not sign anyone out.
- */
-async function isAuthorizationFingerprintCurrent(
- user: WorkOSUser,
- stamped: string | undefined,
-): Promise {
- if (isSyntheticUser(user.id)) return true;
- try {
- return (await authorizationFingerprintFor(user)) === (stamped ?? '');
- } catch (err) {
- logger.warn({ err }, 'Authorization epoch check failed — bypassing session cache');
- return false;
+function sendAuthorizationStateError(error: unknown, res: Response, includeTransientErrors = false): boolean {
+ if (error instanceof WorkOSJWTUnavailableError || (includeTransientErrors && isTransientAuthError(error))) {
+ logger.warn({ err: error }, 'Authentication service unavailable');
+ res.status(503).json({ error: 'Authentication service temporarily unavailable' });
+ return true;
+ }
+ if (error instanceof AuthorizationSnapshotUnavailableError) {
+ logger.warn({ err: error }, 'Primary authorization snapshot unavailable');
+ res.status(503).json({ error: 'Authorization service temporarily unavailable' });
+ return true;
}
+ if (error instanceof InvalidAuthorizationCredentialError) {
+ res.status(401).json({ error: 'Invalid session', login_url: '/auth/login' });
+ return true;
+ }
+ if (error instanceof ConflictingOrganizationSelectionError) {
+ res.status(403).json({ error: 'An unambiguous organization selection is required' });
+ return true;
+ }
+ return false;
}
/**
@@ -844,7 +858,9 @@ async function isAuthorizationFingerprintCurrent(
* Automatically refreshes expired access tokens using the refresh token
*/
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
- const isHtmlRequest = req.accepts('html') && !req.originalUrl.startsWith('/api/');
+ delete req.authenticatedSealedBearer;
+ const bearerToken = getBearerToken(req.headers.authorization);
+ const isHtmlRequest = bearerToken === null && req.accepts('html') && !req.originalUrl.startsWith('/api/');
// Check for static admin API key first (for internal tooling)
if (hasValidAdminApiKey(req)) {
@@ -864,32 +880,17 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
return next();
}
- // Check for WorkOS API key (for programmatic access)
- const apiKey = await validateWorkOSApiKey(req);
- if (apiKey) {
- logger.debug({ path: req.path, apiKeyId: apiKey.id }, 'Authenticated via WorkOS API key');
- // Create a synthetic user for API key auth - the organization owns the key
- req.user = await buildApiKeyUser(apiKey);
- req.accessToken = 'workos-api-key';
- // Store API key info for permission checks
- (req as Request & { apiKey?: ValidatedApiKey }).apiKey = apiKey;
-
- // Check platform ban for API key
- const apiKeyBan = await checkPlatformBan(
- `apikey:${apiKey.id}`,
- () => bansDb.checkPlatformBanForApiKey(apiKey.id, apiKey.organizationId)
- );
- if (apiKeyBan) {
- logger.info({ apiKeyId: apiKey.id, banId: apiKeyBan.id }, 'API key request blocked by platform ban');
- return sendBanResponse(res, apiKeyBan);
- }
-
- return next();
- }
+ if (await authenticateApiKeyRequest(req, res, next)) return;
// Check for OAuth-issued user JWT (user SSO'd via AuthKit through the
// MCP OAuth flow and is now calling the REST API with that token).
- const jwtAuth = await validateWorkOSBearerJWT(req);
+ let jwtAuth: ValidatedBearerJWT | null;
+ try {
+ jwtAuth = await validateWorkOSBearerJWT(req);
+ } catch (error) {
+ if (sendAuthorizationStateError(error, res, true)) return;
+ throw error;
+ }
if (jwtAuth) {
logger.debug({ path: req.path, userId: jwtAuth.user.id }, 'Authenticated via OAuth user JWT');
req.user = jwtAuth.user;
@@ -897,8 +898,8 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
try {
const userBan = await checkPlatformBan(
- `user:${jwtAuth.user.id}`,
- () => bansDb.checkPlatformBan(jwtAuth.user.id),
+ `user:${getOrganizationAuthorizationUserId(jwtAuth.user)}`,
+ () => bansDb.checkPlatformBan(getOrganizationAuthorizationUserId(jwtAuth.user)),
);
if (userBan) {
logger.info({ userId: jwtAuth.user.id, banId: userBan.id }, 'User request blocked by platform ban');
@@ -908,22 +909,25 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
logger.warn({ err: banError, userId: jwtAuth.user.id, path: req.path }, 'Ban check failed — allowing request through');
}
- await attachIdentityId(req.user);
return next();
}
- // Dev mode: check for dev-session cookie
- if (DEV_MODE_ENABLED) {
+ // A rejected JWT or empty bearer is terminal, including with a valid cookie.
+ if (bearerToken !== null && (!bearerToken || looksLikeJWT(bearerToken) || bearerToken.startsWith('eyJ'))) {
+ return res.status(401).json({ error: 'Invalid bearer token' });
+ }
+
+ // Dev cookies cannot replace an explicitly selected bearer credential.
+ if (DEV_MODE_ENABLED && bearerToken === null) {
const devUser = createDevUser(req);
if (devUser) {
- req.user = devUser;
+ req.user = await hydrateDevUser(devUser);
req.accessToken = 'dev-mode-token';
// Carry over dev config flags (isMember, isAdmin) so enrichUserWithMembership skips DB lookup
const devConfig = getDevUser(req);
if (devConfig) {
(req.user as unknown as Record).isMember = devConfig.isMember;
}
- await attachIdentityId(req.user);
return next();
}
// No dev session - redirect to dev login page
@@ -938,7 +942,7 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
});
}
- const sessionCookie = req.cookies['wos-session'];
+ const sessionCookie = bearerToken === null ? req.cookies['wos-session'] : extractSealedSession(req);
logger.debug({ path: req.path, hasCookie: !!sessionCookie, isHtmlRequest }, 'Authentication check');
@@ -978,20 +982,14 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
deadSessionCache.delete(cacheKey);
}
- if (
- cached &&
- cached.expiresAt > now &&
- !(await isAuthorizationFingerprintCurrent(cached.user, cached.authorizationFingerprint))
- ) {
- // Identity bindings changed since this entry was stored. Drop it so
- // this request re-resolves identity and organization context instead
- // of inheriting pre-change authority.
- sessionCache.delete(cacheKey);
- } else if (cached && cached.expiresAt > now) {
+ if (cached && cached.expiresAt > now) {
// Cache hit - use cached session data
logger.debug({ userId: cached.user.id }, 'Using cached session');
- req.user = cached.user;
+ req.user = await hydrateAuthenticatedUser(
+ cached.user, selectedOrganizationForAuthentication(req, cached.orgId),
+ );
req.accessToken = cached.accessToken;
+ markAuthenticatedSealedBearer(req, bearerToken);
// If session was refreshed, update the cookie
if (cached.newSealedSession) {
@@ -1041,6 +1039,9 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
cookiePassword: WORKOS_COOKIE_PASSWORD,
});
+ if (bearerToken !== null && !refreshResult.authenticated && 'retryable' in refreshResult && refreshResult.retryable) {
+ throw new WorkOSJWTUnavailableError();
+ }
if (refreshResult.authenticated && refreshResult.sealedSession) {
logger.debug({ path: req.path }, 'Session refresh succeeded');
newSealedSession = refreshResult.sealedSession;
@@ -1068,6 +1069,7 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
refreshFailed = true;
}
} catch (refreshError) {
+ if (bearerToken !== null) throw refreshError;
logger.warn({ err: refreshError, path: req.path }, 'Session refresh threw an error');
refreshFailed = true;
}
@@ -1098,6 +1100,9 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
const sharedRefresh = await sharedSessionObj.refresh({
cookiePassword: WORKOS_COOKIE_PASSWORD,
});
+ if (bearerToken !== null && !sharedRefresh.authenticated && 'retryable' in sharedRefresh && sharedRefresh.retryable) {
+ throw new WorkOSJWTUnavailableError();
+ }
if (sharedRefresh.authenticated && sharedRefresh.sealedSession) {
logger.info({ path: req.path }, 'Shared session refresh succeeded');
newSealedSession = sharedRefresh.sealedSession;
@@ -1113,6 +1118,7 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
result = await refreshedObj.authenticate();
}
} catch (innerRefreshErr) {
+ if (bearerToken !== null) throw innerRefreshErr;
logger.warn({ err: innerRefreshErr, path: req.path },
'Shared session refresh also failed');
}
@@ -1122,6 +1128,7 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
}
}
} catch (dbError) {
+ if (bearerToken !== null) throw dbError;
logger.warn({ err: dbError, path: req.path }, 'Failed to look up shared session');
}
}
@@ -1165,54 +1172,11 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
impersonator?: { email: string; reason: string | null };
};
- let firstName = result.user.firstName ?? undefined;
- let lastName = result.user.lastName ?? undefined;
-
- // Verify the user exists in our local DB. This catches stale sessions
- // from deleted/merged WorkOS accounts whose JWTs haven't expired yet.
- // Also resolves names from local DB when WorkOS doesn't provide them.
- try {
- const pool = getPool();
- const localUser = await pool.query<{
- first_name: string | null;
- last_name: string | null;
- }>(
- `SELECT first_name, last_name FROM users WHERE workos_user_id = $1`,
- [result.user.id]
- );
- if (localUser.rows.length === 0) {
- logger.warn({ userId: result.user.id, email: result.user.email, path: req.path },
- 'Authenticated user not found in local DB — forcing re-login');
- sessionCache.delete(cacheKey);
- if (deadSessionCache.size >= DEAD_SESSION_MAX_SIZE) {
- const oldest = deadSessionCache.keys().next().value;
- if (oldest) deadSessionCache.delete(oldest);
- }
- deadSessionCache.set(cacheKey, Date.now());
- if (isHtmlRequest) {
- return res.redirect(`/auth/login?return_to=${encodeURIComponent(req.originalUrl)}`);
- }
- return res.status(401).json({
- error: 'Invalid session',
- message: 'Your account could not be verified. Please log in again.',
- login_url: '/auth/login',
- });
- }
- if (!firstName?.trim()) {
- const row = localUser.rows[0];
- if (row.first_name?.trim()) firstName = row.first_name;
- if (row.last_name?.trim()) lastName = row.last_name;
- }
- } catch (err) {
- // Fail open: DB errors should not block authenticated users
- logger.warn({ err, userId: result.user.id }, 'Local user check failed — allowing request through');
- }
-
const user: WorkOSUser = {
id: result.user.id,
email: result.user.email,
- firstName,
- lastName,
+ firstName: result.user.firstName ?? undefined,
+ lastName: result.user.lastName ?? undefined,
emailVerified: result.user.emailVerified,
createdAt: result.user.createdAt,
updatedAt: result.user.updatedAt,
@@ -1227,20 +1191,22 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
);
}
- // Resolve identityId once before caching so cache hits inherit it.
- await attachIdentityId(user);
+ const hydrated = await hydrateAuthenticatedUser(
+ user, selectedOrganizationForAuthentication(req, result.organizationId),
+ );
- // Cache the validated session
+ // Retain authentication only; derived request authority is never cached.
sessionCache.set(cacheKey, {
- user,
+ user: Object.freeze(user),
accessToken: result.accessToken,
expiresAt: now + SESSION_CACHE_TTL_MS,
newSealedSession,
- authorizationFingerprint: await authorizationFingerprintFor(user),
+ orgId: result.organizationId,
});
- req.user = user;
+ req.user = hydrated;
req.accessToken = result.accessToken;
+ markAuthenticatedSealedBearer(req, bearerToken);
// Check platform ban for cookie-authenticated user (fail-open: DB timeout should not log users out)
try {
@@ -1258,6 +1224,10 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
next();
} catch (error) {
+ if (sendAuthorizationStateError(error, res)) return;
+ if (bearerToken !== null && !isInvalidSealedBearerError(error)) {
+ return res.status(503).json({ error: 'Authentication service temporarily unavailable' });
+ }
if (isTransientAuthError(error)) {
logger.warn({ err: error, path: req.path }, 'Transient auth upstream failure — returning 503 without clearing session');
if (isHtmlRequest) {
@@ -1544,9 +1514,8 @@ export async function requireAdmin(req: Request, res: Response, next: NextFuncti
if (!req.user) {
const mockUser = createDevUser(req);
if (mockUser) {
- req.user = mockUser;
+ req.user = await hydrateDevUser(mockUser);
req.accessToken = 'dev-mode-token';
- await attachIdentityId(req.user);
}
}
@@ -1856,34 +1825,32 @@ export function createRequireWorkingGroupMember(
* Native apps send: Authorization: Bearer
*/
function extractSealedSession(req: Request): string | undefined {
- // First check for cookie (web browsers)
- const sessionCookie = req.cookies['wos-session'];
- if (sessionCookie) {
- return sessionCookie;
- }
-
- // Then check Authorization header (native apps like Tauri)
+ // An explicitly supplied bearer credential takes precedence over a cookie.
+ // A failed JWT must never be retried as a sealed session or replaced by a cookie.
// Format: Authorization: Bearer
- // Note: We differentiate from WorkOS API keys by checking prefix
const authHeader = req.headers.authorization;
- if (authHeader?.startsWith('Bearer ')) {
- const token = authHeader.slice(7);
- // WorkOS API keys use known prefixes, sealed sessions don't
- if (!isWorkOSApiKeyFormat(token)) {
+ if (authHeader !== undefined) {
+ const token = getBearerToken(authHeader);
+ if (token && token.trim() && !isWorkOSApiKeyFormat(token) && !looksLikeJWT(token) && !token.startsWith('eyJ')) {
return token;
}
+ return undefined;
}
-
- return undefined;
+ const sessionCookie = req.cookies?.['wos-session'];
+ return typeof sessionCookie === 'string' && sessionCookie ? sessionCookie : undefined;
}
/**
- * Optional auth middleware - loads user if authenticated, but doesn't require it
+ * Optional auth permits anonymous requests only when no credential is supplied.
+ * Invalid presented credentials return 401; verification or authorization
+ * outages return 503. Neither case falls back to anonymous or another credential.
* Uses in-memory cache to reduce WorkOS API calls for session refresh
* Automatically refreshes expired access tokens using the refresh token
* Supports both cookie-based auth (web) and Authorization header (native apps)
*/
export async function optionalAuth(req: Request, res: Response, next: NextFunction) {
+ delete req.authenticatedSealedBearer;
+ const bearerToken = getBearerToken(req.headers.authorization);
if (hasValidAdminApiKey(req)) {
logger.debug({ path: req.path }, 'Authenticated via static admin API key (optional auth)');
req.user = {
@@ -1900,26 +1867,15 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
return next();
}
- const apiKey = await validateWorkOSApiKey(req);
- if (apiKey) {
- logger.debug({ path: req.path, apiKeyId: apiKey.id }, 'Authenticated via WorkOS API key (optional auth)');
- req.user = await buildApiKeyUser(apiKey);
- req.accessToken = 'workos-api-key';
- (req as Request & { apiKey?: ValidatedApiKey }).apiKey = apiKey;
+ if (await authenticateApiKeyRequest(req, res, next)) return;
- const apiKeyBan = await checkPlatformBan(
- `apikey:${apiKey.id}`,
- () => bansDb.checkPlatformBanForApiKey(apiKey.id, apiKey.organizationId)
- );
- if (apiKeyBan) {
- logger.info({ apiKeyId: apiKey.id, banId: apiKeyBan.id }, 'API key optional-auth request blocked by platform ban');
- return sendBanResponse(res, apiKeyBan);
- }
-
- return next();
+ let jwtAuth: ValidatedBearerJWT | null;
+ try {
+ jwtAuth = await validateWorkOSBearerJWT(req);
+ } catch (error) {
+ if (sendAuthorizationStateError(error, res, true)) return;
+ throw error;
}
-
- const jwtAuth = await validateWorkOSBearerJWT(req);
if (jwtAuth) {
logger.debug({ path: req.path, userId: jwtAuth.user.id }, 'Authenticated via OAuth user JWT (optional auth)');
req.user = jwtAuth.user;
@@ -1927,8 +1883,8 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
try {
const userBan = await checkPlatformBan(
- `user:${jwtAuth.user.id}`,
- () => bansDb.checkPlatformBan(jwtAuth.user.id),
+ `user:${getOrganizationAuthorizationUserId(jwtAuth.user)}`,
+ () => bansDb.checkPlatformBan(getOrganizationAuthorizationUserId(jwtAuth.user)),
);
if (userBan) {
logger.info({ userId: jwtAuth.user.id, banId: userBan.id }, 'User optional-auth request blocked by platform ban');
@@ -1938,30 +1894,33 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
logger.warn({ err: banError, userId: jwtAuth.user.id, path: req.path }, 'Ban check failed — allowing optional-auth request through');
}
- await attachIdentityId(req.user);
return next();
}
// Dev mode: set dev user if logged in via dev-session cookie
- if (DEV_MODE_ENABLED) {
+ if (DEV_MODE_ENABLED && req.headers.authorization === undefined) {
const devUser = createDevUser(req);
if (devUser) {
- req.user = devUser;
+ req.user = await hydrateDevUser(devUser);
req.accessToken = 'dev-mode-token';
// Carry over dev config flags (isMember, isAdmin) so enrichUserWithMembership skips DB lookup
const devConfig = getDevUser(req);
if (devConfig) {
(req.user as unknown as Record).isMember = devConfig.isMember;
}
+ return next();
}
- // No dev session = not logged in (which is fine for optional auth)
- return next();
}
const sessionCookie = extractSealedSession(req);
+ // Only complete credential absence permits anonymous access. Keep one return
+ // for the entire branch so CodeQL recognizes this as an early-exit guard;
+ // supplied but unusable credentials still return 401 without a fallback.
if (!sessionCookie) {
- return next();
+ return req.headers.authorization !== undefined || req.cookies?.['wos-session'] !== undefined
+ ? res.status(401).json({ error: 'Invalid session', login_url: '/auth/login' })
+ : next();
}
try {
@@ -1974,22 +1933,19 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
const deadAt = deadSessionCache.get(cacheKey);
if (deadAt) {
if (now - deadAt < DEAD_SESSION_TTL_MS) {
- return next(); // optional auth: just proceed without user
+ return res.status(401).json({ error: 'Invalid session', login_url: '/auth/login' });
}
deadSessionCache.delete(cacheKey);
}
- if (
- cached &&
- cached.expiresAt > now &&
- !(await isAuthorizationFingerprintCurrent(cached.user, cached.authorizationFingerprint))
- ) {
- sessionCache.delete(cacheKey);
- } else if (cached && cached.expiresAt > now) {
+ if (cached && cached.expiresAt > now) {
// Cache hit - use cached session data
logger.debug({ userId: cached.user.id }, 'Using cached session (optional auth)');
- req.user = cached.user;
+ req.user = await hydrateAuthenticatedUser(
+ cached.user, selectedOrganizationForAuthentication(req, cached.orgId),
+ );
req.accessToken = cached.accessToken;
+ markAuthenticatedSealedBearer(req, bearerToken);
// If session was refreshed, update the cookie
if (cached.newSealedSession) {
@@ -2019,6 +1975,9 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
cookiePassword: WORKOS_COOKIE_PASSWORD,
});
+ if (!refreshResult.authenticated && 'retryable' in refreshResult && refreshResult.retryable) {
+ return res.status(503).json({ error: 'Authentication service temporarily unavailable' });
+ }
if (refreshResult.authenticated && refreshResult.sealedSession) {
logger.debug('Session refreshed successfully (optional auth)');
newSealedSession = refreshResult.sealedSession;
@@ -2036,6 +1995,7 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
refreshFailed = true;
}
} catch (refreshError) {
+ if (getBearerToken(req.headers.authorization) !== null || isTransientAuthError(refreshError)) throw refreshError;
logger.debug({ err: refreshError }, 'Optional auth refresh failed');
refreshFailed = true;
}
@@ -2057,6 +2017,9 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
const sharedRefresh = await sharedSessionObj.refresh({
cookiePassword: WORKOS_COOKIE_PASSWORD,
});
+ if (!sharedRefresh.authenticated && 'retryable' in sharedRefresh && sharedRefresh.retryable) {
+ return res.status(503).json({ error: 'Authentication service temporarily unavailable' });
+ }
if (sharedRefresh.authenticated && sharedRefresh.sealedSession) {
newSealedSession = sharedRefresh.sealedSession;
setSessionCookie(res, sharedRefresh.sealedSession);
@@ -2070,6 +2033,7 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
result = await refreshedObj.authenticate();
}
} catch (innerRefreshErr) {
+ if (getBearerToken(req.headers.authorization) !== null || isTransientAuthError(innerRefreshErr)) throw innerRefreshErr;
logger.debug({ err: innerRefreshErr }, 'Shared session refresh also failed (optional auth)');
}
} else {
@@ -2079,6 +2043,7 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
}
} catch (dbError) {
logger.debug({ err: dbError }, 'Failed to look up shared session (optional auth)');
+ return res.status(503).json({ error: 'Authentication service temporarily unavailable' });
}
}
}
@@ -2092,6 +2057,8 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
if (oldest) deadSessionCache.delete(oldest);
}
deadSessionCache.set(cacheKey, Date.now());
+ sessionCache.delete(cacheKey);
+ return res.status(401).json({ error: 'Invalid session', login_url: '/auth/login' });
}
if (result.authenticated && 'user' in result && result.user) {
@@ -2119,26 +2086,30 @@ export async function optionalAuth(req: Request, res: Response, next: NextFuncti
);
}
- // Resolve identityId before caching — sessionCache is shared with
- // requireAuth, so skipping it here would let an optionalAuth request
- // poison subsequent requireAuth cache hits with identityId=undefined.
- await attachIdentityId(user);
+ const hydrated = await hydrateAuthenticatedUser(
+ user, selectedOrganizationForAuthentication(req, result.organizationId),
+ );
- // Cache the validated session
+ // Retain authentication only; derived request authority is never cached.
sessionCache.set(cacheKey, {
- user,
+ user: Object.freeze(user),
accessToken: result.accessToken,
expiresAt: now + SESSION_CACHE_TTL_MS,
newSealedSession,
- authorizationFingerprint: await authorizationFingerprintFor(user),
+ orgId: result.organizationId,
});
- req.user = user;
+ req.user = hydrated;
req.accessToken = result.accessToken;
+ markAuthenticatedSealedBearer(req, bearerToken);
}
} catch (error) {
- // Silently fail for optional auth
+ if (sendAuthorizationStateError(error, res, true)) return;
+ if (getBearerToken(req.headers.authorization) !== null && !isInvalidSealedBearerError(error)) {
+ return res.status(503).json({ error: 'Authentication service temporarily unavailable' });
+ }
logger.debug({ err: error }, 'Optional auth failed');
+ return res.status(401).json({ error: 'Invalid session', login_url: '/auth/login' });
}
next();
diff --git a/server/src/routes/api-keys.ts b/server/src/routes/api-keys.ts
index 32d43a0ad8d..1cc2e871f55 100644
--- a/server/src/routes/api-keys.ts
+++ b/server/src/routes/api-keys.ts
@@ -2,8 +2,8 @@
* API key management routes
*
* Uses WorkOS API for organization API key CRUD.
- * Requires authenticated session (cookie-based auth).
- * Verifies org membership before allowing any operation.
+ * Requires an authenticated user credential and an explicitly selected org.
+ * Verifies fresh, exact-credential authority before every operation.
*/
import type { Request, Response } from "express";
@@ -11,10 +11,12 @@ import { Router } from "express";
import { WorkOS } from "@workos-inc/node";
import { createLogger } from "../logger.js";
import { requireAuth } from "../middleware/auth.js";
+import { getOrganizationAuthorizationUserId } from "../auth/organization-principal.js";
import {
- resolveUserOrgMembership,
- type UserOrgMembership,
-} from "../utils/resolve-user-org-membership.js";
+ evaluateUserOrgRoleAuthorization,
+ resolveUserOrgAuthorization,
+ type UserOrgAuthorizationMembership,
+} from "../utils/resolve-user-org-authorization.js";
const logger = createLogger("api-keys-routes");
@@ -36,6 +38,8 @@ const AUTH_ENABLED = !!(
const workos = AUTH_ENABLED
? new WorkOS(WORKOS_API_KEY!, {
clientId: process.env.WORKOS_CLIENT_ID!,
+ timeout: 5_000,
+ maxRetries: 0,
})
: null;
@@ -88,32 +92,49 @@ async function workosRequest(
}
/**
- * Resolve the authenticated user's active membership in the specified organization.
- * Returns null and sends the appropriate error response when access is denied.
+ * Select only an explicit request organization. Never consult a session's org,
+ * the canonical identity, or a primary/first organization. Conflicting selectors
+ * are ambiguous even when the caller can administer both organizations.
*/
-async function verifyOrgMembership(
+function selectedOrganizationId(
req: Request,
res: Response,
- organizationId: string,
-): Promise {
- const membership = await resolveUserOrgMembership(
- workos,
- req.user!.id,
- organizationId,
- );
-
- // resolveUserOrgMembership currently derives roles from active rows only,
- // but keep the status check at this key-management boundary so a future
- // resolver change (or inconsistent upstream response) cannot create, expose,
- // or revoke keys for a pending or inactive membership.
- if (!membership || membership.status !== "active") {
- res.status(403).json({
- error: "Access denied",
- message: "You are not a member of this organization",
+): string | null {
+ // These are the same explicit selectors recognized by the authorization
+ // observer. Require agreement rather than giving one location precedence.
+ const headerOrg = req.headers["x-organization-id"];
+ const supplied: unknown[] = [
+ req.query.org,
+ req.query.organization_id,
+ req.query.organizationId,
+ req.body?.organizationId,
+ req.body?.organization_id,
+ headerOrg,
+ ].filter((value) => value !== undefined);
+ const headerCount = req.rawHeaders.filter(
+ (value, index) => index % 2 === 0 && value.toLowerCase() === "x-organization-id",
+ ).length;
+ if (
+ supplied.length === 0 ||
+ headerCount > 1 ||
+ (typeof headerOrg === "string" && headerOrg.includes(",")) ||
+ supplied.some((value) => typeof value !== "string" || !value || value !== value.trim()) ||
+ supplied.some((value) => value !== supplied[0])
+ ) {
+ res.status(400).json({
+ error: "Select one organization explicitly",
});
return null;
}
- return membership;
+ return supplied[0] as string;
+}
+
+interface ApiKeyManagementAuthorization extends UserOrgAuthorizationMembership {
+ actor: {
+ userId: string;
+ authWorkosUserId: string;
+ identityId?: string;
+ };
}
/**
@@ -122,18 +143,52 @@ async function verifyOrgMembership(
* automation keys, and revocation can disable them. Keep the full lifecycle
* restricted to active organization owners and admins.
*/
-function requireOrgAdmin(
- membership: UserOrgMembership,
+async function authorizeKeyManagement(
+ req: Request,
res: Response,
+ organizationId: string,
action: "create" | "list" | "revoke",
-): boolean {
- if (membership.role === "owner" || membership.role === "admin") return true;
+): Promise {
+ const credentialId = getOrganizationAuthorizationUserId(req.user!);
+ const authorizationSnapshot = req.user!.authorizationSnapshot;
+ // Preserve authentication-time attribution across provider awaits. Snapshot
+ // state is non-enumerable, so carry it explicitly into the authorization check.
+ const actor = {
+ userId: authorizationSnapshot?.canonicalUserId ?? req.user!.id,
+ authWorkosUserId: credentialId,
+ identityId: authorizationSnapshot
+ ? authorizationSnapshot.identityId ?? undefined
+ : req.user!.identityId,
+ };
+ // Key-use permissions (including tenant admin:*) do not grant key-management
+ // authority or impersonation of a user credential.
+ if (credentialId === "admin_api_key" || credentialId.startsWith("api_key_")) {
+ res.status(403).json({ error: "A user credential is required to manage API keys" });
+ return null;
+ }
+
+ // No cached organization_memberships or linked credentials participate here.
+ // Existing grants explicitly assign a role to this exact credential and org;
+ // they do not delegate use of another binding or inherit its membership.
+ const resolution = await resolveUserOrgAuthorization(
+ workos,
+ { id: actor.userId, authWorkosUserId: actor.authWorkosUserId, authorizationSnapshot },
+ organizationId,
+ );
+ const decision = evaluateUserOrgRoleAuthorization(resolution, "admin");
+ if (decision.status === "unavailable") {
+ res.status(503).json({ error: "Organization authorization unavailable" });
+ return null;
+ }
+ if (decision.status === "authorized" && decision.membership.organizationId === organizationId) {
+ return { ...decision.membership, actor };
+ }
res.status(403).json({
error: "Access denied",
message: `Only organization owners and admins can ${action} API keys`,
});
- return false;
+ return null;
}
/**
@@ -159,20 +214,14 @@ export function createApiKeysRouter(): Router {
router.get("/", requireAuth, async (req, res) => {
try {
if (!workos) {
- return res.status(500).json({ error: "Authentication not configured" });
+ return res.status(503).json({ error: "Organization authorization unavailable" });
}
- const requestedOrganizationId = req.query.org as string;
- // Missing IDs terminate here; authorization below returns a canonical
- // organization ID from an exact-match WorkOS membership.
- if (!requestedOrganizationId) {
- return res
- .status(400)
- .json({ error: "org query parameter is required" });
- }
+ const requestedOrganizationId = selectedOrganizationId(req, res);
+ if (!requestedOrganizationId) return;
- const membership = await verifyOrgMembership(req, res, requestedOrganizationId);
- if (!membership || !requireOrgAdmin(membership, res, "list")) return;
+ const membership = await authorizeKeyManagement(req, res, requestedOrganizationId, "list");
+ if (!membership) return;
const authorizedOrganizationId = membership.organizationId;
const params: Record = {};
@@ -182,7 +231,7 @@ export function createApiKeysRouter(): Router {
const result = await workosRequest(
"GET",
- `/organizations/${authorizedOrganizationId}/api_keys`,
+ `/organizations/${encodeURIComponent(authorizedOrganizationId)}/api_keys`,
{ query: params },
);
@@ -197,21 +246,14 @@ export function createApiKeysRouter(): Router {
router.post("/", requireAuth, async (req, res) => {
try {
if (!workos) {
- return res.status(500).json({ error: "Authentication not configured" });
+ return res.status(503).json({ error: "Organization authorization unavailable" });
}
- const requestedOrganizationId =
- (req.query.org as string) || req.body.organizationId;
- // Missing IDs terminate here; authorization below returns a canonical
- // organization ID from an exact-match WorkOS membership.
- if (!requestedOrganizationId) {
- return res.status(400).json({
- error: "org query parameter or organizationId in body is required",
- });
- }
+ const requestedOrganizationId = selectedOrganizationId(req, res);
+ if (!requestedOrganizationId) return;
- const membership = await verifyOrgMembership(req, res, requestedOrganizationId);
- if (!membership || !requireOrgAdmin(membership, res, "create")) return;
+ const membership = await authorizeKeyManagement(req, res, requestedOrganizationId, "create");
+ if (!membership) return;
const authorizedOrganizationId = membership.organizationId;
const { name, permissions } = req.body;
@@ -250,13 +292,13 @@ export function createApiKeysRouter(): Router {
const result = await workosRequest(
"POST",
- `/organizations/${authorizedOrganizationId}/api_keys`,
+ `/organizations/${encodeURIComponent(authorizedOrganizationId)}/api_keys`,
{ body },
);
logger.info(
{
- userId: req.user!.id,
+ ...membership.actor,
organizationId: authorizedOrganizationId,
keyName: name,
},
@@ -274,28 +316,29 @@ export function createApiKeysRouter(): Router {
router.delete("/:id", requireAuth, async (req, res) => {
try {
if (!workos) {
- return res.status(500).json({ error: "Authentication not configured" });
+ return res.status(503).json({ error: "Organization authorization unavailable" });
}
- const requestedOrganizationId = req.query.org as string;
- // Missing IDs terminate here; authorization below returns a canonical
- // organization ID from an exact-match WorkOS membership.
- if (!requestedOrganizationId) {
- return res
- .status(400)
- .json({ error: "org query parameter is required" });
- }
+ const requestedOrganizationId = selectedOrganizationId(req, res);
+ if (!requestedOrganizationId) return;
- const membership = await verifyOrgMembership(req, res, requestedOrganizationId);
- if (!membership || !requireOrgAdmin(membership, res, "revoke")) return;
+ const membership = await authorizeKeyManagement(req, res, requestedOrganizationId, "revoke");
+ if (!membership) return;
const authorizedOrganizationId = membership.organizationId;
const apiKeyId = req.params.id;
+ // Every accepted route ID first completes exact admin authorization.
+ // URL normalizes dot segments even after encodeURIComponent, so reject
+ // them before a key DELETE can become a request to the organization.
+ if (apiKeyId === "." || apiKeyId === "..") {
+ return res.status(400).json({ error: "Invalid API key ID" });
+ }
+
try {
- await workosRequest("DELETE", `/organizations/${authorizedOrganizationId}/api_keys/${apiKeyId}`);
+ await workosRequest("DELETE", `/organizations/${encodeURIComponent(authorizedOrganizationId)}/api_keys/${encodeURIComponent(apiKeyId)}`);
logger.info(
{
- userId: req.user!.id,
+ ...membership.actor,
organizationId: authorizedOrganizationId,
apiKeyId,
},
@@ -309,7 +352,7 @@ export function createApiKeysRouter(): Router {
if (status !== 404) throw error;
logger.info(
{
- userId: req.user!.id,
+ ...membership.actor,
organizationId: authorizedOrganizationId,
apiKeyId,
},
diff --git a/server/src/routes/community-mirrors.ts b/server/src/routes/community-mirrors.ts
index 28604698804..c21a7238c78 100644
--- a/server/src/routes/community-mirrors.ts
+++ b/server/src/routes/community-mirrors.ts
@@ -8,7 +8,7 @@
*
* GET /api/registry/mirrors — list mirrors (public, with etags)
* GET /api/registry/mirrors/:platform — read one mirror (public)
- * PUT /api/registry/mirrors/:platform — publish for moderators, propose for other callers
+ * PUT /api/registry/mirrors/:platform — publish with admin keys, propose for other callers
* GET /api/registry/mirror-proposals — review queue or caller's own proposals
* POST /api/registry/mirror-proposals/:id/{approve,reject} — moderation
*
@@ -26,12 +26,10 @@ import type { CommunityMirror, CommunityMirrorProposal } from '../db/community-m
import { PublisherDatabase } from '../db/publisher-db.js';
import type { CatalogEventsDatabase } from '../db/catalog-events-db.js';
import { getClient } from '../db/client.js';
-import { isRegistryModerator } from '../services/brand-logo-auth.js';
-import { isWebUserAAOAdmin } from '../addie/admin-status-lookup.js';
import { validateAdagentsDocument } from '../services/adagents-schema-validator.js';
import { registryReadRateLimiter, brandCreationRateLimiter } from '../middleware/rate-limit.js';
import { createLogger } from '../logger.js';
-import { resolveCallerOrgId } from './helpers/resolve-caller-org.js';
+import { resolveCallerOrgId, sendCallerOrganizationAuthError } from './helpers/resolve-caller-org.js';
import {
notifyCommunityMirrorProposalReviewed,
notifyPendingCommunityMirrorProposal,
@@ -83,17 +81,14 @@ export interface CommunityMirrorRouterConfig {
}
/**
- * Organization API keys represent an organization, not a human working-group
- * member, so they intentionally submit proposals instead of passing this gate.
+ * Human management is disabled until #7452's exact-principal registry gate is
+ * integrated with the authorization snapshot. The legacy admin/moderator
+ * lookups consume canonical person IDs and cannot authorize this boundary.
+ * Only the authenticated platform admin API key retains management access;
+ * organization API keys intentionally submit proposals instead.
*/
-async function canManageMirrors(userId: string): Promise {
- if (userId === 'admin_api_key') return true;
- if (userId.startsWith('api_key_')) return false;
- const [isOrganizationAdmin, isModerator] = await Promise.all([
- isWebUserAAOAdmin(userId),
- isRegistryModerator(userId),
- ]);
- return isOrganizationAdmin || isModerator;
+function canManageMirrors(userId: string): boolean {
+ return userId === 'admin_api_key';
}
async function resolveManager(
@@ -105,18 +100,24 @@ async function resolveManager(
res.status(401).json({ error: 'Authentication required' });
return null;
}
- if (!(await canManageMirrors(userId))) {
+ if (!canManageMirrors(userId)) {
res.status(403).json({
- error: 'Only registry moderators or AgenticAdvertising.org administrators can manage community mirrors',
+ error: 'Community mirror management requires a platform admin API key',
});
return null;
}
return userId;
}
-async function callerOrganizationId(req: Request): Promise {
- const attached = (req as Request & { apiKey?: { organizationId?: string } }).apiKey?.organizationId;
- return attached ?? resolveCallerOrgId(req);
+async function callerOrganizationId(req: Request, res: Response): Promise {
+ try {
+ const attached = (req as Request & { apiKey?: { organizationId?: string } }).apiKey?.organizationId;
+ if (req.user?.id.startsWith('api_key_') && attached) return attached;
+ return await resolveCallerOrgId(req);
+ } catch (error) {
+ if (sendCallerOrganizationAuthError(error, res)) return undefined;
+ throw error;
+ }
}
function reviewedContentDigest(document: Record): string {
@@ -205,11 +206,15 @@ export function createCommunityMirrorRouter(config: CommunityMirrorRouterConfig)
if (req.query.review_queue !== undefined && !['true', 'false'].includes(String(req.query.review_queue))) {
return res.status(400).json({ error: 'Invalid review_queue flag' });
}
- const isManager = await canManageMirrors(userId);
+ const isManager = canManageMirrors(userId);
if (reviewQueue && !isManager) {
- return res.status(403).json({ error: 'Registry moderator access is required for the review queue' });
+ return res.status(403).json({ error: 'The review queue requires a platform admin API key' });
+ }
+ const organizationId = isManager ? null : await callerOrganizationId(req, res);
+ if (organizationId === undefined) return;
+ if (!isManager && !organizationId) {
+ return res.status(404).json({ error: 'Community mirror proposal not found' });
}
- const organizationId = isManager ? null : await callerOrganizationId(req);
const limit = req.query.limit ? parseInt(String(req.query.limit), 10) : undefined;
const offset = req.query.offset ? parseInt(String(req.query.offset), 10) : undefined;
try {
@@ -220,7 +225,6 @@ export function createCommunityMirrorRouter(config: CommunityMirrorRouterConfig)
| 'rejected'
| undefined,
proposedByOrganizationId: isManager ? undefined : organizationId ?? undefined,
- proposedByUserId: isManager || organizationId ? undefined : userId,
limit: Number.isFinite(limit) ? limit : undefined,
offset: Number.isFinite(offset) ? offset : undefined,
});
@@ -239,13 +243,15 @@ export function createCommunityMirrorRouter(config: CommunityMirrorRouterConfig)
return res.status(400).json({ error: 'Invalid proposal identifier' });
}
try {
+ const isManager = canManageMirrors(userId);
+ const organizationId = isManager ? null : await callerOrganizationId(req, res);
+ if (organizationId === undefined) return;
+ if (!isManager && !organizationId) {
+ return res.status(404).json({ error: 'Community mirror proposal not found' });
+ }
const proposal = await mirrorDb.getProposalById(proposalId);
if (!proposal) return res.status(404).json({ error: 'Community mirror proposal not found' });
- const isManager = await canManageMirrors(userId);
- const organizationId = isManager ? null : await callerOrganizationId(req);
- const ownsProposal = organizationId
- ? proposal.proposed_by_organization_id === organizationId
- : proposal.proposed_by_user_id === userId;
+ const ownsProposal = organizationId !== null && proposal.proposed_by_organization_id === organizationId;
if (!ownsProposal && !isManager) {
return res.status(404).json({ error: 'Community mirror proposal not found' });
}
@@ -454,8 +460,9 @@ export function createCommunityMirrorRouter(config: CommunityMirrorRouterConfig)
const userId = req.user?.id;
if (!userId) return res.status(401).json({ error: 'Authentication required' });
- const isManager = await canManageMirrors(userId);
- const organizationId = isManager ? null : await callerOrganizationId(req);
+ const isManager = canManageMirrors(userId);
+ const organizationId = isManager ? null : await callerOrganizationId(req, res);
+ if (organizationId === undefined) return;
if (!isManager && !organizationId) {
return res.status(403).json({ error: 'Organization context is required to propose a community mirror' });
}
diff --git a/server/src/routes/helpers/resolve-caller-org.ts b/server/src/routes/helpers/resolve-caller-org.ts
index ae4cc682b6a..5411ba2b09e 100644
--- a/server/src/routes/helpers/resolve-caller-org.ts
+++ b/server/src/routes/helpers/resolve-caller-org.ts
@@ -7,17 +7,27 @@
* against the WorkOS JWKS endpoint.
* 2. WorkOS API key (sk_* / wos_api_key_* prefixes) — server-to-server
* integrations. Validated via the existing `validateWorkOSApiKey` helper.
- * 3. Sealed session — web/native app sessions whose cookie or bearer
- * unsealed in `optionalAuth`, producing `req.user`. Organization is
- * resolved via `resolvePrimaryOrganization`, which falls back to the
- * user's organization_memberships when the cached column is NULL.
+ * 3. Sealed cookie or authenticated native bearer session — middleware supplies a fresh
+ * authorization snapshot. Only an explicitly selected organization
+ * authorized for the exact credential is returned; primary orgs are ignored.
+ *
+ * Native bearer sessions require middleware transport provenance before using
+ * the exact snapshot. No sealed transport selects an implicit organization.
*/
-import type { Request } from 'express';
+import type { Request, Response } from 'express';
+import { createHash } from 'node:crypto';
import { createRemoteJWKSet, decodeJwt, jwtVerify, type JWTVerifyGetKey } from 'jose';
-import { isWorkOSApiKeyFormat } from '../../middleware/api-key-format.js';
+import { getBearerToken, isWorkOSApiKeyFormat } from '../../middleware/api-key-format.js';
+import {
+ ConflictingOrganizationSelectionError,
+ selectedOrganizationForAuthentication,
+} from '../../auth/organization-selection.js';
import { validateWorkOSApiKey } from '../../middleware/auth.js';
-import { resolvePrimaryOrganization } from '../../db/users-db.js';
+import { isInvalidWorkOSJWTError, unavailableJWTKeyService, WorkOSJWTUnavailableError } from '../../auth/workos-jwt.js';
+import { getAuthorizationEnforcementWorkos } from '../../auth/workos-client.js';
+import { getOrganizationAuthorizationUserId, type OrgAuthorizationPrincipal } from '../../auth/organization-principal.js';
+import { resolveUserOrgAuthorization } from '../../utils/resolve-user-org-authorization.js';
import { createLogger } from '../../logger.js';
const logger = createLogger('resolve-caller-org');
@@ -42,17 +52,38 @@ function jwksForIssuer(iss: string): { jwks: JWTVerifyGetKey; clientId: string }
return { jwks, clientId };
}
-export type MinimalReq = Pick & { user?: { id?: string } };
+export type MinimalReq = Pick & Partial> & {
+ user?: Partial;
+};
+
+export class CallerOrganizationAuthError extends Error {
+ constructor(readonly status: 401 | 403 | 503) {
+ super(status === 401 ? 'Invalid bearer token'
+ : status === 403 ? 'Conflicting organization selection'
+ : 'Authorization temporarily unavailable');
+ this.name = 'CallerOrganizationAuthError';
+ }
+}
+
+/** Preserve authentication failures when callers also handle application errors. */
+export function sendCallerOrganizationAuthError(error: unknown, res: Response): boolean {
+ if (!(error instanceof CallerOrganizationAuthError)) return false;
+ res.status(error.status).json({
+ error: error.status === 401 ? 'invalid_bearer_token'
+ : error.status === 403 ? 'organization_selection_conflict'
+ : 'authorization_unavailable',
+ });
+ return true;
+}
/**
* Extract and verify a WorkOS OIDC access token. Returns the `org_id` claim
* on success, or `null` for API keys, sealed sessions, missing tokens, or
- * failed verification. Never throws.
+ * invalid verification. Key-service and unknown verification failures throw 503.
*/
export async function orgIdFromBearerJwt(req: MinimalReq): Promise {
- const auth = req.headers.authorization;
- if (!auth?.startsWith('Bearer ')) return null;
- const token = auth.slice(7);
+ const token = getBearerToken(req.headers.authorization);
+ if (!token) return null;
if (isWorkOSApiKeyFormat(token)) return null;
// Sealed sessions are not JWTs — skip verification to avoid JWKS noise.
if (!token.startsWith('eyJ')) return null;
@@ -69,40 +100,116 @@ export async function orgIdFromBearerJwt(req: MinimalReq): Promise {
- const jwtOrg = await orgIdFromBearerJwt(req);
- if (jwtOrg) return jwtOrg;
+type ResolvedBearerOrganization =
+ | { kind: 'verified'; organizationId: string }
+ | { kind: 'invalid' };
+
+/** Each validator skips unsupported formats without making provider calls. */
+async function resolveBearerOrganization(req: MinimalReq): Promise {
+ try {
+ const jwtOrg = await orgIdFromBearerJwt(req);
+ if (jwtOrg) return { kind: 'verified', organizationId: jwtOrg };
- const apiKey = await validateWorkOSApiKey(req as Request);
- if (apiKey) return apiKey.organizationId;
+ const apiKey = await validateWorkOSApiKey(req as Request);
+ if (apiKey) return { kind: 'verified', organizationId: apiKey.organizationId };
+
+ return { kind: 'invalid' };
+ } catch {
+ throw new CallerOrganizationAuthError(503);
+ }
+}
- if (req.user?.id) {
+/** Missing, denied, stale or unavailable sealed-session authority is public-only.
+ * Nullable consumers must never reinterpret it as canonical-user ownership. */
+async function resolveSealedSessionOrganization(req: MinimalReq): Promise {
+ const user = req.user;
+ if (user?.id && user.id !== 'admin_api_key' && !user.id.startsWith('api_key_')) {
+ const principal: OrgAuthorizationPrincipal = {
+ id: user.id,
+ authWorkosUserId: user.authWorkosUserId,
+ authorizationSnapshot: user.authorizationSnapshot,
+ };
+ const snapshot = principal.authorizationSnapshot;
+ if (!snapshot || snapshot.authenticatedUserId !== getOrganizationAuthorizationUserId(principal)
+ || snapshot.canonicalUserId !== principal.id) return null;
try {
- return await resolvePrimaryOrganization(req.user.id);
+ // Route params may become available only after authentication middleware.
+ // Reuse its conflict policy before consuming the persisted selection.
+ const organizationId = selectedOrganizationForAuthentication(req, snapshot.selectedOrganizationId ?? undefined);
+ if (!organizationId) return null;
+ // A snapshot selection is not a grant. This resolver rechecks exact
+ // membership/grant and brackets WorkOS reads with fresh snapshot checks.
+ const authorization = await resolveUserOrgAuthorization(
+ getAuthorizationEnforcementWorkos(), principal, organizationId,
+ );
+ return authorization.status === 'authorized' ? authorization.membership.organizationId : null;
} catch (err) {
- logger.warn({ err, userId: req.user.id }, 'caller org resolution failed — falling back to public-only');
+ logger.warn({ err }, 'Explicit caller organization authorization unavailable or conflicting');
+ return null;
}
}
return null;
}
+/**
+ * Resolve the caller's organization from a verified credential result.
+ * Authenticated sealed sessions can have no selected organization. Other
+ * supplied bearers must authorize an organization or fail explicitly.
+ */
+export async function resolveCallerOrgId(req: MinimalReq): Promise {
+ const authorization = req.headers.authorization;
+ const bearerToken = getBearerToken(authorization);
+ if (bearerToken === null) return resolveSealedSessionOrganization(req);
+
+ const sealedBearer = req.authenticatedSealedBearer;
+ if (sealedBearer) {
+ // Bind provenance to both the presented token and the exact middleware
+ // snapshot. Replaced credentials or principals cannot reuse it.
+ if (sealedBearer.tokenHash !== createHash('sha256').update(bearerToken).digest('hex')
+ || sealedBearer.authorizationSnapshot !== req.user?.authorizationSnapshot) {
+ throw new CallerOrganizationAuthError(401);
+ }
+ return resolveSealedSessionOrganization(req);
+ }
+
+ // Capture the credential and selection before any await. Later request
+ // mutation cannot substitute another key or erase an organization conflict.
+ const request = req as Request;
+ const credentialRequest = {
+ headers: Object.freeze({ ...req.headers, authorization }),
+ query: Object.freeze({ ...request.query }),
+ body: Object.freeze({ ...request.body }),
+ params: Object.freeze({ ...request.params }),
+ };
+ const credential = await resolveBearerOrganization(credentialRequest);
+ if (credential.kind === 'invalid') throw new CallerOrganizationAuthError(401);
+
+ try {
+ selectedOrganizationForAuthentication(credentialRequest as Request, credential.organizationId);
+ } catch (error) {
+ if (error instanceof ConflictingOrganizationSelectionError) {
+ throw new CallerOrganizationAuthError(403);
+ }
+ throw new CallerOrganizationAuthError(503);
+ }
+ return credential.organizationId;
+}
+
/** Test hook: reset the per-client JWKS cache. */
export function __resetJwksForTests(): void {
jwksByClient.clear();
diff --git a/server/src/routes/registry-api.ts b/server/src/routes/registry-api.ts
index 06d2b7396ae..a20206778cd 100644
--- a/server/src/routes/registry-api.ts
+++ b/server/src/routes/registry-api.ts
@@ -200,7 +200,7 @@ import { isWebUserAAOAdmin } from "../addie/admin-status-lookup.js";
import { isBreakGlassAdminEmail } from "../auth/admin-access.js";
import { getDevUser, isDevModeEnabled } from "../middleware/auth.js";
import { OrganizationDatabase, hasApiAccess, resolveMembershipTier } from "../db/organization-db.js";
-import { resolveCallerOrgId } from "./helpers/resolve-caller-org.js";
+import { resolveCallerOrgId, sendCallerOrganizationAuthError } from "./helpers/resolve-caller-org.js";
import { canonicalizeAgentUrl, PublisherDatabase } from "../db/publisher-db.js";
import { buildCreativeCapabilities } from "../creative-agent/task-handlers.js";
import {
@@ -6050,6 +6050,7 @@ export function createRegistryApiRouters(config: RegistryApiConfig): {
id: saved.id,
});
} catch (error) {
+ if (sendCallerOrganizationAuthError(error, res)) return;
logger.error({ error }, "Failed to save property");
return res.status(500).json({ error: "Failed to save property" });
}
@@ -6091,6 +6092,7 @@ export function createRegistryApiRouters(config: RegistryApiConfig): {
`Origin verification binds this domain to your organization.`,
});
} catch (error) {
+ if (sendCallerOrganizationAuthError(error, res)) return;
logger.error({ error }, 'Failed to issue domain claim');
return res.status(500).json({ error: 'Failed to issue domain claim' });
}
@@ -6131,6 +6133,7 @@ export function createRegistryApiRouters(config: RegistryApiConfig): {
}
return res.json(outcome);
} catch (error) {
+ if (sendCallerOrganizationAuthError(error, res)) return;
logger.error({ error }, 'Origin verification failed');
return res.status(500).json({ error: 'Origin verification failed' });
}
@@ -6783,6 +6786,7 @@ export function createRegistryApiRouters(config: RegistryApiConfig): {
res.json({ agents: enriched, count: enriched.length });
} catch (error) {
+ if (sendCallerOrganizationAuthError(error, res)) return;
logger.error({ err: error, path: req.path }, "Failed to list agents");
res.status(500).json({ error: "Failed to list agents" });
}
@@ -9644,6 +9648,7 @@ export function createRegistryApiRouters(config: RegistryApiConfig): {
agents,
});
} catch (error) {
+ if (sendCallerOrganizationAuthError(error, res)) return;
logger.error({ err: error, path: req.path }, "Operator lookup failed");
res.status(500).json({ error: "Operator lookup failed" });
}
diff --git a/server/src/types.ts b/server/src/types.ts
index f9fef3edb8a..8ff1eba3646 100644
--- a/server/src/types.ts
+++ b/server/src/types.ts
@@ -393,6 +393,8 @@ export interface Impersonator {
}
export interface WorkOSUser {
+ /** Immutable primary-DB snapshot for this request only; never serialize or cache. */
+ readonly authorizationSnapshot?: import('./db/user-authorization-snapshot-db.js').AuthorizationSnapshot;
/**
* The canonical workos_user_id for app-state queries. For singleton
* identities this equals the authenticated WorkOS user. For non-primary
diff --git a/server/src/utils/resolve-user-org-authorization.ts b/server/src/utils/resolve-user-org-authorization.ts
index 142b94b63ee..a0bc851c1f6 100644
--- a/server/src/utils/resolve-user-org-authorization.ts
+++ b/server/src/utils/resolve-user-org-authorization.ts
@@ -1,19 +1,24 @@
import type { WorkOS } from "@workos-inc/node";
import type { OrgAuthorizationPrincipal } from "../auth/organization-principal.js";
import { getOrganizationAuthorizationUserId } from "../auth/organization-principal.js";
-import { query } from "../db/client.js";
+import {
+ loadAuthorizationSnapshot,
+ sameAuthorizationIdentity,
+ sameAuthorizationSnapshot,
+ type AuthorizationSnapshot,
+} from "../db/user-authorization-snapshot-db.js";
import { createLogger } from "../logger.js";
import { resolveUserRole } from "./resolve-user-role.js";
const logger = createLogger("resolve-user-org-authorization");
export type MembershipRole = "owner" | "admin" | "member";
-export type OrgAuthorizationSource = "workos" | "credential_grant";
+export type OrgAuthorizationSource = "workos" | "credential_grant" | "authorization_snapshot";
export interface UserOrgAuthorizationMembership {
organizationId: string;
role: MembershipRole;
- source: OrgAuthorizationSource;
+ source: "workos" | "credential_grant";
}
export type UserOrgAuthorizationResolution =
@@ -52,7 +57,34 @@ export async function resolveUserOrgAuthorization(
principal: OrgAuthorizationPrincipal,
organizationId: string
): Promise {
+ const forbidden: UserOrgAuthorizationResolution = {
+ status: "forbidden", complete: true, unavailableSources: [],
+ };
+ const unavailable = {
+ status: "unavailable", complete: false, unavailableSources: ["authorization_snapshot"],
+ } satisfies UserOrgAuthorizationResolution;
+ // Signing in alone does not select an organization or confer tenant access.
+ if (!organizationId?.trim()) return forbidden;
const userId = getOrganizationAuthorizationUserId(principal);
+ let snapshot: AuthorizationSnapshot | null;
+ try {
+ snapshot = await loadAuthorizationSnapshot(userId, organizationId);
+ } catch (err) {
+ logger.warn({ err }, "Primary authorization snapshot unavailable");
+ return unavailable;
+ }
+ if (!snapshot) return forbidden;
+ const previous = principal.authorizationSnapshot;
+ if (previous) {
+ // A sign-in with no org can bind an explicit route selection here. Once
+ // selected, a context belongs to that org and grant: replay cannot switch
+ // it or silently upgrade a revoked/replaced grant without a fresh request.
+ const current = previous.selectedOrganizationId === null
+ ? sameAuthorizationIdentity(previous, snapshot)
+ : sameAuthorizationSnapshot(previous, snapshot);
+ if (!current) return forbidden;
+ }
+
let directMembership: UserOrgAuthorizationMembership | null = null;
let workosAvailable = false;
@@ -64,7 +96,7 @@ export async function resolveUserOrgAuthorization(
organizationId,
});
const matchingMemberships = memberships.data.filter(
- (membership) => membership.organizationId === organizationId
+ (membership) => membership.userId === userId && membership.organizationId === organizationId
);
const activeRow = matchingMemberships.find(
(membership) => membership.status === "active"
@@ -88,32 +120,25 @@ export async function resolveUserOrgAuthorization(
logger.warn("WorkOS client unavailable; checking explicit credential grant");
}
- let grantAvailable = false;
- let grantMembership: UserOrgAuthorizationMembership | null = null;
+ // WorkOS is an asynchronous authority source. Reject this decision if the
+ // persisted identity, epoch, selection or grant moved while it was in flight.
+ // In particular, never combine a pre-change provider decision with a new grant.
try {
- const grant = await query<{ workos_organization_id: string; role: string }>(
- `SELECT workos_organization_id, role
- FROM organization_credential_grants
- WHERE workos_user_id = $1
- AND workos_organization_id = $2
- AND revoked_at IS NULL
- AND effective_from <= NOW()
- AND (effective_until IS NULL OR effective_until > NOW())
- LIMIT 1`,
- [userId, organizationId]
- );
- grantAvailable = true;
- const row = grant.rows[0];
- if (row && VALID_ROLES.has(row.role)) {
- grantMembership = {
- organizationId: row.workos_organization_id,
- role: row.role as MembershipRole,
- source: "credential_grant",
- };
+ const current = await loadAuthorizationSnapshot(userId, organizationId);
+ if (!current || !sameAuthorizationSnapshot(snapshot, current)) {
+ return forbidden;
}
} catch (err) {
- logger.warn({ err }, "Credential grant lookup failed");
+ logger.warn({ err }, "Primary authorization snapshot revalidation unavailable");
+ return unavailable;
}
+ const grantMembership: UserOrgAuthorizationMembership | null = snapshot.credentialGrant
+ ? {
+ organizationId: snapshot.credentialGrant.organizationId,
+ role: snapshot.credentialGrant.role,
+ source: "credential_grant",
+ }
+ : null;
let membership = directMembership;
if (
@@ -126,7 +151,6 @@ export async function resolveUserOrgAuthorization(
const unavailableSources: OrgAuthorizationSource[] = [];
if (!workosAvailable) unavailableSources.push("workos");
- if (!grantAvailable) unavailableSources.push("credential_grant");
if (membership) {
return {
diff --git a/server/tests/integration/api-key-exact-credential.test.ts b/server/tests/integration/api-key-exact-credential.test.ts
new file mode 100644
index 00000000000..43dcb3a0ae2
--- /dev/null
+++ b/server/tests/integration/api-key-exact-credential.test.ts
@@ -0,0 +1,245 @@
+import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import express from 'express';
+import request from 'supertest';
+import type { Pool } from 'pg';
+import type { AuthorizationSnapshot } from '../../src/db/user-authorization-snapshot-db.js';
+
+const mocks = vi.hoisted(() => ({
+ fetch: vi.fn(), memberships: vi.fn(), previousSnapshot: undefined as AuthorizationSnapshot | undefined,
+}));
+vi.mock('@workos-inc/node', () => ({
+ WorkOS: class WorkOS {
+ userManagement = { listOrganizationMemberships: mocks.memberships };
+ },
+}));
+// Only the authentication and external WorkOS transport are mocked. The route,
+// exact-credential resolver, and grant validity queries run against PostgreSQL.
+vi.mock('../../src/middleware/auth.js', () => ({
+ requireAuth(req: express.Request, _res: express.Response, next: express.NextFunction) {
+ req.user = {
+ id: 'user_api_key_boundary_primary',
+ authWorkosUserId: req.get('x-authenticated-user') ?? 'user_api_key_boundary_primary',
+ email: 'sam@pinnacle.example',
+ emailVerified: true,
+ createdAt: new Date(0).toISOString(),
+ updatedAt: new Date(0).toISOString(),
+ };
+ if (mocks.previousSnapshot) {
+ Object.defineProperty(req.user, 'authorizationSnapshot', { value: mocks.previousSnapshot, enumerable: false });
+ }
+ next();
+ },
+}));
+
+import { initializeDatabase, closeDatabase } from '../../src/db/client.js';
+import { runMigrations } from '../../src/db/migrate.js';
+import { createApiKeysRouter } from '../../src/routes/api-keys.js';
+import { loadAuthorizationSnapshot } from '../../src/db/user-authorization-snapshot-db.js';
+import { bumpAuthorizationEpochs } from '../../src/db/authorization-epoch-db.js';
+
+const PRIMARY = 'user_api_key_boundary_primary';
+const LINKED = 'user_api_key_boundary_linked';
+const PINNACLE = 'org_api_key_boundary_pinnacle';
+const STREAMHAUS = 'org_api_key_boundary_streamhaus';
+
+describe('API key exact credential grants in PostgreSQL', () => {
+ let pool: Pool;
+ let identities: string[] = [];
+ const app = express();
+ app.use(express.json());
+ app.use('/api/me/api-keys', createApiKeysRouter());
+
+ async function cleanup() {
+ await pool.query('DELETE FROM organization_memberships WHERE workos_user_id = ANY($1)', [[PRIMARY, LINKED]]);
+ await pool.query('DELETE FROM organizations WHERE workos_organization_id = ANY($1)', [[PINNACLE, STREAMHAUS]]);
+ await pool.query('DELETE FROM users WHERE workos_user_id = ANY($1)', [[PRIMARY, LINKED]]);
+ if (identities.length) {
+ await pool.query('DELETE FROM identities WHERE id = ANY($1::uuid[])', [identities]);
+ identities = [];
+ }
+ }
+
+ beforeAll(async () => {
+ pool = initializeDatabase({
+ connectionString: process.env.DATABASE_URL || 'postgresql://adcp:localdev@localhost:5432/adcp_test',
+ });
+ await runMigrations();
+ vi.stubGlobal('fetch', mocks.fetch);
+ }, 60000);
+
+ afterAll(async () => {
+ try {
+ await cleanup();
+ } finally {
+ vi.unstubAllGlobals();
+ await closeDatabase();
+ }
+ });
+
+ beforeEach(async () => {
+ await cleanup();
+ mocks.previousSnapshot = undefined;
+ mocks.memberships.mockReset().mockResolvedValue({ data: [] });
+ mocks.fetch.mockReset().mockImplementation(async (_url, options: RequestInit) => ({
+ ok: true,
+ status: options.method === 'DELETE' ? 204 : 200,
+ json: async () => ({ data: [] }),
+ }));
+ await pool.query(
+ 'INSERT INTO organizations (workos_organization_id, name) VALUES ($1, $2), ($3, $4)',
+ [PINNACLE, 'Pinnacle Agency', STREAMHAUS, 'StreamHaus'],
+ );
+ await pool.query(
+ `INSERT INTO users (workos_user_id, email, primary_organization_id)
+ VALUES ($1, $2, $3), ($4, $5, $3)`,
+ [PRIMARY, 'sam@pinnacle.example', PINNACLE, LINKED, 'sam@streamhaus.example'],
+ );
+ const bindings = await pool.query(
+ 'SELECT workos_user_id, identity_id FROM identity_workos_users WHERE workos_user_id = ANY($1)',
+ [[PRIMARY, LINKED]],
+ );
+ identities = bindings.rows.map((row) => row.identity_id);
+ const primaryIdentity = bindings.rows.find((row) => row.workos_user_id === PRIMARY).identity_id;
+ await pool.query(
+ 'UPDATE identity_workos_users SET identity_id = $1, is_primary = FALSE WHERE workos_user_id = $2',
+ [primaryIdentity, LINKED],
+ );
+ });
+
+ function list(credential: string, organizationId?: string) {
+ const query = organizationId ? `?org=${organizationId}` : '';
+ return request(app).get(`/api/me/api-keys${query}`).set('x-authenticated-user', credential);
+ }
+
+ function lifecycle(credential: string, organizationId: string) {
+ return Promise.all([
+ list(credential, organizationId),
+ request(app).post(`/api/me/api-keys?org=${organizationId}`)
+ .set('x-authenticated-user', credential).send({ name: 'Scoped key' }),
+ request(app).delete(`/api/me/api-keys/key_scoped?org=${organizationId}`)
+ .set('x-authenticated-user', credential),
+ ]);
+ }
+
+ async function grant(credential: string, organization: string, role = 'admin') {
+ const result = await pool.query(
+ `INSERT INTO organization_credential_grants
+ (workos_user_id, workos_organization_id, role, granted_by_workos_user_id)
+ VALUES ($1, $2, $3, $4) RETURNING id`,
+ [credential, organization, role, PRIMARY],
+ );
+ return result.rows[0].id as string;
+ }
+
+ it('does not share either credential\'s exact organization grant through their common identity', async () => {
+ await grant(PRIMARY, PINNACLE, 'owner');
+ await grant(LINKED, STREAMHAUS);
+
+ const results = await Promise.all([
+ list(PRIMARY, PINNACLE), list(LINKED, STREAMHAUS),
+ list(PRIMARY, STREAMHAUS), list(LINKED, PINNACLE), list(LINKED),
+ ]);
+ expect(results.map((result) => result.status)).toEqual([200, 200, 403, 403, 400]);
+ expect(mocks.fetch).toHaveBeenCalledTimes(2);
+ expect((await pool.query(
+ 'SELECT * FROM organization_memberships WHERE workos_user_id = ANY($1)', [[PRIMARY, LINKED]],
+ )).rows).toHaveLength(0);
+ });
+
+ it.each(['revoked', 'expired', 'future'] as const)('rejects every next key operation after an active exact grant becomes %s without changing source membership', async (state) => {
+ await pool.query(
+ `INSERT INTO organization_memberships
+ (workos_user_id, workos_organization_id, workos_membership_id, email, role)
+ VALUES ($1, $2, $3, $4, 'owner')`,
+ [PRIMARY, STREAMHAUS, 'mem_api_key_boundary_source', 'sam@pinnacle.example'],
+ );
+ const sourceMemberships = async () => (await pool.query(
+ 'SELECT * FROM organization_memberships WHERE workos_user_id = ANY($1) ORDER BY workos_user_id', [[PRIMARY, LINKED]],
+ )).rows;
+ const before = await sourceMemberships();
+ const id = await grant(LINKED, STREAMHAUS);
+ const active = await lifecycle(LINKED, STREAMHAUS);
+ expect(active.map((result) => result.status)).toEqual([200, 201, 204]);
+ expect(await sourceMemberships()).toEqual(before);
+ mocks.fetch.mockClear();
+
+ if (state === 'revoked') {
+ await pool.query(
+ 'UPDATE organization_credential_grants SET revoked_at = NOW(), revoked_by_workos_user_id = $2 WHERE id = $1',
+ [id, PRIMARY],
+ );
+ } else {
+ await pool.query(
+ `UPDATE organization_credential_grants
+ SET effective_from = NOW() + $2::interval,
+ effective_until = NOW() + $3::interval WHERE id = $1`,
+ [id, state === 'expired' ? '-2 days' : '1 day', state === 'expired' ? '-1 day' : '2 days'],
+ );
+ }
+ const results = await lifecycle(LINKED, STREAMHAUS);
+ expect(results.map((result) => result.status)).toEqual([403, 403, 403]);
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ expect(await sourceMemberships()).toEqual(before);
+ });
+
+ it('reads committed grant revocation and replacement without trusting stale membership or primary org', async () => {
+ await pool.query(
+ `INSERT INTO organization_memberships
+ (workos_user_id, workos_organization_id, workos_membership_id, email, role)
+ VALUES ($1, $2, $3, $4, 'owner')`,
+ [LINKED, PINNACLE, 'mem_api_key_boundary_stale', 'sam@streamhaus.example'],
+ );
+ const id = await grant(LINKED, STREAMHAUS);
+ expect((await list(LINKED, STREAMHAUS)).status).toBe(200);
+ await pool.query(
+ 'UPDATE organization_credential_grants SET revoked_at = NOW(), revoked_by_workos_user_id = $2 WHERE id = $1',
+ [id, PRIMARY],
+ );
+ mocks.fetch.mockClear();
+
+ expect((await list(LINKED, STREAMHAUS)).status).toBe(403);
+ expect((await list(LINKED, PINNACLE)).status).toBe(403);
+ expect((await list(LINKED)).status).toBe(400);
+ await grant(LINKED, STREAMHAUS, 'member');
+ expect((await list(LINKED, STREAMHAUS)).status).toBe(403);
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ expect((await pool.query(
+ 'SELECT workos_organization_id, role FROM organization_memberships WHERE workos_user_id = $1', [LINKED],
+ )).rows).toEqual([{ workos_organization_id: PINNACLE, role: 'owner' }]);
+ expect((await pool.query(
+ 'SELECT primary_organization_id FROM users WHERE workos_user_id = $1', [LINKED],
+ )).rows[0].primary_organization_id).toBe(PINNACLE);
+ });
+
+ it.each(['epoch changed', 'grant expired', 'grant revoked'] as const)('denies replayed selected context after %s even when fresh WorkOS membership could authorize', async (change) => {
+ const id = await grant(LINKED, STREAMHAUS);
+ mocks.previousSnapshot = (await loadAuthorizationSnapshot(LINKED, STREAMHAUS))!;
+ mocks.memberships.mockImplementation(async ({ userId, organizationId }) => ({
+ data: [{ userId, organizationId, status: 'active', role: { slug: 'owner' } }],
+ }));
+ expect((await lifecycle(LINKED, STREAMHAUS)).map((result) => result.status)).toEqual([200, 201, 204]);
+
+ if (change === 'epoch changed') {
+ await bumpAuthorizationEpochs(pool, [LINKED]);
+ } else if (change === 'grant revoked') {
+ await pool.query(
+ 'UPDATE organization_credential_grants SET revoked_at = NOW(), revoked_by_workos_user_id = $2 WHERE id = $1',
+ [id, PRIMARY],
+ );
+ } else {
+ await pool.query(
+ `UPDATE organization_credential_grants SET effective_from = NOW() - INTERVAL '2 days',
+ effective_until = NOW() - INTERVAL '1 day' WHERE id = $1`, [id],
+ );
+ }
+ mocks.memberships.mockClear();
+ mocks.fetch.mockClear();
+
+ expect((await lifecycle(LINKED, STREAMHAUS)).map((result) => result.status)).toEqual([403, 403, 403]);
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ expect((await pool.query(
+ 'SELECT * FROM organization_memberships WHERE workos_user_id = ANY($1)', [[PRIMARY, LINKED]],
+ )).rows).toHaveLength(0);
+ });
+});
diff --git a/server/tests/integration/community-mirrors.test.ts b/server/tests/integration/community-mirrors.test.ts
index 0248fc2913e..8c5e442084c 100644
--- a/server/tests/integration/community-mirrors.test.ts
+++ b/server/tests/integration/community-mirrors.test.ts
@@ -11,10 +11,10 @@ vi.hoisted(() => {
process.env.WORKOS_CLIENT_ID = process.env.WORKOS_CLIENT_ID ?? 'client_test';
});
-// Mutable identity for the authenticated caller so a re-publish can come from a
-// different user (used to assert created_by_* is preserved).
+// Mock the identity produced by credential validation. Static admin keys retain
+// manager access; organization API keys submit and read their own proposals.
const authState = vi.hoisted(() => ({
- userId: 'user_test_mirrors',
+ userId: 'admin_api_key',
email: 'mirrors@test.com',
organizationId: null as string | null,
}));
@@ -194,7 +194,7 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
notifyPendingCommunityMirrorProposal.mockResolvedValue(null);
notifyCommunityMirrorProposalReviewed.mockReset();
notifyCommunityMirrorProposalReviewed.mockResolvedValue(undefined);
- authState.userId = 'user_test_mirrors';
+ authState.userId = 'admin_api_key';
authState.email = 'mirrors@test.com';
authState.organizationId = null;
await clear();
@@ -357,11 +357,13 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
expect(read.body.adagents_json.authorized_agents).toEqual([]);
});
- it('allows an AAO admin who is not a registry moderator to publish', async () => {
+ it('denies human admin publishing without exact manager authorization', async () => {
+ authState.userId = 'user_human_admin';
isRegistryModerator.mockResolvedValue(false);
isWebUserAAOAdmin.mockResolvedValue(true);
const res = await request(app).put(`/api/registry/mirrors/${PLATFORM}`).send(publishBody());
- expect(res.status, JSON.stringify(res.body)).toBe(200);
+ expect(res.status, JSON.stringify(res.body)).toBe(403);
+ expect((await pool.query('SELECT 1 FROM community_mirrors WHERE platform = $1', [PLATFORM])).rows).toHaveLength(0);
});
it('re-publish is idempotent — updates in place, no duplicate row', async () => {
@@ -461,10 +463,12 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
expect(projectedProperties.rows.map((row) => row.publisher_domain)).toEqual([...finalDomains].sort());
});
- it('preserves created_by_* across a re-publish by a different user', async () => {
- authState.userId = 'creator-A';
+ it('preserves legacy created_by_* when an admin key re-publishes', async () => {
await request(app).put(`/api/registry/mirrors/${PLATFORM}`).send(publishBody({ catalog_etag: 'v1' }));
- authState.userId = 'editor-B';
+ await pool.query(
+ 'UPDATE community_mirrors SET created_by_user_id = $1 WHERE platform = $2',
+ ['creator-A', PLATFORM],
+ );
await request(app).put(`/api/registry/mirrors/${PLATFORM}`).send(publishBody({ catalog_etag: 'v2' }));
const { rows } = await pool.query(
@@ -559,7 +563,7 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
.send(publishBody({ catalog_etag: 'changed-v2', formats: [{ ...MINIMAL_FORMAT, display_name: 'Changed' }] }));
expect(changed.body.proposal_id).toBe(reviewed.body.proposal_id);
- authState.userId = 'user_registry_moderator';
+ authState.userId = 'admin_api_key';
authState.organizationId = null;
isRegistryModerator.mockResolvedValue(true);
const staleApproval = await request(app)
@@ -577,7 +581,7 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
authState.organizationId = 'org_test_mirrors';
const submitted = await request(app).put(`/api/registry/mirrors/${PLATFORM}`).send(publishBody());
- authState.userId = 'user_registry_moderator';
+ authState.userId = 'admin_api_key';
authState.organizationId = null;
isRegistryModerator.mockResolvedValue(true);
await request(app)
@@ -599,7 +603,7 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
authState.organizationId = 'org_test_mirrors';
const original = await request(app).put(`/api/registry/mirrors/${PLATFORM}`).send(publishBody());
- authState.userId = 'user_registry_moderator';
+ authState.userId = 'admin_api_key';
authState.organizationId = null;
isRegistryModerator.mockResolvedValue(true);
await request(app)
@@ -613,7 +617,7 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
expect(rebased.body.proposal_id).toBe(original.body.proposal_id);
expect(rebased.body.proposal_digest).not.toBe(original.body.proposal_digest);
- authState.userId = 'user_registry_moderator';
+ authState.userId = 'admin_api_key';
authState.organizationId = null;
isRegistryModerator.mockResolvedValue(true);
const staleApproval = await request(app)
@@ -650,7 +654,7 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
expect(ownDetail.status).toBe(200);
expect(ownDetail.body.proposal).not.toHaveProperty('proposed_by_email');
- authState.userId = 'user_registry_moderator';
+ authState.userId = 'admin_api_key';
authState.organizationId = null;
isRegistryModerator.mockResolvedValue(true);
const managerDetail = await request(app)
@@ -687,7 +691,7 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
expect(row.rows[0]?.slack_thread_ts).toBe('1779110411.874');
});
- it('restricts the cross-organization review queue to moderators', async () => {
+ it('restricts the cross-organization review queue to a validated admin key', async () => {
isRegistryModerator.mockResolvedValue(false);
authState.userId = 'api_key_test_mirrors';
authState.organizationId = 'org_test_mirrors';
@@ -697,7 +701,7 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
.get('/api/registry/mirror-proposals?status=pending&review_queue=true');
expect(denied.status).toBe(403);
- authState.userId = 'user_registry_moderator';
+ authState.userId = 'admin_api_key';
authState.organizationId = null;
isRegistryModerator.mockResolvedValue(true);
const queue = await request(app)
@@ -722,13 +726,13 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
expect(oversized.status).toBe(413);
});
- it('lets a moderator approve and atomically publish a pending proposal', async () => {
+ it('lets a validated admin key approve and atomically publish a pending proposal', async () => {
isRegistryModerator.mockResolvedValue(false);
authState.userId = 'api_key_test_mirrors';
authState.organizationId = 'org_test_mirrors';
const submitted = await request(app).put(`/api/registry/mirrors/${PLATFORM}`).send(publishBody());
- authState.userId = 'user_registry_moderator';
+ authState.userId = 'admin_api_key';
authState.organizationId = null;
isRegistryModerator.mockResolvedValue(true);
const approved = await request(app)
@@ -751,7 +755,7 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
);
expect(proposal.rows[0]).toMatchObject({
status: 'approved',
- reviewed_by_user_id: 'user_registry_moderator',
+ reviewed_by_user_id: 'admin_api_key',
published_at: expect.any(Date),
});
expect((await pool.query('SELECT 1 FROM community_mirrors WHERE platform = $1', [PLATFORM])).rows).toHaveLength(1);
@@ -777,13 +781,13 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
expect((await pool.query('SELECT 1 FROM community_mirrors WHERE platform = $1', [PLATFORM])).rows).toHaveLength(0);
});
- it('lets a moderator reject a pending proposal without publishing it', async () => {
+ it('lets a validated admin key reject a pending proposal without publishing it', async () => {
isRegistryModerator.mockResolvedValue(false);
authState.userId = 'api_key_test_mirrors';
authState.organizationId = 'org_test_mirrors';
const submitted = await request(app).put(`/api/registry/mirrors/${PLATFORM}`).send(publishBody());
- authState.userId = 'user_registry_moderator';
+ authState.userId = 'admin_api_key';
authState.organizationId = null;
isRegistryModerator.mockResolvedValue(true);
const rejected = await request(app)
@@ -972,6 +976,7 @@ describe('Community-mirror lifecycle — /api/registry/mirrors + /translated', (
await request(app)
.put(`/api/registry/mirrors/${PLATFORM}`)
.send(publishBody({ superseded_by: 'https://meta.com/.well-known/adagents.json' }));
+ authState.userId = 'user_non_manager';
isRegistryModerator.mockResolvedValue(false);
isWebUserAAOAdmin.mockResolvedValue(false);
const del = await request(app).delete(`/api/registry/mirrors/${PLATFORM}`);
diff --git a/server/tests/integration/registry-reader-baseline-public-endpoints.test.ts b/server/tests/integration/registry-reader-baseline-public-endpoints.test.ts
index 24687c547bf..6ae452a493d 100644
--- a/server/tests/integration/registry-reader-baseline-public-endpoints.test.ts
+++ b/server/tests/integration/registry-reader-baseline-public-endpoints.test.ts
@@ -25,6 +25,7 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
import request from 'supertest';
import type { Pool } from 'pg';
+import type { WorkOSUser } from '../../src/types.js';
// Set WorkOS env before vi.mock factories run — auth.ts constructs WorkOS
// at module load and the factory's vi.importActual triggers that load.
@@ -46,7 +47,7 @@ vi.hoisted(() => {
// matrix without rebuilding the route's dependency graph.
const DEFAULT_TEST_USER_ID = 'user_test_registry_baseline_endpoints';
const authState = vi.hoisted(() => ({
- optAuthUser: null as { id: string; email: string } | null,
+ optAuthUser: null as WorkOSUser | null,
requireAuthUser: {
id: 'user_test_registry_baseline_endpoints',
email: 'registry-baseline@test.com',
@@ -87,7 +88,7 @@ vi.mock('../../src/middleware/auth.js', async () => {
};
});
-function setOptAuthUser(user: { id: string; email: string } | null) {
+function setOptAuthUser(user: WorkOSUser | null) {
authState.optAuthUser = user;
}
@@ -114,9 +115,19 @@ vi.mock('../../src/billing/stripe-client.js', () => ({
createBillingPortalSession: vi.fn().mockResolvedValue(null),
}));
+// Scope fixtures use real exact-credential grants and snapshots in PostgreSQL.
+// No direct WorkOS membership is needed for those grants to authorize access.
+vi.mock('../../src/auth/workos-client.js', async importOriginal => ({
+ ...await importOriginal(),
+ getAuthorizationEnforcementWorkos: () => ({
+ userManagement: { listOrganizationMemberships: vi.fn().mockResolvedValue({ data: [] }) },
+ }),
+}));
+
import { HTTPServer } from '../../src/http.js';
import { initializeDatabase, closeDatabase } from '../../src/db/client.js';
import { runMigrations } from '../../src/db/migrate.js';
+import { loadAuthorizationSnapshot } from '../../src/db/user-authorization-snapshot-db.js';
import { FederatedIndexDatabase } from '../../src/db/federated-index-db.js';
import { FederatedIndexService } from '../../src/federated-index.js';
import { CrawlerService } from '../../src/crawler.js';
@@ -1214,8 +1225,7 @@ describe('Registry reader baseline — public endpoints', () => {
ON CONFLICT (workos_user_id) DO UPDATE SET primary_organization_id = EXCLUDED.primary_organization_id`,
[userId, `${userId}@example.com`, orgId],
);
- // resolvePrimaryOrganization trusts the cached column only when a
- // current organization_memberships row backs it.
+ // Keep legacy attribution populated; it must not supply tenant authority.
await pool.query(
`INSERT INTO organization_memberships
(workos_user_id, workos_organization_id, role, email, created_at, updated_at)
@@ -1223,6 +1233,12 @@ describe('Registry reader baseline — public endpoints', () => {
ON CONFLICT (workos_user_id, workos_organization_id) DO NOTHING`,
[userId, orgId, `${userId}@example.com`],
);
+ await pool.query(
+ `INSERT INTO organization_credential_grants
+ (workos_user_id, workos_organization_id, role, granted_by_workos_user_id, reason)
+ VALUES ($1, $2, 'member', $1, 'Scope matrix exact-credential fixture')`,
+ [userId, orgId],
+ );
}
async function clearScopeFixtures() {
@@ -1293,20 +1309,30 @@ describe('Registry reader baseline — public endpoints', () => {
});
type CallerKind = 'anonymous' | 'owner' | 'other_api' | 'explorer';
- function setCaller(kind: CallerKind) {
+ async function setCaller(kind: CallerKind, selectOrganization = true) {
if (kind === 'anonymous') return setOptAuthUser(null);
const id = kind === 'owner' ? SCOPE_OWNER_USER
: kind === 'other_api' ? SCOPE_OTHER_API_USER
: SCOPE_EXPLORER_USER;
- setOptAuthUser({ id, email: `${id}@example.com` });
+ const organizationId = kind === 'owner' ? SCOPE_OWNER_ORG
+ : kind === 'other_api' ? SCOPE_OTHER_API_ORG
+ : SCOPE_EXPLORER_ORG;
+ const snapshot = await loadAuthorizationSnapshot(id, selectOrganization ? organizationId : null);
+ expect(snapshot).not.toBeNull();
+ const user: WorkOSUser = { id: snapshot!.canonicalUserId, authWorkosUserId: id, email: `${id}@example.com` };
+ Object.defineProperty(user, 'authorizationSnapshot', { value: snapshot });
+ setOptAuthUser(user);
}
async function fetchAgents(scope: string | null, kind: CallerKind): Promise {
- setCaller(kind);
+ await setCaller(kind);
const qs = scope === null ? '' : `&scope=${encodeURIComponent(scope)}`;
- const res = await request(app).get(
+ const pending = request(app).get(
`/api/registry/operator?domain=${encodeURIComponent(SCOPE_DOMAIN)}${qs}`,
);
+ const organizationId = authState.optAuthUser?.authorizationSnapshot?.selectedOrganizationId;
+ if (organizationId) pending.set('X-Organization-Id', organizationId);
+ const res = await pending;
expect(res.status).toBe(200);
expect(res.body.agent_visibility_summary).toEqual({ public: 1, members_only: 1 });
return (res.body.agents as Array<{ url: string }>).map(a => a.url).sort();
@@ -1319,6 +1345,16 @@ describe('Registry reader baseline — public endpoints', () => {
it('scope omitted, explorer (no API tier) → public only', async () => {
expect(await fetchAgents(null, 'explorer')).toEqual([PUBLIC_URL]);
});
+ it('does not infer owner access without an explicit organization selection', async () => {
+ await setCaller('owner', false);
+ for (const scope of ['all', 'private']) {
+ const res = await request(app).get(
+ `/api/registry/operator?domain=${encodeURIComponent(SCOPE_DOMAIN)}&scope=${scope}`,
+ );
+ expect(res.status).toBe(200);
+ expect(res.body.agents.map((agent: { url: string }) => agent.url)).toEqual(scope === 'all' ? [PUBLIC_URL] : []);
+ }
+ });
it('scope omitted, API-tier non-owner → public + members_only', async () => {
expect(await fetchAgents(null, 'other_api')).toEqual(
[MEMBERS_URL, PUBLIC_URL].sort(),
@@ -1380,7 +1416,7 @@ describe('Registry reader baseline — public endpoints', () => {
// ── validation ────────────────────────────────────────────────────
it('rejects unknown scope with 400', async () => {
- setCaller('owner');
+ await setCaller('owner');
const res = await request(app).get(
`/api/registry/operator?domain=${encodeURIComponent(SCOPE_DOMAIN)}&scope=membr`,
);
diff --git a/server/tests/integration/sealed-bearer-organization.test.ts b/server/tests/integration/sealed-bearer-organization.test.ts
new file mode 100644
index 00000000000..42c298364ea
--- /dev/null
+++ b/server/tests/integration/sealed-bearer-organization.test.ts
@@ -0,0 +1,462 @@
+/** Real mounted authentication, snapshot SQL and org resolution; only WorkOS is mocked. */
+import express, { type Request } from 'express';
+import { randomUUID } from 'node:crypto';
+import cookieParser from 'cookie-parser';
+import request, { type Test } from 'supertest';
+import type { Pool } from 'pg';
+import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+
+const mocks = vi.hoisted(() => ({
+ authenticate: vi.fn(), refresh: vi.fn(), loadSealedSession: vi.fn(),
+ memberships: vi.fn(), validateApiKey: vi.fn(),
+}));
+vi.mock('@workos-inc/node', () => ({
+ WorkOS: class {
+ userManagement = {
+ loadSealedSession: mocks.loadSealedSession,
+ listOrganizationMemberships: mocks.memberships,
+ };
+ apiKeys = { createValidation: mocks.validateApiKey };
+ },
+}));
+
+import { initializeDatabase, closeDatabase } from '../../src/db/client.js';
+import * as database from '../../src/db/client.js';
+import { runMigrations } from '../../src/db/migrate.js';
+import { bumpAuthorizationEpochs } from '../../src/db/authorization-epoch-db.js';
+import { invalidateSessionCache, optionalAuth, requireAuth, stopAuthTimers } from '../../src/middleware/auth.js';
+import { resolveCallerOrgId, sendCallerOrganizationAuthError } from '../../src/routes/helpers/resolve-caller-org.js';
+import { buildConformanceTokenRouter } from '../../src/conformance/token-route.js';
+import { verifyConformanceToken } from '../../src/conformance/token.js';
+
+const PRIMARY = 'user_sealed_bearer_primary';
+const EXACT = 'user_sealed_bearer_exact';
+const ORG = 'org_sealed_bearer_pinnacle';
+const OTHER = 'org_sealed_bearer_streamhaus';
+const PROVIDER_USER = {
+ id: EXACT, email: 'sam@streamhaus.example', firstName: 'Sam', lastName: 'Adeyemi',
+ emailVerified: true, createdAt: new Date(0).toISOString(), updatedAt: new Date(0).toISOString(),
+};
+let pool: Pool;
+let identities: string[] = [];
+let sequence = 0;
+const tokenPrefix = randomUUID();
+const tokens: string[] = [];
+function token() {
+ const value = `sealed-native-${tokenPrefix}-${++sequence}`;
+ tokens.push(value);
+ return value;
+}
+function membership(userId = EXACT, organizationId = ORG, status = 'active') {
+ return { userId, organizationId, status, role: { slug: 'member' } };
+}
+
+async function cleanup() {
+ await pool.query('DELETE FROM organization_memberships WHERE workos_user_id = ANY($1)', [[PRIMARY, EXACT]]);
+ await pool.query('DELETE FROM organizations WHERE workos_organization_id = ANY($1)', [[ORG, OTHER]]);
+ await pool.query('DELETE FROM users WHERE workos_user_id = ANY($1)', [[PRIMARY, EXACT]]);
+ await pool.query('DELETE FROM identities WHERE id = ANY($1::uuid[])', [identities]);
+ identities = [];
+}
+
+beforeAll(async () => {
+ pool = initializeDatabase({
+ connectionString: process.env.DATABASE_URL || 'postgresql://adcp:localdev@localhost:5432/adcp_test',
+ });
+ await runMigrations();
+}, 60000);
+
+beforeEach(async () => {
+ vi.restoreAllMocks();
+ vi.unstubAllEnvs();
+ for (const value of tokens.splice(0)) invalidateSessionCache(value);
+ await cleanup();
+ mocks.authenticate.mockReset().mockResolvedValue({
+ authenticated: true, user: { ...PROVIDER_USER }, accessToken: 'native-access-token',
+ });
+ mocks.refresh.mockReset().mockResolvedValue({ authenticated: false, reason: 'invalid_grant', retryable: false });
+ mocks.loadSealedSession.mockReset().mockReturnValue({ authenticate: mocks.authenticate, refresh: mocks.refresh });
+ mocks.memberships.mockReset().mockResolvedValue({ data: [] });
+ mocks.validateApiKey.mockReset().mockResolvedValue(null);
+ await pool.query(
+ 'INSERT INTO organizations (workos_organization_id, name) VALUES ($1, $2), ($3, $4)',
+ [ORG, 'Pinnacle Agency', OTHER, 'StreamHaus'],
+ );
+ // Both credentials have a tempting primary org, even when no memberships exist.
+ await pool.query(
+ `INSERT INTO users (workos_user_id, email, primary_organization_id, email_verified)
+ VALUES ($1, $2, $3, true), ($4, $5, $3, true)`,
+ [PRIMARY, 'sam@pinnacle.example', ORG, EXACT, PROVIDER_USER.email],
+ );
+ const bindings = await pool.query(
+ 'SELECT workos_user_id, identity_id FROM identity_workos_users WHERE workos_user_id = ANY($1)', [[PRIMARY, EXACT]],
+ );
+ identities = bindings.rows.map(row => row.identity_id);
+ await pool.query(
+ 'UPDATE identity_workos_users SET identity_id = $1, is_primary = FALSE WHERE workos_user_id = $2',
+ [bindings.rows.find(row => row.workos_user_id === PRIMARY).identity_id, EXACT],
+ );
+});
+
+afterAll(async () => {
+ vi.restoreAllMocks();
+ vi.unstubAllEnvs();
+ stopAuthTimers();
+ try { if (pool) await cleanup(); } finally { await closeDatabase(); }
+});
+
+async function grant(state = 'valid', userId = EXACT, organizationId = ORG) {
+ const result = await pool.query(
+ `INSERT INTO organization_credential_grants
+ (workos_user_id, workos_organization_id, role, granted_by_workos_user_id,
+ effective_from, effective_until, revoked_at, revoked_by_workos_user_id)
+ VALUES ($1, $2, 'member', $3, $4, $5,
+ CASE WHEN $6 THEN NOW() ELSE NULL END,
+ CASE WHEN $6 THEN $3::varchar ELSE NULL END) RETURNING id`,
+ [userId, organizationId, PRIMARY,
+ state === 'future' ? '2999-01-01' : '2000-01-01',
+ state === 'expired' ? '2001-01-01' : null, state === 'revoked'],
+ );
+ return result.rows[0].id as string;
+}
+
+type Mode = 'required' | 'optional';
+function mount(mode: Mode, beforeResolve?: (req: Request) => void | Promise) {
+ const app = express();
+ app.use(cookieParser());
+ app.use(express.json());
+ app.use(mode === 'required' ? requireAuth : optionalAuth);
+ const reached: Request[] = [];
+ const handler = async (req: Request, res: express.Response, next: express.NextFunction) => {
+ reached.push(req);
+ try {
+ await beforeResolve?.(req);
+ const organizationId = await resolveCallerOrgId(req);
+ res.status(organizationId ? 200 : 403).json({ organizationId });
+ } catch (err) { next(err); }
+ };
+ app.post('/scope', handler);
+ // These params are intentionally populated AFTER authentication.
+ for (const field of ['org_id', 'orgId', 'organizationId']) app.post(`/${field}/:${field}/scope`, handler);
+ app.use((err: unknown, _req: Request, res: express.Response, _next: express.NextFunction) => {
+ if (!sendCallerOrganizationAuthError(err, res)) res.status(500).json({ error: 'unexpected_test_error' });
+ });
+ return { app, reached };
+}
+type Mounted = ReturnType;
+function post(mounted: Mounted, value: string, path = '/scope') {
+ return request(mounted.app).post(path).set('Authorization', `Bearer ${value}`);
+}
+function expectProvenance(req: Request) {
+ expect(req.authenticatedSealedBearer?.authorizationSnapshot).toBe(req.user?.authorizationSnapshot);
+ expect(req.user?.authorizationSnapshot).toMatchObject({ authenticatedUserId: EXACT, canonicalUserId: PRIMARY });
+ expect(req.user?.authWorkosUserId).toBe(EXACT);
+ expect(Object.keys(req)).not.toContain('authenticatedSealedBearer');
+ expect(Object.isFrozen(req.authenticatedSealedBearer)).toBe(true);
+ expect({ ...req.user }).not.toHaveProperty('authorizationSnapshot');
+}
+
+const selectors: Array<[string, string, (pending: Test, org: string) => Test]> = [
+ ['header', '/scope', (pending, org) => pending.set('X-Organization-Id', org)],
+ ...['org', 'org_id', 'organization_id', 'organizationId'].map(field =>
+ [`query ${field}`, '/scope', (pending: Test, org: string) => pending.query({ [field]: org })] as [string, string, (pending: Test, org: string) => Test]),
+ ...['org_id', 'organization_id', 'organizationId'].map(field =>
+ [`body ${field}`, '/scope', (pending: Test, org: string) => pending.send({ [field]: org })] as [string, string, (pending: Test, org: string) => Test]),
+ ...['org_id', 'orgId', 'organizationId'].map(field =>
+ [`path ${field}`, `/${field}/${ORG}/scope`, (pending: Test) => pending] as [string, string, (pending: Test, org: string) => Test]),
+];
+
+describe.each(['required', 'optional'] as const)('%s native sealed bearer', mode => {
+ describe.each(['miss', 'hit'] as const)('cache %s', cache => {
+ async function prepare(beforeResolve?: (req: Request) => void | Promise) {
+ const mounted = mount(mode, beforeResolve);
+ const value = token();
+ if (cache === 'hit') {
+ expect((await post(mount(mode), value)).status).toBe(403);
+ mocks.memberships.mockClear();
+ }
+ return { mounted, value };
+ }
+
+ it.each([0, 1, 2])('leaves organization null with %i orgs and no selector', async count => {
+ for (const organizationId of [ORG, OTHER].slice(0, count)) {
+ await pool.query(
+ `INSERT INTO organization_memberships (workos_user_id, workos_organization_id, email, role)
+ VALUES ($1, $2, $3, 'owner')`, [EXACT, organizationId, PROVIDER_USER.email],
+ );
+ await grant('valid', EXACT, organizationId);
+ }
+ mocks.memberships.mockResolvedValue({ data: [ORG, OTHER].slice(0, count).map(org => membership(EXACT, org)) });
+ const { mounted, value } = await prepare();
+ const response = await post(mounted, value);
+ expect(response.status).toBe(403);
+ expect(response.body).toEqual({ organizationId: null });
+ expectProvenance(mounted.reached[0]);
+ expect(mounted.reached[0].user?.authorizationSnapshot?.selectedOrganizationId).toBeNull();
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ expect(mocks.validateApiKey).not.toHaveBeenCalled();
+ expect(mocks.authenticate).toHaveBeenCalledOnce();
+ });
+
+ it.each(selectors)('authorizes exact direct membership selected by %s', async (_name, path, select) => {
+ mocks.memberships.mockResolvedValue({ data: [membership()] });
+ const { mounted, value } = await prepare();
+ const response = await select(post(mounted, value, path), ORG);
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual({ organizationId: ORG });
+ expectProvenance(mounted.reached[0]);
+ expect(mocks.memberships).toHaveBeenCalledExactlyOnceWith({ userId: EXACT, organizationId: ORG });
+ expect(mocks.authenticate).toHaveBeenCalledOnce();
+ expect(mocks.validateApiKey).not.toHaveBeenCalled();
+ });
+
+ it.each(['absent', 'unavailable'])('authorizes a valid grant with %s direct membership', async state => {
+ await grant();
+ if (state === 'unavailable') mocks.memberships.mockRejectedValue(new Error('WorkOS unavailable'));
+ const { mounted, value } = await prepare();
+ expect((await post(mounted, value).set('X-Organization-Id', ORG)).status).toBe(200);
+ expectProvenance(mounted.reached[0]);
+ expect(mounted.reached[0].user?.authorizationSnapshot?.credentialGrant?.organizationId).toBe(ORG);
+ expect(mocks.memberships).toHaveBeenCalledExactlyOnceWith({ userId: EXACT, organizationId: ORG });
+ });
+
+ it.each(['missing', 'expired', 'future', 'revoked', 'other org', 'canonical sibling', 'inactive membership'])('denies %s authority', async state => {
+ if (['expired', 'future', 'revoked'].includes(state)) await grant(state);
+ if (state === 'other org') await grant('valid', EXACT, OTHER);
+ if (state === 'canonical sibling') {
+ await grant('valid', PRIMARY);
+ mocks.memberships.mockResolvedValue({ data: [membership(PRIMARY)] });
+ }
+ if (state === 'inactive membership') mocks.memberships.mockResolvedValue({ data: [membership(EXACT, ORG, 'inactive')] });
+ const { mounted, value } = await prepare();
+ const response = await post(mounted, value).set('X-Organization-Id', ORG);
+ expect(response.status).toBe(403);
+ expect(response.body).toEqual({ organizationId: null });
+ expect(mounted.reached[0].user?.authorizationSnapshot?.credentialGrant).toBeNull();
+ expect(mocks.memberships).toHaveBeenCalledExactlyOnceWith({ userId: EXACT, organizationId: ORG });
+ });
+
+ it.each(selectors)('denies mismatched %s selection before provider membership reads', async (_name, path, select) => {
+ mocks.authenticate.mockResolvedValue({
+ authenticated: true, user: { ...PROVIDER_USER }, accessToken: 'native-access-token', organizationId: OTHER,
+ });
+ const mounted = mount(mode);
+ const value = token();
+ if (cache === 'hit') expect((await post(mounted, value)).status).toBe(403);
+ mocks.memberships.mockClear();
+ const response = await select(post(mounted, value, path), ORG);
+ expect(response.status).toBe(403);
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ expect(mocks.authenticate).toHaveBeenCalledOnce();
+ });
+
+ it.each(['epoch', 'grant revocation', 'database unavailable'])('denies %s between middleware and resolver without provider reads', async state => {
+ const id = await grant();
+ const { mounted, value } = await prepare(async () => {
+ if (state === 'epoch') await bumpAuthorizationEpochs(pool, [EXACT]);
+ if (state === 'grant revocation') await pool.query(
+ 'UPDATE organization_credential_grants SET revoked_at = NOW(), revoked_by_workos_user_id = $2 WHERE id = $1', [id, PRIMARY],
+ );
+ if (state === 'database unavailable') vi.spyOn(database, 'queryWithTimeout').mockRejectedValue(new Error('private database outage'));
+ });
+ const response = await post(mounted, value).set('X-Organization-Id', ORG);
+ expect(response.status).toBe(403);
+ expect(response.body).toEqual({ organizationId: null });
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it('returns 503 before the handler when snapshot hydration is unavailable', async () => {
+ const { mounted, value } = await prepare();
+ vi.spyOn(database, 'queryWithTimeout').mockRejectedValue(new Error('private database outage'));
+ const response = await post(mounted, value).set('X-Organization-Id', ORG);
+ expect(response.status).toBe(503);
+ expect(mounted.reached).toHaveLength(0);
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ expect(response.text).not.toContain('private');
+ });
+
+ it('denies unavailable membership without a grant instead of selecting a primary org', async () => {
+ mocks.memberships.mockRejectedValue(new Error('private provider outage'));
+ const { mounted, value } = await prepare();
+ const response = await post(mounted, value).set('X-Organization-Id', ORG);
+ expect(response.status).toBe(403);
+ expect(response.body).toEqual({ organizationId: null });
+ });
+
+ it('denies a missing exact credential even when its canonical sibling remains', async () => {
+ const { mounted, value } = await prepare();
+ await pool.query('DELETE FROM users WHERE workos_user_id = $1', [EXACT]);
+ expect((await post(mounted, value).set('X-Organization-Id', ORG)).status).toBe(401);
+ expect(mounted.reached).toHaveLength(0);
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+ });
+
+ it.each(['invalid', 'revoked'])('rejects an %s opaque bearer and dead-cache replay without using a valid cookie', async state => {
+ const mounted = mount(mode);
+ const validCookie = token();
+ expect((await request(mounted.app).post('/scope').set('Cookie', `wos-session=${validCookie}`)).status).toBe(403);
+ mounted.reached.length = 0;
+ mocks.authenticate.mockClear().mockResolvedValue({ authenticated: false, reason: state === 'revoked' ? 'invalid_grant' : 'invalid_session' });
+ const invalid = token();
+ for (let i = 0; i < 2; i++) {
+ const response = await post(mounted, invalid).set('Cookie', `wos-session=${validCookie}`).set('X-Organization-Id', ORG)
+ .set('authenticatedSealedBearer', 'true').send({ authenticatedSealedBearer: { tokenHash: invalid } });
+ expect(response.status).toBe(401);
+ }
+ expect(mocks.authenticate).toHaveBeenCalledOnce();
+ expect(mounted.reached).toHaveLength(0);
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it('rejects a previously cached bearer after provider revocation and cache invalidation', async () => {
+ mocks.memberships.mockResolvedValue({ data: [membership()] });
+ const mounted = mount(mode);
+ const value = token();
+ expect((await post(mounted, value).set('X-Organization-Id', ORG)).status).toBe(200);
+ invalidateSessionCache(value);
+ mocks.authenticate.mockResolvedValue({ authenticated: false, reason: 'invalid_grant' });
+ mocks.memberships.mockClear();
+ expect((await post(mounted, value).set('X-Organization-Id', ORG)).status).toBe(401);
+ expect(mounted.reached).toHaveLength(1);
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it.each(['token', 'snapshot', 'principal'])('rejects a replaced %s after middleware authentication', async state => {
+ const mounted = mount(mode, req => {
+ if (state === 'token') req.headers.authorization = 'Bearer different-invalid-token';
+ if (state === 'snapshot') req.user = { ...req.user! };
+ if (state === 'principal') req.user!.authWorkosUserId = PRIMARY;
+ });
+ const response = await post(mounted, token()).set('X-Organization-Id', ORG);
+ expect(response.status).toBe(state === 'principal' ? 403 : 401);
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it('does not trust an opaque bearer added to an authenticated cookie request without provenance', async () => {
+ await grant();
+ const mounted = mount(mode, req => { req.headers.authorization = 'Bearer unauthenticated-opaque'; });
+ const response = await request(mounted.app).post('/scope')
+ .set('Cookie', `wos-session=${token()}`).set('X-Organization-Id', ORG);
+ expect(response.status).toBe(401);
+ expect(mounted.reached[0].user?.authorizationSnapshot?.credentialGrant).not.toBeNull();
+ expect(mounted.reached[0].authenticatedSealedBearer).toBeUndefined();
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it.each(['', null, [], {}])('rejects malformed explicit selector %j without provider membership reads', async selector => {
+ const mounted = mount(mode);
+ expect((await post(mounted, token()).send({ organization_id: selector })).status).toBe(403);
+ expect(mounted.reached).toHaveLength(0);
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it('returns 503 on provider authentication outage without reaching organization resolution', async () => {
+ mocks.authenticate.mockRejectedValue(Object.assign(new Error('private provider outage'), { code: 'ETIMEDOUT' }));
+ const mounted = mount(mode);
+ const response = await post(mounted, token()).set('X-Organization-Id', ORG);
+ expect(response.status).toBe(503);
+ expect(mounted.reached).toHaveLength(0);
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it('keeps overlapping cache hits bound to independent explicit selections', async () => {
+ const mounted = mount(mode);
+ const value = token();
+ expect((await post(mounted, value)).status).toBe(403);
+ mocks.memberships.mockImplementation(async ({ organizationId }) => ({ data: [membership(EXACT, organizationId)] }));
+ const responses = await Promise.all([ORG, OTHER].map(org => post(mounted, value).set('X-Organization-Id', org)));
+ expect(responses.map(response => response.body.organizationId)).toEqual([ORG, OTHER]);
+ const requests = mounted.reached.slice(1);
+ requests.forEach(expectProvenance);
+ expect(new Set(requests.map(req => req.user?.authorizationSnapshot?.selectedOrganizationId))).toEqual(new Set([ORG, OTHER]));
+ expect(requests[0].authenticatedSealedBearer).not.toBe(requests[1].authenticatedSealedBearer);
+ expect(mocks.authenticate).toHaveBeenCalledOnce();
+ });
+
+ it('rehydrates a fresh snapshot and selection on every cache hit without retaining handler mutation', async () => {
+ mocks.memberships.mockResolvedValue({ data: [membership(), membership(EXACT, OTHER)] });
+ const mounted = mount(mode);
+ const value = token();
+ expect((await post(mounted, value).set('X-Organization-Id', ORG)).status).toBe(200);
+ const first = mounted.reached[0];
+ first.user!.id = 'user_mutated_by_handler';
+ first.user!.authWorkosUserId = 'user_mutated_credential';
+ await bumpAuthorizationEpochs(pool, [EXACT]);
+ expect((await post(mounted, value).set('X-Organization-Id', OTHER)).status).toBe(200);
+ expect((await post(mounted, value)).body).toEqual({ organizationId: null });
+ const second = mounted.reached[1];
+ expectProvenance(second);
+ expect(second.user?.authorizationSnapshot?.authorizationEpoch).toBe('1');
+ expect(second.authenticatedSealedBearer).not.toBe(first.authenticatedSealedBearer);
+ expect(second.user?.authorizationSnapshot).not.toBe(first.user?.authorizationSnapshot);
+ expect(mocks.authenticate).toHaveBeenCalledOnce();
+ });
+
+ it('does not reuse a cached grant after committed revocation', async () => {
+ const id = await grant();
+ const mounted = mount(mode);
+ const value = token();
+ expect((await post(mounted, value).set('X-Organization-Id', ORG)).status).toBe(200);
+ await pool.query(
+ 'UPDATE organization_credential_grants SET revoked_at = NOW(), revoked_by_workos_user_id = $2 WHERE id = $1', [id, PRIMARY],
+ );
+ expect((await post(mounted, value).set('X-Organization-Id', ORG)).status).toBe(403);
+ expect(mounted.reached[1].user?.authorizationSnapshot?.credentialGrant).toBeNull();
+ expect(mocks.authenticate).toHaveBeenCalledOnce();
+ });
+
+ it('rejects an epoch change during the provider membership read', async () => {
+ mocks.memberships.mockImplementation(async () => {
+ await bumpAuthorizationEpochs(pool, [EXACT]);
+ return { data: [membership()] };
+ });
+ expect((await post(mount(mode), token()).set('X-Organization-Id', ORG)).status).toBe(403);
+ });
+
+ it('preserves successful refresh provenance on the initial request and cache hit', async () => {
+ mocks.authenticate.mockResolvedValueOnce({ authenticated: false });
+ mocks.refresh.mockResolvedValueOnce({ authenticated: true, sealedSession: token() });
+ mocks.memberships.mockResolvedValue({ data: [membership()] });
+ const mounted = mount(mode);
+ const value = token();
+ for (let i = 0; i < 2; i++) expect((await post(mounted, value).set('X-Organization-Id', ORG)).status).toBe(200);
+ mounted.reached.forEach(expectProvenance);
+ expect(mocks.authenticate).toHaveBeenCalledTimes(2);
+ expect(mocks.refresh).toHaveBeenCalledOnce();
+ });
+});
+
+describe('shared sealed-session cache transport isolation', () => {
+ it.each(['required', 'optional'] as const)('keeps provenance local when %s auth seeds the cache', async mode => {
+ mocks.memberships.mockResolvedValue({ data: [membership()] });
+ const first = mount(mode);
+ const second = mount(mode === 'required' ? 'optional' : 'required');
+ const value = token();
+ const cookie = (mounted: Mounted) => request(mounted.app).post('/scope').set('Cookie', `wos-session=${value}`);
+ expect((await cookie(first).set('X-Organization-Id', ORG)).status).toBe(200);
+ expect(first.reached[0].authenticatedSealedBearer).toBeUndefined();
+ expect((await post(second, value).set('X-Organization-Id', ORG)).status).toBe(200);
+ expectProvenance(second.reached[0]);
+ expect((await cookie(first)).body).toEqual({ organizationId: null });
+ expect(first.reached[1].authenticatedSealedBearer).toBeUndefined();
+ expect(mocks.authenticate).toHaveBeenCalledOnce();
+ });
+
+ it('issues an org-bound conformance token through the real required-auth consumer', async () => {
+ vi.stubEnv('CONFORMANCE_JWT_SECRET', 'sealed-bearer-conformance-test-secret');
+ await grant();
+ const app = express();
+ app.use(cookieParser(), express.json());
+ app.use('/api/conformance', buildConformanceTokenRouter());
+ const value = token();
+ for (let i = 0; i < 2; i++) {
+ const response = await request(app).post('/api/conformance/token')
+ .set('Authorization', `bEaReR\t ${value}`).send({ organization_id: ORG });
+ expect(response.status).toBe(200);
+ expect(verifyConformanceToken(response.body.token).sub).toBe(ORG);
+ }
+ expect(mocks.authenticate).toHaveBeenCalledOnce();
+ expect((await request(app).post('/api/conformance/token').set('Authorization', `Bearer ${value}`)).status).toBe(403);
+ });
+});
diff --git a/server/tests/integration/user-authorization-snapshot.test.ts b/server/tests/integration/user-authorization-snapshot.test.ts
new file mode 100644
index 00000000000..dd00ef27621
--- /dev/null
+++ b/server/tests/integration/user-authorization-snapshot.test.ts
@@ -0,0 +1,380 @@
+import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Pool, PoolClient } from 'pg';
+import { closeDatabase, initializeDatabase } from '../../src/db/client.js';
+import * as database from '../../src/db/client.js';
+import { runMigrations } from '../../src/db/migrate.js';
+import {
+ AuthorizationSnapshotUnavailableError,
+ loadAuthorizationSnapshot,
+ sameAuthorizationIdentity,
+ sameAuthorizationSnapshot,
+ type AuthorizationSnapshot,
+} from '../../src/db/user-authorization-snapshot-db.js';
+import { bumpAuthorizationEpochs } from '../../src/db/authorization-epoch-db.js';
+
+const PERSONAL_ID = 'user_authorization_snapshot_personal';
+const CORPORATE_ID = 'user_authorization_snapshot_corporate';
+const PERSONAL_ORG = 'org_authorization_snapshot_personal';
+const CORPORATE_ORG = 'org_authorization_snapshot_corporate';
+const USER_IDS = [PERSONAL_ID, CORPORATE_ID];
+const ORG_IDS = [PERSONAL_ORG, CORPORATE_ORG];
+
+describe('primary database authorization snapshots', () => {
+ let pool: Pool;
+ let fixtureIdentityIds: string[] = [];
+
+ beforeAll(async () => {
+ pool = initializeDatabase({
+ connectionString: process.env.DATABASE_URL || 'postgresql://adcp:localdev@localhost:5432/adcp_test',
+ });
+ await runMigrations();
+ }, 60000);
+
+ async function cleanup() {
+ await pool.query('DELETE FROM organization_memberships WHERE workos_organization_id = ANY($1)', [ORG_IDS]);
+ await pool.query('DELETE FROM organizations WHERE workos_organization_id = ANY($1)', [ORG_IDS]);
+ const identities = await pool.query<{ identity_id: string }>(
+ 'SELECT identity_id FROM identity_workos_users WHERE workos_user_id = ANY($1)', [USER_IDS],
+ );
+ await pool.query('DELETE FROM users WHERE workos_user_id = ANY($1)', [USER_IDS]);
+ await pool.query('DELETE FROM identities WHERE id = ANY($1)', [
+ [...fixtureIdentityIds, ...identities.rows.map(row => row.identity_id)],
+ ]);
+ fixtureIdentityIds = [];
+ }
+
+ beforeEach(async () => {
+ vi.restoreAllMocks();
+ await cleanup();
+ await pool.query(
+ `INSERT INTO users (workos_user_id, email, first_name, last_name, email_verified,
+ workos_created_at, workos_updated_at)
+ VALUES ($1, 'sam.personal@example.test', 'Sam', 'Adeyemi', true, NOW(), NOW()),
+ ($2, 'sam@pinnacle.example', 'Sam', 'Adeyemi', true, NOW(), NOW())`,
+ USER_IDS,
+ );
+ await pool.query(
+ `INSERT INTO organizations (workos_organization_id, name)
+ VALUES ($1, 'Sam personal organization'), ($2, 'Pinnacle Agency')`,
+ ORG_IDS,
+ );
+ const identities = await pool.query<{ identity_id: string }>(
+ 'SELECT identity_id FROM identity_workos_users WHERE workos_user_id = ANY($1)', [USER_IDS],
+ );
+ fixtureIdentityIds = identities.rows.map(row => row.identity_id);
+ });
+
+ afterAll(async () => {
+ vi.restoreAllMocks();
+ if (pool) await cleanup();
+ await closeDatabase();
+ });
+
+ async function snapshot(userId = PERSONAL_ID, organizationId: string | null = null) {
+ const result = await loadAuthorizationSnapshot(userId, organizationId);
+ expect(result).not.toBeNull();
+ return result!;
+ }
+
+ async function link(primaryId: string, secondaryId: string, db: Pool | PoolClient = pool) {
+ await db.query(
+ `UPDATE identity_workos_users
+ SET identity_id = (SELECT identity_id FROM identity_workos_users WHERE workos_user_id = $1),
+ is_primary = FALSE
+ WHERE workos_user_id = $2`,
+ [primaryId, secondaryId],
+ );
+ }
+
+ async function grant(
+ userId: string,
+ organizationId: string,
+ options: { from?: string; until?: string | null; revoked?: boolean; role?: 'member' | 'admin' | 'owner' } = {},
+ ) {
+ const result = await pool.query<{ id: string }>(
+ `INSERT INTO organization_credential_grants
+ (workos_user_id, workos_organization_id, role, granted_by_workos_user_id,
+ effective_from, effective_until, revoked_at, revoked_by_workos_user_id)
+ VALUES ($1, $2, $3, $1, $4, $5,
+ CASE WHEN $6 THEN NOW() ELSE NULL END,
+ CASE WHEN $6 THEN $1::varchar ELSE NULL END)
+ RETURNING id`,
+ [userId, organizationId, options.role ?? 'member', options.from ?? '2000-01-01T00:00:00.123456Z',
+ options.until ?? null, options.revoked ?? false],
+ );
+ return result.rows[0].id;
+ }
+
+ it('hydrates a cold exact credential with epoch zero and no implicit organization', async () => {
+ await grant(PERSONAL_ID, PERSONAL_ORG);
+ await pool.query(
+ `INSERT INTO organization_memberships (workos_user_id, workos_organization_id, email, role)
+ VALUES ($1, $2, 'sam.personal@example.test', 'owner')`,
+ [PERSONAL_ID, CORPORATE_ORG],
+ );
+
+ const cold = await snapshot();
+ expect(cold).toMatchObject({
+ authenticatedUserId: PERSONAL_ID,
+ canonicalUserId: PERSONAL_ID,
+ authorizationEpoch: '0',
+ selectedOrganizationId: null,
+ credentialGrant: null,
+ credential: { email: 'sam.personal@example.test', firstName: 'Sam', lastName: 'Adeyemi' },
+ });
+ expect(cold.identityId).toBeTruthy();
+ expect(Object.isFrozen(cold)).toBe(true);
+ expect(Object.isFrozen(cold.credential)).toBe(true);
+ expect(await snapshot(PERSONAL_ID, '')).toEqual(cold);
+ expect((await snapshot(PERSONAL_ID, CORPORATE_ORG)).credentialGrant).toBeNull();
+ });
+
+ it.each([
+ { direction: 'personal primary', primaryId: PERSONAL_ID, secondaryId: CORPORATE_ID },
+ { direction: 'corporate primary', primaryId: CORPORATE_ID, secondaryId: PERSONAL_ID },
+ ])('keeps exact credential grants isolated with $direction', async ({ primaryId, secondaryId }) => {
+ await link(primaryId, secondaryId);
+ const personalGrantId = await grant(PERSONAL_ID, PERSONAL_ORG);
+ const corporateGrantId = await grant(CORPORATE_ID, CORPORATE_ORG, { role: 'admin' });
+ // Stale consolidated membership state is deliberately stronger than the
+ // exact grants. Neither identity direction may inherit it as authority.
+ await pool.query(
+ `INSERT INTO organization_memberships (workos_user_id, workos_organization_id, email, role)
+ VALUES ($1, $2, 'sam@pinnacle.example', 'owner'),
+ ($1, $3, 'sam@pinnacle.example', 'owner')`,
+ [primaryId, PERSONAL_ORG, CORPORATE_ORG],
+ );
+
+ const personal = await snapshot(PERSONAL_ID, PERSONAL_ORG);
+ const corporate = await snapshot(CORPORATE_ID, CORPORATE_ORG);
+ expect(personal.canonicalUserId).toBe(primaryId);
+ expect(corporate.canonicalUserId).toBe(primaryId);
+ expect(personal.identityId).toBe(corporate.identityId);
+ expect(personal.credential.email).toBe('sam.personal@example.test');
+ expect(corporate.credential.email).toBe('sam@pinnacle.example');
+ expect(personal.credentialGrant).toEqual({
+ id: personalGrantId, organizationId: PERSONAL_ORG, role: 'member',
+ effectiveFrom: '2000-01-01T00:00:00.123456Z', effectiveUntil: null,
+ });
+ expect(corporate.credentialGrant?.id).toBe(corporateGrantId);
+ expect(corporate.credentialGrant?.role).toBe('admin');
+ expect(Object.isFrozen(personal.credentialGrant)).toBe(true);
+ expect((await snapshot(PERSONAL_ID, CORPORATE_ORG)).credentialGrant).toBeNull();
+ expect((await snapshot(CORPORATE_ID, PERSONAL_ORG)).credentialGrant).toBeNull();
+ expect((await snapshot(secondaryId, null)).credentialGrant).toBeNull();
+ expect(sameAuthorizationIdentity(personal, corporate)).toBe(false);
+ });
+
+ it.each([
+ { state: 'expired', from: '2000-01-01T00:00:00Z', until: '2001-01-01T00:00:00Z' },
+ { state: 'future', from: '2999-01-01T00:00:00Z', until: null },
+ { state: 'revoked', revoked: true },
+ ])('excludes $state grants on a cold read', async options => {
+ await grant(PERSONAL_ID, PERSONAL_ORG, options);
+ const cold = await snapshot(PERSONAL_ID, PERSONAL_ORG);
+ expect(cold.selectedOrganizationId).toBe(PERSONAL_ORG);
+ expect(cold.credentialGrant).toBeNull();
+ });
+
+ it('does not reuse a previously loaded grant after revocation', async () => {
+ const grantId = await grant(PERSONAL_ID, PERSONAL_ORG);
+ const cached = await snapshot(PERSONAL_ID, PERSONAL_ORG);
+ await pool.query(
+ `UPDATE organization_credential_grants
+ SET revoked_at = NOW(), revoked_by_workos_user_id = $2 WHERE id = $1`,
+ [grantId, PERSONAL_ID],
+ );
+ const fresh = await snapshot(PERSONAL_ID, PERSONAL_ORG);
+ expect(fresh.credentialGrant).toBeNull();
+ expect(sameAuthorizationIdentity(cached, fresh)).toBe(true);
+ expect(sameAuthorizationSnapshot(cached, fresh)).toBe(false);
+ });
+
+ it('preserves bigint epochs and rejects forwards and backwards replay', async () => {
+ await pool.query(
+ 'INSERT INTO authorization_epochs (workos_user_id, epoch) VALUES ($1, $2)',
+ [PERSONAL_ID, '9007199254740992'],
+ );
+ const old = await snapshot();
+ await bumpAuthorizationEpochs(pool, [PERSONAL_ID]);
+ const bumped = await snapshot();
+ expect(old.authorizationEpoch).toBe('9007199254740992');
+ expect(bumped.authorizationEpoch).toBe('9007199254740993');
+ expect(sameAuthorizationSnapshot(old, bumped)).toBe(false);
+ expect(sameAuthorizationSnapshot(bumped, old)).toBe(false);
+ await pool.query('DELETE FROM authorization_epochs WHERE workos_user_id = $1', [PERSONAL_ID]);
+ const absent = await snapshot();
+ expect(absent.authorizationEpoch).toBe('0');
+ expect(sameAuthorizationSnapshot(bumped, absent)).toBe(false);
+ });
+
+ it('rejects replays with altered credential, identity, selection, or grant details', async () => {
+ await grant(PERSONAL_ID, PERSONAL_ORG, { until: '2999-01-01T00:00:00.654321Z' });
+ const current = await snapshot(PERSONAL_ID, PERSONAL_ORG);
+ expect(current.credentialGrant?.effectiveUntil).toBe('2999-01-01T00:00:00.654321Z');
+ expect(sameAuthorizationSnapshot(current, structuredClone(current))).toBe(true);
+ const identityChanges: Partial[] = [
+ { authenticatedUserId: CORPORATE_ID },
+ { canonicalUserId: CORPORATE_ID },
+ { identityId: null },
+ { authorizationEpoch: '1' },
+ { credential: { ...current.credential, email: 'changed@example.test' } },
+ ];
+ for (const change of identityChanges) {
+ expect(sameAuthorizationIdentity(current, { ...current, ...change })).toBe(false);
+ expect(sameAuthorizationSnapshot(current, { ...current, ...change })).toBe(false);
+ }
+ expect(sameAuthorizationSnapshot(current, { ...current, selectedOrganizationId: CORPORATE_ORG })).toBe(false);
+ expect(sameAuthorizationSnapshot(current, { ...current, credentialGrant: null })).toBe(false);
+ const grantChanges: Partial>[] = [
+ { id: 'replacement-grant' },
+ { organizationId: CORPORATE_ORG },
+ { role: 'owner' },
+ { effectiveFrom: '2000-01-01T00:00:00.123457Z' },
+ { effectiveUntil: '2999-01-01T00:00:00.654322Z' },
+ ];
+ for (const change of grantChanges) {
+ expect(sameAuthorizationSnapshot(current, {
+ ...current, credentialGrant: { ...current.credentialGrant!, ...change },
+ })).toBe(false);
+ }
+ });
+
+ it.each([
+ { direction: 'personal into corporate', primaryId: CORPORATE_ID, secondaryId: PERSONAL_ID },
+ { direction: 'corporate into personal', primaryId: PERSONAL_ID, secondaryId: CORPORATE_ID },
+ ])('cannot stamp old identity with a newly committed epoch: $direction', async ({ primaryId, secondaryId }) => {
+ const before = await snapshot(secondaryId);
+ const writer = await pool.connect();
+ let releaseRead!: () => void;
+ let readCompleted!: () => void;
+ const readBarrier = new Promise(resolve => { readCompleted = resolve; });
+ const deliveryBarrier = new Promise(resolve => { releaseRead = resolve; });
+ let pendingRead: Promise | undefined;
+ const actualQuery = database.queryWithTimeout;
+ try {
+ await writer.query('BEGIN');
+ await link(primaryId, secondaryId, writer);
+ // An observer while identity changed but before the transactional epoch
+ // write still sees the complete old state through PostgreSQL MVCC.
+ expect(await snapshot(secondaryId)).toEqual(before);
+ await bumpAuthorizationEpochs(writer, [primaryId, secondaryId]);
+
+ // Hold delivery of the first real SQL result while the writer commits.
+ // A split identity/epoch loader would then issue its epoch query against
+ // the new commit and return old identity + new epoch. No rows are mocked.
+ const readSpy = vi.spyOn(database, 'queryWithTimeout').mockImplementationOnce(async (...args) => {
+ const result = await actualQuery(...args);
+ readCompleted();
+ await deliveryBarrier;
+ return result;
+ });
+ pendingRead = loadAuthorizationSnapshot(secondaryId, null);
+ await Promise.race([readBarrier, pendingRead.then(() => {
+ throw new Error('Snapshot returned without waiting for the database read');
+ })]);
+ await writer.query('COMMIT');
+ releaseRead();
+ const overlapping = await pendingRead;
+ expect(overlapping).toEqual(before);
+ expect(readSpy).toHaveBeenCalledTimes(1);
+ readSpy.mockRestore();
+
+ const after = await snapshot(secondaryId);
+ expect(after.canonicalUserId).toBe(primaryId);
+ expect(after.identityId).not.toBe(before.identityId);
+ expect(after.authorizationEpoch).toBe('1');
+ expect(sameAuthorizationSnapshot(before, after)).toBe(false);
+ expect(sameAuthorizationSnapshot(after, before)).toBe(false);
+ } finally {
+ releaseRead();
+ await writer.query('ROLLBACK');
+ await pendingRead?.catch(() => undefined);
+ vi.restoreAllMocks();
+ writer.release();
+ }
+ });
+
+ it('returns null for a missing exact credential, without using its former canonical person', async () => {
+ await link(CORPORATE_ID, PERSONAL_ID);
+ const cached = await snapshot(PERSONAL_ID);
+ await pool.query('DELETE FROM users WHERE workos_user_id = $1', [PERSONAL_ID]);
+ expect(await loadAuthorizationSnapshot(PERSONAL_ID, CORPORATE_ORG)).toBeNull();
+ expect((await snapshot(CORPORATE_ID)).canonicalUserId).toBe(cached.canonicalUserId);
+ });
+
+ it.each(['binding', 'primary'])('fails unavailable for a missing identity %s', async missing => {
+ if (missing === 'binding') {
+ await pool.query('DELETE FROM identity_workos_users WHERE workos_user_id = $1', [PERSONAL_ID]);
+ } else {
+ await pool.query('UPDATE identity_workos_users SET is_primary = FALSE WHERE workos_user_id = $1', [PERSONAL_ID]);
+ }
+ await expect(loadAuthorizationSnapshot(PERSONAL_ID, null))
+ .rejects.toBeInstanceOf(AuthorizationSnapshotUnavailableError);
+ });
+
+ it('fails unavailable on database errors instead of replaying a warm snapshot', async () => {
+ await snapshot(PERSONAL_ID, PERSONAL_ORG);
+ vi.spyOn(database, 'queryWithTimeout').mockRejectedValueOnce(new Error('private database connection details'));
+ await expect(loadAuthorizationSnapshot(PERSONAL_ID, PERSONAL_ORG))
+ .rejects.toThrow(new AuthorizationSnapshotUnavailableError());
+ });
+
+ it.each([
+ { state: 'valid credential', hasUser: true },
+ { state: 'absent credential', hasUser: false },
+ ])('rejects a recovering replica with $state', async ({ hasUser }) => {
+ const primary = await snapshot(PERSONAL_ID, PERSONAL_ORG);
+ // The integration database is a primary. Simulate only the single query
+ // result from a recovering replica, including its no-user anchor row.
+ const readSpy = vi.spyOn(database, 'queryWithTimeout').mockResolvedValueOnce({
+ rows: [{
+ in_recovery: true,
+ authenticated_user_id: hasUser ? primary.authenticatedUserId : null,
+ canonical_user_id: hasUser ? primary.canonicalUserId : null,
+ identity_id: hasUser ? primary.identityId : null,
+ authorization_epoch: '0',
+ email: hasUser ? primary.credential.email : null,
+ first_name: hasUser ? primary.credential.firstName : null,
+ last_name: hasUser ? primary.credential.lastName : null,
+ grant_id: null,
+ grant_organization_id: null,
+ grant_role: null,
+ grant_effective_from: null,
+ grant_effective_until: null,
+ }],
+ rowCount: 1,
+ command: 'SELECT',
+ oid: 0,
+ fields: [],
+ });
+ await expect(loadAuthorizationSnapshot(PERSONAL_ID, PERSONAL_ORG))
+ .rejects.toBeInstanceOf(AuthorizationSnapshotUnavailableError);
+ expect(readSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('expires a blocked statement, releases its transaction, and loads fresh authority after recovery', async () => {
+ const before = await snapshot(PERSONAL_ID, PERSONAL_ORG);
+ const writer = await pool.connect();
+ const boundedQuery = vi.spyOn(database, 'queryWithTimeout');
+ try {
+ await writer.query('BEGIN');
+ await writer.query('LOCK TABLE identity_workos_users IN ACCESS EXCLUSIVE MODE');
+ const startedAt = Date.now();
+ await expect(loadAuthorizationSnapshot(PERSONAL_ID, PERSONAL_ORG))
+ .rejects.toBeInstanceOf(AuthorizationSnapshotUnavailableError);
+ expect(Date.now() - startedAt).toBeLessThan(5_000);
+ expect(boundedQuery).toHaveBeenCalledTimes(1);
+
+ await bumpAuthorizationEpochs(writer, [PERSONAL_ID]);
+ await writer.query('COMMIT');
+ const after = await snapshot(PERSONAL_ID, PERSONAL_ORG);
+ expect(after.authorizationEpoch).toBe('1');
+ expect(sameAuthorizationSnapshot(before, after)).toBe(false);
+ expect(boundedQuery).toHaveBeenCalledTimes(2);
+ } finally {
+ await writer.query('ROLLBACK');
+ writer.release();
+ }
+ });
+});
diff --git a/server/tests/unit/addie-admin-global-auth.test.ts b/server/tests/unit/addie-admin-global-auth.test.ts
index 38fa4c89bee..56421ec1f66 100644
--- a/server/tests/unit/addie-admin-global-auth.test.ts
+++ b/server/tests/unit/addie-admin-global-auth.test.ts
@@ -51,6 +51,7 @@ vi.mock('../../src/db/org-filters.js', () => ({
vi.mock('../../src/db/client.js', async (importOriginal) => ({
...(await importOriginal()),
getPool: () => ({ query: mocks.poolQuery }),
+ queryWithTimeout: mocks.poolQuery,
}));
vi.mock('../../src/db/working-group-db.js', () => ({
@@ -104,7 +105,7 @@ function validatedTenantKey(permission: 'admin:*' | 'admin:read') {
return {
apiKey: {
id: `key_${permission}`,
- owner: { id: 'org_tenant' },
+ owner: { type: 'organization', id: 'org_tenant' },
name: 'Tenant admin key',
permissions: [permission],
},
@@ -130,9 +131,17 @@ describe('Addie real global-admin boundary', () => {
});
mocks.isAdminGroupMember.mockResolvedValue(true);
mocks.poolQuery.mockImplementation((sql: string) => {
- if (sql.includes('FROM users')) {
+ if (sql.includes('pg_catalog.pg_is_in_recovery()')) {
return Promise.resolve({
- rows: [{ first_name: 'SSO', last_name: 'Admin' }],
+ rows: [{
+ in_recovery: false,
+ authenticated_user_id: 'user_sso_admin',
+ canonical_user_id: 'user_sso_admin',
+ identity_id: 'identity_sso_admin',
+ authorization_epoch: '0',
+ email: 'sso-admin@example.test', email_verified: true,
+ first_name: 'SSO', last_name: 'Admin', grant_id: null,
+ }],
rowCount: 1,
});
}
diff --git a/server/tests/unit/api-key-dashboard-role-gate.test.ts b/server/tests/unit/api-key-dashboard-role-gate.test.ts
index ebbe4f19503..a8777376a2c 100644
--- a/server/tests/unit/api-key-dashboard-role-gate.test.ts
+++ b/server/tests/unit/api-key-dashboard-role-gate.test.ts
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
-import { describe, expect, it } from 'vitest';
+import vm from 'node:vm';
+import { describe, expect, it, vi } from 'vitest';
const dashboardHtml = readFileSync(
join(process.cwd(), 'server/public/dashboard-api-keys.html'),
@@ -31,3 +32,64 @@ describe('API key dashboard organization-role gate', () => {
);
});
});
+
+describe('API key dashboard explicit organization selection', () => {
+ const primary = { id: 'org_pinnacle', name: 'Pinnacle Agency', role: 'owner', status: 'active', is_primary: true };
+ const secondary = { id: 'org_streamhaus', name: 'StreamHaus', role: 'admin', status: 'active' };
+
+ async function initialize(search: string, organizations = [primary, secondary]) {
+ const elements = Object.fromEntries(['loading', 'content', 'accessDenied'].map((id) => [
+ id, { style: { display: id === 'loading' ? 'block' : 'none' }, textContent: '', innerHTML: '' },
+ ]));
+ const nav = {
+ init: vi.fn(), renderOrgPicker: vi.fn(), setOrgOptions: vi.fn(),
+ resolveOrg: vi.fn(() => ({ org: primary, needsSelection: false })),
+ };
+ const loadKeys = vi.fn();
+ const localStorage = { getItem: vi.fn(() => primary.id), setItem: vi.fn() };
+ const context = vm.createContext({
+ document: { getElementById: (id: string) => elements[id] },
+ window: { location: { search, pathname: '/dashboard/api-keys' } },
+ URLSearchParams, localStorage, DashboardNav: nav, currentOrg: null, loadKeys,
+ fetch: vi.fn(async () => ({ ok: true, json: async () => ({ organizations }) })),
+ console,
+ });
+ const start = dashboardHtml.indexOf('async function init()');
+ const end = dashboardHtml.indexOf('// Close modals on Escape key', start);
+ expect(start).toBeGreaterThan(-1);
+ expect(end).toBeGreaterThan(start);
+ vm.runInContext(dashboardHtml.slice(start, end), context);
+ await context.init();
+ return { elements, nav, loadKeys, context, localStorage };
+ }
+
+ it.each([[primary], [primary, secondary]])('requires a selection even with %j and a remembered primary organization', async (...organizations) => {
+ const result = await initialize('', organizations);
+ expect(result.nav.renderOrgPicker).toHaveBeenCalledWith(organizations, result.elements.loading);
+ expect(result.nav.resolveOrg).not.toHaveBeenCalled();
+ expect(result.localStorage.getItem).not.toHaveBeenCalled();
+ expect(result.elements.content.style.display).toBe('none');
+ expect(result.loadKeys).not.toHaveBeenCalled();
+ });
+
+ it.each(['?org=org_unknown', '?org=', '?org=org_pinnacle&org=org_streamhaus'])('refuses unavailable or ambiguous selection %s without falling back', async (search) => {
+ const result = await initialize(search);
+ expect(result.elements.loading.textContent).toContain('selected organization is unavailable');
+ expect(result.elements.content.style.display).toBe('none');
+ expect(result.loadKeys).not.toHaveBeenCalled();
+ });
+
+ it('loads keys only for the explicitly selected active admin organization', async () => {
+ const result = await initialize('?org=org_streamhaus');
+ expect(result.context.currentOrg.id).toBe(secondary.id);
+ expect(result.elements.content.style.display).toBe('block');
+ expect(result.loadKeys).toHaveBeenCalledOnce();
+ });
+
+ it('denies a selected member organization even when the other organization has an owner role', async () => {
+ const result = await initialize('?org=org_streamhaus', [primary, { ...secondary, role: 'member' }]);
+ expect(result.elements.accessDenied.style.display).toBe('block');
+ expect(result.elements.content.style.display).toBe('none');
+ expect(result.loadKeys).not.toHaveBeenCalled();
+ });
+});
diff --git a/server/tests/unit/api-key-exact-credential-auth.test.ts b/server/tests/unit/api-key-exact-credential-auth.test.ts
new file mode 100644
index 00000000000..f39e26b3250
--- /dev/null
+++ b/server/tests/unit/api-key-exact-credential-auth.test.ts
@@ -0,0 +1,545 @@
+import type { Request, Response } from 'express';
+import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
+
+const mocks = vi.hoisted(() => ({
+ createValidation: vi.fn(),
+ checkPlatformBanForApiKey: vi.fn(),
+ checkPlatformBan: vi.fn(),
+ resolveEffectiveMembership: vi.fn(),
+ loadSealedSession: vi.fn(),
+ verifyWorkOSJWT: vi.fn(),
+ loadAuthorizationSnapshot: vi.fn(),
+ getRefreshedSession: vi.fn(),
+}));
+
+vi.hoisted(() => {
+ process.env.WORKOS_API_KEY = 'sk_server_test';
+ process.env.WORKOS_CLIENT_ID = 'client_test';
+ process.env.WORKOS_COOKIE_PASSWORD = 'test-cookie-password-at-least-32-characters';
+ process.env.ADMIN_API_KEY = 'static-admin-test';
+ delete process.env.DEV_USER_EMAIL;
+ delete process.env.DEV_USER_ID;
+});
+
+vi.mock('@workos-inc/node', () => ({
+ WorkOS: class WorkOS {
+ apiKeys = { createValidation: mocks.createValidation };
+ userManagement = { loadSealedSession: mocks.loadSealedSession };
+ },
+}));
+vi.mock('../../src/db/bans-db.js', () => ({
+ bansDb: {
+ checkPlatformBanForApiKey: mocks.checkPlatformBanForApiKey,
+ checkPlatformBan: mocks.checkPlatformBan,
+ },
+}));
+vi.mock('../../src/db/org-filters.js', () => ({
+ resolveEffectiveMembership: mocks.resolveEffectiveMembership,
+}));
+vi.mock('../../src/auth/workos-jwt.js', async (importOriginal) => ({
+ ...await importOriginal(),
+ verifyWorkOSJWT: mocks.verifyWorkOSJWT,
+}));
+vi.mock('../../src/db/user-authorization-snapshot-db.js', async (importOriginal) => ({
+ ...await importOriginal(),
+ loadAuthorizationSnapshot: mocks.loadAuthorizationSnapshot,
+}));
+vi.mock('../../src/db/session-refresh-db.js', () => ({
+ getRefreshedSession: mocks.getRefreshedSession,
+ storeRefreshedSession: vi.fn().mockResolvedValue(undefined),
+ cleanExpiredRefreshes: vi.fn().mockResolvedValue(0),
+}));
+
+import {
+ optionalAuth,
+ requireAuth,
+ requireAdmin,
+ requireTenantAdminForOrganization,
+ stopAuthTimers,
+ type ValidatedApiKey,
+} from '../../src/middleware/auth.js';
+
+type KeyRequest = Request & { apiKey?: ValidatedApiKey };
+
+function makeRequest(token = 'sk_tenant_key', orgId?: string): KeyRequest {
+ return {
+ headers: { authorization: `Bearer ${token}` },
+ cookies: { 'wos-session': 'valid-broader-session' },
+ params: orgId ? { orgId } : {},
+ path: '/api/admin/organizations',
+ originalUrl: '/api/admin/organizations',
+ method: 'GET',
+ accepts: () => false,
+ } as unknown as KeyRequest;
+}
+
+function makeResponse() {
+ return {
+ status: vi.fn().mockReturnThis(),
+ json: vi.fn().mockReturnThis(),
+ } as unknown as Response;
+}
+
+function tenantKey(id = 'key_a', organizationId = 'org_a', permissions = ['admin:*']) {
+ return {
+ apiKey: {
+ id,
+ owner: { type: 'organization', id: organizationId },
+ name: 'Tenant key',
+ permissions,
+ },
+ };
+}
+
+beforeEach(() => {
+ vi.resetAllMocks();
+ mocks.createValidation.mockResolvedValue(tenantKey());
+ mocks.checkPlatformBanForApiKey.mockResolvedValue({ banned: false });
+ mocks.checkPlatformBan.mockResolvedValue({ banned: false });
+ mocks.resolveEffectiveMembership.mockResolvedValue({ is_member: false });
+ mocks.getRefreshedSession.mockResolvedValue(null);
+ mocks.loadAuthorizationSnapshot.mockImplementation(async (id: string, org: string | null) => ({
+ authenticatedUserId: id, canonicalUserId: id, identityId: 'identity_one',
+ authorizationEpoch: '1', selectedOrganizationId: org, credentialGrant: null,
+ credential: { email: 'sam@example.test', emailVerified: true, firstName: 'Sam', lastName: 'Adeyemi' },
+ }));
+});
+afterAll(stopAuthTimers);
+
+describe.each([
+ ['requireAuth', requireAuth],
+ ['optionalAuth', optionalAuth],
+] as const)('%s exact API-key credential', (_name, authenticate) => {
+ it.each(['bearer sk_revoked', 'BEARER sk_revoked', 'Bearer sk_revoked', 'Bearer\tsk_revoked', 'bEaReR wos_api_key_revoked'])(
+ 'recognizes the selected API key with alternate header formatting: %s', async (authorization) => {
+ mocks.createValidation.mockResolvedValue({ apiKey: null });
+ const req = makeRequest();
+ req.headers.authorization = authorization;
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(mocks.createValidation).toHaveBeenCalledOnce();
+ expect(next).not.toHaveBeenCalled();
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ },
+ );
+
+ it('rejects an invalid or revoked key without using an accompanying session', async () => {
+ mocks.createValidation.mockResolvedValue({ apiKey: null });
+ const req = makeRequest();
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(res.json).toHaveBeenCalledWith({ error: 'invalid_api_key' });
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ expect(req.apiKey).toBeUndefined();
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ expect(mocks.verifyWorkOSJWT).not.toHaveBeenCalled();
+ });
+
+ it.each([undefined, 401, 403, 429, 503])('fails unavailable validation closed for provider status %s', async (status) => {
+ mocks.createValidation.mockRejectedValue(Object.assign(new Error('provider error'), { status }));
+ const req = makeRequest();
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(503);
+ expect(res.json).toHaveBeenCalledWith({ error: 'authorization_unavailable' });
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ });
+
+ it('rejects a user-owned key without inferring organization authority', async () => {
+ const owner = { type: 'user', id: 'user_linked', organizationId: 'org_a' };
+ mocks.createValidation.mockResolvedValue({ apiKey: { ...tenantKey().apiKey, owner } });
+ const req = makeRequest();
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(next).not.toHaveBeenCalled();
+ expect(mocks.resolveEffectiveMembership).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ ['header', (req: KeyRequest) => { req.headers['x-organization-id'] = 'org_b'; }],
+ ['query org', (req: KeyRequest) => { req.query = { org: 'org_b' }; }],
+ ['query organization_id', (req: KeyRequest) => { req.query = { organization_id: 'org_b' }; }],
+ ['query organizationId', (req: KeyRequest) => { req.query = { organizationId: 'org_b' }; }],
+ ['body organization_id', (req: KeyRequest) => { req.body = { organization_id: 'org_b' }; }],
+ ['body organizationId', (req: KeyRequest) => { req.body = { organizationId: 'org_b' }; }],
+ ['route orgId', (req: KeyRequest) => { req.params = { orgId: 'org_b' }; }],
+ ['route organizationId', (req: KeyRequest) => { req.params = { organizationId: 'org_b' }; }],
+ ] as const)('rejects an API key provider conflict from %s before attaching authority', async (_location, select) => {
+ const req = makeRequest();
+ select(req);
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(403);
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ expect(req.apiKey).toBeUndefined();
+ expect(mocks.checkPlatformBanForApiKey).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ { owner: { id: 'org_a' } },
+ { owner: null },
+ { owner: { type: 'organization', id: '' } },
+ { id: '' },
+ { permissions: undefined },
+ { permissions: ['admin:*', null] },
+ ])('refuses incomplete provider authorization data: %j', async (invalidFields) => {
+ mocks.createValidation.mockResolvedValue({ apiKey: { ...tenantKey().apiKey, ...invalidFields } });
+ const req = makeRequest();
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(503);
+ expect(next).not.toHaveBeenCalled();
+ expect(req.apiKey).toBeUndefined();
+ });
+
+ it('does not attach a principal when the key ban check is unavailable', async () => {
+ mocks.checkPlatformBanForApiKey.mockRejectedValue(new Error('database unavailable'));
+ const req = makeRequest();
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(503);
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ expect(req.apiKey).toBeUndefined();
+ });
+
+ it('does not cache an accepted key across revocation or rotation', async () => {
+ mocks.createValidation
+ .mockResolvedValueOnce(tenantKey('old_key'))
+ .mockResolvedValueOnce({ apiKey: null })
+ .mockResolvedValueOnce(tenantKey('new_key'))
+ .mockResolvedValueOnce({ apiKey: null });
+ const requests = [makeRequest('sk_old'), makeRequest('sk_old'), makeRequest('sk_new'), makeRequest('sk_old')];
+ const responses = requests.map(() => makeResponse());
+ const next = vi.fn();
+
+ for (const [index, req] of requests.entries()) {
+ await authenticate(req, responses[index], next);
+ }
+
+ expect(mocks.createValidation.mock.calls.map(([args]) => args.value)).toEqual(['sk_old', 'sk_old', 'sk_new', 'sk_old']);
+ expect(next).toHaveBeenCalledTimes(2);
+ expect(requests[0].apiKey?.id).toBe('old_key');
+ expect(requests[2].apiKey?.id).toBe('new_key');
+ expect(responses[1].status).toHaveBeenCalledWith(401);
+ expect(responses[3].status).toHaveBeenCalledWith(401);
+ });
+
+ it('does not reuse a stale ban allow decision', async () => {
+ mocks.checkPlatformBanForApiKey
+ .mockResolvedValueOnce({ banned: false })
+ .mockResolvedValueOnce({ banned: true });
+ const next = vi.fn();
+ const deniedRequest = makeRequest();
+ const deniedResponse = makeResponse();
+
+ await authenticate(makeRequest(), makeResponse(), next);
+ await authenticate(deniedRequest, deniedResponse, next);
+
+ expect(mocks.checkPlatformBanForApiKey).toHaveBeenCalledTimes(2);
+ expect(deniedResponse.status).toHaveBeenCalledWith(403);
+ expect(deniedRequest.user).toBeUndefined();
+ expect(next).toHaveBeenCalledOnce();
+ });
+
+ it('keeps concurrent validation decisions and tenant principals separate', async () => {
+ let releaseFirst!: (result: ReturnType) => void;
+ mocks.createValidation.mockImplementation(({ value }: { value: string }) => value === 'sk_a'
+ ? new Promise>((resolve) => { releaseFirst = resolve; })
+ : Promise.resolve(tenantKey('key_b', 'org_b', ['admin:read'])));
+ const requestA = makeRequest('sk_a');
+ const requestB = makeRequest('sk_b');
+ const nextA = vi.fn();
+ const nextB = vi.fn();
+
+ const pendingA = authenticate(requestA, makeResponse(), nextA);
+ await authenticate(requestB, makeResponse(), nextB);
+ expect(nextA).not.toHaveBeenCalled();
+ releaseFirst(tenantKey('key_a', 'org_a'));
+ await pendingA;
+
+ expect(requestA.apiKey).toMatchObject({ id: 'key_a', organizationId: 'org_a', permissions: ['admin:*'] });
+ expect(requestB.apiKey).toMatchObject({ id: 'key_b', organizationId: 'org_b', permissions: ['admin:read'] });
+ expect(requestA.user).not.toBe(requestB.user);
+ expect(nextA).toHaveBeenCalledOnce();
+ expect(nextB).toHaveBeenCalledOnce();
+ });
+});
+
+describe('tenant-key use scope', () => {
+ it.each([
+ ['org_a', 'GET', ['admin:*'], true],
+ ['org_a', 'GET', ['admin:read'], true],
+ ['org_b', 'GET', ['admin:*'], false],
+ [undefined, 'GET', ['admin:*'], false],
+ ['org_a', 'DELETE', ['admin:read'], false],
+ ['org_a', 'GET', ['admin'], false],
+ ['org_a', 'GET', [], false],
+ ] as const)('requires exact selected org and permission: %s %s %j', async (orgId, method, permissions, allowed) => {
+ mocks.createValidation.mockResolvedValue(tenantKey('key_a', 'org_a', [...permissions]));
+ const req = makeRequest('sk_a', orgId);
+ req.method = method;
+ const res = makeResponse();
+ const authenticated = vi.fn();
+ const authorized = vi.fn();
+
+ await requireAuth(req, res, authenticated);
+ await requireTenantAdminForOrganization(req, res, authorized);
+
+ if (orgId === 'org_b') {
+ expect(authenticated).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ expect(req.apiKey).toBeUndefined();
+ } else {
+ expect(authenticated).toHaveBeenCalledOnce();
+ }
+ if (allowed) expect(authorized).toHaveBeenCalledOnce();
+ else {
+ expect(authorized).not.toHaveBeenCalled();
+ expect(res.status).toHaveBeenCalledWith(403);
+ }
+ });
+
+ it('uses current permissions when the same key is revalidated', async () => {
+ mocks.createValidation
+ .mockResolvedValueOnce(tenantKey('key_a', 'org_a', ['admin:*']))
+ .mockResolvedValueOnce(tenantKey('key_a', 'org_a', ['admin:read']));
+ const authorized = vi.fn();
+ for (let i = 0; i < 2; i++) {
+ const req = makeRequest('sk_a', 'org_a');
+ req.method = 'DELETE';
+ await requireAuth(req, makeResponse(), vi.fn());
+ await requireTenantAdminForOrganization(req, makeResponse(), authorized);
+ }
+ expect(authorized).toHaveBeenCalledOnce();
+ });
+
+ it.each(['admin:*', 'admin:read'])('never upgrades a tenant %s key to platform administration', async (permission) => {
+ mocks.createValidation.mockResolvedValue(tenantKey('key_a', 'org_a', [permission]));
+ const req = makeRequest('sk_a', 'org_a');
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await requireAuth(req, res, vi.fn());
+ await requireAdmin(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(403);
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ error: 'global_admin_required' }));
+ expect(next).not.toHaveBeenCalled();
+ });
+});
+
+let bearerSequence = 0;
+
+describe.each([
+ ['requireAuth', requireAuth], ['optionalAuth', optionalAuth],
+] as const)('%s explicit bearer selection', (_name, authenticate) => {
+ function noCookieRequest(authorization: string): KeyRequest {
+ const req = makeRequest();
+ req.headers.authorization = authorization;
+ Object.defineProperty(req, 'cookies', {
+ get: () => { throw new Error('An explicit bearer must never read a cookie'); },
+ });
+ return req;
+ }
+
+ it.each(['Bearer', 'Bearer ', 'bearer ', 'BEARER\t', 'Bearer eyJmalformed', 'bEaReR eyJmalformed..'])(
+ 'rejects empty or malformed JWT bearer %j without reading the cookie', async (authorization) => {
+ const req = noCookieRequest(authorization);
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(next).not.toHaveBeenCalled();
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
+ },
+ );
+
+ it.each([
+ 'ERR_JWT_EXPIRED', 'ERR_JWT_INVALID', 'ERR_JWT_CLAIM_VALIDATION_FAILED',
+ 'ERR_JWS_INVALID', 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED',
+ 'ERR_JWKS_NO_MATCHING_KEY',
+ 'ERR_JOSE_ALG_NOT_ALLOWED', 'ERR_JOSE_NOT_SUPPORTED',
+ ])('returns 401 for definitive JWT rejection %s without cookie fallback', async (code) => {
+ mocks.verifyWorkOSJWT.mockRejectedValue(Object.assign(new Error('Invalid token'), { code }));
+ const req = noCookieRequest(`bearer eyJ.reject${++bearerSequence}.signature`);
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(next).not.toHaveBeenCalled();
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ new Error('Unknown provider failure'),
+ Object.assign(new Error('Unknown JWKS failure'), { code: 'ERR_JOSE_GENERIC' }),
+ Object.assign(new Error('Invalid key service response'), { code: 'ERR_JWKS_INVALID' }),
+ Object.assign(new Error('Timeout'), { code: 'ERR_JWKS_TIMEOUT' }),
+ Object.assign(new Error('Fetch failed'), { code: 'ECONNRESET' }),
+ Object.assign(new Error('Provider authorization unavailable'), { status: 401 }),
+ Object.assign(new Error('Provider unavailable'), { status: 503 }),
+ ])('returns 503 for unavailable JWT verification: %s', async (error) => {
+ mocks.verifyWorkOSJWT.mockRejectedValue(error);
+ const req = noCookieRequest(`BEARER eyJ.unavailable${++bearerSequence}.signature`);
+ const res = makeResponse();
+ const next = vi.fn();
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(503);
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
+ });
+
+ it.each(['wrong-static-token', 'malformed-opaque-token'])(
+ 'validates only the selected opaque bearer %s, never an accompanying valid cookie', async (token) => {
+ const req = makeRequest(`${token}-${++bearerSequence}`);
+ const res = makeResponse();
+ const next = vi.fn();
+ mocks.loadSealedSession.mockImplementation(({ sessionData }: { sessionData: string }) => ({
+ authenticate: vi.fn().mockResolvedValue(sessionData === 'valid-broader-session' ? {
+ authenticated: true,
+ user: { id: 'broader_cookie_user', email: 'cookie@example.test' },
+ accessToken: 'cookie-access',
+ } : { authenticated: false }),
+ refresh: vi.fn().mockResolvedValue({ authenticated: false }),
+ }));
+
+ await authenticate(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(next).not.toHaveBeenCalled();
+ expect(mocks.loadSealedSession).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sessionData: `${token}-${bearerSequence}` }));
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
+ },
+ );
+
+ it('continues to authenticate a valid JWT with its exact snapshot and selected org', async () => {
+ mocks.verifyWorkOSJWT.mockResolvedValue({ sub: 'user_authenticated', orgId: 'org_selected', isM2M: false });
+ const req = noCookieRequest(`bEaReR\teyJ.valid${++bearerSequence}.signature`);
+ const next = vi.fn();
+
+ await authenticate(req, makeResponse(), next);
+
+ expect(next).toHaveBeenCalledOnce();
+ expect(req.user?.authorizationSnapshot?.authenticatedUserId).toBe('user_authenticated');
+ expect(mocks.loadAuthorizationSnapshot).toHaveBeenCalledExactlyOnceWith('user_authenticated', 'org_selected');
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ });
+
+ it('continues to authenticate native sealed bearer sessions without reading a cookie', async () => {
+ const token = `native-sealed-${++bearerSequence}`;
+ const req = noCookieRequest(`BEARER ${token}`);
+ const next = vi.fn();
+ mocks.loadSealedSession.mockReturnValue({ authenticate: vi.fn().mockResolvedValue({
+ authenticated: true, user: { id: 'user_native', email: 'native@example.test' }, accessToken: 'native-access',
+ }) });
+
+ await authenticate(req, makeResponse(), next);
+
+ expect(next).toHaveBeenCalledOnce();
+ expect(req.user?.authorizationSnapshot?.authenticatedUserId).toBe('user_native');
+ expect(mocks.loadSealedSession).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sessionData: token }));
+ });
+
+ it('continues to authenticate the configured static bearer credential', async () => {
+ const req = noCookieRequest('bearer static-admin-test');
+ const next = vi.fn();
+
+ await authenticate(req, makeResponse(), next);
+
+ expect(next).toHaveBeenCalledOnce();
+ expect(req.user?.id).toBe('admin_api_key');
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ expect(mocks.verifyWorkOSJWT).not.toHaveBeenCalled();
+ });
+});
+
+describe.each([
+ ['requireAuth', requireAuth], ['optionalAuth', optionalAuth],
+] as const)('%s sealed bearer unavailable state', (_label, authenticate) => {
+ it.each(['initial', 'shared'] as const)('does not mark a retryable %s refresh as a dead credential', async (stage) => {
+ const token = `retryable-sealed-${++bearerSequence}`;
+ const authenticateSession = vi.fn().mockResolvedValue({ authenticated: false });
+ const refresh = vi.fn().mockResolvedValue({ authenticated: false, retryable: true });
+ if (stage === 'shared') {
+ refresh.mockResolvedValueOnce({ authenticated: false, retryable: false });
+ mocks.getRefreshedSession.mockResolvedValueOnce('shared-sealed');
+ }
+ mocks.loadSealedSession.mockReturnValue({ authenticate: authenticateSession, refresh });
+ const unavailableResponse = makeResponse();
+ const next = vi.fn();
+ await authenticate(makeRequest(token), unavailableResponse, next);
+ expect(unavailableResponse.status).toHaveBeenCalledWith(503);
+ expect(next).not.toHaveBeenCalled();
+
+ authenticateSession.mockResolvedValue({
+ authenticated: true, user: { id: 'user_recovered', email: 'sam@example.test' }, accessToken: 'recovered',
+ });
+ const recoveredResponse = makeResponse();
+ await authenticate(makeRequest(token), recoveredResponse, next);
+ expect(next).toHaveBeenCalledOnce();
+ expect(recoveredResponse.status).not.toHaveBeenCalled();
+ });
+
+ it.each(['authenticate', 'refresh', 'shared refresh'] as const)(
+ 'keeps unknown provider/JWKS failures in %s unavailable and retryable', async (stage) => {
+ const token = `unavailable-sealed-${++bearerSequence}`;
+ const authenticateSession = vi.fn().mockResolvedValue({ authenticated: false });
+ const refresh = vi.fn().mockRejectedValue(new Error('Unknown provider or key-service failure'));
+ if (stage === 'authenticate') authenticateSession.mockRejectedValueOnce(new Error('Unknown JWKS failure'));
+ if (stage === 'shared refresh') {
+ refresh.mockResolvedValueOnce({ authenticated: false, retryable: false });
+ mocks.getRefreshedSession.mockResolvedValueOnce('shared-sealed');
+ }
+ mocks.loadSealedSession.mockReturnValue({ authenticate: authenticateSession, refresh });
+ const res = makeResponse();
+ const next = vi.fn();
+ await authenticate(makeRequest(token), res, next);
+ expect(res.status).toHaveBeenCalledWith(503);
+ expect(next).not.toHaveBeenCalled();
+ authenticateSession.mockResolvedValue({
+ authenticated: true, user: { id: 'user_recovered', email: 'sam@example.test' }, accessToken: 'recovered',
+ });
+ await authenticate(makeRequest(token), makeResponse(), next);
+ expect(next).toHaveBeenCalledOnce();
+ },
+ );
+});
diff --git a/server/tests/unit/api-key-issuance-permissions.test.ts b/server/tests/unit/api-key-issuance-permissions.test.ts
index 97de73617ad..717d489fdf3 100644
--- a/server/tests/unit/api-key-issuance-permissions.test.ts
+++ b/server/tests/unit/api-key-issuance-permissions.test.ts
@@ -1,10 +1,18 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import express from 'express';
import request from 'supertest';
+import { request as httpRequest } from 'node:http';
+import { once } from 'node:events';
+import type { AddressInfo } from 'node:net';
+import type { AuthorizationSnapshot } from '../../src/db/user-authorization-snapshot-db.js';
const mocks = vi.hoisted(() => ({
listOrganizationMemberships: vi.fn(),
+ queryWithTimeout: vi.fn(),
fetch: vi.fn(),
+ info: vi.fn(),
+ request: null as express.Request | null,
+ previousSnapshot: undefined as AuthorizationSnapshot | undefined,
}));
vi.hoisted(() => {
@@ -21,22 +29,59 @@ vi.mock('@workos-inc/node', () => ({
},
}));
+vi.mock('../../src/db/client.js', () => ({
+ queryWithTimeout: mocks.queryWithTimeout, isTransientConnectionError: () => false,
+}));
+vi.mock('../../src/logger.js', () => ({
+ createLogger: () => ({ info: mocks.info, warn: vi.fn(), error: vi.fn() }),
+}));
+
vi.mock('../../src/middleware/auth.js', () => ({
DEV_USERS: {},
isDevModeEnabled: () => false,
requireAuth: (req: express.Request, _res: express.Response, next: express.NextFunction) => {
req.user = {
- id: 'user_caller',
+ id: req.get('x-canonical-user') ?? 'user_caller',
+ authWorkosUserId: req.get('x-authenticated-user'),
+ identityId: req.get('x-identity-id'),
email: 'caller@example.test',
emailVerified: true,
createdAt: new Date(0).toISOString(),
updatedAt: new Date(0).toISOString(),
};
+ if (mocks.previousSnapshot) {
+ Object.defineProperty(req.user, 'authorizationSnapshot', { value: mocks.previousSnapshot, enumerable: false });
+ }
+ mocks.request = req;
next();
},
}));
const { createApiKeysRouter } = await import('../../src/routes/api-keys.js');
+const { loadAuthorizationSnapshot } = await import('../../src/db/user-authorization-snapshot-db.js');
+
+function snapshotRow(userId: string, organizationId?: string, role?: 'owner' | 'admin' | 'member') {
+ return {
+ in_recovery: false, authenticated_user_id: userId,
+ canonical_user_id: userId === 'user_linked' ? 'user_primary' : userId,
+ identity_id: 'identity_original', authorization_epoch: '1',
+ email: 'caller@example.test', email_verified: true, first_name: null, last_name: null,
+ grant_id: role ? 'grant_scoped' : null, grant_organization_id: role ? organizationId : null,
+ grant_role: role ?? null, grant_effective_from: role ? '2026-01-01T00:00:00.000000Z' : null,
+ grant_effective_until: null,
+ };
+}
+
+function setSnapshotWithoutGrant() {
+ mocks.queryWithTimeout.mockImplementation(async (_sql, [userId]) => ({ rows: [snapshotRow(userId)] }));
+}
+
+beforeEach(() => {
+ mocks.queryWithTimeout.mockReset();
+ setSnapshotWithoutGrant();
+ mocks.request = null;
+ mocks.previousSnapshot = undefined;
+});
// Own the process-global transport for the lifetime of this file. Installing
// and restoring it independently in sibling suites lets one suite's teardown
@@ -62,6 +107,7 @@ function setMembership(
mocks.listOrganizationMemberships.mockResolvedValue({
data: [
{
+ userId: 'user_caller',
organizationId: 'org_target',
role: { slug: role },
status,
@@ -74,6 +120,7 @@ function setUnboundMembership(organizationId?: string) {
mocks.listOrganizationMemberships.mockResolvedValue({
data: [
{
+ userId: 'user_caller',
...(organizationId ? { organizationId } : {}),
role: { slug: 'owner' },
status: 'active',
@@ -216,6 +263,71 @@ describe('organization-wide API key management permissions', () => {
});
});
+ async function rawDelete(path: string) {
+ const server = app.listen(0, '127.0.0.1');
+ await once(server, 'listening');
+ try {
+ return await new Promise((resolve, reject) => {
+ // Passing a raw HTTP path avoids the client's URL dot normalization.
+ const req = httpRequest({
+ hostname: '127.0.0.1', port: (server.address() as AddressInfo).port,
+ method: 'DELETE', path: `${path}?org=org_target`,
+ headers: { 'x-canonical-user': 'user_primary', 'x-authenticated-user': 'user_linked' },
+ }, (res) => {
+ res.resume();
+ res.on('end', () => resolve(res.statusCode));
+ });
+ req.on('error', reject);
+ req.end();
+ });
+ } finally {
+ await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
+ }
+ }
+
+ function exactLinkedMembership(role: 'admin' | 'member') {
+ mocks.listOrganizationMemberships.mockResolvedValue({ data: [{
+ userId: 'user_linked', organizationId: 'org_target', status: 'active', role: { slug: role },
+ }] });
+ }
+
+ function expectExactAuthorizationWithoutDelete() {
+ expect(mocks.listOrganizationMemberships).toHaveBeenCalledExactlyOnceWith({
+ userId: 'user_linked', organizationId: 'org_target',
+ });
+ expect(mocks.queryWithTimeout).toHaveBeenCalledWith(
+ expect.any(String), ['user_linked', 'org_target'], expect.any(Number),
+ { retryTransientCheckout: false },
+ );
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ }
+
+ it.each([
+ '.', '..', '%2e', '%2E', '.%2e', '.%2E', '%2e.', '%2E.',
+ '%2e%2e', '%2e%2E', '%2E%2e', '%2E%2E', './', '../', '%2E/', '%2e%2E/',
+ ])('authorizes the exact credential before rejecting raw dot-segment path %s without a provider DELETE', async (keyPath) => {
+ exactLinkedMembership('admin');
+ expect(await rawDelete(`/api/me/api-keys/${keyPath}`)).toBe(400);
+ expectExactAuthorizationWithoutDelete();
+ });
+
+ it('authorizes before rejecting dot segments on a case-insensitive route', async () => {
+ exactLinkedMembership('admin');
+ expect(await rawDelete('/API/ME/API-KEYS/%2E%2E/')).toBe(400);
+ expectExactAuthorizationWithoutDelete();
+ });
+
+ it.each(['.', '..', '%2E', '%2e%2E'])('preserves forbidden and unavailable exact authorization for raw path %s', async (keyPath) => {
+ exactLinkedMembership('member');
+ expect(await rawDelete(`/api/me/api-keys/${keyPath}`)).toBe(403);
+ expectExactAuthorizationWithoutDelete();
+
+ vi.clearAllMocks();
+ mocks.listOrganizationMemberships.mockRejectedValue(new Error('Authorization provider unavailable'));
+ expect(await rawDelete(`/api/me/api-keys/${keyPath}`)).toBe(503);
+ expectExactAuthorizationWithoutDelete();
+ });
+
it('refuses an ordinary member listing organization API keys before WorkOS is called', async () => {
setMembership('member');
@@ -344,3 +456,354 @@ describe('inactive API key lifecycle principals', () => {
},
);
});
+
+describe('exact credential API key authorization', () => {
+ const app = createApp();
+ const operations = ['create', 'list', 'revoke'] as const;
+
+ function perform(operation: typeof operations[number], org?: string) {
+ const query = org === undefined ? '' : `?org=${encodeURIComponent(org)}`;
+ return operation === 'create'
+ ? request(app).post(`/api/me/api-keys${query}`).send({ name: 'Scoped automation' })
+ : operation === 'list'
+ ? request(app).get(`/api/me/api-keys${query}`)
+ : request(app).delete(`/api/me/api-keys/key_scoped${query}`);
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mocks.listOrganizationMemberships.mockReset().mockResolvedValue({ data: [] });
+ mocks.fetch.mockReset().mockImplementation(async (_url, options: RequestInit) => ({
+ ok: true,
+ status: options.method === 'DELETE' ? 204 : 200,
+ json: async () => ({ data: [] }),
+ }));
+ });
+
+ it.each(operations)('isolates linked credentials in both directions when they %s keys', async (operation) => {
+ const organizations: Record = {
+ user_primary: 'org_pinnacle',
+ user_linked: 'org_streamhaus',
+ };
+ mocks.listOrganizationMemberships.mockImplementation(async ({ userId, organizationId }) => ({
+ data: organizations[userId] === organizationId
+ ? [{ userId, organizationId, status: 'active', role: { slug: 'owner' } }]
+ : [],
+ }));
+
+ for (const [credential, ownOrg] of Object.entries(organizations)) {
+ const otherOrg = ownOrg === 'org_pinnacle' ? 'org_streamhaus' : 'org_pinnacle';
+ const denied = await perform(operation, otherOrg)
+ .set('x-canonical-user', 'user_primary')
+ .set('x-authenticated-user', credential);
+ expect(denied.status).toBe(403);
+ expect(mocks.fetch).not.toHaveBeenCalled();
+
+ const allowed = await perform(operation, ownOrg)
+ .set('x-canonical-user', 'user_primary')
+ .set('x-authenticated-user', credential);
+ expect(allowed.status).toBe(operation === 'create' ? 201 : operation === 'revoke' ? 204 : 200);
+ expect(mocks.listOrganizationMemberships).toHaveBeenLastCalledWith({
+ userId: credential, organizationId: ownOrg,
+ });
+ expect(mocks.queryWithTimeout).toHaveBeenLastCalledWith(
+ expect.any(String), [credential, ownOrg], expect.any(Number),
+ { retryTransientCheckout: false },
+ );
+ expect(new URL(mocks.fetch.mock.calls[0][0]).pathname)
+ .toContain(`/organizations/${ownOrg}/api_keys`);
+ mocks.fetch.mockClear();
+ }
+ });
+
+ it.each(operations)('requires an explicit organization to %s even for an owner', async (operation) => {
+ setMembership('owner');
+
+ expect((await perform(operation)).status).toBe(400);
+ expect(mocks.listOrganizationMemberships).not.toHaveBeenCalled();
+ expect(mocks.queryWithTimeout).not.toHaveBeenCalled();
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ '/api/me/api-keys?org=org_target&org=org_other',
+ '/api/me/api-keys?org=%20',
+ ])('rejects an ambiguous or malformed selected organization: %s', async (url) => {
+ setMembership('owner');
+ expect((await request(app).get(url)).status).toBe(400);
+ expect(mocks.listOrganizationMemberships).not.toHaveBeenCalled();
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it('cannot use a path-like selected organization to target another organization', async () => {
+ setMembership('owner');
+ const response = await request(app).get('/api/me/api-keys?org=org_target%2F..%2Forg_other');
+ expect(response.status).toBe(403);
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it('rejects conflicting query and body organization selections', async () => {
+ setMembership('owner');
+ const response = await request(app).post('/api/me/api-keys?org=org_target')
+ .send({ organizationId: 'org_other', name: 'Ambiguous key' });
+ expect(response.status).toBe(400);
+ expect(mocks.listOrganizationMemberships).not.toHaveBeenCalled();
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it.each(['api_key_key_tenant', 'admin_api_key'])('refuses synthetic principal %s for all key management', async (principal) => {
+ setMembership('owner');
+ for (const operation of operations) {
+ expect((await perform(operation, 'org_target').set('x-canonical-user', principal)).status).toBe(403);
+ }
+ expect(mocks.listOrganizationMemberships).not.toHaveBeenCalled();
+ expect(mocks.queryWithTimeout).not.toHaveBeenCalled();
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it.each(operations)('returns unavailable separately from forbidden when authority cannot establish permission to %s', async (operation) => {
+ mocks.listOrganizationMemberships.mockRejectedValue(new Error('WorkOS unavailable'));
+ expect((await perform(operation, 'org_target')).status).toBe(503);
+ expect(mocks.fetch).not.toHaveBeenCalled();
+
+ setMembership('member');
+ mocks.queryWithTimeout.mockRejectedValue(new Error('Grant database unavailable'));
+ expect((await perform(operation, 'org_target')).status).toBe(503);
+ expect(mocks.fetch).not.toHaveBeenCalled();
+
+ setSnapshotWithoutGrant();
+ expect((await perform(operation, 'org_target')).status).toBe(403);
+ });
+
+ it('does not cache organization authority between operations', async () => {
+ setMembership('owner');
+ expect((await perform('list', 'org_target')).status).toBe(200);
+ mocks.fetch.mockClear();
+ setMembership('member');
+
+ expect((await perform('create', 'org_target')).status).toBe(403);
+ expect((await perform('revoke', 'org_target')).status).toBe(403);
+ expect(mocks.listOrganizationMemberships).toHaveBeenCalledTimes(3);
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it('does not inherit a linked credential grant or a higher canonical role', async () => {
+ mocks.listOrganizationMemberships.mockImplementation(async ({ userId }) => ({
+ data: userId === 'user_primary'
+ ? [{ userId, organizationId: 'org_target', status: 'active', role: { slug: 'owner' } }]
+ : [],
+ }));
+ mocks.queryWithTimeout.mockImplementation(async (_sql, [userId, orgId]) => ({
+ rows: [snapshotRow(userId, orgId, orgId === 'org_target' ? userId === 'user_primary' ? 'owner' : 'member' : undefined)],
+ }));
+
+ for (const operation of operations) {
+ expect((await perform(operation, 'org_target')
+ .set('x-canonical-user', 'user_primary')
+ .set('x-authenticated-user', 'user_linked')).status).toBe(403);
+ }
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it('authorizes only the exact credential and organization named by an explicit admin grant', async () => {
+ mocks.queryWithTimeout.mockImplementation(async (_sql, [userId, orgId]) => ({
+ rows: [snapshotRow(userId, orgId, userId === 'user_linked' && orgId === 'org_target' ? 'admin' : undefined)],
+ }));
+
+ expect((await perform('list', 'org_target')
+ .set('x-canonical-user', 'user_primary').set('x-authenticated-user', 'user_linked')).status).toBe(200);
+ mocks.fetch.mockClear();
+ expect((await perform('list', 'org_target').set('x-canonical-user', 'user_primary')).status).toBe(403);
+ expect((await perform('list', 'org_other')
+ .set('x-canonical-user', 'user_primary').set('x-authenticated-user', 'user_linked')).status).toBe(403);
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it.each(['epoch changed', 'grant expired', 'grant replaced', 'organization changed'])('preserves the non-enumerable snapshot and rejects replay after %s', async (change) => {
+ const original = snapshotRow('user_linked', 'org_target', 'admin');
+ mocks.queryWithTimeout.mockResolvedValue({ rows: [original] });
+ mocks.previousSnapshot = (await loadAuthorizationSnapshot(
+ 'user_linked', change === 'organization changed' ? 'org_other' : 'org_target',
+ ))!;
+ const current = change === 'epoch changed' ? { ...original, authorization_epoch: '2' }
+ : change === 'grant expired' ? snapshotRow('user_linked')
+ : change === 'grant replaced' ? { ...original, grant_id: 'grant_replacement' } : original;
+ mocks.queryWithTimeout.mockResolvedValue({ rows: [current] });
+ mocks.listOrganizationMemberships.mockResolvedValue({
+ data: [{ userId: 'user_linked', organizationId: 'org_target', status: 'active', role: { slug: 'owner' } }],
+ });
+
+ for (const operation of operations) {
+ expect((await perform(operation, 'org_target')
+ .set('x-canonical-user', 'user_primary').set('x-authenticated-user', 'user_linked')).status).toBe(403);
+ }
+ expect(Object.keys(mocks.request!.user!)).not.toContain('authorizationSnapshot');
+ expect(mocks.listOrganizationMemberships).not.toHaveBeenCalled();
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it('keeps concurrent requests isolated when the linked credential lookup completes first', async () => {
+ const primaryStarted = Promise.withResolvers();
+ const releasePrimary = Promise.withResolvers();
+ mocks.listOrganizationMemberships.mockImplementation(async ({ userId, organizationId }) => {
+ if (userId === 'user_primary') {
+ primaryStarted.resolve();
+ await releasePrimary.promise;
+ return { data: [{ userId, organizationId, status: 'active', role: { slug: 'owner' } }] };
+ }
+ return { data: [] };
+ });
+ const primary = perform('list', 'org_target')
+ .set('x-canonical-user', 'user_primary').then((response) => response);
+ await primaryStarted.promise;
+ try {
+ const linked = await perform('list', 'org_target')
+ .set('x-canonical-user', 'user_primary').set('x-authenticated-user', 'user_linked');
+ expect(linked.status).toBe(403);
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ } finally {
+ releasePrimary.resolve();
+ }
+ expect((await primary).status).toBe(200);
+ expect(mocks.fetch).toHaveBeenCalledOnce();
+ });
+});
+
+describe('API key selector agreement and immutable audit actor', () => {
+ const app = createApp();
+ const methods = ['GET', 'POST', 'DELETE'] as const;
+ const locations = [
+ ['query', 'org'], ['query', 'organization_id'], ['query', 'organizationId'],
+ ['body', 'organizationId'], ['body', 'organization_id'],
+ ['header', 'x-organization-id'],
+ ] as const;
+ type Selection = { query?: Record; body?: Record; header?: string | string[] };
+
+ function select(entries: Array): Selection {
+ const selection: Selection = {};
+ for (const [[source, field], value] of entries) {
+ if (source === 'header') selection.header = value as string | string[];
+ else (selection[source] ??= {})[field] = value;
+ }
+ return selection;
+ }
+
+ function perform(method: typeof methods[number], selection: Selection) {
+ const route = '/api/me/api-keys';
+ const call = method === 'GET' ? request(app).get(route)
+ : method === 'POST' ? request(app).post(route)
+ : request(app).delete(`${route}/key_scoped`);
+ if (selection.query) call.query(selection.query);
+ if (selection.header !== undefined) call.set('X-Organization-ID', selection.header);
+ return call.send({ ...(method === 'POST' ? { name: 'Scoped automation' } : {}), ...selection.body });
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mocks.listOrganizationMemberships.mockReset();
+ setMembership('owner');
+ mocks.fetch.mockReset().mockImplementation(async (_url, options: RequestInit) => ({
+ ok: true,
+ status: options.method === 'DELETE' ? 204 : 200,
+ json: async () => ({ data: [] }),
+ }));
+ });
+
+ it.each(methods)('accepts each explicit selector alone and all agreeing selectors on %s', async (method) => {
+ const selections = [
+ ...locations.map((location) => select([[location, 'org_target']])),
+ select(locations.map((location) => [location, 'org_target'])),
+ ];
+ for (const selection of selections) {
+ const response = await perform(method, selection);
+ expect(response.status, JSON.stringify(selection)).toBe(method === 'GET' ? 200 : method === 'POST' ? 201 : 204);
+ expect(mocks.listOrganizationMemberships).toHaveBeenLastCalledWith({ userId: 'user_caller', organizationId: 'org_target' });
+ expect(new URL(mocks.fetch.mock.lastCall![0]).pathname).toContain('/organizations/org_target/api_keys');
+ }
+ });
+
+ it.each(methods)('rejects every conflicting selector pair before authorization on %s', async (method) => {
+ for (const [index, left] of locations.entries()) {
+ for (const right of locations.slice(index + 1)) {
+ const selection = select([[left, 'org_target'], [right, 'org_other']]);
+ expect((await perform(method, selection)).status, JSON.stringify(selection)).toBe(400);
+ }
+ }
+ expect(mocks.listOrganizationMemberships).not.toHaveBeenCalled();
+ expect(mocks.queryWithTimeout).not.toHaveBeenCalled();
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it.each(methods)('rejects malformed and repeated selectors even alongside a valid selection on %s', async (method) => {
+ for (const location of locations) {
+ const invalidValues: unknown[] = location[0] === 'header'
+ ? ['', ' ', 'org_target,org_other', 'org_target, org_target', ['org_target', 'org_target'], ['org_target', 'org_other']]
+ : location[0] === 'query'
+ ? ['', ' ', ' org_target', 'org_target ', ['org_target', 'org_target']]
+ : ['', ' ', ' org_target', 'org_target ', null, 7, [], ['org_target'], { id: 'org_target' }];
+ for (const value of invalidValues) {
+ const fallback = location[0] === 'query' ? locations[2] : locations[0];
+ const selection = select([[fallback, 'org_target'], [location, value]]);
+ expect((await perform(method, selection)).status, JSON.stringify(selection)).toBe(400);
+ }
+ }
+ expect(mocks.listOrganizationMemberships).not.toHaveBeenCalled();
+ expect(mocks.queryWithTimeout).not.toHaveBeenCalled();
+ expect(mocks.fetch).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ ['POST', 201, 'API key created'],
+ ['DELETE', 204, 'API key revoked'],
+ ['DELETE', 404, 'API key revoke: already absent in WorkOS'],
+ ] as const)('keeps the original actor for %s provider status %s across both asynchronous boundaries', async (method, providerStatus, message) => {
+ const lookupStarted = Promise.withResolvers();
+ const releaseLookup = Promise.withResolvers();
+ const providerStarted = Promise.withResolvers();
+ const releaseProvider = Promise.withResolvers();
+ mocks.listOrganizationMemberships.mockImplementation(async () => {
+ lookupStarted.resolve();
+ await releaseLookup.promise;
+ return { data: [{ userId: 'user_linked', organizationId: 'org_target', status: 'active', role: { slug: 'owner' } }] };
+ });
+ mocks.fetch.mockImplementation(async () => {
+ providerStarted.resolve();
+ await releaseProvider.promise;
+ return {
+ ok: providerStatus !== 404,
+ status: providerStatus,
+ json: async () => ({ id: 'key_scoped' }),
+ text: async () => 'Already absent',
+ };
+ });
+
+ const pending = perform(method, { query: { org: 'org_target' } })
+ .set('x-canonical-user', 'user_primary').set('x-authenticated-user', 'user_linked')
+ .set('x-identity-id', 'identity_original').then((response) => response);
+ try {
+ await lookupStarted.promise;
+ Object.assign(mocks.request!.user!, {
+ id: 'user_changed_during_lookup', authWorkosUserId: 'user_wrong_lookup', identityId: 'identity_wrong_lookup',
+ });
+ releaseLookup.resolve();
+ await providerStarted.promise;
+ Object.assign(mocks.request!.user!, {
+ id: 'user_changed_during_provider', authWorkosUserId: 'user_wrong_provider', identityId: 'identity_wrong_provider',
+ });
+ } finally {
+ releaseLookup.resolve();
+ releaseProvider.resolve();
+ }
+
+ expect((await pending).status).toBe(method === 'POST' ? 201 : 204);
+ expect(mocks.listOrganizationMemberships).toHaveBeenCalledWith({ userId: 'user_linked', organizationId: 'org_target' });
+ expect(mocks.queryWithTimeout).toHaveBeenCalledWith(
+ expect.any(String), ['user_linked', 'org_target'], expect.any(Number),
+ { retryTransientCheckout: false },
+ );
+ expect(mocks.info).toHaveBeenCalledWith({
+ userId: 'user_primary', authWorkosUserId: 'user_linked', identityId: 'identity_original',
+ organizationId: 'org_target', ...(method === 'POST' ? { keyName: 'Scoped automation' } : { apiKeyId: 'key_scoped' }),
+ }, message);
+ });
+});
diff --git a/server/tests/unit/api-key-validation-timeout.test.ts b/server/tests/unit/api-key-validation-timeout.test.ts
new file mode 100644
index 00000000000..1effd7815ab
--- /dev/null
+++ b/server/tests/unit/api-key-validation-timeout.test.ts
@@ -0,0 +1,147 @@
+import type { Request, Response } from 'express';
+import { WorkOS } from '@workos-inc/node';
+import { exportJWK, generateKeyPair, SignJWT } from 'jose';
+import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+const fetchMock = vi.hoisted(() => vi.fn());
+const snapshotMock = vi.hoisted(() => vi.fn());
+vi.hoisted(() => {
+ process.env.WORKOS_API_KEY = 'sk_server_test';
+ process.env.WORKOS_CLIENT_ID = 'client_test';
+ process.env.WORKOS_COOKIE_PASSWORD = 'test-cookie-password-at-least-32-characters';
+ delete process.env.DEV_USER_EMAIL;
+ delete process.env.DEV_USER_ID;
+ vi.stubGlobal('fetch', fetchMock);
+});
+vi.mock('../../src/db/bans-db.js', () => ({
+ bansDb: {
+ checkPlatformBanForApiKey: vi.fn().mockResolvedValue({ banned: false }),
+ checkPlatformBan: vi.fn().mockResolvedValue({ banned: false }),
+ },
+}));
+vi.mock('../../src/db/org-filters.js', () => ({
+ resolveEffectiveMembership: vi.fn().mockResolvedValue({ is_member: false }),
+}));
+vi.mock('../../src/db/user-authorization-snapshot-db.js', async (importOriginal) => ({
+ ...await importOriginal(),
+ loadAuthorizationSnapshot: snapshotMock,
+}));
+import { optionalAuth, requireAuth, stopAuthTimers } from '../../src/middleware/auth.js';
+
+function request(token: string): Request {
+ return {
+ headers: { authorization: `Bearer ${token}` },
+ cookies: { 'wos-session': 'valid-broader-cookie' },
+ path: '/api/test', originalUrl: '/api/test', accepts: () => false,
+ } as unknown as Request;
+}
+function response(): Response {
+ return { status: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis() } as unknown as Response;
+}
+beforeEach(() => { vi.useFakeTimers(); fetchMock.mockReset(); snapshotMock.mockReset(); });
+afterEach(() => vi.useRealTimers());
+afterAll(() => { stopAuthTimers(); vi.unstubAllGlobals(); });
+
+describe('actual WorkOS client API-key request budget', () => {
+ it('aborts at five seconds without SDK retries and permits a fresh request', async () => {
+ fetchMock.mockImplementation((_url: string, init: RequestInit) => new Promise((_resolve, reject) => {
+ init.signal!.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
+ }));
+ const res = response();
+ const next = vi.fn();
+ const pending = requireAuth(request('sk_retry'), res, next);
+ await vi.advanceTimersByTimeAsync(4_999);
+ expect(fetchMock).toHaveBeenCalledOnce();
+ expect(res.status).not.toHaveBeenCalled();
+ await vi.advanceTimersByTimeAsync(1);
+ await pending;
+ expect(fetchMock).toHaveBeenCalledOnce();
+ expect(fetchMock.mock.calls[0][0]).toBe('https://api.workos.com/api_keys/validations');
+ expect(res.status).toHaveBeenCalledWith(503);
+ expect(next).not.toHaveBeenCalled();
+
+ fetchMock.mockResolvedValue(new globalThis.Response(JSON.stringify({ api_key: {
+ id: 'key_retry', owner: { type: 'organization', id: 'org_selected' }, name: 'Retried key', permissions: [],
+ } }), { status: 200, headers: { 'Content-Type': 'application/json' } }));
+ await requireAuth(request('sk_retry'), response(), next);
+ expect(fetchMock).toHaveBeenCalledTimes(2);
+ expect(next).toHaveBeenCalledOnce();
+ });
+
+ it.each([requireAuth, optionalAuth])('rejects actual SDK malformed sealed bearer errors as 401 (%s)', async (authenticate) => {
+ for (const token of [
+ 'Fe26.2*1*salt*iv*encrypted*nope*hmacsalt*hmac~2',
+ 'Fe26.2*1*salt*iv*encrypted**hmacsalt*!~2',
+ ]) {
+ const res = response();
+ const next = vi.fn();
+ await authenticate(request(token), res, next);
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(next).not.toHaveBeenCalled();
+ }
+ expect(fetchMock).not.toHaveBeenCalled();
+ });
+
+ it('returns 503 for an unsupported provider JWK inside a sealed Bearer despite a valid cookie', async () => {
+ const { publicKey, privateKey } = await generateKeyPair('RS256');
+ const jwk = await exportJWK(publicKey);
+ const accessTokens = new Map();
+ for (const credential of ['cookie', 'bearer']) {
+ accessTokens.set(credential, await new SignJWT({ sub: `user_${credential}`, org_id: `org_${credential}` })
+ .setProtectedHeader({ alg: 'RS256', kid: credential }).setExpirationTime('1h').sign(privateKey));
+ }
+ fetchMock.mockImplementation(async (url: string, init?: RequestInit) => {
+ if (url === 'https://api.workos.com/sso/jwks/client_test') {
+ return new globalThis.Response(JSON.stringify({ keys: [
+ { ...jwk, kid: 'cookie' }, { ...jwk, kid: 'bearer', oth: [] },
+ ] }), { status: 200, headers: { 'Content-Type': 'application/json' } });
+ }
+ if (url === 'https://api.workos.com/user_management/authenticate') {
+ const { code } = JSON.parse(init!.body as string);
+ return new globalThis.Response(JSON.stringify({
+ access_token: accessTokens.get(code), refresh_token: `refresh_${code}`,
+ user: { id: `user_${code}`, email: `${code}@example.test`, email_verified: true },
+ }), { status: 200, headers: { 'Content-Type': 'application/json' } });
+ }
+ throw new Error(`Unexpected WorkOS request: ${url}`);
+ });
+ const sdk = new WorkOS(process.env.WORKOS_API_KEY!, { clientId: process.env.WORKOS_CLIENT_ID });
+ const seal = async (code: string) => (await sdk.userManagement.authenticateWithCode({
+ code, session: { sealSession: true, cookiePassword: process.env.WORKOS_COOKIE_PASSWORD! },
+ })).sealedSession!;
+ const cookie = await seal('cookie');
+ const bearer = await seal('bearer');
+ await expect(sdk.userManagement.loadSealedSession({
+ sessionData: bearer, cookiePassword: process.env.WORKOS_COOKIE_PASSWORD!,
+ }).authenticate()).rejects.toMatchObject({ code: 'ERR_JOSE_NOT_SUPPORTED' });
+ snapshotMock.mockImplementation(async (id: string, org: string | null) => ({
+ authenticatedUserId: id, canonicalUserId: id, identityId: null, authorizationEpoch: '1',
+ selectedOrganizationId: org, credentialGrant: null,
+ credential: { email: `${id}@example.test`, emailVerified: true, firstName: null, lastName: null },
+ }));
+
+ // Prove the accompanying cookie really authenticates through the installed SDK.
+ const cookieRequest = request(bearer);
+ cookieRequest.headers = {};
+ cookieRequest.cookies['wos-session'] = cookie;
+ const cookieNext = vi.fn();
+ await requireAuth(cookieRequest, response(), cookieNext);
+ expect(cookieNext).toHaveBeenCalledOnce();
+ expect(cookieRequest.user?.id).toBe('user_cookie');
+ snapshotMock.mockClear();
+
+ for (const authenticate of [requireAuth, optionalAuth]) {
+ const req = request(bearer);
+ req.cookies['wos-session'] = cookie;
+ const res = response();
+ const next = vi.fn();
+ await authenticate(req, res, next);
+ expect(res.status).toHaveBeenCalledWith(503);
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ }
+ expect(snapshotMock).not.toHaveBeenCalled();
+ expect(fetchMock.mock.calls.filter(([url]) => url === 'https://api.workos.com/user_management/authenticate'))
+ .toHaveLength(2); // Creating the fixtures only; no credential refresh/fallback.
+ });
+});
diff --git a/server/tests/unit/auth-dev-bearer-terminal.test.ts b/server/tests/unit/auth-dev-bearer-terminal.test.ts
new file mode 100644
index 00000000000..b05a85365a3
--- /dev/null
+++ b/server/tests/unit/auth-dev-bearer-terminal.test.ts
@@ -0,0 +1,87 @@
+import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { NextFunction, Request, Response } from 'express';
+
+const mocks = vi.hoisted(() => {
+ vi.stubEnv('NODE_ENV', 'test');
+ vi.stubEnv('DEV_USER_EMAIL', 'dev@example.test');
+ vi.stubEnv('DEV_USER_ID', 'user_dev_test');
+ vi.stubEnv('WORKOS_API_KEY', 'sk_test_dev_bearer');
+ vi.stubEnv('WORKOS_CLIENT_ID', 'client_test_dev_bearer');
+ vi.stubEnv('WORKOS_COOKIE_PASSWORD', 'test-cookie-password-at-least-32-characters');
+ return {
+ authenticate: vi.fn(),
+ refresh: vi.fn(),
+ getRefreshedSession: vi.fn(),
+ };
+});
+
+vi.mock('@workos-inc/node', () => ({
+ WorkOS: class WorkOS {
+ apiKeys = { createValidation: vi.fn() };
+ userManagement = {
+ loadSealedSession: () => ({ authenticate: mocks.authenticate, refresh: mocks.refresh }),
+ };
+ },
+}));
+
+vi.mock('../../src/db/session-refresh-db.js', () => ({
+ cleanExpiredRefreshes: vi.fn().mockResolvedValue(0),
+ getRefreshedSession: (...args: unknown[]) => mocks.getRefreshedSession(...args),
+ storeRefreshedSession: vi.fn().mockResolvedValue(undefined),
+}));
+
+import {
+ encodeDevSessionCookie,
+ optionalAuth,
+ requireAuth,
+ stopAuthTimers,
+} from '../../src/middleware/auth.js';
+
+function bearerRequest(): Request {
+ return {
+ headers: { authorization: 'bEaReR\tinvalid-opaque-session' },
+ cookies: { 'dev-session': encodeDevSessionCookie('admin') },
+ query: {}, body: {}, params: {},
+ accepts: vi.fn().mockReturnValue(false),
+ originalUrl: '/api/protected',
+ path: '/api/protected',
+ } as unknown as Request;
+}
+
+function response(): Response {
+ return {
+ status: vi.fn().mockReturnThis(),
+ json: vi.fn().mockReturnThis(),
+ cookie: vi.fn().mockReturnThis(),
+ redirect: vi.fn().mockReturnThis(),
+ send: vi.fn().mockReturnThis(),
+ } as unknown as Response;
+}
+
+beforeEach(() => {
+ mocks.authenticate.mockReset().mockResolvedValue({ authenticated: false });
+ mocks.refresh.mockReset().mockResolvedValue({ authenticated: false, reason: 'invalid_grant', retryable: false });
+ mocks.getRefreshedSession.mockReset().mockResolvedValue(null);
+});
+
+afterAll(() => {
+ stopAuthTimers();
+ vi.unstubAllEnvs();
+});
+
+describe.each([
+ ['required', requireAuth],
+ ['optional', optionalAuth],
+] as const)('%s auth in dev mode', (_label, middleware) => {
+ it('does not replace an explicit opaque Bearer with a valid dev-session cookie', async () => {
+ const req = bearerRequest();
+ const res = response();
+ const next = vi.fn() as NextFunction;
+
+ await middleware(req, res, next);
+
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ });
+});
diff --git a/server/tests/unit/authorization-epoch-session.test.ts b/server/tests/unit/authorization-epoch-session.test.ts
index 0ad33ee6e2a..f2f2c7bf2ea 100644
--- a/server/tests/unit/authorization-epoch-session.test.ts
+++ b/server/tests/unit/authorization-epoch-session.test.ts
@@ -1,161 +1,649 @@
-/**
- * Persisted authorization epoch enforcement (#6827).
- *
- * The session cache is per-instance, so evicting it cannot revoke authority
- * granted before an identity-binding change — another instance still serves
- * its own cached entry. These tests pin the replacement: every cache hit
- * revalidates the persisted epoch fingerprint stamped when the entry was
- * stored, and a moved fingerprint forces full re-validation.
- */
-
+/** Provider caches must never become identity, organization, or epoch authority. */
import type { NextFunction, Request, Response } from 'express';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
+import express from 'express';
+import supertest from 'supertest';
+import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { AuthorizationSnapshot } from '../../src/db/user-authorization-snapshot-db.js';
const mocks = vi.hoisted(() => ({
- authenticate: vi.fn(),
- loadSealedSession: vi.fn(),
- checkPlatformBan: vi.fn(),
- checkPlatformBanForApiKey: vi.fn(),
- getAuthorizationFingerprint: vi.fn(),
- poolQuery: vi.fn(),
+ authenticate: vi.fn(), loadSealedSession: vi.fn(), checkPlatformBan: vi.fn(),
+ loadAuthorizationSnapshot: vi.fn(), verifyWorkOSJWT: vi.fn(), poolQuery: vi.fn(),
+ refresh: vi.fn(), getRefreshedSession: vi.fn(),
}));
-
vi.hoisted(() => {
- process.env.WORKOS_API_KEY = process.env.WORKOS_API_KEY ?? 'sk_test';
- process.env.WORKOS_CLIENT_ID = process.env.WORKOS_CLIENT_ID ?? 'client_test';
- process.env.WORKOS_COOKIE_PASSWORD =
- process.env.WORKOS_COOKIE_PASSWORD ?? 'placeholder-cookie-password-32-bytes-min';
+ process.env.DEV_USER_EMAIL = '';
+ process.env.DEV_USER_ID = '';
+ process.env.WORKOS_API_KEY ??= 'sk_test';
+ process.env.WORKOS_CLIENT_ID ??= 'client_test';
+ process.env.WORKOS_COOKIE_PASSWORD ??= 'placeholder-cookie-password-32-bytes-min';
});
-
vi.mock('@workos-inc/node', () => ({
WorkOS: vi.fn(function WorkOS() {
return {
- userManagement: {
- loadSealedSession: mocks.loadSealedSession,
- },
+ userManagement: { loadSealedSession: mocks.loadSealedSession },
apiKeys: { createValidation: vi.fn() },
};
}),
}));
-
vi.mock('../../src/db/bans-db.js', () => ({
- bansDb: {
- checkPlatformBan: mocks.checkPlatformBan,
- checkPlatformBanForApiKey: mocks.checkPlatformBanForApiKey,
- },
+ bansDb: { checkPlatformBan: mocks.checkPlatformBan, checkPlatformBanForApiKey: vi.fn() },
}));
-
-vi.mock('../../src/db/authorization-epoch-db.js', () => ({
- getAuthorizationFingerprint: mocks.getAuthorizationFingerprint,
- bumpAuthorizationEpochs: vi.fn(),
+vi.mock('../../src/db/user-authorization-snapshot-db.js', async (importOriginal) => ({
+ ...await importOriginal(),
+ loadAuthorizationSnapshot: mocks.loadAuthorizationSnapshot,
+}));
+vi.mock('../../src/auth/workos-jwt.js', async (importOriginal) => ({
+ ...await importOriginal(),
+ verifyWorkOSJWT: mocks.verifyWorkOSJWT,
+ looksLikeJWT: (token: string) => token.split('.').length === 3,
}));
-
vi.mock('../../src/db/client.js', () => ({
- getPool: () => ({ query: mocks.poolQuery }),
- query: mocks.poolQuery,
- isDatabaseInitialized: () => true,
+ getPool: () => ({ query: mocks.poolQuery }), query: mocks.poolQuery, isDatabaseInitialized: () => true,
}));
+vi.mock('../../src/db/session-refresh-db.js', () => ({
+ getRefreshedSession: mocks.getRefreshedSession,
+ storeRefreshedSession: vi.fn().mockResolvedValue(undefined),
+ cleanExpiredRefreshes: vi.fn().mockResolvedValue(0),
+}));
+import { AuthorizationSnapshotUnavailableError } from '../../src/db/user-authorization-snapshot-db.js';
+import { invalidateBanCache, optionalAuth, requireAuth, stopAuthTimers } from '../../src/middleware/auth.js';
+import { csrfProtection } from '../../src/middleware/csrf.js';
-import { requireAuth } from '../../src/middleware/auth.js';
-
-const SESSION_USER = {
- id: 'user_epoch_primary',
- email: 'primary@epoch.test',
- firstName: 'Epoch',
- lastName: 'User',
- emailVerified: true,
- createdAt: '2026-01-01T00:00:00.000Z',
- updatedAt: '2026-01-01T00:00:00.000Z',
+const AUTHENTICATED_ID = 'user_epoch_authenticated';
+const PROVIDER_USER = {
+ id: AUTHENTICATED_ID, email: 'sam@example.test', firstName: 'Sam', lastName: 'Adeyemi',
+ emailVerified: true, createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
};
-
-function makeRequest(cookie: string): Request {
+let sequence = 0;
+function snapshot(overrides: Partial = {}): AuthorizationSnapshot {
return {
- headers: {},
- cookies: { 'wos-session': cookie },
- path: '/api/me',
- originalUrl: '/api/me',
- accepts: () => false,
+ authenticatedUserId: AUTHENTICATED_ID, canonicalUserId: AUTHENTICATED_ID,
+ identityId: 'identity_epoch', authorizationEpoch: '1', selectedOrganizationId: null,
+ credential: { email: PROVIDER_USER.email, firstName: 'Sam', lastName: 'Adeyemi', emailVerified: true },
+ credentialGrant: null, ...overrides,
+ };
+}
+function request(token: string, bearer = false): Request {
+ return {
+ headers: bearer ? { authorization: `Bearer ${token}` } : {},
+ cookies: bearer ? {} : { 'wos-session': token }, query: {}, body: {}, params: {},
+ path: '/api/me', originalUrl: '/api/me', accepts: () => false,
} as unknown as Request;
}
-
-function makeResponse(): Response {
+function response(): Response {
return {
- cookie: vi.fn(),
- status: vi.fn().mockReturnThis(),
- json: vi.fn().mockReturnThis(),
+ cookie: vi.fn(), status: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(),
redirect: vi.fn().mockReturnThis(),
} as unknown as Response;
}
+beforeEach(() => {
+ vi.clearAllMocks();
+ mocks.loadAuthorizationSnapshot.mockReset();
+ mocks.loadAuthorizationSnapshot.mockImplementation(async (_id: string, org: string | null) =>
+ snapshot({ selectedOrganizationId: org }));
+ invalidateBanCache('user', AUTHENTICATED_ID);
+ mocks.checkPlatformBan.mockResolvedValue({ banned: false });
+ mocks.authenticate.mockResolvedValue({ authenticated: true, user: { ...PROVIDER_USER }, accessToken: 'access-token' });
+ mocks.refresh.mockReset().mockResolvedValue({ authenticated: false });
+ mocks.getRefreshedSession.mockReset().mockResolvedValue(null);
+ mocks.loadSealedSession.mockReturnValue({ authenticate: mocks.authenticate, refresh: mocks.refresh });
+ mocks.verifyWorkOSJWT.mockResolvedValue({ sub: AUTHENTICATED_ID, email: PROVIDER_USER.email, isM2M: false });
+});
-describe('persisted authorization epoch gates the session cache', () => {
+const organizationSelectors: Array<[string, (req: Request, organizationId: string) => void]> = [
+ ['header', (req, org) => { req.headers['x-organization-id'] = org; }],
+ ['query org', (req, org) => { req.query.org = org; }],
+ ['query organization_id', (req, org) => { req.query.organization_id = org; }],
+ ['query organizationId', (req, org) => { req.query.organizationId = org; }],
+ ['body organization_id', (req, org) => { req.body.organization_id = org; }],
+ ['body organizationId', (req, org) => { req.body.organizationId = org; }],
+ ['path orgId', (req, org) => { req.params.orgId = org; }],
+ ['path organizationId', (req, org) => { req.params.organizationId = org; }],
+];
+
+describe.each([
+ ['required cookie', requireAuth, false], ['optional cookie', optionalAuth, false],
+ ['required bearer', requireAuth, true], ['optional bearer', optionalAuth, true],
+] as const)('%s provider organization binding', (_label, middleware, bearer) => {
beforeEach(() => {
- vi.clearAllMocks();
- mocks.checkPlatformBan.mockResolvedValue({ banned: false, ban: null });
- // Local user lookup resolves; attachIdentityId sees a singleton identity
- // (primary === authenticated credential), so there is no id-swap.
- mocks.poolQuery.mockResolvedValue({
- rows: [
- {
- first_name: 'Epoch',
- last_name: 'User',
- identity_id: 'identity_epoch',
- primary_workos_user_id: 'user_epoch_primary',
- },
- ],
- rowCount: 1,
+ mocks.authenticate.mockResolvedValue({
+ authenticated: true, user: { ...PROVIDER_USER }, accessToken: 'access-token', organizationId: 'org_pinnacle',
});
+ mocks.verifyWorkOSJWT.mockResolvedValue({
+ sub: AUTHENTICATED_ID, email: PROVIDER_USER.email, isM2M: false, orgId: 'org_pinnacle',
+ });
+ });
+
+ describe.each(['cold', 'warm'] as const)('%s cache', (cacheState) => {
+ it.each(organizationSelectors)('rejects a mismatched %s selector and permits a matching selector', async (_location, select) => {
+ for (const selectedOrg of ['org_streamhaus', 'org_pinnacle']) {
+ const token = bearer ? `header.org${++sequence}.signature` : `sealed-org-${++sequence}`;
+ if (cacheState === 'warm') await middleware(request(token, bearer), response(), vi.fn());
+ const req = request(token, bearer);
+ select(req, selectedOrg);
+ const res = response();
+ const next = vi.fn();
+ const snapshotsBefore = mocks.loadAuthorizationSnapshot.mock.calls.length;
+ await middleware(req, res, next);
+ if (selectedOrg === 'org_streamhaus') {
+ expect(res.status).toHaveBeenCalledWith(403);
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ expect(mocks.loadAuthorizationSnapshot).toHaveBeenCalledTimes(snapshotsBefore);
+ } else {
+ expect(next).toHaveBeenCalledOnce();
+ expect(res.status).not.toHaveBeenCalled();
+ expect(req.user?.authorizationSnapshot?.selectedOrganizationId).toBe('org_pinnacle');
+ }
+ }
+ });
+ });
+
+ it.each(['', ' ', null, [], {}])('rejects malformed supplied provider organization %j', async (providerOrganization) => {
mocks.authenticate.mockResolvedValue({
- authenticated: true,
- user: SESSION_USER,
- accessToken: 'access-token',
+ authenticated: true, user: { ...PROVIDER_USER }, accessToken: 'access-token', organizationId: providerOrganization,
});
- mocks.loadSealedSession.mockReturnValue({
- authenticate: mocks.authenticate,
- refresh: vi.fn(),
+ mocks.verifyWorkOSJWT.mockResolvedValue({
+ sub: AUTHENTICATED_ID, email: PROVIDER_USER.email, isM2M: false, orgId: providerOrganization,
});
- mocks.getAuthorizationFingerprint.mockResolvedValue('user_epoch_primary:1');
+ const token = bearer ? `header.malformed${++sequence}.signature` : `sealed-malformed-${++sequence}`;
+ const req = request(token, bearer);
+ req.headers['x-organization-id'] = 'org_pinnacle';
+ const res = response();
+ const next = vi.fn();
+ await middleware(req, res, next);
+ expect(res.status).toHaveBeenCalledWith(403);
+ expect(next).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
});
+});
- it('serves the cached session while the fingerprint is unchanged', async () => {
- const cookie = `sealed-unchanged-${Date.now()}`;
- const next = vi.fn() as NextFunction;
+describe('optional authentication credential presence', () => {
+ function expectRejected(req: Request, res: Response, next: ReturnType, status: number): void {
+ expect(res.status).toHaveBeenCalledWith(status);
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ expect(res.redirect).not.toHaveBeenCalled();
+ }
- await requireAuth(makeRequest(cookie), makeResponse(), next);
- await requireAuth(makeRequest(cookie), makeResponse(), next);
+ it('permits anonymous access only when no credential is supplied', async () => {
+ const req = request('unused');
+ req.cookies = {};
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expect(next).toHaveBeenCalledOnce();
+ expect(req.user).toBeUndefined();
+ expect(res.status).not.toHaveBeenCalled();
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
+ });
- expect(next).toHaveBeenCalledTimes(2);
- // Second request came from cache — the sealed session was unsealed once.
- expect(mocks.authenticate).toHaveBeenCalledTimes(1);
+ it.each([false, true])('rejects a failed JWT without anonymous or cookie fallback (cookie present: %s)', async (cookiePresent) => {
+ const req = request(`header.invalid${++sequence}.signature`, true);
+ if (cookiePresent) req.cookies['wos-session'] = 'otherwise-valid-cookie';
+ mocks.verifyWorkOSJWT.mockRejectedValue(Object.assign(new Error('Invalid JWT signature'), { code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' }));
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 401);
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
});
- it('re-validates when an identity-binding change moved the fingerprint', async () => {
- const cookie = `sealed-bumped-${Date.now()}`;
- const next = vi.fn() as NextFunction;
+ it.each(['Bearer ', 'Bearer ', 'Basic invalid', ''])('rejects malformed authorization header %j without cookie fallback', async (header) => {
+ const req = request('otherwise-valid-cookie');
+ req.headers.authorization = header;
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 401);
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ });
+
+ it('rejects an unrecognized bearer after sealed-session validation instead of using a valid cookie', async () => {
+ const token = `invalid-opaque-${++sequence}`;
+ const req = request(token, true);
+ req.cookies['wos-session'] = 'otherwise-valid-cookie';
+ mocks.authenticate.mockResolvedValue({ authenticated: false });
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 401);
+ expect(mocks.loadSealedSession).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sessionData: token }));
+ });
+
+ it('continues to authenticate native sealed bearer sessions', async () => {
+ const token = `native-sealed-${++sequence}`;
+ const req = request(token, true);
+ const next = vi.fn();
+ await optionalAuth(req, response(), next);
+ expect(next).toHaveBeenCalledOnce();
+ expect(mocks.loadSealedSession).toHaveBeenCalledWith(expect.objectContaining({ sessionData: token }));
+ expect(req.user?.authorizationSnapshot?.authenticatedUserId).toBe(AUTHENTICATED_ID);
+ });
- await requireAuth(makeRequest(cookie), makeResponse(), next);
- expect(mocks.authenticate).toHaveBeenCalledTimes(1);
+ it.each(['', null, {}])('rejects malformed supplied session cookie %j', async (cookie) => {
+ const req = request('unused');
+ req.cookies['wos-session'] = cookie;
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 401);
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ });
- // A binding mutation bumped the credential's epoch on another instance.
- mocks.getAuthorizationFingerprint.mockResolvedValue('user_epoch_primary:2');
+ it('rejects expired sessions and their dead-cache replay instead of making them anonymous', async () => {
+ const token = `dead-session-${++sequence}`;
+ mocks.authenticate.mockResolvedValue({ authenticated: false });
+ for (let attempt = 0; attempt < 2; attempt += 1) {
+ const req = request(token);
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 401);
+ }
+ expect(mocks.authenticate).toHaveBeenCalledOnce();
+ expect(mocks.refresh).toHaveBeenCalledOnce();
+ });
- await requireAuth(makeRequest(cookie), makeResponse(), next);
+ it('rejects a provider result claiming authentication without a user', async () => {
+ mocks.authenticate.mockResolvedValue({ authenticated: true });
+ const req = request(`missing-user-${++sequence}`);
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 401);
+ });
- expect(next).toHaveBeenCalledTimes(2);
- expect(mocks.authenticate).toHaveBeenCalledTimes(2);
+ it.each([false, true])('rejects a missing local credential on cold authentication (bearer: %s)', async (bearer) => {
+ mocks.loadAuthorizationSnapshot.mockResolvedValue(null);
+ const req = request(bearer ? `header.missing${++sequence}.signature` : `sealed-missing-${++sequence}`, bearer);
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 401);
});
- it('bypasses the cache when the epoch lookup fails rather than serving unconfirmed state', async () => {
- const cookie = `sealed-lookup-error-${Date.now()}`;
- const next = vi.fn() as NextFunction;
+ it.each(['JWT verification', 'session authentication', 'session refresh'] as const)('returns 503 on transient %s failure without anonymous fallback', async (stage) => {
+ const transient = Object.assign(new Error('Authentication upstream timed out'), { code: 'ETIMEDOUT' });
+ const bearer = stage === 'JWT verification';
+ const req = request(bearer ? `header.timeout${++sequence}.signature` : `sealed-timeout-${++sequence}`, bearer);
+ if (stage === 'JWT verification') mocks.verifyWorkOSJWT.mockRejectedValue(transient);
+ if (stage === 'session authentication') mocks.authenticate.mockRejectedValue(transient);
+ if (stage === 'session refresh') {
+ mocks.authenticate.mockResolvedValue({ authenticated: false });
+ mocks.refresh.mockRejectedValue(transient);
+ }
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 503);
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
+ });
+
+ it('returns 503 if shared session recovery is unavailable', async () => {
+ mocks.authenticate.mockResolvedValue({ authenticated: false });
+ mocks.getRefreshedSession.mockRejectedValue(new Error('Session database unavailable'));
+ const req = request(`sealed-recovery-${++sequence}`);
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 503);
+ });
+
+ it.each([
+ ['JWKS HTTP response', Object.assign(new Error('Expected 200 OK from the JSON Web Key Set HTTP response'), {
+ name: 'JOSEError', code: 'ERR_JOSE_GENERIC',
+ })],
+ ['malformed JWKS response', Object.assign(new Error('Failed to parse the JSON Web Key Set HTTP response as JSON'), {
+ name: 'JOSEError', code: 'ERR_JOSE_GENERIC',
+ })],
+ ['provider HTTP 503', Object.assign(new Error('Service unavailable'), { status: 503 })],
+ ['provider HTTP 429', Object.assign(new Error('Rate limited'), { statusCode: 429 })],
+ ])('returns 503 for unavailable %s during JWT verification', async (_label, error) => {
+ mocks.verifyWorkOSJWT.mockRejectedValue(error);
+ const req = request(`header.unavailable${++sequence}.signature`, true);
+ req.cookies['wos-session'] = 'otherwise-valid-cookie';
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 503);
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ });
+
+ it.each(['initial', 'shared'] as const)('returns 503 for a retryable %s refresh result and permits a later retry', async (stage) => {
+ const token = `retryable-${stage}-${++sequence}`;
+ mocks.authenticate.mockResolvedValue({ authenticated: false });
+ if (stage === 'shared') {
+ mocks.getRefreshedSession.mockResolvedValueOnce('shared-sealed-session');
+ mocks.refresh.mockResolvedValueOnce({ authenticated: false, reason: 'invalid_grant', retryable: false });
+ }
+ mocks.refresh.mockResolvedValue({ authenticated: false, reason: 'server_error', retryable: true });
+ const req = request(token);
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 503);
- await requireAuth(makeRequest(cookie), makeResponse(), next);
- expect(mocks.authenticate).toHaveBeenCalledTimes(1);
+ mocks.authenticate.mockResolvedValue({ authenticated: true, user: { ...PROVIDER_USER }, accessToken: 'access-token' });
+ const retry = request(token);
+ const retryNext = vi.fn();
+ await optionalAuth(retry, response(), retryNext);
+ expect(retryNext).toHaveBeenCalledOnce();
+ expect(retry.user?.authorizationSnapshot?.authenticatedUserId).toBe(AUTHENTICATED_ID);
+ });
- mocks.getAuthorizationFingerprint.mockRejectedValueOnce(new Error('db down'));
+ it('returns 503 on a thrown provider HTTP 503 during session refresh', async () => {
+ mocks.authenticate.mockResolvedValue({ authenticated: false });
+ mocks.refresh.mockRejectedValue(Object.assign(new Error('Service unavailable'), { status: 503 }));
+ const req = request(`refresh-503-${++sequence}`);
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 503);
+ });
- await requireAuth(makeRequest(cookie), makeResponse(), next);
+ it('returns 401 when sealed-session decoding rejects the presented credential', async () => {
+ mocks.loadSealedSession.mockImplementationOnce(() => { throw new Error('Invalid sealed session'); });
+ const req = request(`invalid-sealed-${++sequence}`);
+ const res = response();
+ const next = vi.fn();
+ await optionalAuth(req, res, next);
+ expectRejected(req, res, next, 401);
+ });
+});
+afterAll(stopAuthTimers);
+describe.each([
+ ['required cookie', requireAuth, false], ['optional cookie', optionalAuth, false],
+ ['required bearer', requireAuth, true], ['optional bearer', optionalAuth, true],
+] as const)('%s authorization snapshots', (_label, middleware, bearer) => {
+ function credential(): string {
+ sequence += 1;
+ return bearer ? `header.payload${sequence}.signature` : `sealed-snapshot-${sequence}`;
+ }
+ it('hydrates an atomic snapshot on cold start and every cached request', async () => {
+ const token = credential();
+ const first = request(token, bearer);
+ const second = request(token, bearer);
+ const next = vi.fn() as NextFunction;
+ await middleware(first, response(), next);
+ mocks.loadAuthorizationSnapshot.mockResolvedValue(snapshot({ authorizationEpoch: '2' }));
+ await middleware(second, response(), next);
expect(next).toHaveBeenCalledTimes(2);
- expect(mocks.authenticate).toHaveBeenCalledTimes(2);
+ expect(mocks.loadAuthorizationSnapshot).toHaveBeenCalledTimes(2);
+ expect(mocks.loadAuthorizationSnapshot).toHaveBeenNthCalledWith(2, AUTHENTICATED_ID, null);
+ expect(first.user?.authorizationSnapshot?.authorizationEpoch).toBe('1');
+ expect(second.user?.authorizationSnapshot?.authorizationEpoch).toBe('2');
+ expect(bearer ? mocks.verifyWorkOSJWT : mocks.authenticate).toHaveBeenCalledTimes(1);
+ expect(mocks.poolQuery).not.toHaveBeenCalled();
+ });
+ it.each([
+ ['secondary becomes primary', 'user_other_primary', AUTHENTICATED_ID],
+ ['primary becomes secondary', AUTHENTICATED_ID, 'user_other_primary'],
+ ])('keeps the exact credential when %s', async (_direction, before, after) => {
+ const token = credential();
+ mocks.loadAuthorizationSnapshot.mockResolvedValue(snapshot({ canonicalUserId: before }));
+ const first = request(token, bearer);
+ await middleware(first, response(), vi.fn());
+ // A handler mutates its request object, which must never poison provider authentication.
+ first.user!.id = 'user_attacker_canonical';
+ first.user!.authWorkosUserId = 'user_attacker_credential';
+ first.user!.identityId = 'identity_attacker';
+ mocks.loadAuthorizationSnapshot.mockResolvedValue(snapshot({
+ canonicalUserId: after, identityId: 'identity_changed', authorizationEpoch: '2',
+ }));
+ const second = request(token, bearer);
+ const next = vi.fn();
+ await middleware(second, response(), next);
+ expect(next).toHaveBeenCalledOnce();
+ expect(mocks.loadAuthorizationSnapshot).toHaveBeenLastCalledWith(AUTHENTICATED_ID, null);
+ expect(second.user).toMatchObject({ id: after, identityId: 'identity_changed' });
+ expect(second.user?.authWorkosUserId ?? second.user?.id).toBe(AUTHENTICATED_ID);
+ expect(second.user?.authorizationSnapshot?.authorizationEpoch).toBe('2');
+ });
+ it.each(['cold', 'warm'] as const)('returns unavailable with no fallback on a %s-cache database outage', async (cacheState) => {
+ const token = credential();
+ if (cacheState === 'warm') await middleware(request(token, bearer), response(), vi.fn());
+ mocks.loadAuthorizationSnapshot.mockRejectedValue(new AuthorizationSnapshotUnavailableError());
+ const req = request(token, bearer);
+ const res = response();
+ const next = vi.fn();
+ await middleware(req, res, next);
+ expect(res.status).toHaveBeenCalledWith(503);
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ expect(res.redirect).not.toHaveBeenCalled();
+ expect(bearer ? mocks.verifyWorkOSJWT : mocks.authenticate).toHaveBeenCalledTimes(1);
+ });
+ it('rejects a deleted exact credential instead of recovering its canonical user', async () => {
+ const token = credential();
+ await middleware(request(token, bearer), response(), vi.fn());
+ mocks.loadAuthorizationSnapshot.mockResolvedValue(null);
+ const req = request(token, bearer);
+ const res = response();
+ const next = vi.fn();
+ await middleware(req, res, next);
+ expect(res.status).toHaveBeenCalledWith(401);
+ expect(next).not.toHaveBeenCalled();
+ expect(req.user).toBeUndefined();
+ });
+ it('keeps organization selection request-local across cache hits and leaves no selection empty', async () => {
+ const token = credential();
+ for (const org of ['org_pinnacle', 'org_streamhaus', null]) {
+ const req = request(token, bearer);
+ if (org) req.headers['x-organization-id'] = org;
+ await middleware(req, response(), vi.fn());
+ expect(mocks.loadAuthorizationSnapshot).toHaveBeenLastCalledWith(AUTHENTICATED_ID, org);
+ expect(req.user?.authorizationSnapshot?.selectedOrganizationId).toBe(org);
+ }
+ expect(bearer ? mocks.verifyWorkOSJWT : mocks.authenticate).toHaveBeenCalledTimes(1);
+ });
+ it('uses current local credential email instead of a stale provider email', async () => {
+ const token = credential();
+ await middleware(request(token, bearer), response(), vi.fn());
+ mocks.loadAuthorizationSnapshot.mockResolvedValue(snapshot({
+ credential: { email: 'sam-current@example.test', firstName: 'Sam', lastName: 'Adeyemi', emailVerified: true },
+ }));
+ const req = request(token, bearer);
+ await middleware(req, response(), vi.fn());
+ expect(req.user?.email).toBe('sam-current@example.test');
+ });
+ it('does not apply an old provider verification to the current unverified email', async () => {
+ mocks.loadAuthorizationSnapshot.mockResolvedValue(snapshot({
+ credential: { email: 'sam-unverified@example.test', firstName: 'Sam', lastName: 'Adeyemi', emailVerified: false },
+ }));
+ const req = request(credential(), bearer);
+ await middleware(req, response(), vi.fn());
+ expect(req.user).toMatchObject({ email: 'sam-unverified@example.test', emailVerified: false });
+ });
+ it('rejects conflicting explicit organization selectors before hydrating authority', async () => {
+ const req = request(credential(), bearer);
+ req.headers['x-organization-id'] = 'org_pinnacle';
+ req.params.orgId = 'org_streamhaus';
+ const res = response();
+ const next = vi.fn();
+ await middleware(req, res, next);
+ expect(res.status).toHaveBeenCalledWith(403);
+ expect(next).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
+ });
+ it('keeps authorization provenance out of serialized and spread user objects', async () => {
+ const req = request(credential(), bearer);
+ await middleware(req, response(), vi.fn());
+ expect(req.user?.authorizationSnapshot?.authenticatedUserId).toBe(AUTHENTICATED_ID);
+ expect(JSON.stringify(req.user)).not.toContain('authorizationSnapshot');
+ expect({ ...req.user }).not.toHaveProperty('authorizationSnapshot');
+ });
+});
+it('a cookie cached by optional auth is rehydrated before required auth', async () => {
+ const token = `shared-optional-${++sequence}`;
+ await optionalAuth(request(token), response(), vi.fn());
+ mocks.loadAuthorizationSnapshot.mockResolvedValue(snapshot({ canonicalUserId: 'user_changed', authorizationEpoch: '7' }));
+ const second = request(token);
+ await requireAuth(second, response(), vi.fn());
+ expect(mocks.authenticate).toHaveBeenCalledTimes(1);
+ expect(second.user).toMatchObject({ id: 'user_changed', authWorkosUserId: AUTHENTICATED_ID });
+ expect(second.user?.authorizationSnapshot?.authorizationEpoch).toBe('7');
+});
+
+describe.each([['required', requireAuth], ['optional', optionalAuth]] as const)('%s bearer bans', (_label, middleware) => {
+ it.each(['cold', 'warm'] as const)('checks the banned exact linked credential on a %s cache', async (cacheState) => {
+ const token = `header.banned${++sequence}.signature`;
+ mocks.loadAuthorizationSnapshot.mockResolvedValue(snapshot({ canonicalUserId: 'user_unbanned_primary' }));
+ if (cacheState === 'warm') {
+ await middleware(request(token, true), response(), vi.fn());
+ invalidateBanCache('user', AUTHENTICATED_ID);
+ }
+ mocks.checkPlatformBan.mockImplementation(async (userId: string) => userId === AUTHENTICATED_ID
+ ? { banned: true, ban: { id: 'ban_exact_credential', reason: 'Account suspended' } }
+ : { banned: false });
+ const res = response();
+ const next = vi.fn();
+ await middleware(request(token, true), res, next);
+ expect(mocks.checkPlatformBan).toHaveBeenLastCalledWith(AUTHENTICATED_ID);
+ expect(res.status).toHaveBeenCalledWith(403);
+ expect(next).not.toHaveBeenCalled();
+ });
+});
+
+describe.each([['required', requireAuth], ['optional', optionalAuth]] as const)('%s concurrent bearer hydration', (_label, middleware) => {
+ it('keeps overlapping cached JWT requests bound to the original authenticated credential', async () => {
+ const token = `header.overlapping${++sequence}.signature`;
+ await middleware(request(token, true), response(), vi.fn());
+
+ let resolveFirst!: (value: AuthorizationSnapshot) => void;
+ let resolveSecond!: (value: AuthorizationSnapshot) => void;
+ let markFirstStarted!: () => void;
+ let markSecondStarted!: () => void;
+ const firstStarted = new Promise((resolve) => { markFirstStarted = resolve; });
+ const secondStarted = new Promise((resolve) => { markSecondStarted = resolve; });
+ const firstSnapshot = snapshot({ canonicalUserId: 'user_primary_a' });
+ const secondSnapshot = snapshot({ canonicalUserId: 'user_primary_b', authorizationEpoch: '2' });
+ mocks.loadAuthorizationSnapshot
+ .mockImplementationOnce(() => {
+ markFirstStarted();
+ return new Promise((resolve) => { resolveFirst = resolve; });
+ })
+ .mockImplementationOnce(() => {
+ markSecondStarted();
+ return new Promise((resolve) => { resolveSecond = resolve; });
+ })
+ .mockResolvedValue(secondSnapshot);
+
+ const first = request(token, true);
+ const second = request(token, true);
+ const firstNext = vi.fn();
+ const secondNext = vi.fn();
+ const firstPending = middleware(first, response(), firstNext);
+ const secondPending = middleware(second, response(), secondNext);
+ await Promise.all([firstStarted, secondStarted]);
+ expect(mocks.loadAuthorizationSnapshot).toHaveBeenNthCalledWith(2, AUTHENTICATED_ID, null);
+ expect(mocks.loadAuthorizationSnapshot).toHaveBeenNthCalledWith(3, AUTHENTICATED_ID, null);
+
+ resolveFirst(firstSnapshot);
+ await firstPending;
+ const firstUser = first.user;
+ expect(firstUser).toMatchObject({ id: 'user_primary_a', authWorkosUserId: AUTHENTICATED_ID });
+ expect(firstUser?.authorizationSnapshot).toBe(firstSnapshot);
+ expect(secondNext).not.toHaveBeenCalled();
+
+ resolveSecond(secondSnapshot);
+ await secondPending;
+ expect(firstNext).toHaveBeenCalledOnce();
+ expect(secondNext).toHaveBeenCalledOnce();
+ expect(second.user).not.toBe(firstUser);
+ expect(second.user).toMatchObject({ id: 'user_primary_b', authWorkosUserId: AUTHENTICATED_ID });
+ expect(second.user?.authorizationSnapshot).toBe(secondSnapshot);
+ expect(first.user).toBe(firstUser);
+ expect(first.user).toMatchObject({ id: 'user_primary_a', authWorkosUserId: AUTHENTICATED_ID });
+ expect(first.user?.authorizationSnapshot).toBe(firstSnapshot);
+
+ const following = request(token, true);
+ await middleware(following, response(), vi.fn());
+ expect(mocks.loadAuthorizationSnapshot).toHaveBeenNthCalledWith(4, AUTHENTICATED_ID, null);
+ expect(following.user?.authorizationSnapshot?.authenticatedUserId).toBe(AUTHENTICATED_ID);
+ expect(following.user?.authWorkosUserId).toBe(AUTHENTICATED_ID);
+ expect(mocks.verifyWorkOSJWT).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe('optional authentication HTTP route boundary', () => {
+ function route(parsedCookies: Record = {}) {
+ const app = express();
+ // Match network-health-security.test.ts: authentication is under test, not
+ // cookie-parser. Inject its parsed output from a fixture owned by this app;
+ // the HTTP requests below send the corresponding Cookie header. An absent
+ // cookie remains {}, while a presented empty cookie retains its empty value.
+ app.use((req, _res, next) => {
+ req.cookies = { ...parsedCookies };
+ next();
+ });
+ app.use(csrfProtection);
+ const handler = vi.fn((req: Request, res: Response) => {
+ res.json({ authenticated: Boolean(req.user) });
+ });
+ app.get('/optional-auth-test', optionalAuth, handler);
+ return { app, handler };
+ }
+
+ it('reaches the anonymous handler when neither Authorization nor Cookie is sent', async () => {
+ const { app, handler } = route();
+ const result = await supertest(app).get('/optional-auth-test');
+ expect(result.status).toBe(200);
+ expect(result.body).toEqual({ authenticated: false });
+ expect(handler).toHaveBeenCalledOnce();
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
+ });
+
+ it.each(['', 'Basic invalid', 'Bearer header.invalid.signature'])('rejects supplied Authorization %j without using an otherwise valid cookie', async (authorization) => {
+ mocks.verifyWorkOSJWT.mockRejectedValue(Object.assign(new Error('Invalid JWT signature'), { code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' }));
+ const cookie = `otherwise-valid-${++sequence}`;
+ const { app, handler } = route({ 'wos-session': cookie });
+ const result = await supertest(app).get('/optional-auth-test')
+ .set('Authorization', authorization)
+ .set('Cookie', `wos-session=${cookie}`);
+ expect(result.status).toBe(401);
+ expect(handler).not.toHaveBeenCalled();
+ expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
+ });
+
+ it('rejects an opaque invalid bearer after validating that bearer rather than its companion cookie', async () => {
+ mocks.authenticate.mockResolvedValue({ authenticated: false });
+ const token = `invalid-native-route-${++sequence}`;
+ const { app, handler } = route({ 'wos-session': 'otherwise-valid' });
+ const result = await supertest(app).get('/optional-auth-test')
+ .set('Authorization', `Bearer ${token}`)
+ .set('Cookie', 'wos-session=otherwise-valid');
+ expect(result.status).toBe(401);
+ expect(handler).not.toHaveBeenCalled();
+ expect(mocks.loadSealedSession).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ sessionData: token }));
+ });
+
+ it.each(['empty', 'invalid'] as const)('rejects an %s presented wos-session cookie before the handler', async (kind) => {
+ mocks.authenticate.mockResolvedValue({ authenticated: false });
+ const token = kind === 'empty' ? '' : `invalid-cookie-route-${++sequence}`;
+ const { app, handler } = route({ 'wos-session': token });
+ const result = await supertest(app).get('/optional-auth-test').set('Cookie', `wos-session=${token}`);
+ expect(result.status).toBe(401);
+ expect(handler).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).not.toHaveBeenCalled();
+ if (kind === 'empty') expect(mocks.loadSealedSession).not.toHaveBeenCalled();
+ });
+
+ it.each(['bearer', 'cookie'] as const)('returns 503 for unavailable authorization on a presented %s without running the handler', async (kind) => {
+ mocks.loadAuthorizationSnapshot.mockRejectedValue(new AuthorizationSnapshotUnavailableError());
+ const cookie = `unavailable-route-${++sequence}`;
+ const { app, handler } = route(kind === 'cookie' ? { 'wos-session': cookie } : {});
+ const httpRequest = supertest(app).get('/optional-auth-test');
+ if (kind === 'bearer') httpRequest.set('Authorization', `Bearer header.route${++sequence}.signature`);
+ else httpRequest.set('Cookie', `wos-session=${cookie}`);
+ const result = await httpRequest;
+ expect(result.status).toBe(503);
+ expect(handler).not.toHaveBeenCalled();
+ expect(mocks.loadAuthorizationSnapshot).toHaveBeenCalledOnce();
});
});
diff --git a/server/tests/unit/caller-org-auth-errors.test.ts b/server/tests/unit/caller-org-auth-errors.test.ts
new file mode 100644
index 00000000000..8fca9793183
--- /dev/null
+++ b/server/tests/unit/caller-org-auth-errors.test.ts
@@ -0,0 +1,185 @@
+import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import express from 'express';
+import request from 'supertest';
+
+const mocks = vi.hoisted(() => ({
+ validateApiKey: vi.fn(), jwtVerify: vi.fn(), decodeJwt: vi.fn(), query: vi.fn(), listAllAgents: vi.fn(),
+ getProfileByDomain: vi.fn(), issueDomainClaim: vi.fn(),
+ listProposals: vi.fn(), getProposalById: vi.fn(), submitProposal: vi.fn(),
+}));
+vi.hoisted(() => {
+ process.env.WORKOS_API_KEY = 'sk_test';
+ process.env.WORKOS_CLIENT_ID = 'client_test';
+});
+vi.mock('../../src/middleware/auth.js', async (importOriginal) => ({
+ ...await importOriginal(),
+ validateWorkOSApiKey: mocks.validateApiKey,
+}));
+vi.mock('jose', async (importOriginal) => ({
+ ...await importOriginal(),
+ createRemoteJWKSet: () => vi.fn(),
+ decodeJwt: mocks.decodeJwt,
+ jwtVerify: mocks.jwtVerify,
+}));
+vi.mock('../../src/db/client.js', async (importOriginal) => ({
+ ...await importOriginal(),
+ query: mocks.query,
+}));
+vi.mock('../../src/db/member-db.js', () => ({
+ MemberDatabase: class { getProfileByDomain = mocks.getProfileByDomain; },
+}));
+vi.mock('../../src/db/community-mirror-db.js', () => ({
+ CommunityMirrorDatabase: class {
+ listProposals = mocks.listProposals;
+ getProposalById = mocks.getProposalById;
+ submitProposal = mocks.submitProposal;
+ },
+}));
+vi.mock('../../src/services/brand-logo-auth.js', async (importOriginal) => ({
+ ...await importOriginal(),
+ isRegistryModerator: vi.fn().mockResolvedValue(false),
+}));
+vi.mock('../../src/addie/admin-status-lookup.js', () => ({ isWebUserAAOAdmin: vi.fn().mockResolvedValue(false) }));
+vi.mock('../../src/middleware/rate-limit.js', async (importOriginal) => {
+ const original = await importOriginal>();
+ const pass: express.RequestHandler = (_req, _res, next) => next();
+ return Object.fromEntries(Object.entries(original).map(([name, value]) => [name, name.endsWith('RateLimiter') ? pass : value]));
+});
+
+import { createRegistryApiRouter, type RegistryApiConfig } from '../../src/routes/registry-api.js';
+import { createCommunityMirrorRouter } from '../../src/routes/community-mirrors.js';
+import { stopAuthTimers } from '../../src/middleware/auth.js';
+
+const attachedCookieUser: express.RequestHandler = (req, _res, next) => {
+ // Exercise the resolver boundary after another middleware attached a user.
+ req.user = { id: 'cookie_user', email: 'cookie@example.test' } as typeof req.user;
+ next();
+};
+function app() {
+ const application = express();
+ application.use(express.json());
+ const config = {
+ brandManager: {}, brandDb: {}, propertyDb: { issueDomainClaim: mocks.issueDomainClaim },
+ adagentsManager: {}, healthChecker: {}, capabilityDiscovery: {},
+ crawler: { getFederatedIndex: () => ({ listAllAgents: mocks.listAllAgents }) },
+ registryRequestsDb: { trackRequest: async () => {}, markResolved: async () => true },
+ requireAuth: attachedCookieUser, optionalAuth: attachedCookieUser,
+ } as unknown as RegistryApiConfig;
+ application.use('/api', createRegistryApiRouter(config));
+ application.use('/api/registry', createCommunityMirrorRouter({ requireAuth: attachedCookieUser }));
+ return application;
+}
+const application = app();
+afterAll(stopAuthTimers);
+beforeEach(() => {
+ vi.clearAllMocks();
+ mocks.validateApiKey.mockResolvedValue(null);
+ mocks.decodeJwt.mockImplementation((token: string) => {
+ if (token === 'eyJmalformed') {
+ throw Object.assign(new Error('Malformed JWT'), { code: 'ERR_JWT_INVALID' });
+ }
+ return { iss: 'https://auth.example.test/user_management/client_test' };
+ });
+ mocks.jwtVerify.mockRejectedValue(Object.assign(new Error('Expired token'), { code: 'ERR_JWT_EXPIRED' }));
+ mocks.query.mockResolvedValue({ rows: [{ primary_organization_id: 'org_cookie', joins_valid: true }] });
+ mocks.getProfileByDomain.mockResolvedValue(null);
+ mocks.listAllAgents.mockResolvedValue([]);
+ mocks.getProposalById.mockResolvedValue({ id: '11111111-1111-4111-8111-111111111111', proposed_by_user_id: 'cookie_user' });
+});
+
+describe.each([
+ ['agent listing', 'get', '/api/registry/agents'],
+ ['operator listing', 'get', '/api/registry/operator?domain=example.test'],
+ ['domain claim', 'post', '/api/properties/hosted/example.test/claim'],
+ ['proposal listing', 'get', '/api/registry/mirror-proposals'],
+ ['proposal detail', 'get', '/api/registry/mirror-proposals/11111111-1111-4111-8111-111111111111'],
+ ['proposal submission', 'put', '/api/registry/mirrors/test-platform'],
+] as const)('%s preserves bearer authorization failures', (_name, method, path) => {
+ it.each([
+ ['Authorization', 'Bearer sk_revoked'],
+ ['authorization', 'bearer wrong-static-token'],
+ ['AUTHORIZATION', 'BEARER eyJexpired.payload.sig'],
+ ['aUtHoRiZaTiOn', 'bEaReR\t sk_revoked'],
+ ['Authorization', 'Bearer'],
+ ['Authorization', 'Bearer malformed opaque token'],
+ ['Authorization', 'Bearer eyJmalformed'],
+ ['Authorization', 'Bearer sk_revoked, Bearer sk_other'],
+ ])(
+ 'returns 401 for %s: %s instead of anonymous or cookie authority', async (header, authorization) => {
+ const response = await request(application)[method](path)
+ .set(header, authorization)
+ .set('Cookie', 'wos-session=verified-cookie')
+ .send({});
+ expect(response.status).toBe(401);
+ expect(response.body).toEqual({ error: 'invalid_bearer_token' });
+ expect(mocks.query).not.toHaveBeenCalled();
+ expect(mocks.listAllAgents).not.toHaveBeenCalled();
+ expect(mocks.listProposals).not.toHaveBeenCalled();
+ expect(mocks.submitProposal).not.toHaveBeenCalled();
+ expect(mocks.issueDomainClaim).not.toHaveBeenCalled();
+ },
+ );
+ it.each(['Bearer sk_unavailable', 'Bearer eyJunavailable.payload.sig'])(
+ 'returns 503 for unavailable %s instead of 500 or anonymous access', async (authorization) => {
+ mocks.validateApiKey.mockRejectedValue(new Error('Provider unavailable'));
+ mocks.jwtVerify.mockRejectedValue(new Error('JWKS unavailable'));
+ const response = await request(application)[method](path)
+ .set('Authorization', authorization)
+ .set('Cookie', 'wos-session=verified-cookie')
+ .send({});
+ expect(response.status).toBe(503);
+ expect(response.body).toEqual({ error: 'authorization_unavailable' });
+ expect(mocks.query).not.toHaveBeenCalled();
+ expect(mocks.listAllAgents).not.toHaveBeenCalled();
+ expect(mocks.listProposals).not.toHaveBeenCalled();
+ expect(mocks.submitProposal).not.toHaveBeenCalled();
+ expect(mocks.issueDomainClaim).not.toHaveBeenCalled();
+ },
+ );
+});
+
+it('preserves an API-key provider/request organization conflict as 403', async () => {
+ mocks.validateApiKey.mockResolvedValue({ organizationId: 'org_provider' });
+
+ const response = await request(application)
+ .get('/api/registry/agents?organizationId=org_request')
+ .set('Authorization', 'bEaReR\tsk_live');
+
+ expect(response.status).toBe(403);
+ expect(response.body).toEqual({ error: 'organization_selection_conflict' });
+ expect(mocks.listAllAgents).not.toHaveBeenCalled();
+});
+
+it('does not let spoofed authorization headers replace an invalid selected bearer', async () => {
+ mocks.validateApiKey.mockImplementation(async (req: express.Request) => (
+ req.headers.authorization === 'Bearer sk_other' ? { organizationId: 'org_other' } : null
+ ));
+
+ const response = await request(application)
+ .get('/api/registry/agents')
+ .set('Authorization', 'Bearer invalid-selected-token')
+ .set('X-Authorization', 'Bearer sk_other')
+ .set('X-Forwarded-Authorization', 'Bearer sk_other')
+ .set('Cookie', 'wos-session=verified-cookie');
+
+ expect(response.status).toBe(401);
+ expect(response.body).toEqual({ error: 'invalid_bearer_token' });
+ expect(mocks.query).not.toHaveBeenCalled();
+ expect(mocks.listAllAgents).not.toHaveBeenCalled();
+});
+
+it.each([undefined, 'Basic dXNlcjpwYXNz'])('denies cookie authority without explicit organization provenance: %s', async (authorization) => {
+ mocks.issueDomainClaim.mockResolvedValue({ token: 'claim_cookie', lockedToOrgId: null });
+ const claim = request(application)
+ .post('/api/properties/hosted/example.test/claim')
+ .set('Cookie', 'wos-session=verified-cookie');
+ if (authorization !== undefined) claim.set('Authorization', authorization);
+
+ const response = await claim.send({});
+
+ expect(response.status).toBe(403);
+ expect(mocks.query).not.toHaveBeenCalled();
+ expect(mocks.issueDomainClaim).not.toHaveBeenCalled();
+ expect(mocks.jwtVerify).not.toHaveBeenCalled();
+ expect(mocks.validateApiKey).not.toHaveBeenCalled();
+});
diff --git a/server/tests/unit/certification-api-key-membership-gate.test.ts b/server/tests/unit/certification-api-key-membership-gate.test.ts
index 3dc2cb8b648..ddcff9c1a87 100644
--- a/server/tests/unit/certification-api-key-membership-gate.test.ts
+++ b/server/tests/unit/certification-api-key-membership-gate.test.ts
@@ -72,7 +72,7 @@ describe('certification API-key membership gate', () => {
mocks.createValidation.mockResolvedValue({
apiKey: {
id: 'key_123',
- owner: { id: 'org_free_tier' },
+ owner: { type: 'organization', id: 'org_free_tier' },
name: 'Free tier API key',
permissions: [],
},
diff --git a/server/tests/unit/conformance-token-route.test.ts b/server/tests/unit/conformance-token-route.test.ts
index 54eb8883bf5..77e54b2d4e8 100644
--- a/server/tests/unit/conformance-token-route.test.ts
+++ b/server/tests/unit/conformance-token-route.test.ts
@@ -7,19 +7,33 @@ process.env.CONFORMANCE_JWT_SECRET = 'test-conformance-secret';
vi.mock('../../src/middleware/auth.js', () => ({
requireAuth: (req: any, _res: any, next: any) => {
req.user = { id: 'user_test', email: 'test@test' };
+ req.isStaticAdminApiKey = req.headers['x-test-static-admin'] === 'true';
next();
},
}));
-vi.mock('../../src/routes/helpers/resolve-caller-org.js', () => ({
- resolveCallerOrgId: vi.fn(async (req: any) => req.headers['x-test-org'] ?? null),
-}));
+vi.mock('../../src/routes/helpers/resolve-caller-org.js', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ resolveCallerOrgId: vi.fn(async (req: express.Request) => {
+ const status = Number(req.headers['x-test-auth-error']);
+ if (status === 401 || status === 503) throw new actual.CallerOrganizationAuthError(status);
+ return req.headers['x-test-org'] ?? null;
+ }),
+ };
+});
async function buildApp() {
const { buildConformanceTokenRouter } = await import('../../src/conformance/token-route.js');
const app = express();
app.use(express.json());
app.use('/api/conformance', buildConformanceTokenRouter());
+ // Model the production generic handler, which otherwise flattens 5xx to 500.
+ const errorHandler: express.ErrorRequestHandler = (_error, _req, res, _next) => {
+ res.status(500).json({ error: 'internal_error' });
+ };
+ app.use(errorHandler);
return app;
}
@@ -66,4 +80,24 @@ describe('POST /api/conformance/token', () => {
expect(res.status).toBe(500);
expect(res.body.error).toBe('token_issuance_failed');
});
+
+ it.each([401, 503])('preserves resolver status %s at every conformance credential boundary', async (status) => {
+ const app = await buildApp();
+ for (const [method, path] of [['post', '/token'], ['get', '/_debug'], ['post', '/_debug/run-storyboard']] as const) {
+ const res = await request(app)[method](`/api/conformance${path}`)
+ .set('Authorization', 'Bearer sk_selected').set('Cookie', 'wos-session=otherwise-valid')
+ .set('x-test-auth-error', String(status));
+ expect(res.status, path).toBe(status);
+ expect(res.body.error).toBe(status === 401 ? 'invalid_bearer_token' : 'authorization_unavailable');
+ expect(res.body.token).toBeUndefined();
+ }
+ });
+
+ it('keeps authenticated static-admin dev smoke selection explicit without resolving a tenant', async () => {
+ const app = await buildApp();
+ const res = await request(app).post('/api/conformance/_debug/run-storyboard')
+ .set('x-test-static-admin', 'true').set('x-test-auth-error', '401').send({ org_id: 'org_selected' });
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe('storyboard_id required');
+ });
});
diff --git a/server/tests/unit/db-client-checkout.test.ts b/server/tests/unit/db-client-checkout.test.ts
index b813b903fb6..dc097bc9db9 100644
--- a/server/tests/unit/db-client-checkout.test.ts
+++ b/server/tests/unit/db-client-checkout.test.ts
@@ -311,6 +311,116 @@ describe('db client checkout and health checks', () => {
await db.closeDatabase();
});
+ it('fails an exhausted snapshot pool at the absolute deadline and releases a late client', async () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
+ const pg = mockPg();
+ let deliverClient!: (client: { query: ReturnType; release: ReturnType }) => void;
+ pg.poolConnect.mockReturnValue(new Promise((resolve) => {
+ deliverClient = resolve;
+ }));
+
+ const db = await import('../../src/db/client.js');
+ const snapshots = await import('../../src/db/user-authorization-snapshot-db.js');
+ db.initializeDatabase({ connectionString: 'postgresql://localhost/test' });
+
+ const pending = snapshots.loadAuthorizationSnapshot('user_exhausted_pool', 'org_exhausted_pool');
+ const rejection = expect(pending).rejects.toBeInstanceOf(
+ snapshots.AuthorizationSnapshotUnavailableError,
+ );
+ await vi.advanceTimersByTimeAsync(1_999);
+ expect(pg.poolConnect).toHaveBeenCalledTimes(1);
+ await vi.advanceTimersByTimeAsync(1);
+ await rejection;
+
+ const lateClient = { query: vi.fn(), release: vi.fn() };
+ deliverClient(lateClient);
+ await Promise.resolve();
+ await Promise.resolve();
+ expect(lateClient.query).not.toHaveBeenCalled();
+ expect(lateClient.release).toHaveBeenCalledTimes(1);
+ expect(pg.poolConnect).toHaveBeenCalledTimes(1);
+
+ await db.closeDatabase();
+ });
+
+ it('retries one transient snapshot checkout with only the remaining absolute budget', async () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
+ const pg = mockPg();
+ const release = vi.fn();
+ const query = vi.fn().mockImplementation(async (text: string) => {
+ if (text.includes('pg_catalog.pg_is_in_recovery')) {
+ return {
+ rows: [{
+ in_recovery: false,
+ authenticated_user_id: 'user_retry_checkout',
+ canonical_user_id: 'user_retry_checkout',
+ identity_id: 'fd3043f7-cb4f-43c7-9b81-22ac97576150',
+ authorization_epoch: '11',
+ email: 'sam@pinnacle.example',
+ email_verified: true,
+ first_name: 'Sam',
+ last_name: 'Adeyemi',
+ grant_id: null,
+ grant_organization_id: null,
+ grant_role: null,
+ grant_effective_from: null,
+ grant_effective_until: null,
+ }],
+ };
+ }
+ return { rows: [] };
+ });
+ pg.poolConnect
+ .mockImplementationOnce(async () => {
+ vi.setSystemTime(Date.now() + 700);
+ throw Object.assign(new Error('connection reset'), { code: 'ECONNRESET' });
+ })
+ .mockResolvedValueOnce({ query, release });
+
+ const db = await import('../../src/db/client.js');
+ const snapshots = await import('../../src/db/user-authorization-snapshot-db.js');
+ db.initializeDatabase({ connectionString: 'postgresql://localhost/test' });
+
+ await expect(snapshots.loadAuthorizationSnapshot('user_retry_checkout', 'org_retry_checkout'))
+ .resolves.toMatchObject({
+ authenticatedUserId: 'user_retry_checkout',
+ selectedOrganizationId: 'org_retry_checkout',
+ authorizationEpoch: '11',
+ });
+
+ expect(pg.poolConnect).toHaveBeenCalledTimes(2);
+ expect(query).toHaveBeenCalledWith(
+ "SELECT set_config('statement_timeout', $1, true)",
+ ['1300ms'],
+ );
+ expect(release).toHaveBeenCalledTimes(1);
+
+ await db.closeDatabase();
+ });
+
+ it('does not multiply snapshot retries inside pool checkout', async () => {
+ const pg = mockPg();
+ const unexpectedClient = { query: vi.fn(), release: vi.fn() };
+ pg.poolConnect
+ .mockRejectedValueOnce(Object.assign(new Error('first reset'), { code: 'ECONNRESET' }))
+ .mockRejectedValueOnce(Object.assign(new Error('second reset'), { code: 'ECONNRESET' }))
+ .mockResolvedValueOnce(unexpectedClient);
+
+ const db = await import('../../src/db/client.js');
+ const snapshots = await import('../../src/db/user-authorization-snapshot-db.js');
+ db.initializeDatabase({ connectionString: 'postgresql://localhost/test' });
+
+ await expect(snapshots.loadAuthorizationSnapshot('user_two_attempts', 'org_two_attempts'))
+ .rejects.toBeInstanceOf(snapshots.AuthorizationSnapshotUnavailableError);
+ expect(pg.poolConnect).toHaveBeenCalledTimes(2);
+ expect(unexpectedClient.query).not.toHaveBeenCalled();
+ expect(unexpectedClient.release).not.toHaveBeenCalled();
+
+ await db.closeDatabase();
+ });
+
it('rolls back and releases the client after a timed query fails', async () => {
const pg = mockPg();
const release = vi.fn();
diff --git a/server/tests/unit/member-profile-admin-global-auth.test.ts b/server/tests/unit/member-profile-admin-global-auth.test.ts
index 5edff42ddf3..1904d2f6053 100644
--- a/server/tests/unit/member-profile-admin-global-auth.test.ts
+++ b/server/tests/unit/member-profile-admin-global-auth.test.ts
@@ -67,7 +67,7 @@ function validatedTenantKey(permission: 'admin:*' | 'admin:read') {
return {
apiKey: {
id: `key_${permission}`,
- owner: { id: 'org_tenant' },
+ owner: { type: 'organization', id: 'org_tenant' },
name: 'Tenant admin key',
permissions: [permission],
},
diff --git a/server/tests/unit/network-health-security.test.ts b/server/tests/unit/network-health-security.test.ts
index 94f35fa58c4..2e126be2de7 100644
--- a/server/tests/unit/network-health-security.test.ts
+++ b/server/tests/unit/network-health-security.test.ts
@@ -54,6 +54,7 @@ vi.mock('../../src/db/org-filters.js', () => ({
vi.mock('../../src/db/client.js', async (importOriginal) => ({
...(await importOriginal()),
getPool: () => ({ query: mocks.poolQuery }),
+ queryWithTimeout: mocks.poolQuery,
}));
vi.mock('../../src/addie/mcp/admin-tools.js', () => ({
@@ -112,7 +113,7 @@ function validatedTenantKey(permission: 'admin:*' | 'admin:read') {
return {
apiKey: {
id: `key_${permission}`,
- owner: { id: 'org_tenant' },
+ owner: { type: 'organization', id: 'org_tenant' },
name: 'Tenant admin key',
permissions: [permission],
},
@@ -182,10 +183,27 @@ describe('network-health global authorization boundary', () => {
mocks.checkPlatformBanForApiKey.mockResolvedValue({ banned: false });
mocks.checkPlatformBan.mockResolvedValue({ banned: false });
mocks.isWebUserAAOAdmin.mockResolvedValue(false);
- mocks.poolQuery.mockImplementation((sql: string) => {
- if (sql.includes('FROM users')) {
+ mocks.poolQuery.mockImplementation((sql: string, params: unknown[]) => {
+ if (sql.includes('pg_catalog.pg_is_in_recovery()')) {
+ const userId = params[0] as string;
+ const isAdmin = userId === 'user_platform_admin';
return Promise.resolve({
- rows: [{ first_name: 'Regular', last_name: 'User' }],
+ rows: [{
+ in_recovery: false,
+ authenticated_user_id: userId,
+ canonical_user_id: userId,
+ identity_id: `identity_${userId}`,
+ authorization_epoch: '0',
+ email: isAdmin ? 'platform-admin@example.test' : 'user@example.test',
+ email_verified: true,
+ first_name: isAdmin ? 'Platform' : 'Regular',
+ last_name: isAdmin ? 'Admin' : 'User',
+ grant_id: null,
+ grant_organization_id: null,
+ grant_role: null,
+ grant_effective_from: null,
+ grant_effective_until: null,
+ }],
rowCount: 1,
});
}
diff --git a/server/tests/unit/optional-auth-api-key-membership.test.ts b/server/tests/unit/optional-auth-api-key-membership.test.ts
index f6c7881a560..2f8dd6d6283 100644
--- a/server/tests/unit/optional-auth-api-key-membership.test.ts
+++ b/server/tests/unit/optional-auth-api-key-membership.test.ts
@@ -49,7 +49,7 @@ describe('WorkOS API key membership hydration', () => {
mocks.createValidation.mockResolvedValue({
apiKey: {
id: 'key_123',
- owner: { id: 'org_member' },
+ owner: { type: 'organization', id: 'org_member' },
name: 'Member API key',
permissions: [],
},
@@ -85,7 +85,7 @@ describe('WorkOS API key membership hydration', () => {
mocks.createValidation.mockResolvedValue({
apiKey: {
id: 'key_456',
- owner: { id: 'org_free_tier' },
+ owner: { type: 'organization', id: 'org_free_tier' },
name: 'Free tier API key',
permissions: [],
},
diff --git a/server/tests/unit/portrait-admin-global-auth.test.ts b/server/tests/unit/portrait-admin-global-auth.test.ts
index 834d7bdeecc..afed4813d8e 100644
--- a/server/tests/unit/portrait-admin-global-auth.test.ts
+++ b/server/tests/unit/portrait-admin-global-auth.test.ts
@@ -67,7 +67,7 @@ function validatedTenantKey(permission: 'admin:*' | 'admin:read') {
return {
apiKey: {
id: `key_${permission}`,
- owner: { id: 'org_tenant' },
+ owner: { type: 'organization', id: 'org_tenant' },
name: 'Tenant admin key',
permissions: [permission],
},
diff --git a/server/tests/unit/resolve-caller-org.test.ts b/server/tests/unit/resolve-caller-org.test.ts
index db9fd038938..9f24df7726b 100644
--- a/server/tests/unit/resolve-caller-org.test.ts
+++ b/server/tests/unit/resolve-caller-org.test.ts
@@ -7,7 +7,7 @@
* 1. WorkOS OIDC access token (RS256 JWT, `org_id` claim) — JWKS is picked
* per-token from the `iss` claim, not from a server-wide env var.
* 2. WorkOS API key (sk_* / wos_api_key_*)
- * 3. Sealed session (middleware sets `req.user`)
+ * 3. Sealed session (middleware supplies a fresh authorization snapshot)
*
* Regression guard for the issue where OIDC JWTs silently fell through to
* public-only, leaving `agents: []` for authenticated callers.
@@ -19,12 +19,13 @@ const validateWorkOSApiKeyMock = vi.fn();
const jwtVerifyMock = vi.fn();
const decodeJwtMock = vi.fn();
const dbQueryMock = vi.fn();
-
-vi.mock('../../src/middleware/auth.js', () => ({
+vi.mock('../../src/middleware/auth.js', async () => ({
+ ...await import('../../src/auth/organization-selection.js'),
validateWorkOSApiKey: (...args: unknown[]) => validateWorkOSApiKeyMock(...args),
}));
-vi.mock('jose', () => ({
+vi.mock('jose', async (importOriginal) => ({
+ ...await importOriginal(),
createRemoteJWKSet: () => 'fake-jwks-set',
jwtVerify: (...args: unknown[]) => jwtVerifyMock(...args),
decodeJwt: (...args: unknown[]) => decodeJwtMock(...args),
@@ -39,9 +40,18 @@ const { resolveCallerOrgId, orgIdFromBearerJwt, __resetJwksForTests } = await im
'../../src/routes/helpers/resolve-caller-org.js'
);
-function reqWith(authHeader?: string, user?: { id?: string }) {
+function reqWith(
+ authHeader?: string,
+ user?: { id?: string },
+ selectors: Record = {},
+) {
return {
- headers: authHeader ? { authorization: authHeader } : {},
+ query: {}, body: {}, params: {},
+ ...selectors,
+ headers: {
+ ...(authHeader ? { authorization: authHeader } : {}),
+ ...((selectors.headers as Record | undefined) ?? {}),
+ },
user,
};
}
@@ -73,26 +83,24 @@ describe('resolveCallerOrgId', () => {
expect(dbQueryMock).not.toHaveBeenCalled();
});
- it('falls through to API key / session when JWT verification fails', async () => {
+ it('rejects an invalid JWT without selecting an attached user organization', async () => {
decodeJwtMock.mockReturnValueOnce({ iss: ISS });
- jwtVerifyMock.mockRejectedValueOnce(new Error('bad signature'));
+ jwtVerifyMock.mockRejectedValueOnce(Object.assign(new Error('bad signature'), { code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' }));
validateWorkOSApiKeyMock.mockResolvedValueOnce(null);
- const orgId = await resolveCallerOrgId(reqWith('Bearer eyJabc.def.ghi'));
-
- expect(orgId).toBeNull();
+ await expect(resolveCallerOrgId(reqWith('Bearer eyJabc.def.ghi')))
+ .rejects.toMatchObject({ status: 401 });
expect(jwtVerifyMock).toHaveBeenCalledTimes(1);
expect(validateWorkOSApiKeyMock).toHaveBeenCalledTimes(1);
});
- it('falls through when the JWT has no org_id claim', async () => {
+ it('does not infer an organization when the JWT has no org_id claim', async () => {
decodeJwtMock.mockReturnValueOnce({ iss: ISS });
jwtVerifyMock.mockResolvedValueOnce({ payload: { sub: 'user_123' } });
validateWorkOSApiKeyMock.mockResolvedValueOnce(null);
- const orgId = await resolveCallerOrgId(reqWith('Bearer eyJabc.def.ghi'));
-
- expect(orgId).toBeNull();
+ await expect(resolveCallerOrgId(reqWith('Bearer eyJabc.def.ghi')))
+ .rejects.toMatchObject({ status: 401 });
expect(validateWorkOSApiKeyMock).toHaveBeenCalledTimes(1);
});
@@ -100,9 +108,8 @@ describe('resolveCallerOrgId', () => {
decodeJwtMock.mockReturnValueOnce({ iss: 'https://evil.example.com/issuer' });
validateWorkOSApiKeyMock.mockResolvedValueOnce(null);
- const orgId = await resolveCallerOrgId(reqWith('Bearer eyJabc.def.ghi'));
-
- expect(orgId).toBeNull();
+ await expect(resolveCallerOrgId(reqWith('Bearer eyJabc.def.ghi')))
+ .rejects.toMatchObject({ status: 401 });
expect(jwtVerifyMock).not.toHaveBeenCalled();
});
@@ -110,9 +117,8 @@ describe('resolveCallerOrgId', () => {
decodeJwtMock.mockReturnValueOnce({ sub: 'user_no_iss' });
validateWorkOSApiKeyMock.mockResolvedValueOnce(null);
- const orgId = await resolveCallerOrgId(reqWith('Bearer eyJabc.def.ghi'));
-
- expect(orgId).toBeNull();
+ await expect(resolveCallerOrgId(reqWith('Bearer eyJabc.def.ghi')))
+ .rejects.toMatchObject({ status: 401 });
expect(jwtVerifyMock).not.toHaveBeenCalled();
});
@@ -131,6 +137,26 @@ describe('resolveCallerOrgId', () => {
expect(dbQueryMock).not.toHaveBeenCalled();
});
+ it.each([
+ ['header', { headers: { 'x-organization-id': 'org_other' } }],
+ ['query org', { query: { org: 'org_other' } }],
+ ['query org_id', { query: { org_id: 'org_other' } }],
+ ['query organization_id', { query: { organization_id: 'org_other' } }],
+ ['query organizationId', { query: { organizationId: 'org_other' } }],
+ ['body org_id', { body: { org_id: 'org_other' } }],
+ ['body organization_id', { body: { organization_id: 'org_other' } }],
+ ['body organizationId', { body: { organizationId: 'org_other' } }],
+ ['route org_id', { params: { org_id: 'org_other' } }],
+ ['route orgId', { params: { orgId: 'org_other' } }],
+ ['route organizationId', { params: { organizationId: 'org_other' } }],
+ ] as const)('rejects an API-key provider conflict from %s without caller fallback', async (_location, selectors) => {
+ validateWorkOSApiKeyMock.mockResolvedValueOnce({ organizationId: 'org_from_apikey' });
+
+ await expect(resolveCallerOrgId(reqWith('Bearer sk_live_abc123', { id: 'cookie_user' }, selectors)))
+ .rejects.toMatchObject({ status: 403 });
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ });
+
it('returns org from a legacy wos_api_key_ prefix key', async () => {
validateWorkOSApiKeyMock.mockResolvedValueOnce({ organizationId: 'org_legacy' });
@@ -141,42 +167,210 @@ describe('resolveCallerOrgId', () => {
expect(jwtVerifyMock).not.toHaveBeenCalled();
});
- // ── Sealed-session path (existing behavior) ─────────────────────
+ it.each(['Bearer sk_revoked', 'Bearer wos_api_key_rotated', 'bearer sk_revoked', 'BEARER sk_revoked'])(
+ 'does not fall back to an attached user or implicit org for rejected key %s', async (authorization) => {
+ validateWorkOSApiKeyMock.mockResolvedValueOnce(null);
+ dbQueryMock.mockResolvedValue({ rows: [{ primary_organization_id: 'org_other', joins_valid: true }] });
- it('falls back to users.primary_organization_id when only req.user is set', async () => {
- validateWorkOSApiKeyMock.mockResolvedValueOnce(null);
- // resolvePrimaryOrganization: fast-path read returns the cached column
- // alongside joins_valid, so a dangling pointer can fall through.
- dbQueryMock.mockResolvedValueOnce({ rows: [{ primary_organization_id: 'org_from_session', joins_valid: true }] });
+ await expect(resolveCallerOrgId(reqWith(authorization, { id: 'user_other' }))).rejects.toMatchObject({ status: 401 });
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ },
+ );
+
+ it('propagates unavailable key authorization without a primary-org fallback', async () => {
+ const unavailable = Object.assign(new Error('API key validation unavailable'), { status: 503 });
+ validateWorkOSApiKeyMock.mockRejectedValueOnce(unavailable);
- const orgId = await resolveCallerOrgId(reqWith(undefined, { id: 'user_session' }));
+ await expect(resolveCallerOrgId(reqWith('Bearer sk_unavailable', { id: 'user_other' })))
+ .rejects.toMatchObject({ status: 503 });
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ });
- expect(orgId).toBe('org_from_session');
- // Assert the fast-path SQL — joins_valid checks both organizations and
- // organization_memberships so a dangling pointer falls through.
- expect(dbQueryMock.mock.calls[0][0]).toMatch(/SELECT[\s\S]*primary_organization_id[\s\S]*EXISTS[\s\S]*organizations[\s\S]*EXISTS[\s\S]*organization_memberships[\s\S]*joins_valid[\s\S]*FROM users[\s\S]*workos_user_id\s*=\s*\$1/);
- expect(dbQueryMock.mock.calls[0][1]).toEqual(['user_session']);
+ it.each(['Bearer', 'bearer ', 'BEARER ', 'Bearer wrong-static-token', 'Bearer malformed-opaque-token'])(
+ 'does not select the cookie organization for explicit bearer %j', async (authorization) => {
+ validateWorkOSApiKeyMock.mockResolvedValue(null);
+ dbQueryMock.mockResolvedValue({ rows: [{ primary_organization_id: 'org_other', joins_valid: true }] });
+
+ await expect(resolveCallerOrgId(reqWith(authorization, { id: 'user_other' }))).rejects.toMatchObject({ status: 401 });
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ expect(jwtVerifyMock).not.toHaveBeenCalled();
+ },
+ );
+
+ it.each(['ERR_JWT_EXPIRED', 'ERR_JWT_INVALID', 'ERR_JWT_CLAIM_VALIDATION_FAILED', 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED', 'ERR_JWKS_NO_MATCHING_KEY'])(
+ 'does not select the cookie organization for definitive JWT failure %s', async (code) => {
+ decodeJwtMock.mockReturnValue({ iss: ISS });
+ jwtVerifyMock.mockRejectedValue(Object.assign(new Error('Invalid token'), { code }));
+ validateWorkOSApiKeyMock.mockResolvedValue(null);
+
+ await expect(resolveCallerOrgId(reqWith('bearer eyJinvalid.payload.sig', { id: 'user_other' }))).rejects.toMatchObject({ status: 401 });
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ },
+ );
+
+ it('does not infer the cookie organization when a verified JWT lacks an organization', async () => {
+ decodeJwtMock.mockReturnValue({ iss: ISS });
+ jwtVerifyMock.mockResolvedValue({ payload: { sub: 'user_jwt' } });
+ validateWorkOSApiKeyMock.mockResolvedValue(null);
+
+ await expect(resolveCallerOrgId(reqWith('BEARER eyJnoorg.payload.sig', { id: 'user_other' }))).rejects.toMatchObject({ status: 401 });
+ expect(dbQueryMock).not.toHaveBeenCalled();
});
- it('returns null when session user has no primary org and no memberships', async () => {
- validateWorkOSApiKeyMock.mockResolvedValueOnce(null);
- // Fast-path: no row (column was NULL or user row missing).
- dbQueryMock.mockResolvedValueOnce({ rows: [] });
- // Fallback: resolvePreferredOrganization finds no memberships.
- dbQueryMock.mockResolvedValueOnce({ rows: [] });
+ it.each([
+ new Error('Unknown provider failure'),
+ Object.assign(new Error('JWKS HTTP failure'), { code: 'ERR_JOSE_GENERIC' }),
+ Object.assign(new Error('Malformed JWKS'), { code: 'ERR_JWKS_INVALID' }),
+ Object.assign(new Error('JWKS timeout'), { code: 'ERR_JWKS_TIMEOUT' }),
+ Object.assign(new Error('Connection failed'), { code: 'ECONNRESET' }),
+ Object.assign(new Error('Provider error'), { status: 503 }),
+ ])('propagates unavailable JWT verification without cookie or API-key fallback: %s', async (error) => {
+ decodeJwtMock.mockReturnValue({ iss: ISS });
+ jwtVerifyMock.mockRejectedValue(error);
+
+ await expect(resolveCallerOrgId(reqWith('bEaReR\teyJunavailable.payload.sig', { id: 'user_other' })))
+ .rejects.toMatchObject({ status: 503 });
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ expect(validateWorkOSApiKeyMock).not.toHaveBeenCalled();
+ });
- const orgId = await resolveCallerOrgId(reqWith(undefined, { id: 'user_no_org' }));
+ it('recognizes alternate bearer formatting for valid JWTs without cookie fallback', async () => {
+ decodeJwtMock.mockReturnValue({ iss: ISS });
+ jwtVerifyMock.mockResolvedValue({ payload: { org_id: 'org_selected' } });
- expect(orgId).toBeNull();
+ expect(await resolveCallerOrgId(reqWith('bEaReR\t eyJvalid.payload.sig', { id: 'user_other' }))).toBe('org_selected');
+ expect(dbQueryMock).not.toHaveBeenCalled();
});
- it('swallows DB errors and returns null rather than throwing', async () => {
- validateWorkOSApiKeyMock.mockResolvedValueOnce(null);
- dbQueryMock.mockRejectedValueOnce(new Error('connection reset'));
+ it.each([
+ ['removed', undefined],
+ ['replaced with a valid API key', 'Bearer sk_injected'],
+ ['replaced with a different authentication scheme', 'Basic dXNlcjpwYXNz'],
+ ] as const)('keeps an invalid JWT terminal when Authorization is %s during verification', async (_change, replacement) => {
+ let rejectVerification!: (error: Error) => void;
+ const verification = new Promise((_resolve, reject) => { rejectVerification = reject; });
+ decodeJwtMock.mockReturnValue({ iss: ISS });
+ jwtVerifyMock.mockReturnValueOnce(verification);
+ validateWorkOSApiKeyMock.mockImplementation(async (validationReq: { headers: { authorization?: string } }) => (
+ validationReq.headers.authorization === 'Bearer sk_injected'
+ ? { organizationId: 'org_injected' }
+ : null
+ ));
+ dbQueryMock.mockResolvedValue({ rows: [{ primary_organization_id: 'org_cookie', joins_valid: true }] });
+ const incoming = reqWith('Bearer eyJoriginal.payload.sig', { id: 'user_cookie' });
+
+ const result = resolveCallerOrgId(incoming).then(
+ (organizationId) => ({ organizationId }),
+ (error: { status: number }) => ({ status: error.status }),
+ );
+ expect(jwtVerifyMock).toHaveBeenCalledWith('eyJoriginal.payload.sig', expect.any(Function), expect.any(Object));
+ if (replacement === undefined) delete incoming.headers.authorization;
+ else incoming.headers.authorization = replacement;
+ rejectVerification(Object.assign(new Error('Expired original JWT'), { code: 'ERR_JWT_EXPIRED' }));
+
+ expect(await result).toEqual({ status: 401 });
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ });
- const orgId = await resolveCallerOrgId(reqWith(undefined, { id: 'user_db_err' }));
+ it('keeps unavailable JWT verification terminal after the bearer header is removed', async () => {
+ let rejectVerification!: (error: Error) => void;
+ const verification = new Promise((_resolve, reject) => { rejectVerification = reject; });
+ decodeJwtMock.mockReturnValue({ iss: ISS });
+ jwtVerifyMock.mockReturnValueOnce(verification);
+ const incoming = reqWith('bEaReR\teyJoriginal.payload.sig', { id: 'user_cookie' });
+
+ const result = resolveCallerOrgId(incoming).then(
+ (organizationId) => ({ organizationId }),
+ (error: { status: number }) => ({ status: error.status }),
+ );
+ expect(jwtVerifyMock).toHaveBeenCalledTimes(1);
+ delete incoming.headers.authorization;
+ rejectVerification(new Error('JWKS unavailable'));
- expect(orgId).toBeNull();
+ expect(await result).toEqual({ status: 503 });
+ expect(validateWorkOSApiKeyMock).not.toHaveBeenCalled();
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ ['headers', 'x-organization-id'],
+ ['query', 'organizationId'],
+ ['query', 'org_id'],
+ ['body', 'org_id'],
+ ['params', 'org_id'],
+ ['body', 'organizationId'],
+ ['params', 'organizationId'],
+ ] as const)('retains an original %s selector conflict across JWT verification', async (location, field) => {
+ let finishVerification!: (result: { payload: { org_id: string } }) => void;
+ const verification = new Promise((resolve) => { finishVerification = resolve; });
+ decodeJwtMock.mockReturnValue({ iss: ISS });
+ jwtVerifyMock.mockReturnValueOnce(verification);
+ const incoming = reqWith('Bearer eyJoriginal.payload.sig', { id: 'user_cookie' }, {
+ [location]: { [field]: 'org_conflicting' },
+ });
+
+ const result = resolveCallerOrgId(incoming).then(
+ (organizationId) => ({ organizationId }),
+ (error: { status: number }) => ({ status: error.status }),
+ );
+ expect(jwtVerifyMock).toHaveBeenCalledTimes(1);
+ (incoming[location] as Record)[field] = 'org_verified';
+ finishVerification({ payload: { org_id: 'org_verified' } });
+
+ expect(await result).toEqual({ status: 403 });
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ });
+
+ it('retains a matching organization selector when another middleware replaces it during JWT verification', async () => {
+ let finishVerification!: (result: { payload: { org_id: string } }) => void;
+ const verification = new Promise((resolve) => { finishVerification = resolve; });
+ decodeJwtMock.mockReturnValue({ iss: ISS });
+ jwtVerifyMock.mockReturnValueOnce(verification);
+ const incoming = reqWith('Bearer eyJoriginal.payload.sig', { id: 'user_cookie' }, {
+ query: { organizationId: 'org_verified' },
+ });
+
+ const result = resolveCallerOrgId(incoming);
+ expect(jwtVerifyMock).toHaveBeenCalledTimes(1);
+ incoming.query = { organizationId: 'org_other' };
+ finishVerification({ payload: { org_id: 'org_verified' } });
+
+ expect(await result).toBe('org_verified');
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ [Object.assign(new Error('Expired token'), { code: 'ERR_JWT_EXPIRED' }), 401],
+ [new Error('JWT provider unavailable'), 503],
+ ] as const)('reports credential failure before inspecting conflicting selectors: %s', async (error, status) => {
+ decodeJwtMock.mockReturnValue({ iss: ISS });
+ jwtVerifyMock.mockRejectedValue(error);
+ validateWorkOSApiKeyMock.mockResolvedValue(null);
+
+ await expect(resolveCallerOrgId(reqWith('Bearer eyJfailure.payload.sig', { id: 'user_cookie' }, {
+ query: { organizationId: 'org_conflicting' },
+ }))).rejects.toMatchObject({ status });
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ });
+
+ // ── Sealed-session path (existing behavior) ─────────────────────
+
+ it('does not infer a primary or sole org from a bare authenticated user', async () => {
+ dbQueryMock.mockResolvedValue({ rows: [{ primary_organization_id: 'org_primary', joins_valid: true }] });
+
+ expect(await resolveCallerOrgId(reqWith(undefined, { id: 'user_session' }))).toBeNull();
+ expect(dbQueryMock).not.toHaveBeenCalled();
+ expect(validateWorkOSApiKeyMock).not.toHaveBeenCalled();
+ });
+
+ it('uses verified bearer authority independently of an attached cookie user without a snapshot', async () => {
+ decodeJwtMock.mockReturnValueOnce({ iss: ISS });
+ jwtVerifyMock.mockResolvedValue({ payload: { org_id: 'org_bearer' } });
+
+ expect(await resolveCallerOrgId(reqWith('Bearer eyJabc.def.ghi', { id: 'user_session' }))).toBe('org_bearer');
+ expect(jwtVerifyMock).toHaveBeenCalledTimes(1);
+ expect(validateWorkOSApiKeyMock).not.toHaveBeenCalled();
+ expect(dbQueryMock).not.toHaveBeenCalled();
});
// ── Unauthenticated / malformed ────────────────────────────────
@@ -232,9 +426,9 @@ describe('orgIdFromBearerJwt', () => {
await orgIdFromBearerJwt(reqWith('Bearer eyJa.b.c'));
await orgIdFromBearerJwt(reqWith('Bearer eyJa.b.c'));
- // Same jwks instance passed on repeats of same client, new one for other.
+ // Each verification wraps the cached resolver to preserve key-service failures.
const jwksArgs = jwtVerifyMock.mock.calls.map(c => c[1]);
- expect(jwksArgs[0]).toBe(jwksArgs[1]);
+ expect(jwksArgs.every((resolver) => typeof resolver === 'function')).toBe(true);
// (Both JWKSets are the same fake string from the mock factory, but the
// behavior under test is that we call createRemoteJWKSet once per client.
// We rely on the cache map to dedupe — if it didn't, Map.size would be 2.)
diff --git a/server/tests/unit/resolve-user-org-authorization.test.ts b/server/tests/unit/resolve-user-org-authorization.test.ts
index 8d4aa180609..75912943f00 100644
--- a/server/tests/unit/resolve-user-org-authorization.test.ts
+++ b/server/tests/unit/resolve-user-org-authorization.test.ts
@@ -1,159 +1,197 @@
-import { beforeEach, describe, expect, it, vi } from "vitest";
-import type { WorkOS } from "@workos-inc/node";
-
-const queryMock = vi.hoisted(() => vi.fn());
-
-vi.mock("../../src/db/client.js", () => ({ query: queryMock }));
-
-import {
- evaluateUserOrgRoleAuthorization,
- resolveUserOrgAuthorization,
-} from "../../src/utils/resolve-user-org-authorization.js";
-
-const ORGANIZATION_ID = "org_authorization_test";
-
-function workosWithMemberships(data: unknown[]): WorkOS {
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { WorkOS } from '@workos-inc/node';
+import type { AuthorizationSnapshot } from '../../src/db/user-authorization-snapshot-db.js';
+
+const snapshotMock = vi.hoisted(() => vi.fn());
+vi.mock('../../src/db/user-authorization-snapshot-db.js', async (importOriginal) => ({
+ ...await importOriginal(),
+ loadAuthorizationSnapshot: snapshotMock,
+}));
+import { evaluateUserOrgRoleAuthorization, resolveUserOrgAuthorization } from '../../src/utils/resolve-user-org-authorization.js';
+
+const ORGANIZATION_ID = 'org_pinnacle';
+const AUTHENTICATED_ID = 'user_authenticated';
+function snapshot(overrides: Partial = {}): AuthorizationSnapshot {
+ return {
+ authenticatedUserId: AUTHENTICATED_ID, canonicalUserId: 'user_canonical',
+ identityId: 'identity_linked', selectedOrganizationId: ORGANIZATION_ID, authorizationEpoch: '1',
+ credential: { email: 'sam@example.test', firstName: 'Sam', lastName: 'Adeyemi', emailVerified: true },
+ credentialGrant: null, ...overrides,
+ };
+}
+function grant(role: 'owner' | 'admin' | 'member' = 'member'): NonNullable {
return {
- userManagement: {
- listOrganizationMemberships: vi.fn().mockResolvedValue({ data }),
- },
+ id: 'grant_pinnacle', organizationId: ORGANIZATION_ID, role,
+ effectiveFrom: '2026-01-01T00:00:00.000000Z', effectiveUntil: null,
+ };
+}
+function principal(authorizationSnapshot?: AuthorizationSnapshot) {
+ return { id: 'user_canonical', authWorkosUserId: AUTHENTICATED_ID, authorizationSnapshot };
+}
+function workosWithMemberships(data: unknown[] = []): WorkOS {
+ return {
+ userManagement: { listOrganizationMemberships: vi.fn().mockResolvedValue({ data }) },
} as unknown as WorkOS;
}
+function membership(organizationId = ORGANIZATION_ID, role = 'member', userId = AUTHENTICATED_ID) {
+ return { userId, organizationId, status: 'active', role: { slug: role } };
+}
+const FORBIDDEN = { status: 'forbidden', complete: true, unavailableSources: [] };
+const SNAPSHOT_UNAVAILABLE = { status: 'unavailable', complete: false, unavailableSources: ['authorization_snapshot'] };
-describe("resolveUserOrgAuthorization", () => {
+describe('resolveUserOrgAuthorization', () => {
beforeEach(() => {
- queryMock.mockReset();
- queryMock.mockResolvedValue({ rows: [] });
+ snapshotMock.mockReset();
+ snapshotMock.mockImplementation(async (id: string, org: string | null) => snapshot({
+ authenticatedUserId: id, selectedOrganizationId: org,
+ }));
});
-
- it("uses the exact authenticated credential rather than the canonical identity user", async () => {
- const workos = workosWithMemberships([
- {
- organizationId: ORGANIZATION_ID,
- status: "active",
- role: { slug: "member" },
- },
- ]);
-
- await resolveUserOrgAuthorization(
- workos,
- { id: "user_canonical", authWorkosUserId: "user_authenticated" },
- ORGANIZATION_ID
- );
-
- expect(
- workos.userManagement.listOrganizationMemberships
- ).toHaveBeenCalledWith({
- userId: "user_authenticated",
- organizationId: ORGANIZATION_ID,
+ it.each([
+ ['secondary credential', 'user_canonical', AUTHENTICATED_ID],
+ ['primary credential', AUTHENTICATED_ID, undefined],
+ ])('uses exact %s authority independently of canonical attribution', async (_label, id, authWorkosUserId) => {
+ const workos = workosWithMemberships([membership()]);
+ const result = await resolveUserOrgAuthorization(workos, { id, authWorkosUserId }, ORGANIZATION_ID);
+ expect(result).toMatchObject({ status: 'authorized', membership: { source: 'workos' } });
+ expect(workos.userManagement.listOrganizationMemberships).toHaveBeenCalledExactlyOnceWith({
+ userId: AUTHENTICATED_ID, organizationId: ORGANIZATION_ID,
});
- expect(queryMock).toHaveBeenCalledWith(expect.any(String), [
- "user_authenticated",
- ORGANIZATION_ID,
- ]);
+ expect(snapshotMock).toHaveBeenCalledTimes(2);
+ expect(snapshotMock).toHaveBeenNthCalledWith(1, AUTHENTICATED_ID, ORGANIZATION_ID);
+ expect(snapshotMock).toHaveBeenNthCalledWith(2, AUTHENTICATED_ID, ORGANIZATION_ID);
});
-
- it("returns a definitive denial only when both authority sources are available", async () => {
- const result = await resolveUserOrgAuthorization(
- workosWithMemberships([]),
- { id: "user_without_access" },
- ORGANIZATION_ID
- );
-
- expect(result).toEqual({
- status: "forbidden",
- complete: true,
- unavailableSources: [],
- });
+ it('returns a definitive denial only when both authority sources are available', async () => {
+ expect(await resolveUserOrgAuthorization(workosWithMemberships(), principal(), ORGANIZATION_ID)).toEqual(FORBIDDEN);
});
-
- it("distinguishes an authority-source outage from a denial", async () => {
- queryMock.mockRejectedValue(new Error("database unavailable"));
-
- const result = await resolveUserOrgAuthorization(
- workosWithMemberships([]),
- { id: "user_unknown" },
- ORGANIZATION_ID
- );
-
- expect(result).toEqual({
- status: "unavailable",
- complete: false,
- unavailableSources: ["credential_grant"],
- });
- expect(evaluateUserOrgRoleAuthorization(result)).toEqual({
- status: "unavailable",
- unavailableSources: ["credential_grant"],
- });
+ it.each(['', ' '])('never chooses an implicit organization for missing selection %j', async (org) => {
+ const workos = workosWithMemberships([membership()]);
+ expect(await resolveUserOrgAuthorization(workos, principal(), org)).toEqual(FORBIDDEN);
+ expect(workos.userManagement.listOrganizationMemberships).not.toHaveBeenCalled();
+ expect(snapshotMock).not.toHaveBeenCalled();
});
-
- it("uses an active exact-credential grant and preserves partial role uncertainty", async () => {
- queryMock.mockResolvedValue({
- rows: [{ workos_organization_id: ORGANIZATION_ID, role: "member" }],
+ it.each([
+ ['another linked credential', membership(ORGANIZATION_ID, 'owner', 'user_canonical')],
+ ['another organization', membership('org_streamhaus', 'owner')],
+ ])('does not inherit membership from %s', async (_label, otherMembership) => {
+ const workos = workosWithMemberships([otherMembership]);
+ expect(await resolveUserOrgAuthorization(workos, principal(), ORGANIZATION_ID)).toEqual(FORBIDDEN);
+ expect(workos.userManagement.listOrganizationMemberships).toHaveBeenCalledWith({
+ userId: AUTHENTICATED_ID, organizationId: ORGANIZATION_ID,
});
- const workos = {
- userManagement: {
- listOrganizationMemberships: vi
- .fn()
- .mockRejectedValue(new Error("WorkOS unavailable")),
- },
- } as unknown as WorkOS;
-
+ });
+ it('rejects replaying a selected organization snapshot against another organization', async () => {
+ const workos = workosWithMemberships([membership('org_streamhaus')]);
+ const result = await resolveUserOrgAuthorization(workos, principal(snapshot()), 'org_streamhaus');
+ expect(result).toEqual(FORBIDDEN);
+ expect(workos.userManagement.listOrganizationMemberships).not.toHaveBeenCalled();
+ });
+ it('binds an unselected authenticated snapshot to an explicit organization using fresh authority', async () => {
const result = await resolveUserOrgAuthorization(
- workos,
- { id: "user_granted" },
- ORGANIZATION_ID
+ workosWithMemberships([membership('org_streamhaus')]),
+ principal(snapshot({ selectedOrganizationId: null })), 'org_streamhaus',
);
-
+ expect(result).toMatchObject({ status: 'authorized', membership: { organizationId: 'org_streamhaus', source: 'workos' } });
+ });
+ it('distinguishes snapshot outage from denial without falling back to valid provider membership', async () => {
+ snapshotMock.mockRejectedValue(new Error('database unavailable'));
+ const workos = workosWithMemberships([membership(ORGANIZATION_ID, 'owner')]);
+ const result = await resolveUserOrgAuthorization(workos, principal(), ORGANIZATION_ID);
+ expect(result).toEqual(SNAPSHOT_UNAVAILABLE);
+ expect(evaluateUserOrgRoleAuthorization(result)).toEqual({ status: 'unavailable', unavailableSources: ['authorization_snapshot'] });
+ expect(workos.userManagement.listOrganizationMemberships).not.toHaveBeenCalled();
+ });
+ it('fails unavailable if the post-provider primary revalidation fails', async () => {
+ snapshotMock.mockResolvedValueOnce(snapshot({ credentialGrant: grant('owner') }))
+ .mockRejectedValueOnce(new Error('database unavailable'));
+ const result = await resolveUserOrgAuthorization(workosWithMemberships([membership()]), principal(), ORGANIZATION_ID);
+ expect(result).toEqual(SNAPSHOT_UNAVAILABLE);
+ });
+ it('uses an active exact-credential grant and preserves partial role uncertainty', async () => {
+ snapshotMock.mockResolvedValue(snapshot({ credentialGrant: grant() }));
+ const workos = workosWithMemberships();
+ vi.mocked(workos.userManagement.listOrganizationMemberships).mockRejectedValue(new Error('WorkOS unavailable'));
+ const result = await resolveUserOrgAuthorization(workos, principal(), ORGANIZATION_ID);
expect(result).toMatchObject({
- status: "authorized",
- complete: false,
- unavailableSources: ["workos"],
- membership: { role: "member", source: "credential_grant" },
- });
- expect(evaluateUserOrgRoleAuthorization(result, "member")).toMatchObject({
- status: "authorized",
- membership: { role: "member" },
+ status: 'authorized', complete: false, unavailableSources: ['workos'],
+ membership: { role: 'member', source: 'credential_grant' },
});
- expect(evaluateUserOrgRoleAuthorization(result, "admin")).toEqual({
- status: "unavailable",
- unavailableSources: ["workos"],
+ expect(evaluateUserOrgRoleAuthorization(result, 'member')).toMatchObject({ status: 'authorized' });
+ expect(evaluateUserOrgRoleAuthorization(result, 'admin')).toEqual({ status: 'unavailable', unavailableSources: ['workos'] });
+ });
+ it('returns unavailable without an independent grant when WorkOS cannot answer', async () => {
+ expect(await resolveUserOrgAuthorization(null, principal(), ORGANIZATION_ID)).toEqual({
+ status: 'unavailable', complete: false, unavailableSources: ['workos'],
});
});
-
- it("definitively denies a complete exact membership below the required role", () => {
+ it('definitively denies a complete exact membership below the required role', () => {
expect(evaluateUserOrgRoleAuthorization({
- status: "authorized",
- membership: {
- organizationId: ORGANIZATION_ID,
- role: "member",
- source: "workos",
- },
- complete: true,
- unavailableSources: [],
- }, "admin")).toEqual({ status: "forbidden" });
+ status: 'authorized', complete: true, unavailableSources: [],
+ membership: { organizationId: ORGANIZATION_ID, role: 'member', source: 'workos' },
+ }, 'admin')).toEqual({ status: 'forbidden' });
});
-
- it("chooses the highest role while retaining its authority source", async () => {
- queryMock.mockResolvedValue({
- rows: [{ workos_organization_id: ORGANIZATION_ID, role: "owner" }],
- });
- const result = await resolveUserOrgAuthorization(
- workosWithMemberships([
- {
- organizationId: ORGANIZATION_ID,
- status: "active",
- role: { slug: "admin" },
- },
- ]),
- { id: "user_with_two_sources" },
- ORGANIZATION_ID
- );
-
- expect(result).toMatchObject({
- status: "authorized",
- complete: true,
- membership: { role: "owner", source: "credential_grant" },
+ it('chooses the highest role while retaining its authority source', async () => {
+ snapshotMock.mockResolvedValue(snapshot({ credentialGrant: grant('owner') }));
+ const result = await resolveUserOrgAuthorization(workosWithMemberships([membership(ORGANIZATION_ID, 'admin')]), principal(), ORGANIZATION_ID);
+ expect(result).toMatchObject({ status: 'authorized', complete: true, membership: { role: 'owner', source: 'credential_grant' } });
+ });
+ it.each([
+ ['epoch advanced', { authorizationEpoch: '2' }],
+ ['epoch moved backwards', { authorizationEpoch: '0' }],
+ ['canonical identity changed', { canonicalUserId: 'user_new_primary' }],
+ ['binding changed without a matching epoch', { identityId: 'identity_new' }],
+ ['credential changed', { authenticatedUserId: 'user_other' }],
+ ] as const)('rejects replayed request state when %s', async (_case, change) => {
+ const workos = workosWithMemberships([membership()]);
+ snapshotMock.mockResolvedValue(snapshot(change));
+ expect(await resolveUserOrgAuthorization(workos, principal(snapshot()), ORGANIZATION_ID)).toEqual(FORBIDDEN);
+ expect(workos.userManagement.listOrganizationMemberships).not.toHaveBeenCalled();
+ });
+ it.each([
+ ['revoked', null], ['replaced', { ...grant('owner'), id: 'grant_replacement' }],
+ ])('rejects a %s grant in prior request state even when the epoch is unchanged', async (_label, currentGrant) => {
+ snapshotMock.mockResolvedValue(snapshot({ credentialGrant: currentGrant }));
+ const workos = workosWithMemberships([membership(ORGANIZATION_ID, 'owner')]);
+ const result = await resolveUserOrgAuthorization(workos, principal(snapshot({ credentialGrant: grant('owner') })), ORGANIZATION_ID);
+ expect(result).toEqual(FORBIDDEN);
+ expect(workos.userManagement.listOrganizationMemberships).not.toHaveBeenCalled();
+ });
+ it.each([
+ ['identity binding', { canonicalUserId: 'user_new_primary', identityId: 'identity_new', authorizationEpoch: '2' }],
+ ['epoch alone', { authorizationEpoch: '2' }],
+ ['grant revoked', { credentialGrant: null }],
+ ['grant role', { credentialGrant: grant('member') }],
+ ['grant replaced', { credentialGrant: { ...grant('owner'), id: 'grant_replacement' } }],
+ ['grant end date', { credentialGrant: { ...grant('owner'), effectiveUntil: '2026-09-13T00:00:00.000000Z' } }],
+ ['selected organization', { selectedOrganizationId: 'org_streamhaus' }],
+ ] as const)('rejects a %s change while a provider decision is in flight', async (_case, change) => {
+ const before = snapshot({ credentialGrant: grant('owner') });
+ let current = before;
+ snapshotMock.mockImplementation(async () => current);
+ let finishProvider!: (value: { data: ReturnType[] }) => void;
+ let providerStarted!: () => void;
+ const started = new Promise((resolve) => { providerStarted = resolve; });
+ const workos = workosWithMemberships();
+ vi.mocked(workos.userManagement.listOrganizationMemberships).mockImplementation(() => {
+ providerStarted();
+ return new Promise((resolve) => { finishProvider = resolve as typeof finishProvider; });
});
+ const pending = resolveUserOrgAuthorization(workos, principal(before), ORGANIZATION_ID);
+ await started;
+ current = snapshot({ ...before, ...change });
+ finishProvider({ data: [membership(ORGANIZATION_ID, 'owner')] });
+ expect(await pending).toEqual(FORBIDDEN);
+ expect(snapshotMock).toHaveBeenCalledTimes(2);
+ });
+ it('rejects a credential deleted during provider authorization', async () => {
+ snapshotMock.mockResolvedValueOnce(snapshot()).mockResolvedValueOnce(null);
+ expect(await resolveUserOrgAuthorization(workosWithMemberships([membership()]), principal(), ORGANIZATION_ID)).toEqual(FORBIDDEN);
+ });
+ it('checks persisted epoch each turn and rejects replayed prior context', async () => {
+ const before = snapshot();
+ const workos = workosWithMemberships([membership()]);
+ expect(await resolveUserOrgAuthorization(workos, principal(before), ORGANIZATION_ID)).toMatchObject({ status: 'authorized' });
+ snapshotMock.mockResolvedValue(snapshot({ authorizationEpoch: '2' }));
+ expect(await resolveUserOrgAuthorization(workos, principal(before), ORGANIZATION_ID)).toEqual(FORBIDDEN);
+ expect(workos.userManagement.listOrganizationMemberships).toHaveBeenCalledTimes(1);
});
});
diff --git a/server/tests/unit/sealed-organization-boundary.test.ts b/server/tests/unit/sealed-organization-boundary.test.ts
new file mode 100644
index 00000000000..9a938680b20
--- /dev/null
+++ b/server/tests/unit/sealed-organization-boundary.test.ts
@@ -0,0 +1,402 @@
+/** Mounted consumers with the real snapshot and organization resolvers. */
+import express from 'express';
+import request from 'supertest';
+import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { WorkOSUser } from '../../src/types.js';
+
+const mocks = vi.hoisted(() => ({
+ principal: undefined as WorkOSUser | undefined,
+ boundedQuery: vi.fn(),
+ memberships: vi.fn(),
+ validateApiKey: vi.fn(),
+ listProposals: vi.fn(),
+ getProposal: vi.fn(),
+ getClient: vi.fn(),
+ isAdmin: vi.fn(),
+ isModerator: vi.fn(),
+}));
+
+vi.mock('../../src/middleware/auth.js', () => ({
+ requireAuth: (req: express.Request, _res: express.Response, next: express.NextFunction) => {
+ req.user = mocks.principal;
+ next();
+ },
+ validateWorkOSApiKey: mocks.validateApiKey,
+}));
+vi.mock('../../src/db/client.js', async importOriginal => ({
+ ...await importOriginal(),
+ queryWithTimeout: mocks.boundedQuery,
+ getClient: mocks.getClient,
+}));
+vi.mock('../../src/auth/workos-client.js', () => ({
+ getAuthorizationEnforcementWorkos: () => ({ userManagement: { listOrganizationMemberships: mocks.memberships } }),
+}));
+vi.mock('../../src/db/community-mirror-db.js', () => ({
+ CommunityMirrorDatabase: class {
+ listProposals = mocks.listProposals;
+ getProposalById = mocks.getProposal;
+ },
+}));
+vi.mock('../../src/db/publisher-db.js', () => ({ PublisherDatabase: class {} }));
+vi.mock('../../src/addie/admin-status-lookup.js', () => ({ isWebUserAAOAdmin: mocks.isAdmin }));
+vi.mock('../../src/services/brand-logo-auth.js', () => ({ isRegistryModerator: mocks.isModerator }));
+vi.mock('../../src/notifications/registry.js', () => ({}));
+vi.mock('../../src/middleware/rate-limit.js', () => {
+ const pass = (_req: express.Request, _res: express.Response, next: express.NextFunction) => next();
+ return { registryReadRateLimiter: pass, brandCreationRateLimiter: pass };
+});
+
+import { buildConformanceTokenRouter } from '../../src/conformance/token-route.js';
+import { createCommunityMirrorRouter } from '../../src/routes/community-mirrors.js';
+import { verifyConformanceToken } from '../../src/conformance/token.js';
+import { loadAuthorizationSnapshot } from '../../src/db/user-authorization-snapshot-db.js';
+import { resolveCallerOrgId } from '../../src/routes/helpers/resolve-caller-org.js';
+
+const ORG = 'org_pinnacle';
+const OTHER_ORG = 'org_streamhaus';
+const PROPOSAL_ID = '00000000-0000-4000-8000-000000000001';
+const originalSecret = process.env.CONFORMANCE_JWT_SECRET;
+let row: Record;
+
+function app() {
+ const instance = express();
+ instance.use(express.json());
+ instance.use('/api/conformance', buildConformanceTokenRouter());
+ return instance;
+}
+
+function mirrorApp(attachedOrganizationId?: string) {
+ const instance = express();
+ instance.use(express.json());
+ instance.use('/api/registry', createCommunityMirrorRouter({
+ requireAuth: (req, _res, next) => {
+ req.user = mocks.principal;
+ if (attachedOrganizationId) Object.assign(req, { apiKey: { organizationId: attachedOrganizationId } });
+ next();
+ },
+ }));
+ return instance;
+}
+
+async function authenticate(authenticatedUserId = 'user_primary', canonicalUserId = 'user_linked', organizationId: string | null = ORG) {
+ row.authenticated_user_id = authenticatedUserId;
+ row.canonical_user_id = canonicalUserId;
+ const snapshot = await loadAuthorizationSnapshot(authenticatedUserId, organizationId);
+ mocks.principal = {
+ id: canonicalUserId,
+ authWorkosUserId: authenticatedUserId,
+ email: 'sam@pinnacle.example',
+ };
+ Object.defineProperty(mocks.principal, 'authorizationSnapshot', { value: snapshot });
+ mocks.boundedQuery.mockClear();
+}
+
+function post() {
+ return request(app()).post('/api/conformance/token').set('Cookie', 'wos-session=sealed-session-fixture');
+}
+
+beforeEach(() => {
+ vi.resetAllMocks();
+ process.env.CONFORMANCE_JWT_SECRET = 'sealed-organization-test-secret';
+ mocks.principal = undefined;
+ row = {
+ in_recovery: false,
+ authenticated_user_id: 'user_primary', canonical_user_id: 'user_linked', identity_id: 'identity_linked',
+ authorization_epoch: '7', email: 'sam@pinnacle.example', email_verified: true,
+ first_name: 'Sam', last_name: 'Adeyemi',
+ grant_id: null, grant_organization_id: null, grant_role: null,
+ grant_effective_from: null, grant_effective_until: null,
+ };
+ mocks.boundedQuery.mockImplementation(async () => ({ rows: [{ ...row }], rowCount: 1 }));
+ mocks.memberships.mockImplementation(async ({ userId, organizationId }) => ({
+ data: [{ id: 'membership_pinnacle', userId, organizationId, status: 'active', role: { slug: 'member' } }],
+ }));
+ mocks.validateApiKey.mockResolvedValue(null);
+ mocks.listProposals.mockResolvedValue({ proposals: [{ id: PROPOSAL_ID }], total: 1 });
+ mocks.getProposal.mockImplementation(async () => ({
+ id: PROPOSAL_ID, proposed_by_user_id: mocks.principal?.id,
+ proposed_by_organization_id: ORG, adagents_json: { private_proposal: true },
+ }));
+ // A canonical admin/moderator result must never rescue denied exact authority.
+ mocks.isAdmin.mockResolvedValue(true);
+ mocks.isModerator.mockResolvedValue(true);
+});
+
+afterAll(() => {
+ if (originalSecret === undefined) delete process.env.CONFORMANCE_JWT_SECRET;
+ else process.env.CONFORMANCE_JWT_SECRET = originalSecret;
+});
+
+describe.each([
+ ['user_primary', 'user_linked'],
+ ['user_linked', 'user_primary'],
+])('explicit Bearer with authenticated %s attributed to %s', (authenticated, canonical) => {
+ it.each([
+ 'Bearer',
+ 'Bearer malformed opaque token',
+ 'Bearer sealed-session-fixture',
+ 'bEaReR sealed-session-fixture',
+ 'bEaReR\t sealed-session-fixture',
+ ])('rejects %s before using valid cookie authority in mounted consumers', async authorization => {
+ await authenticate(authenticated, canonical);
+ const mirror = mirrorApp();
+ const responses = [
+ await post().set('Authorization', authorization).set('X-Organization-Id', ORG),
+ await request(mirror).get('/api/registry/mirror-proposals')
+ .set('Cookie', 'wos-session=sealed-session-fixture')
+ .set('Authorization', authorization).set('X-Organization-Id', ORG),
+ await request(mirror).get(`/api/registry/mirror-proposals/${PROPOSAL_ID}`)
+ .set('Cookie', 'wos-session=sealed-session-fixture')
+ .set('Authorization', authorization).set('X-Organization-Id', ORG),
+ ];
+ for (const response of responses) {
+ expect(response.status).toBe(401);
+ expect(response.body).toEqual({ error: 'invalid_bearer_token' });
+ }
+ expect(mocks.boundedQuery).not.toHaveBeenCalled();
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ expect(mocks.listProposals).not.toHaveBeenCalled();
+ expect(mocks.getProposal).not.toHaveBeenCalled();
+ expect(mocks.getClient).not.toHaveBeenCalled();
+ expect(mocks.isAdmin).not.toHaveBeenCalled();
+ expect(mocks.isModerator).not.toHaveBeenCalled();
+ });
+});
+
+describe.each([
+ ['user_primary', 'user_linked'],
+ ['user_linked', 'user_primary'],
+])('community mirror proposals with authenticated %s attributed to %s', (authenticated, canonical) => {
+ it.each(['no selection', 'no membership', 'canonical membership', 'conflict', 'stale epoch', 'unavailable'])(
+ 'denies list and detail for %s without canonical-user fallback', async denied => {
+ await authenticate(authenticated, canonical, denied === 'no selection' ? null : ORG);
+ if (denied === 'no membership') mocks.memberships.mockResolvedValue({ data: [] });
+ if (denied === 'canonical membership') mocks.memberships.mockResolvedValue({
+ data: [{ userId: canonical, organizationId: ORG, status: 'active', role: { slug: 'owner' } }],
+ });
+ if (denied === 'stale epoch') row.authorization_epoch = '8';
+ if (denied === 'unavailable') mocks.boundedQuery.mockRejectedValue(new Error('database unavailable'));
+ for (const path of ['/mirror-proposals', `/mirror-proposals/${PROPOSAL_ID}`]) {
+ const pending = request(mirrorApp()).get(`/api/registry${path}`);
+ if (denied === 'conflict') pending.set('X-Organization-Id', OTHER_ORG);
+ const response = await pending;
+ expect(response.status).toBe(404);
+ expect(response.body).toEqual({ error: 'Community mirror proposal not found' });
+ }
+ expect(mocks.listProposals).not.toHaveBeenCalled();
+ expect(mocks.getProposal).not.toHaveBeenCalled();
+ expect(mocks.isAdmin).not.toHaveBeenCalled();
+ expect(mocks.isModerator).not.toHaveBeenCalled();
+ },
+ );
+
+ it('scopes authorized list/detail to the selected org and denies other-org or unscoped rows', async () => {
+ await authenticate(authenticated, canonical);
+ const instance = mirrorApp();
+ expect((await request(instance).get('/api/registry/mirror-proposals')).status).toBe(200);
+ expect(mocks.listProposals).toHaveBeenCalledWith(expect.objectContaining({ proposedByOrganizationId: ORG }));
+ expect(mocks.listProposals.mock.calls[0][0].proposedByUserId).toBeUndefined();
+ expect((await request(instance).get(`/api/registry/mirror-proposals/${PROPOSAL_ID}`)).status).toBe(200);
+ for (const organizationId of [OTHER_ORG, null]) {
+ mocks.getProposal.mockResolvedValue({
+ id: PROPOSAL_ID, proposed_by_user_id: canonical, proposed_by_organization_id: organizationId,
+ });
+ expect((await request(instance).get(`/api/registry/mirror-proposals/${PROPOSAL_ID}`)).status).toBe(404);
+ }
+ expect(mocks.memberships.mock.calls.every(([input]) => input.userId === authenticated)).toBe(true);
+ });
+
+ it('rejects list/detail replay after revocation before reading proposals', async () => {
+ await authenticate(authenticated, canonical);
+ const instance = mirrorApp();
+ for (const path of ['/mirror-proposals', `/mirror-proposals/${PROPOSAL_ID}`]) {
+ expect((await request(instance).get(`/api/registry${path}`)).status).toBe(200);
+ }
+ row.authorization_epoch = '8';
+ mocks.listProposals.mockClear();
+ mocks.getProposal.mockClear();
+ for (const path of ['/mirror-proposals', `/mirror-proposals/${PROPOSAL_ID}`]) {
+ expect((await request(instance).get(`/api/registry${path}`)).status).toBe(404);
+ }
+ expect(mocks.listProposals).not.toHaveBeenCalled();
+ expect(mocks.getProposal).not.toHaveBeenCalled();
+ });
+
+ it.each(['no selection', 'stale epoch', 'conflict'])('does not rescue %s with an attached API-key organization', async denied => {
+ await authenticate(authenticated, canonical, denied === 'no selection' ? null : ORG);
+ if (denied === 'stale epoch') row.authorization_epoch = '8';
+ for (const path of ['/mirror-proposals', `/mirror-proposals/${PROPOSAL_ID}`]) {
+ const pending = request(mirrorApp(ORG)).get(`/api/registry${path}`);
+ if (denied === 'conflict') pending.set('X-Organization-Id', OTHER_ORG);
+ expect((await pending).status).toBe(404);
+ }
+ expect(mocks.listProposals).not.toHaveBeenCalled();
+ expect(mocks.getProposal).not.toHaveBeenCalled();
+ });
+
+ it('denies human manager fallback for queue, approve, reject, publish and retire', async () => {
+ await authenticate(authenticated, canonical, null);
+ const instance = mirrorApp();
+ expect((await request(instance).get('/api/registry/mirror-proposals?review_queue=true')).status).toBe(403);
+ for (const action of ['approve', 'reject']) {
+ expect((await request(instance).post(`/api/registry/mirror-proposals/${PROPOSAL_ID}/${action}`).send({})).status).toBe(403);
+ }
+ expect((await request(instance).put('/api/registry/mirrors/example').send({})).status).toBe(403);
+ expect((await request(instance).delete('/api/registry/mirrors/example')).status).toBe(403);
+ expect(mocks.getClient).not.toHaveBeenCalled();
+ expect(mocks.isAdmin).not.toHaveBeenCalled();
+ expect(mocks.isModerator).not.toHaveBeenCalled();
+ });
+});
+
+describe('sealed-session organization boundary on POST /api/conformance/token', () => {
+ it.each([
+ ['user_primary', 'user_linked'],
+ ['user_linked', 'user_primary'],
+ ])('uses the exact authenticated %s credential when person attribution is %s', async (authenticated, canonical) => {
+ await authenticate(authenticated, canonical);
+ const response = await post();
+ expect(response.status).toBe(200);
+ expect(verifyConformanceToken(response.body.token).sub).toBe(ORG);
+ expect(mocks.memberships).toHaveBeenCalledExactlyOnceWith({ userId: authenticated, organizationId: ORG });
+ expect(mocks.boundedQuery.mock.calls.every(([, parameters]) => parameters[0] === authenticated)).toBe(true);
+ });
+
+ it.each([
+ ['user_primary', 'user_linked'],
+ ['user_linked', 'user_primary'],
+ ])('does not copy %s authority into authenticated %s', async (canonical, authenticated) => {
+ await authenticate(authenticated, canonical);
+ mocks.memberships.mockResolvedValue({
+ data: [{ userId: canonical, organizationId: ORG, status: 'active', role: { slug: 'owner' } }],
+ });
+ const response = await post();
+ expect(response.status).toBe(403);
+ expect(response.body.token).toBeUndefined();
+ expect(mocks.memberships).toHaveBeenCalledExactlyOnceWith({ userId: authenticated, organizationId: ORG });
+ });
+
+ it('requires explicit organization context even when the credential has a sole membership', async () => {
+ await authenticate('user_primary', 'user_linked', null);
+ const response = await post();
+ expect(response.status).toBe(403);
+ expect(response.body.token).toBeUndefined();
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it('accepts an explicit request selection when authentication had no selected organization', async () => {
+ await authenticate('user_primary', 'user_linked', null);
+ const response = await post().send({ organization_id: ORG });
+ expect(response.status).toBe(200);
+ expect(verifyConformanceToken(response.body.token).sub).toBe(ORG);
+ });
+
+ it.each(['absent', 'unavailable'])('preserves an exact credential grant when WorkOS membership is %s', async state => {
+ Object.assign(row, {
+ grant_id: 'grant_exact', grant_organization_id: ORG, grant_role: 'member',
+ grant_effective_from: '2026-01-01T00:00:00.000000Z',
+ });
+ await authenticate('user_linked', 'user_primary');
+ if (state === 'absent') mocks.memberships.mockResolvedValue({ data: [] });
+ else mocks.memberships.mockRejectedValue(new Error('provider unavailable'));
+ const response = await post();
+ expect(response.status).toBe(200);
+ expect(verifyConformanceToken(response.body.token).sub).toBe(ORG);
+ expect(mocks.boundedQuery.mock.calls.every(([, parameters]) => parameters[0] === 'user_linked')).toBe(true);
+ });
+
+ it.each(['id', 'authWorkosUserId'] as const)('rejects a snapshot transplanted to a different principal %s', async field => {
+ await authenticate();
+ mocks.principal![field] = 'user_other';
+ const response = await post();
+ expect(response.status).toBe(403);
+ expect(response.body.token).toBeUndefined();
+ expect(mocks.boundedQuery).not.toHaveBeenCalled();
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it('checks path selectors populated after authentication middleware', async () => {
+ await authenticate();
+ const instance = express();
+ // Authentication commonly runs before Express has matched a tenant route.
+ instance.use((req, _res, next) => { req.user = mocks.principal; next(); });
+ instance.get('/organizations/:orgId/check', async (req, res) => {
+ const organizationId = await resolveCallerOrgId(req);
+ res.status(organizationId ? 200 : 403).json({ organizationId });
+ });
+ expect((await request(instance).get(`/organizations/${ORG}/check`)).status).toBe(200);
+ mocks.memberships.mockClear();
+ const conflict = await request(instance).get(`/organizations/${OTHER_ORG}/check`);
+ expect(conflict.status).toBe(403);
+ expect(conflict.body.organizationId).toBeNull();
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it('rejects a canonical user object without authenticated snapshot provenance', async () => {
+ mocks.principal = { id: 'user_linked', email: 'sam@pinnacle.example' };
+ const response = await post().send({ organization_id: ORG });
+ expect(response.status).toBe(403);
+ expect(response.body.token).toBeUndefined();
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ expect(mocks.boundedQuery).not.toHaveBeenCalled();
+ });
+
+ it.each(['header', 'query', 'query organizationId', 'query org_id', 'body', 'org_id alias'])('rejects a conflicting %s selector before provider authorization', async location => {
+ await authenticate();
+ const pending = post();
+ if (location === 'header') pending.set('X-Organization-Id', OTHER_ORG);
+ if (location === 'query') pending.query({ organization_id: OTHER_ORG });
+ if (location === 'query organizationId') pending.query({ organizationId: OTHER_ORG });
+ if (location === 'query org_id') pending.query({ org_id: OTHER_ORG });
+ if (location === 'body') pending.send({ organizationId: OTHER_ORG });
+ if (location === 'org_id alias') pending.send({ org_id: OTHER_ORG });
+ const response = await pending;
+ expect(response.status).toBe(403);
+ expect(response.body.token).toBeUndefined();
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it('accepts matching selectors across header, query, and body', async () => {
+ await authenticate();
+ const response = await post().set('X-Organization-Id', ORG).query({ org: ORG }).send({ organization_id: ORG });
+ expect(response.status).toBe(200);
+ expect(verifyConformanceToken(response.body.token).sub).toBe(ORG);
+ });
+
+ it.each(['epoch', 'identity', 'credential grant'])('rejects replay of a request snapshot after its %s changes', async changed => {
+ if (changed === 'credential grant') {
+ Object.assign(row, { grant_id: 'grant_original', grant_organization_id: ORG, grant_role: 'member', grant_effective_from: '2026-01-01T00:00:00.000000Z' });
+ }
+ await authenticate();
+ expect((await post()).status).toBe(200);
+ if (changed === 'epoch') row.authorization_epoch = '8';
+ if (changed === 'identity') row.canonical_user_id = 'user_rebound';
+ if (changed === 'credential grant') row.grant_id = 'grant_replacement';
+ mocks.memberships.mockClear();
+ const response = await post();
+ expect(response.status).toBe(403);
+ expect(response.body.token).toBeUndefined();
+ expect(mocks.memberships).not.toHaveBeenCalled();
+ });
+
+ it('rejects a concurrent epoch change while WorkOS membership is in flight', async () => {
+ await authenticate();
+ mocks.memberships.mockImplementation(async ({ userId, organizationId }) => {
+ row.authorization_epoch = '8';
+ return { data: [{ userId, organizationId, status: 'active', role: { slug: 'member' } }] };
+ });
+ const response = await post();
+ expect(response.status).toBe(403);
+ expect(response.body.token).toBeUndefined();
+ });
+
+ it.each(['database', 'provider'])('fails closed without issuing a token during a %s outage', async source => {
+ await authenticate();
+ if (source === 'database') mocks.boundedQuery.mockRejectedValue(new Error('private database details'));
+ else mocks.memberships.mockRejectedValue(new Error('private provider details'));
+ const response = await post();
+ expect(response.status).toBe(403);
+ expect(response.body.token).toBeUndefined();
+ expect(response.text).not.toContain('private');
+ });
+});
diff --git a/server/tests/unit/user-authorization-snapshot-timeout.test.ts b/server/tests/unit/user-authorization-snapshot-timeout.test.ts
new file mode 100644
index 00000000000..aa295be55f0
--- /dev/null
+++ b/server/tests/unit/user-authorization-snapshot-timeout.test.ts
@@ -0,0 +1,126 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { queryWithTimeout } from '../../src/db/client.js';
+import {
+ AuthorizationSnapshotUnavailableError,
+ loadAuthorizationSnapshot,
+} from '../../src/db/user-authorization-snapshot-db.js';
+
+vi.mock('../../src/db/client.js', async importOriginal => ({
+ ...await importOriginal(),
+ queryWithTimeout: vi.fn(),
+}));
+
+const boundedQuery = vi.mocked(queryWithTimeout);
+const USER_ID = 'user_snapshot_timeout';
+const ORGANIZATION_ID = 'org_snapshot_timeout';
+
+function queryResult(overrides: Record = {}) {
+ return {
+ rows: [{
+ in_recovery: false,
+ authenticated_user_id: USER_ID,
+ canonical_user_id: USER_ID,
+ identity_id: 'fd3043f7-cb4f-43c7-9b81-22ac97576150',
+ authorization_epoch: '7',
+ email: 'sam@pinnacle.example',
+ email_verified: true,
+ first_name: 'Sam',
+ last_name: 'Adeyemi',
+ grant_id: null,
+ grant_organization_id: null,
+ grant_role: null,
+ grant_effective_from: null,
+ grant_effective_until: null,
+ ...overrides,
+ }],
+ rowCount: 1,
+ command: 'SELECT',
+ oid: 0,
+ fields: [],
+ };
+}
+
+describe('authorization snapshot query deadline and connection retry', () => {
+ beforeEach(() => {
+ boundedQuery.mockReset();
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('retries one transient connection failure with only the remaining absolute budget', async () => {
+ boundedQuery.mockImplementationOnce(async () => {
+ vi.setSystemTime(Date.now() + 700);
+ throw Object.assign(new Error('connection reset'), { code: 'ECONNRESET' });
+ }).mockResolvedValueOnce(queryResult());
+
+ await expect(loadAuthorizationSnapshot(USER_ID, ORGANIZATION_ID)).resolves.toMatchObject({
+ authenticatedUserId: USER_ID,
+ selectedOrganizationId: ORGANIZATION_ID,
+ authorizationEpoch: '7',
+ });
+ expect(boundedQuery).toHaveBeenCalledTimes(2);
+ const [first, second] = boundedQuery.mock.calls;
+ expect(first[2]).toBe(2_000);
+ expect(first[3]).toEqual({ retryTransientCheckout: false });
+ expect(second).toEqual([first[0], first[1], 1_300, { retryTransientCheckout: false }]);
+ expect(first[1]).toEqual([USER_ID, ORGANIZATION_ID]);
+ });
+
+ it('stops after two connection failures and permits a clean request after recovery', async () => {
+ boundedQuery.mockRejectedValue(Object.assign(new Error('private connection details'), { code: 'EPIPE' }));
+ await expect(loadAuthorizationSnapshot(USER_ID, ORGANIZATION_ID))
+ .rejects.toThrow(new AuthorizationSnapshotUnavailableError());
+ expect(boundedQuery).toHaveBeenCalledTimes(2);
+
+ boundedQuery.mockResolvedValueOnce(queryResult());
+ await expect(loadAuthorizationSnapshot(USER_ID, ORGANIZATION_ID)).resolves.toMatchObject({
+ authenticatedUserId: USER_ID, authorizationEpoch: '7',
+ });
+ expect(boundedQuery).toHaveBeenCalledTimes(3);
+ });
+
+ it('does not start a retry after the first attempt exhausts the deadline', async () => {
+ boundedQuery.mockImplementationOnce(async () => {
+ vi.setSystemTime(Date.now() + 2_000);
+ throw Object.assign(new Error('connection reset'), { code: 'ECONNRESET' });
+ });
+ await expect(loadAuthorizationSnapshot(USER_ID, ORGANIZATION_ID))
+ .rejects.toBeInstanceOf(AuthorizationSnapshotUnavailableError);
+ expect(boundedQuery).toHaveBeenCalledTimes(1);
+ });
+
+ it('rejects a result delivered after the absolute deadline without retrying', async () => {
+ boundedQuery.mockImplementationOnce(async () => {
+ vi.setSystemTime(Date.now() + 2_001);
+ return queryResult();
+ });
+ await expect(loadAuthorizationSnapshot(USER_ID, ORGANIZATION_ID))
+ .rejects.toBeInstanceOf(AuthorizationSnapshotUnavailableError);
+ expect(boundedQuery).toHaveBeenCalledTimes(1);
+ });
+
+ it.each([
+ { code: '57014', message: 'canceling statement due to statement timeout' },
+ { code: '55P03', message: 'canceling statement due to lock timeout' },
+ { code: '42P01', message: 'relation does not exist' },
+ ])('does not retry a non-connection database error ($code)', async ({ code, message }) => {
+ boundedQuery.mockRejectedValue(Object.assign(new Error(message), { code }));
+ await expect(loadAuthorizationSnapshot(USER_ID, ORGANIZATION_ID))
+ .rejects.toThrow(new AuthorizationSnapshotUnavailableError());
+ expect(boundedQuery).toHaveBeenCalledTimes(1);
+ });
+
+ it.each([
+ { state: 'replica', row: { in_recovery: true } },
+ { state: 'missing primary identity', row: { canonical_user_id: null } },
+ ])('does not retry an unavailable $state snapshot', async ({ row }) => {
+ boundedQuery.mockResolvedValue(queryResult(row));
+ await expect(loadAuthorizationSnapshot(USER_ID, ORGANIZATION_ID))
+ .rejects.toBeInstanceOf(AuthorizationSnapshotUnavailableError);
+ expect(boundedQuery).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/server/tests/unit/working-group-admin-global-auth.test.ts b/server/tests/unit/working-group-admin-global-auth.test.ts
index 7c1623375bc..9034f526f9c 100644
--- a/server/tests/unit/working-group-admin-global-auth.test.ts
+++ b/server/tests/unit/working-group-admin-global-auth.test.ts
@@ -56,6 +56,7 @@ vi.mock('../../src/db/org-filters.js', () => ({
vi.mock('../../src/db/client.js', async (importOriginal) => ({
...(await importOriginal()),
getPool: () => ({ query: mocks.poolQuery }),
+ queryWithTimeout: mocks.poolQuery,
}));
vi.mock('../../src/addie/mcp/admin-tools.js', async () => {
@@ -108,7 +109,7 @@ function validatedTenantKey(permission: 'admin:*' | 'admin:read') {
return {
apiKey: {
id: `key_${permission}`,
- owner: { id: 'org_tenant' },
+ owner: { type: 'organization', id: 'org_tenant' },
name: 'Tenant admin key',
permissions: [permission],
},
@@ -136,9 +137,17 @@ describe('working-group real global-admin boundary', () => {
);
mocks.isMember.mockResolvedValue(true);
mocks.poolQuery.mockImplementation((sql: string) => {
- if (sql.includes('FROM users')) {
+ if (sql.includes('pg_catalog.pg_is_in_recovery()')) {
return Promise.resolve({
- rows: [{ first_name: 'SSO', last_name: 'Admin' }],
+ rows: [{
+ in_recovery: false,
+ authenticated_user_id: 'user_sso_admin',
+ canonical_user_id: 'user_sso_admin',
+ identity_id: 'identity_sso_admin',
+ authorization_epoch: '0',
+ email: 'sso-admin@example.test', email_verified: true,
+ first_name: 'SSO', last_name: 'Admin', grant_id: null,
+ }],
rowCount: 1,
});
}
diff --git a/server/tests/unit/workos-jwt.test.ts b/server/tests/unit/workos-jwt.test.ts
index db2dea87903..a4118b9fe8b 100644
--- a/server/tests/unit/workos-jwt.test.ts
+++ b/server/tests/unit/workos-jwt.test.ts
@@ -1,9 +1,10 @@
-import { describe, it, expect, beforeAll, afterAll } from 'vitest';
-import { SignJWT, generateKeyPair, exportJWK, type KeyLike } from 'jose';
+import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
+import { SignJWT, generateKeyPair, exportJWK, createLocalJWKSet, errors, type KeyLike } from 'jose';
import {
looksLikeJWT,
verifyWorkOSJWT,
__setJWKSForTesting,
+ isInvalidWorkOSJWTError,
} from '../../src/auth/workos-jwt.js';
// WORKOS_CLIENT_ID must be set before workos-jwt.ts is imported for the
@@ -55,6 +56,9 @@ describe('verifyWorkOSJWT', () => {
const kp = await generateKeyPair('RS256');
privateKey = kp.privateKey;
publicKey = kp.publicKey;
+ });
+
+ beforeEach(async () => {
const jwk = await exportJWK(publicKey);
// Swap in a static-key resolver that matches any `alg: RS256` kid.
__setJWKSForTesting(async () => ({ ...jwk, alg: 'RS256' }));
@@ -104,7 +108,7 @@ describe('verifyWorkOSJWT', () => {
sub: 'user_01ABC',
azp: 'client_01DIFFERENT_APP',
});
- await expect(verifyWorkOSJWT(token)).rejects.toThrow(/application id/);
+ await expect(verifyWorkOSJWT(token)).rejects.toMatchObject({ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: expect.stringMatching(/application id/) });
});
it('rejects a token whose client_id does not match WORKOS_CLIENT_ID', async () => {
@@ -112,26 +116,72 @@ describe('verifyWorkOSJWT', () => {
sub: 'user_01ABC',
client_id: 'client_01DIFFERENT_APP',
});
- await expect(verifyWorkOSJWT(token)).rejects.toThrow(/application id/);
+ await expect(verifyWorkOSJWT(token)).rejects.toMatchObject({ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: expect.stringMatching(/application id/) });
});
it('rejects a token with no azp and no client_id', async () => {
const token = await mint({ sub: 'user_01ABC' });
- await expect(verifyWorkOSJWT(token)).rejects.toThrow(/application id/);
+ await expect(verifyWorkOSJWT(token)).rejects.toMatchObject({ code: 'ERR_JWT_CLAIM_VALIDATION_FAILED', message: expect.stringMatching(/application id/) });
});
it('rejects an expired token', async () => {
const token = await mint({ sub: 'user_01ABC', azp: EXPECTED_AZP }, '-1s');
- await expect(verifyWorkOSJWT(token)).rejects.toThrow();
+ await expect(verifyWorkOSJWT(token)).rejects.toMatchObject({ code: 'ERR_JWT_EXPIRED' });
});
it('rejects a token signed by an unknown key', async () => {
const other = await generateKeyPair('RS256');
+ const jwk = await exportJWK(publicKey);
+ __setJWKSForTesting(createLocalJWKSet({ keys: [{ ...jwk, alg: 'RS256', kid: 'known' }] }));
const token = await new SignJWT({ sub: 'user_01ABC', azp: EXPECTED_AZP })
- .setProtectedHeader({ alg: 'RS256' })
+ .setProtectedHeader({ alg: 'RS256', kid: 'unknown' })
.setExpirationTime('5m')
.sign(other.privateKey);
- await expect(verifyWorkOSJWT(token)).rejects.toThrow();
+ const error = await verifyWorkOSJWT(token).catch((error: unknown) => error);
+ expect(error).toMatchObject({ code: 'ERR_JWKS_NO_MATCHING_KEY' });
+ expect(isInvalidWorkOSJWTError(error)).toBe(true);
+ });
+
+ it('separates malformed provider key material from an invalid token', async () => {
+ const token = await mint({ sub: 'user_01ABC', azp: EXPECTED_AZP });
+ const jwk = await exportJWK(publicKey);
+ __setJWKSForTesting(createLocalJWKSet({ keys: [{ ...jwk, alg: 'RS256' }] }));
+ await expect(verifyWorkOSJWT(token)).resolves.toMatchObject({ sub: 'user_01ABC' });
+
+ // An RSA key without its required modulus cannot be imported by Web Crypto.
+ __setJWKSForTesting(createLocalJWKSet({ keys: [{ kty: 'RSA', e: jwk.e, alg: 'RS256' }] }));
+ await expect(verifyWorkOSJWT(token)).rejects.toMatchObject({ status: 503 });
+ });
+
+ it('treats unsupported provider key resolver errors as unavailable', async () => {
+ const token = await mint({ sub: 'user_01ABC', azp: EXPECTED_AZP });
+ __setJWKSForTesting(async () => {
+ throw new errors.JOSENotSupported('Unsupported provider key material');
+ });
+
+ const error = await verifyWorkOSJWT(token).catch((error: unknown) => error);
+
+ expect(error).toMatchObject({ status: 503 });
+ expect(isInvalidWorkOSJWTError(error)).toBe(false);
+ });
+
+ it('still identifies unsupported token headers as invalid before key lookup', async () => {
+ const token = await mint({ sub: 'user_01ABC', azp: EXPECTED_AZP });
+ const parts = token.split('.');
+ parts[0] = Buffer.from(JSON.stringify({ alg: 'RS256', crit: ['unknown'], unknown: true })).toString('base64url');
+ const error = await verifyWorkOSJWT(parts.join('.')).catch((error: unknown) => error);
+ expect(error).toMatchObject({ code: 'ERR_JOSE_NOT_SUPPORTED' });
+ expect(isInvalidWorkOSJWTError(error)).toBe(true);
+ });
+
+ it.each(['HS256', 'unknown', 'none'])('rejects token algorithm %s before calling the key service', async (alg) => {
+ __setJWKSForTesting(async () => { throw new Error('Key service unavailable'); });
+ const token = await mint({ sub: 'user_01ABC', azp: EXPECTED_AZP });
+ const parts = token.split('.');
+ parts[0] = Buffer.from(JSON.stringify({ alg })).toString('base64url');
+ const error = await verifyWorkOSJWT(parts.join('.')).catch((error: unknown) => error);
+ expect(error).toMatchObject({ code: 'ERR_JOSE_ALG_NOT_ALLOWED' });
+ expect(isInvalidWorkOSJWTError(error)).toBe(true);
});
it('flags M2M tokens (client_credentials)', async () => {