A single API key seems convenient—until two people use it.
The intent-brain system, now rebranded as the teamkb plugin v0.4.0, powers a team knowledge base where developers query institutional memory directly from their coding environment. Previously, a shared TEAMKB_API_KEY handled authentication, but that approach fails as soon as a second teammate joins. Without individual identities, audit logs can’t pinpoint who made a request. Revoking access for one user forces a full team-wide key rotation, creating unnecessary disruption. These aren’t just edge-case bugs—they’re fundamental design flaws that demand structural fixes.
The solution introduces three critical changes: per-user tokens for identity, a server-side write gate for authorization, and a separate access logging system for auditing. At its core, the API itself becomes the security boundary—not the client-side tooling in tools like Claude Code. The MCP server acts as a user-friendly interface, not a security gate.
Identity Over Shared Keys: Assigning Per-User Tokens
The new authentication system replaces the single shared key with individual tokens stored in apps/api/src/auth/token-registry.ts. Each token maps to a record containing { actor, role }, where role is either 'admin' or 'member'. This change immediately resolves the two major flaws of shared keys:
- Every request now carries a unique
actoridentifier, making audit trails meaningful. - Revoking one user’s access only requires deleting their token record, not rotating keys for the entire team.
Tokens originate from multiple sources in specific precedence order:
- Explicit records defined in code
- A
TEAMKB_TOKENSJSON environment variable - A
TEAMKB_TOKENS_FILE(defaulting to~/.teamkb/tokens.json) - The legacy
TEAMKB_API_KEY, converted to a single admin token with actor"shared"for backward compatibility
Each token is a bearer token resolved to an identity during request processing. Malformed entries are skipped to prevent crashes. Tokens without a defined role default to member—enforcing the principle of least privilege. During development, if the registry is empty, every request runs under actor 'dev'. In production environments, an empty registry triggers a fail-closed state, blocking all access until tokens are properly configured.
The InMemoryTokenRegistry class implements secure token resolution with two deliberate safeguards:
export class InMemoryTokenRegistry implements TokenRegistry {
private readonly records: ReadonlyArray<TokenRecord>;
constructor(records: readonly TokenRecord[]) {
this.records = records;
}
isEmpty(): boolean {
return this.records.length === 0;
}
resolve(token: string): TokenIdentity | undefined {
let found: TokenIdentity | undefined;
for (const rec of this.records) {
// Do not early-return on match — compare against all records so the
// response time is independent of which (if any) token matched.
if (timingSafeStrEq(rec.token, token)) {
found = { actor: rec.actor, role: rec.role };
}
}
return found;
}
}The resolver uses constant-time comparison (timingSafeStrEq) to prevent timing attacks that could expose token contents. Crucially, it doesn’t exit early upon finding a match—it checks every record to ensure response times remain consistent regardless of which token matches. This prevents attackers from inferring token existence or position based on request timing.
Authorization Through Write Gates: Restricting Dangerous Actions
The apps/api/src/middleware/write-gate.ts file enforces role-based restrictions on write operations. While members can read and propose changes, only administrators can promote memories, edit policies, or perform bulk imports. This is implemented as a Fastify onRequest hook that executes after authentication:
const ADMIN_WRITE_PREFIXES = ['/api/memories', '/api/policies', '/api/import'];
const MUTATION_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
export function registerWriteGate(app: FastifyInstance): void {
app.addHook('onRequest', async (request, reply) => {
if (!MUTATION_METHODS.has(request.method)) return;
const path = request.url.split('?')[0] ?? request.url;
const isAdminWrite = ADMIN_WRITE_PREFIXES.some(
(prefix) => path === prefix || path.startsWith(`${prefix}/`),
);
if (!isAdminWrite) return;
if (request.role !== 'admin') {
reply.status(403);
throw new Error('This action requires an admin token. Members may read and propose; promoting, policy edits, and imports are admin-only.');
}
});
}The gate evaluates both the HTTP method (mutating vs. non-mutating) and the path prefix. Non-admin routes remain unrestricted, but any mutating operation targeting /api/memories, /api/policies, or /api/import—including sub-paths—must pass the admin role check. This enforcement happens at the API boundary, ensuring consistent authorization regardless of how requests are initiated.
Separating Audit Logs from Governance Trails
The MCP server’s client-side tools handle user experience, not security. Read operations like teamkb_search, teamkb_status, and teamkb_neighbors remain available to all authenticated users. Write tools, however, only register for admin installations, determined by the canWrite flag which mirrors the server’s authorization logic:
const canWrite = options.canWrite ?? config.role === 'admin';
// Read tools — always registered (members + admins)
server.tool('teamkb_search', /* ...cited retrieval... */This separation ensures the MCP client remains a usability layer while security enforcement stays centralized in the API. The per-read access log maintains a distinct trail from governance audit records, preventing overlap while ensuring comprehensive accountability.
Building Scalable, Maintainable Security for MCP Systems
The shift from shared keys to per-user tokens, coupled with strict server-side authorization, transforms how teams secure knowledge-sharing systems. Identity becomes explicit, revocation becomes surgical, and audit trails become actionable. The MCP server excels at delivering seamless user experiences, but the real security boundary lies in the API layer—where every request must prove its authorization before proceeding. As teams grow and tooling evolves, this architecture ensures that security scales alongside functionality, not as an afterthought.
AI summary
Ekip bilgi tabanlarınızı güvenli hale getirmek için kullanıcı özel tokenleri, sunucu tarafı yazma koruması ve ayrıntılı erişim kayıtları kullanın. API katmanı gerçek güvenlik sınırıdır.