Files
curriculum-project-hub/.scratch/saas-production-readiness/assets/product-surface-feishu-primary-sources.md
T

21 KiB

Feishu multi-app identity and event-routing primary sources

This note answers one narrow design question for the production SaaS audit: what identity and routing boundaries apply when every Hub Organization uses its own Feishu App and bot. External claims below use only first-party Feishu Open Platform documentation. Statements labelled Derived requirement are Hub design implications, not guarantees made by Feishu.

Research date: 2026-07-10.

Executive answer

  1. A customer-owned enterprise custom app is a single-tenant app: Feishu says it is developed, reviewed, published, and used inside one tenant and cannot be used by another organization. That fits “one Organization, one Feishu App,” but it means the Hub is operating many independent app security and identity domains, not one shared Feishu identity domain. (custom apps versus store apps)
  2. A bare open_id is not a SaaS-global user key. It identifies a user inside one app, and the same user has different Open IDs in different apps. user_id is tenant-scoped; it is stable across apps in one tenant but differs across tenants. union_id crosses apps only for apps from the same application provider/developer; it differs between providers. (user resource overview, official ID parameter definitions, Union ID guide)
  3. OAuth must be routed to the Organization's app before authorization. The authorization request contains that app's App ID as client_id; the callback contains code and the echoed state, not an independently trustworthy Organization choice. Exchanging the code requires the matching app's client_id and client_secret, and the same redirect_uri. (obtain authorization code, obtain user access token)
  4. A version 2 event or callback carries header.app_id and header.tenant_key after parsing/decryption. An encrypted HTTP delivery, however, exposes only an encrypt envelope before decryption, so a shared endpoint cannot safely choose among many apps' Encrypt Keys from the body. (callback structure, encrypted Webhook delivery)
  5. A WebSocket/long-connection client is authenticated with one app's App ID and App Secret. Therefore its local client instance is the pre-authenticated source-app route. It is not a process-global “all customer apps” listener. (long-connection event subscription)
  6. Feishu documents chat_id as the conversation identifier, but the cited documentation does not promise that a raw chat_id is globally unique across every tenant and app. Chat APIs explicitly reject an operator and chat from different tenants and often require the app bot to be in the chat. A bot is represented by its application's app_id. (common parameters, get chat members, message resource)

1. Application and tenant boundary

Official facts

  • app_id is the immutable identifier generated by Feishu for an application. tenant_key is the enterprise identifier; one tenant_key corresponds to one enterprise in actual use. (common parameters)
  • An enterprise custom app is restricted to one tenant. Its developers, administrators, and users belong to that tenant, and another organization cannot use it. A store app is the contrasting model that can be installed by multiple tenants. (custom apps versus store apps)
  • Access tokens tell Feishu which app/user is calling and which tenant's data is being accessed. A tenant_access_token calls as the application and its data range is determined by that application's permissions. (access-token overview)
  • A custom app obtains its tenant_access_token using that app's app_id and app_secret. (custom-app tenant access token)

Derived requirements

  • Model a durable FeishuAppConnection (or equivalent) owned by exactly one Hub Organization. At minimum it binds the internal connection ID, organization_id, app_id, expected tenant_key, encrypted app_secret, event/callback credentials, lifecycle state, and credential version.
  • Do not select Feishu credentials from a process-global default. Every OAuth exchange, token cache entry, OpenAPI call, Webhook verification, and long-connection client must start from the Organization's connection.
  • Pin the first verified tenant_key to the connection. Later login, event, and callback data must match both the expected app_id and pinned tenant_key; unknown or mismatched pairs must be rejected rather than auto-attached to another Organization.

2. User identifier scopes

Official scope matrix

Identifier Feishu's documented scope Cross-app/tenant behavior Availability caveat
open_id User identity inside one application The same user has a different open_id in different apps The normal app-relative user identifier
user_id User identity inside one tenant Stable across all apps in the same tenant; different in tenant A and tenant B Reading it is a sensitive field permission and is limited to custom apps in the cited user API
union_id User identity across apps from the same application provider/developer Same across that provider's apps; different across different providers It does not make unrelated customer-owned apps one identity domain
tenant_key Enterprise identity One key corresponds to one enterprise Returned by login and carried in events/callbacks

Sources:

  • Feishu's current user resource overview states that user_id is tenant identity, open_id is app identity, and union_id is identity across apps developed by the same application provider. It expressly says the same user's Open ID differs between apps. (user resource overview)
  • Feishu's API parameter text is more explicit: the same user's User ID differs between tenants but remains the same across apps within one tenant; the same user's Union ID is the same under one developer and different under another. (official ID parameter definitions)
  • Feishu describes Union ID as the identity linking one user across multiple applications supplied by the same application service provider. (Union ID guide)
  • Feishu's common-error guidance says not to use an Open ID obtained from app A in app B, and even a test app and its production app produce different Open IDs for the same user. It also reports cross-tenant errors for both User ID and Union ID use. (common errors)
  • The current login user-info API returns open_id, union_id, optional sensitive user_id, and tenant_key from a user_access_token. (get login user information)

Answer: can different customer Apps share one global open_id key?

No. This directly contradicts Feishu's documented Open ID semantics. Two customer-owned custom apps are different applications, so the same person has different Open IDs in them. Because those apps are independently owned custom apps, the Hub also has no documented basis to assume they are applications of one application provider and therefore no basis to auto-merge on union_id. user_id cannot solve cross-customer identity either because it changes across tenants.

Derived identity model:

  • The safe external login key is (feishu_app_connection_id, open_id); retain tenant_key, user_id, and union_id as scoped attributes, not as an unqualified SaaS-global primary key.
  • If the product needs one Hub human account to participate in multiple Organizations, represent each Feishu login as a separate external identity attached through an explicit, separately verified account-linking flow. Do not silently merge on Open ID, Union ID, email, phone, or display name.
  • Do not make user_id mandatory for login unless every customer app is required to obtain its sensitive field permission. open_id and tenant_key are returned by the user-info flow without using user_id as the login key.
  • Replacing an Organization's Feishu App changes the application identity domain and therefore its users' Open IDs. Treat app replacement as an explicit connection migration/re-link operation, not an in-place secret edit.

3. OAuth login must be app-routed

Official facts

  • The authorization URL requires client_id, which is the selected application's App ID. redirect_uri must already appear in that application's security settings. On success Feishu redirects with code and, when supplied, the original state. (obtain authorization code)
  • Feishu says state maintains request/callback context and must be compared to prevent CSRF. Its official sample rejects a callback whose state differs from the stored state. (obtain authorization code, user-access-token sample)
  • The code lasts five minutes and can be used once. Token exchange requires client_id, client_secret, code, and, when used during authorization, the same redirect_uri. Feishu defines error 20024 for a code/refresh token that does not match the supplied client ID and error 20071 for a mismatched redirect URI. (obtain user access token)
  • Only redirect URLs configured for the selected application pass Feishu's security check; an app may configure multiple URLs. (access-token acquisition flow)

Derived request flow

  1. Begin at an Organization-qualified route or invitation, resolve exactly one active FeishuAppConnection, and create a short-lived, one-time server-side login transaction bound to that Organization, connection, redirect URI, desired return path, and PKCE verifier if used.
  2. Put only a high-entropy opaque transaction nonce in OAuth state; generate the authorization URL with that connection's app_id as client_id.
  3. On callback, atomically consume and validate state before selecting credentials. The callback's untrusted query parameters must never be allowed to choose another Organization or App.
  4. Exchange code with the connection's app_id, decrypted app_secret, and the exact stored redirect URI. Then call user-info and require its tenant_key to equal the connection's pinned tenant.
  5. Resolve or create the membership through (feishu_app_connection_id, open_id). Never search all Organizations for a bare Open ID and accept the first match.

A single physical callback URL may be configured independently in multiple apps, but a shared URL does not remove the need for state-bound app routing.

4. HTTP event and callback routing

Official facts

  • A version 2 callback header contains the application's Verification Token, event_type, tenant_key (the callback application's tenant), and app_id (the callback application's App ID). (callback overview and structure)
  • Version 2 event examples likewise place app_id and tenant_key in the event header. (user-leaves-chat event)
  • Verification Token is an application validation identifier. Feishu says the server can compare the event's token with that application's token to check that a request is for the specified app. With an Encrypt Key, the request signature is calculated from request timestamp, nonce, that app's Encrypt Key, and the original request body. (receive and verify events)
  • When an Encrypt Key is configured, the HTTP request body visible before decryption is an envelope containing only {"encrypt":"..."}; app_id and tenant_key are inside the ciphertext. (send events to developer server)

Derived routing and verification requirements

  • Give every Feishu connection a stable, unguessable callback route such as a connection-specific path token. Resolve the connection from that route, load exactly its Encrypt Key/Verification Token, verify and decrypt, then require the authenticated payload's app_id and tenant_key to match the route's connection.
  • For an unencrypted common endpoint, header.app_id may identify a candidate connection, but it is untrusted until that connection's signature/token has been verified. Never route the side effect solely from an unverified body.
  • A single encrypted URL shared by every customer app has no documented pre-decryption app selector. Trying every tenant's secret is not an identity protocol. Use connection-specific routing instead.
  • Namespace delivery deduplication by the authenticated connection (for example (connection_id, event_id)), and dispatch business work only after app/tenant verification. Unknown, disabled, or mismatched connections fail closed and produce an auditable security log.

5. Long-connection routing

Official facts

  • Feishu's SDK creates a WebSocket client with one app's required APP_ID and APP_SECRET. Events subscribed by that application are then delivered over that channel. Authentication is performed when the connection is established. (long-connection event subscription)
  • Long-connection event subscription supports enterprise custom apps, not store apps. Each app may establish at most 50 connections. If one app has multiple clients, delivery is cluster-mode rather than broadcast: one random client receives a message. (long-connection event subscription)
  • Long-connection callbacks use the same per-app App ID/App Secret client construction and have the same cluster-mode single-recipient behavior. (long-connection callback subscription)

Derived routing requirements

  • Create one logical client binding per enabled FeishuAppConnection, even if an in-process supervisor manages many clients. Attach immutable organization_id and connection_id context to each handler; the client that received the frame is the pre-authenticated application route.
  • Never infer the receiving Organization by looking up a bare actor Open ID or chat ID. First use the authenticated client binding; then cross-check any delivered app_id/tenant_key fields available in the event structure.
  • Scaling several replicas for the same app does not create duplicate broadcast processing according to Feishu's contract, but delivery still requires application-level idempotency and health ownership. Capacity must count clients per app against the documented 50-connection ceiling.

6. Bot, chat, and message identifiers

Official facts

  • In the message resource, a sender can be a user or an application. For an application sender, id_type is app_id; the sender also carries a tenant_key. Thus the Feishu bot identity used by messaging is its application identity, not a Hub-global bot ID. (message management overview)
  • chat_id identifies a conversation, including direct and group chats. Feishu's group resource also exposes the chat's tenant_key. (common parameters, group management overview)
  • Chat APIs require the operator to be in the same tenant as an internal chat; common errors include Operator and chat can NOT be in different tenants. Many operations additionally require the calling app's bot to be in the group and reject an app that is unavailable in that tenant. (get chat members)
  • The first bot direct-chat event calls chat_id the conversation ID between that robot and user and includes app_id, tenant_key, and app-scoped user Open IDs in the same event. (user and bot first conversation)

What the official sources do not establish

The cited documentation calls chat_id a conversation/group identifier but does not state that its value is globally unique across every Feishu tenant and every independently created app. It does establish tenant and bot-membership authorization boundaries. Under the repository rule that unspecified scope must not be assumed, raw global uniqueness would be an unsupported inference.

Derived storage/call rules:

  • Store chats as (feishu_app_connection_id, chat_id) and retain observed tenant_key and chat type. Do the same for other resource identifiers unless Feishu explicitly contracts a wider scope.
  • Always call chat/message APIs with the token obtained from the chat's bound connection. Never retry an authorization failure with another Organization's token.
  • Treat bot direct chats as connection-specific. A chat_id for user X's conversation with Organization A's bot is not evidence of a conversation with Organization B's bot.
  • For group chats, two customer apps may both be members of a real Feishu group, but that does not authorize the Hub to move configuration, Projects, or memberships between Organizations. The authenticated connection remains the tenant route and Hub Organization remains the authorization root.

Minimum invariants for implementation review

  1. FeishuIdentity uniqueness is (connection_id, open_id), never bare open_id.
  2. OAuth state resolves one server-side transaction bound to one Organization and connection; code exchange uses only that connection's credentials and stored redirect URI.
  3. Login accepts the returned identity only when tenant_key matches the connection's pinned tenant.
  4. HTTP Webhooks choose a connection-specific verification/decryption context before trusting payload routing fields; authenticated app_id and tenant_key must match it.
  5. Every long-connection handler carries the local connection/Organization binding of the App ID/App Secret client that received the frame.
  6. Bot, chat, event-deduplication, token-cache, and user identifiers are all namespaced by the Feishu connection unless Feishu explicitly guarantees a wider scope.
  7. A credential, app, or tenant mismatch is a hard failure with traceable logging; there is no fallback to a default app or search across tenant secrets.