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
- 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)
- A bare
open_idis 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_idis tenant-scoped; it is stable across apps in one tenant but differs across tenants.union_idcrosses apps only for apps from the same application provider/developer; it differs between providers. (user resource overview, official ID parameter definitions, Union ID guide) - 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 containscodeand the echoedstate, not an independently trustworthy Organization choice. Exchanging the code requires the matching app'sclient_idandclient_secret, and the sameredirect_uri. (obtain authorization code, obtain user access token) - A version 2 event or callback carries
header.app_idandheader.tenant_keyafter parsing/decryption. An encrypted HTTP delivery, however, exposes only anencryptenvelope before decryption, so a shared endpoint cannot safely choose among many apps' Encrypt Keys from the body. (callback structure, encrypted Webhook delivery) - 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)
- Feishu documents
chat_idas the conversation identifier, but the cited documentation does not promise that a rawchat_idis 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'sapp_id. (common parameters, get chat members, message resource)
1. Application and tenant boundary
Official facts
app_idis the immutable identifier generated by Feishu for an application.tenant_keyis the enterprise identifier; onetenant_keycorresponds 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_tokencalls as the application and its data range is determined by that application's permissions. (access-token overview) - A custom app obtains its
tenant_access_tokenusing that app'sapp_idandapp_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, expectedtenant_key, encryptedapp_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_keyto the connection. Later login, event, and callback data must match both the expectedapp_idand pinnedtenant_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_idis tenant identity,open_idis app identity, andunion_idis 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 sensitiveuser_id, andtenant_keyfrom auser_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); retaintenant_key,user_id, andunion_idas 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_idmandatory for login unless every customer app is required to obtain its sensitive field permission.open_idandtenant_keyare returned by the user-info flow without usinguser_idas 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_urimust already appear in that application's security settings. On success Feishu redirects withcodeand, when supplied, the originalstate. (obtain authorization code) - Feishu says
statemaintains 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 sameredirect_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
- 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. - Put only a high-entropy opaque transaction nonce in OAuth
state; generate the authorization URL with that connection'sapp_idasclient_id. - On callback, atomically consume and validate
statebefore selecting credentials. The callback's untrusted query parameters must never be allowed to choose another Organization or App. - Exchange
codewith the connection'sapp_id, decryptedapp_secret, and the exact stored redirect URI. Then call user-info and require itstenant_keyto equal the connection's pinned tenant. - 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), andapp_id(the callback application's App ID). (callback overview and structure) - Version 2 event examples likewise place
app_idandtenant_keyin 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_idandtenant_keyare 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_idandtenant_keyto match the route's connection. - For an unencrypted common endpoint,
header.app_idmay 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_IDandAPP_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 immutableorganization_idandconnection_idcontext 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_keyfields 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_typeisapp_id; the sender also carries atenant_key. Thus the Feishu bot identity used by messaging is its application identity, not a Hub-global bot ID. (message management overview) chat_ididentifies a conversation, including direct and group chats. Feishu's group resource also exposes the chat'stenant_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_idthe conversation ID between that robot and user and includesapp_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 observedtenant_keyand 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_idfor 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
FeishuIdentityuniqueness is(connection_id, open_id), never bareopen_id.- OAuth
stateresolves one server-side transaction bound to one Organization and connection; code exchange uses only that connection's credentials and stored redirect URI. - Login accepts the returned identity only when
tenant_keymatches the connection's pinned tenant. - HTTP Webhooks choose a connection-specific verification/decryption context
before trusting payload routing fields; authenticated
app_idandtenant_keymust match it. - Every long-connection handler carries the local connection/Organization binding of the App ID/App Secret client that received the frame.
- Bot, chat, event-deduplication, token-cache, and user identifiers are all namespaced by the Feishu connection unless Feishu explicitly guarantees a wider scope.
- 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.