Multi-Tenant `singleFlight` Concurrency Bypass & OAuth Token Invalidation
Author: codeCraft-RitikCreated Sep 21, 2026Updated Sep 21, 2026
Labelsbug
Steps to reproduce
1. Multi-Tenant singleFlight Concurrency Bypass & OAuth Token Invalidation
️ Overview
- Severity: CRITICAL (Data Loss / Connection Revocation)
- Components Affected:
Expected Behavior
Root Cause Analysis
In Corsair, token refresh deduplication is managed by singleFlight() in core/auth/single-flight.ts:
// packages/corsair/core/auth/single-flight.ts
const flightsByStore = new WeakMap<object, Map<string, Promise<unknown>>>();
export function singleFlight<T>(
store: object,
key: string,
run: () => Promise<T>,
): Promise<T> {
let flights = flightsByStore.get(store);
if (!flights) {
flights = new Map();
flightsByStore.set(store, flights);
}
const existing = flights.get(key) as Promise<T> | undefined;
if (existing !== undefined) return existing;
const pending = run().finally(() => {
flights.delete(key);
});
flights.set(key, pending);
return pending;
}In core/auth/oauth-access.ts, singleFlight is invoked passing ctx.keys as the store argument:
// packages/corsair/core/auth/oauth-access.ts:85-87
const runRefresh = (force: boolean): Promise<string> =>
singleFlight(ctx.keys, flightKey, async () => { ... });However, in multi-tenant mode (multiTenancy: true), every call to corsair.withTenant(tenantId) generates a brand-new client instance via buildCorsairClient():
// packages/corsair/core/index.ts:117-138
if (config.multiTenancy) {
const tenantWrapper = Object.assign(
{
withTenant: (tenantId: string) => {
const client = buildCorsairClient(config.plugins, {
database: resolvedDatabase,
tenantId,
...
});
return client;
},
...
}
);
return tenantWrapper;
}Inside buildCorsairClient(), a new accountKeyManager object is instantiated on every single call:
// packages/corsair/core/client/index.ts:470-479
accountKeyManager = createAccountKeyManager({
authType: pluginOptions.authType,
integrationName: plugin.id,
tenantId: effectiveTenantId,
kek,
database,
extraAccountFields,
ensureProvisioned,
});
apiUnsafe[plugin.id]!.keys = accountKeyManager;Because WeakMap compares keys by reference identity (===), every invocation of corsair.withTenant('tenant-123') receives a distinct object reference for ctx.keys.
Corsair version
Release Version: [email protected] (declared in packages/corsair/package.json )
Environment details
Runtime: Node.js (v18, v20, v22, v24) or Bun.
Operating System: OS-independent (Linux, macOS, Windows).
Execution Context:
Multi-tenant backend servers (Next.js App Router route handlers, Express, Fastify, Hono, NestJS).
Serverless or containerized environments receiving parallel HTTP requests for the same tenant.
AI agent workflows where an agent invokes multiple asynchronous tools in parallel (Promise.all([...]) or subagents).
Storage Layer: Any persistent database supported by Corsair (PostgreSQL, SQLite, MySQL via Kysely) storing OAuth tokens on corsair_accountsWhat part of Corsair is affected?
Other
Configuration (if relevant)
Anything else
No response
Source: corsairdev/corsair