feat(mcp-server): identity mapping to business scope (MCP Prompt 09)#4010
feat(mcp-server): identity mapping to business scope (MCP Prompt 09)#4010gilgardosh wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces identity mapping and tenant scoping to the MCP server package, mapping verified tokens to an internal user and business membership context (McpAuthContext) to enforce tenant isolation. The changes include parsing memberships from token claims, narrowing read scopes, and integrating this context resolution into the request handler. Feedback is provided to improve the robustness of the coerceMembership function by explicitly excluding arrays and refining roleId type coercion.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function coerceMembership(entry: unknown): BusinessMembership | null { | ||
| if (!entry || typeof entry !== 'object') { | ||
| return null; | ||
| } | ||
| const record = entry as Record<string, unknown>; | ||
| const businessId = record.businessId ?? record.business_id; | ||
| if (typeof businessId !== 'string' || businessId.length === 0) { | ||
| return null; | ||
| } | ||
| const roleId = record.roleId ?? record.role_id ?? ''; | ||
| return { businessId, roleId: typeof roleId === 'string' ? roleId : String(roleId) }; | ||
| } |
There was a problem hiding this comment.
To improve robustness when parsing untrusted token claims, we should explicitly exclude arrays (since typeof [] === 'object') and avoid converting arbitrary objects or arrays to strings for roleId (only allow string or number, otherwise default to an empty string).
function coerceMembership(entry: unknown): BusinessMembership | null {
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
return null;
}
const record = entry as Record<string, unknown>;
const businessId = record.businessId ?? record.business_id;
if (typeof businessId !== 'string' || businessId.length === 0) {
return null;
}
const rawRoleId = record.roleId ?? record.role_id;
const roleId =
typeof rawRoleId === 'string'
? rawRoleId
: typeof rawRoleId === 'number'
? String(rawRoleId)
: '';
return { businessId, roleId };
}8956f31 to
e5706a6
Compare
…t (Prompt 09) Build an internal auth context from a verified token, mirroring the server's tenant-isolation model (docs/mcp/spec.md §7). - src/auth/identity.ts: McpAuthContext (userId, roles, memberships, defaultReadScope); readScopeFromMemberships + narrowReadScope + resolveRequestedReadScope (subset-only narrowing, reject out-of-scope); pluggable MembershipSource defaulting to a `memberships` token claim; IdentityMappingError for an unresolvable user; per-request context store - handler: resolve and store the auth context after token verification - unit tests for multi-business mapping, default/narrowed scope, claim parsing, and the missing-subject failure mode Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo
Address review: exclude arrays from object membership entries and only accept a string/number roleId (never stringify arbitrary objects/arrays). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPMszz9gdZFhNjobRm6Ndo
743418f to
9080929
Compare
Summary
Implements Prompt 09 – Identity mapping to business scope (see
docs/mcp/spec.md§7). Maps a verified tokento an internal user + business-membership context, mirroring the server's
tenant-isolation model.
Changes
src/auth/identity.tsMcpAuthContext—userId,auth0UserId,email,roles(token scopes),memberships,defaultReadScope, and the sourceprincipal.packages/server/.../auth-scope.ts:readScopeFromMemberships(default = all memberships),narrowReadScope(subset-only; returnsnullif any requested id is outside memberships), andresolveRequestedReadScope(no request ⇒ default; otherwise the narrowed subset ornull).MembershipSource— pluggable; defaultmembershipsFromClaimsreads the token'smembershipscustom claim (camelCase or snake_case entries, de-duplicated). A later step can swap in a GraphQL-backed source.IdentityMappingError— the clear failure mode when a verified token has no subject. An empty membership set is a valid user with no access (authorization decides what that permits).setAuthContext/getAuthContext).src/mcp/handler.ts— after token verification, resolve and store the auth context on the request.README.md— Identity & tenant scope section.Validation
yarn workspace @accounter/mcp-server test→ 112 passedyarn workspace @accounter/mcp-server lint/typecheck/build→ passNotes / open decisions
@accounter/server, which is a graphql-modules/Postgres app rather than a library. I mirrored the shapes and rules (BusinessMembership,AuthorizedReadScope,narrowReadScopesemantics) here and flagged extracting a shared@accounter/authpackage as a follow-up.membershipstoken claim (Auth0 configured to emit it) and keeps the source pluggable. This keeps the step self-contained and lets Prompt 11/12 wire a DB/GraphQL-backed resolver without another refactor.AUTHORIZATION_ERROR) is Prompt 11.🤖 Generated with Claude Code
Generated by Claude Code