Files
larksuite-cli/sidecar/server-multi-tenant-demo
hGrany ffcf7781b4 feat(sidecar): support multi-client identity isolation in server-demo (#934)
* feat(sidecar): support multi-client identity isolation in server-demo

When multiple CLI sandbox environments share a single sidecar instance,
user tokens (UAT) were not isolated -- the last user to log in would
overwrite previous users' tokens, causing identity cross-contamination.

This change introduces per-client HMAC key isolation:
- Each client gets a unique client-*.key file for data-plane HMAC signing,
  allowing the sidecar to identify request origin.
- A new auth_bridge.go handles management endpoints (login/poll/status)
  with explicit client-to-feishuOpenId binding.
- User token resolution is strictly bound to the matched client -- no
  fallback to other users' tokens when a client has no mapping.
- The shared proxy.key is reused across restarts instead of regenerated,
  fixing a race condition when multiple sidecar instances start together.

Wire protocol (sidecar package) is unchanged; existing single-client
deployments are fully backward compatible.

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)

* fix(sidecar): address review feedback on filesystem and safety

- Replace os.ReadFile/WriteFile/ReadDir with vfs.* equivalents for test
  mockability, consistent with project coding guidelines.
- Limit auth bridge request body to 64KB to prevent memory exhaustion.
- Log errors in saveUserMap instead of silently discarding them.
- Reject client keys that collide with the shared proxy key.
- Reject duplicate client keys instead of silently overwriting.

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)

* refactor(sidecar): remove workspace-specific naming and backward compat

- parseClientID: only accept "client_id" field, remove legacy fallback
- loadClientKeys: scan all *.key (excluding proxy.key), no prefix required
- Remove legacy file migration logic in newAuthBridge
- Update flag description to reflect generic key scanning

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)

* refactor(sidecar): extract multi-tenant demo and add unit tests

Address review feedback from sang-neo03:

1. Extract multi-client code into sidecar/server-multi-tenant-demo/,
   keeping server-demo as the minimal single-tenant reference.

2. Add unit tests for the isolation guarantee:
   - loadClientKeys: shared-key collision and duplicate keyHex are skipped
   - verifyWithClientKeys: correct client matched, unknown key rejected
   - loadUserMap/saveUserMap: round-trip persistence across restart

3. Cross-link READMEs between server-demo and server-multi-tenant-demo.

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)

* docs(sidecar): rewrite multi-tenant demo README with problem statement and client guide

- Explain the multi-app credential isolation problem (app_secret must
  not be exposed to client environments)
- Document typical deployment topology with multiple sidecar instances
- Add complete client setup guide: env vars, multi-app switching, login
  flow, and end-to-end workflow example
- Document design decisions and management endpoint details

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)

* fix(sidecar): address CodeRabbit review feedback on tests and docs

- Make TestProxyHandler_AcceptsAllowedAuthHeaders fully offline by using
  httptest.NewTLSServer instead of depending on open.feishu.cn
- Isolate TestRun_RejectsSelfProxy config state with t.Setenv and temp dirs
- Check os.MkdirAll error in test fixture setup
- Add language identifiers to fenced code blocks (MD040)
- Validate user-supplied CLI paths with validate.SafeInputPath

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)

---------

Signed-off-by: Gao Yang <grany@yeah.net> (topwin.tech)
2026-05-22 15:25:00 +08:00
..

Multi-Tenant Sidecar Server Demo

⚠️ This is a demo. For production deployment, implement your own sidecar server conforming to the wire protocol in github.com/larksuite/cli/sidecar.

Problem

Organizations often manage multiple Lark/Feishu apps (e.g. one per department, one per product line), each with its own app_id and app_secret. These credentials must never be exposed to end-user environments (CI runners, developer sandboxes, containerized workspaces). At the same time, when multiple users share the same sidecar infrastructure, their Feishu identities must be strictly isolated — user A must never accidentally operate as user B.

The single-tenant server-demo solves the credential-hiding problem for one app with one user. This multi-tenant demo extends it to support:

  1. Multiple apps — run one sidecar instance per app; each instance holds its own app_id / app_secret and listens on a separate port. Clients choose which app to use by pointing LARKSUITE_CLI_AUTH_PROXY to the corresponding port.
  2. Per-client identity isolation — each client environment gets a unique HMAC key. The sidecar identifies request origin by matching the HMAC signature and injects the correct user's token. No fallback to other users' tokens.
  3. Self-service user login — management endpoints let each client initiate an OAuth device-flow login to bind their own Feishu identity, without exposing app_secret to the client.

Typical deployment

                    Trusted Host
    ┌──────────────────────────────────────────────┐
    │  sidecar instance A (port 16384)             │
    │    app_id=cli_aaa  app_secret=***            │
    │    keys/proxy.key  keys/alice.key  keys/bob… │
    │                                              │
    │  sidecar instance B (port 16385)             │
    │    app_id=cli_bbb  app_secret=***            │
    │    keys/proxy.key  keys/charlie.key  ...     │
    └─────────────┬────────────────────────────────┘
                  │ same machine (loopback / docker bridge)
    ┌─────────────┴────────────────────────────────┐
    │  Client sandbox (container / CI runner)       │
    │                                              │
    │  LARKSUITE_CLI_AUTH_PROXY=http://host:16384   │
    │  LARKSUITE_CLI_PROXY_KEY=<contents of         │
    │                           alice.key>          │
    │  LARKSUITE_CLI_APP_ID=cli_aaa                │
    │  LARKSUITE_CLI_BRAND=feishu                  │
    │                                              │
    │  $ lark api GET /open-apis/... --as user     │
    │    → sidecar matches alice.key               │
    │    → injects alice's Feishu user token       │
    └──────────────────────────────────────────────┘

Key points:

  • app_id and app_secret live only on the trusted host — clients only know app_id (needed for the CLI's credential pipeline) and their own HMAC key.
  • Each sidecar instance binds one app. Multiple apps = multiple instances on different ports.
  • Clients select which app to use by choosing which sidecar port to connect to (via LARKSUITE_CLI_AUTH_PROXY).

Architecture

┌──────────────────────────────────────────────────────┐
│                    Sidecar Server                     │
│                                                      │
│  ┌─────────────┐  ┌──────────────────────────────┐  │
│  │ Shared Key   │  │ Per-Client Keys              │  │
│  │ (proxy.key)  │  │ alice.key, bob.key, ...      │  │
│  └──────┬──────┘  └──────────────┬───────────────┘  │
│         │ management plane       │ data plane        │
│         ▼                        ▼                   │
│  ┌─────────────┐  ┌──────────────────────────────┐  │
│  │ Auth Bridge  │  │ Proxy Handler                │  │
│  │ login/poll/  │  │ HMAC verify → identify       │  │
│  │ status       │  │ client → inject user token   │  │
│  └─────────────┘  └──────────────────────────────┘  │
└──────────────────────────────────────────────────────┘

Dual-key design:

  • Management plane (login flow): all clients use the shared proxy.key. This allows any client to initiate login and query status without needing individual key files pre-provisioned.
  • Data plane (API proxy): each client uses its own {name}.key for HMAC signing. The sidecar identifies the client by matching which key verifies the request signature, then injects that client's bound user token.

Build

go build -tags authsidecar_multi_tenant_demo \
  -o sidecar-multi-tenant-demo \
  ./sidecar/server-multi-tenant-demo/

Server setup

1. Configure the Lark app (trusted side only)

lark-cli config init --new   # set app_id / app_secret

2. Prepare the keys directory

keys/
├── proxy.key          # shared key (auto-generated on first run)
├── alice.key          # client "alice" — generate with: openssl rand -hex 32 > alice.key
├── bob.key            # client "bob"
└── charlie.key        # client "charlie"
  • Each file contains a 64-character hex string (32 bytes).
  • Filename stem (without .key) becomes the client identity.
  • proxy.key is excluded from client key scanning.
  • Keys are auto-rescanned on cache miss — add a new .key file and the next unrecognized request will trigger a rescan; no restart needed.
  • Duplicate key values and shared-key collisions are rejected with a warning.

3. Start the server

./sidecar-multi-tenant-demo \
  --listen 127.0.0.1:16384 \
  --key-file /path/to/keys/proxy.key \
  --keys-dir /path/to/keys/ \
  --log-file /path/to/audit.log
Flag Default Purpose
--listen 127.0.0.1:16384 Address to bind the HTTP listener
--key-file ~/.lark-sidecar/proxy.key Shared HMAC key path (created if absent)
--keys-dir (parent of --key-file) Directory containing per-client *.key files
--log-file (stderr) Audit log output path
--profile (active profile) lark-cli profile name for credential lookup

Client setup

No changes to lark-cli itself are required. The standard sidecar env vars are all that's needed — the multi-tenant isolation is entirely server-side.

Required environment variables

# Point to the sidecar instance for the desired app
export LARKSUITE_CLI_AUTH_PROXY="http://127.0.0.1:16384"

# Client-specific HMAC key (data-plane identity)
export LARKSUITE_CLI_PROXY_KEY="$(cat /path/to/keys/alice.key)"

# Must match the app configured on the sidecar instance
export LARKSUITE_CLI_APP_ID="cli_xxx"

# feishu or lark
export LARKSUITE_CLI_BRAND="feishu"

Multi-app switching (multiple sidecar instances)

When the server operator runs multiple sidecar instances (one per app), clients switch between apps by changing LARKSUITE_CLI_AUTH_PROXY to point to the appropriate port:

# App A (e.g. "Marketing" app)
export LARKSUITE_CLI_AUTH_PROXY="http://127.0.0.1:16384"
export LARKSUITE_CLI_APP_ID="cli_marketing_app"

# App B (e.g. "Engineering" app)
export LARKSUITE_CLI_AUTH_PROXY="http://127.0.0.1:16385"
export LARKSUITE_CLI_APP_ID="cli_engineering_app"

A client-side helper script can present these as a menu (e.g. "Select company"), reading from a local config file that maps app names to ports. The sidecar itself does not implement app selection — it is one instance per app by design.

User login flow

Once the env vars are set, the client authenticates via the management endpoints. A helper script (or manual curl) calls:

  1. Login: POST /_sidecar/auth/login with {"client_id": "alice"} → returns a device code and verification URL.
  2. User opens the URL in a browser and authorizes the app.
  3. Poll: POST /_sidecar/auth/poll with {"device_code": "...", "client_id": "alice"} → blocks until authorization completes.
  4. Status: POST /_sidecar/auth/status with {"client_id": "alice"} → returns the bound user name and token status.

All management requests are signed with the shared proxy.key (not the client-specific key). The client_id in the body tells the sidecar which client→user mapping to update.

After login, lark-cli commands (lark api ..., lark doc ..., etc.) work immediately — the sidecar injects the correct user token based on the client's HMAC key, with no additional configuration needed.

Example: end-to-end workflow

# 1. Server operator generates a key for a new client
openssl rand -hex 32 > /path/to/keys/alice.key

# 2. Client environment is configured (e.g. in .bashrc or container init)
export LARKSUITE_CLI_AUTH_PROXY="http://host.docker.internal:16384"
export LARKSUITE_CLI_PROXY_KEY="$(cat /path/to/keys/alice.key)"
export LARKSUITE_CLI_APP_ID="cli_xxx"
export LARKSUITE_CLI_BRAND="feishu"

# 3. Client logs in (one-time)
#    (using a helper script that calls the management endpoints)
lark-auth login

# 4. Client uses lark-cli as normal — identity is automatically resolved
lark api GET /open-apis/authen/v1/user_info --as user
# → returns alice's Feishu identity, not another user's

Management endpoints

Endpoint Method Body Purpose
/_sidecar/auth/login POST {"client_id": "...", "domains": [...]} Start OAuth device-flow
/_sidecar/auth/poll POST {"device_code": "...", "client_id": "..."} Poll for completion
/_sidecar/auth/status POST {"client_id": "..."} Query status and mapping

All management requests require HMAC signing with the shared proxy.key. The HMAC covers method, path, timestamp, and body SHA-256 — see verifyManagementHMAC in auth_bridge.go for the canonical string format.

Design decisions

  1. HMAC key as client identity — the key is the existing trust anchor. Using it for identification introduces no new trust assumptions and prevents a malicious client from spoofing another client's identity (unlike a header-based approach).

  2. No fallback on unmapped clients — this is authentication. Silently falling back to another user's token is a security violation. Unmapped clients receive an explicit error prompting them to log in.

  3. One sidecar instance per app — keeps app_secret scoping simple and avoids cross-app token confusion. Multi-app support is achieved by running multiple instances on different ports.

  4. Proxy.key reuse across restarts — when multiple sidecar instances start concurrently, they all write to the same key file. The last writer wins, leaving other instances with stale in-memory keys. Reusing the existing key eliminates this race.

Source layout

File Purpose
main.go Entry point: flag parsing, key loading, server lifecycle
handler.go proxyHandler.ServeHTTP — multi-key HMAC verification and request forwarding
auth_bridge.go Management endpoints: login, poll, status, user mapping persistence
forward.go Forwarding HTTP client + proxy-header filter
allowlist.go Target host / identity allowlists
audit.go Log path/error sanitization
handler_test.go Unit tests

See also