Files
larksuite-cli/sidecar/server-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
..
2026-04-20 20:24:51 +08:00
2026-04-20 20:24:51 +08:00
2026-04-20 20:24:51 +08:00
2026-04-20 20:24:51 +08:00
2026-04-20 20:24:51 +08:00

Sidecar Server Reference Implementation

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

This example shows how to implement a sidecar auth proxy server that receives HMAC-signed requests from lark-cli sandbox clients and forwards them to the Lark/Feishu API with real credentials injected.

What this demo shows

  • HMAC-SHA256 request verification (timestamp drift, body digest, signature)
  • Target host allowlist + https-only target validation (anti-SSRF / anti-downgrade)
  • Identity-based token resolution (UAT for user, TAT for bot)
  • Auth-header allowlist: real token may only be injected into Authorization / X-Lark-MCP-UAT / X-Lark-MCP-TAT, rejecting attempts to smuggle it into Cookie, User-Agent, or other intermediate-logged headers
  • Audit logging with path ID-segment sanitization and upstream error truncation
  • Safe request forwarding (strips client-supplied auth headers)

What this demo does NOT handle

  • TAT refresh — the shared DefaultTokenProvider caches the TAT via sync.Once, which never refreshes. A long-running server will return an expired TAT after 2 hours. Production implementations should maintain a TTL-based cache with early renewal.
  • High availability / load balancing / hot key rotation
  • TLS termination
  • Rate limiting / per-identity quotas

Both sides need the right build tags

Sidecar is split into two separate binaries with different build tags:

Side Binary Build tag How to build
Sandbox (client) lark-cli authsidecar go build -tags authsidecar -o lark-cli .
Trusted (server) sidecar-server-demo authsidecar_demo go build -tags authsidecar_demo -o sidecar-server-demo ./sidecar/server-demo/

If the sandbox runs a standard lark-cli without -tags authsidecar, the LARKSUITE_CLI_AUTH_PROXY env var is ignored and requests bypass the sidecar entirely — real credentials (if any) leak to the sandbox.

Prerequisites

The demo reuses the lark-cli credential pipeline, so the trusted machine must have an app configured:

lark-cli config init --new   # configure app_id / app_secret (required)
lark-cli auth login          # store user refresh_token in keychain
                              # (only required if sandbox will use --as user)

auth login is only required for user identity. If the server will only serve bot requests (TAT), config init alone is enough because the TAT is minted from app_id + app_secret.

Also, the server process must not inherit LARKSUITE_CLI_AUTH_PROXY — if it does, the sidecar credential provider would activate inside the server and return sentinel tokens instead of real ones. The demo rejects this at startup with a clear error, but you should make sure to unset LARKSUITE_CLI_AUTH_PROXY in the server shell before launching.

Run

./sidecar-server-demo \
  --listen 127.0.0.1:16384 \
  --key-file <HOME>/.lark-sidecar/proxy.key \
  --log-file <HOME>/.lark-sidecar/audit.log

Flags

Flag Default Purpose
--listen 127.0.0.1:16384 Address to bind the HTTP listener
--key-file <HOME>/.lark-sidecar/proxy.key Path to write the generated HMAC key (mode 0600)
--log-file (empty, stderr) Audit log output path
--profile (empty, active profile) lark-cli profile name for credential lookup

Startup output

Auth sidecar listening on http://127.0.0.1:16384
HMAC key prefix: a3b2c1d4
Full key written to /Users/alice/.lark-sidecar/proxy.key (mode 0600)

Set in sandbox:
  export LARKSUITE_CLI_AUTH_PROXY="http://127.0.0.1:16384"
  export LARKSUITE_CLI_PROXY_KEY="<read from /Users/alice/.lark-sidecar/proxy.key>"
  export LARKSUITE_CLI_APP_ID="cli_xxx"
  export LARKSUITE_CLI_BRAND="feishu"

The key-file path is printed exactly as passed on the command line (relative paths stay relative). The HMAC key prefix is the first 8 characters for identification without revealing the full key.

Sandbox env vars (complete list)

The startup banner only prints the required variables. Two more are optional:

export LARKSUITE_CLI_AUTH_PROXY="http://..."       # required (see constraints below)
export LARKSUITE_CLI_PROXY_KEY="..."               # required
export LARKSUITE_CLI_APP_ID="cli_xxx"              # required
export LARKSUITE_CLI_BRAND="feishu"                # required (feishu | lark)
export LARKSUITE_CLI_DEFAULT_AS="user"             # optional: force default identity
export LARKSUITE_CLI_STRICT_MODE="user"            # optional: lock sandbox to one identity

LARKSUITE_CLI_AUTH_PROXY constraints — validated by the CLI on startup:

  • Scheme must be http:// (or bare host:port). https:// is rejected today because the interceptor does not yet perform TLS; a future PR that wires up real TLS will relax this.
  • Host must be loopback (127.0.0.1, ::1) or one of the recognized same-host aliases: localhost, host.docker.internal, host.containers.internal, host.lima.internal, gateway.docker.internal. The sidecar pattern is inherently same-machine; cross-machine deployment is a different product (auth broker / STS) with different security requirements (mTLS, cert rotation, per-client keys) and is not supported by this feature.
  • No path, query, fragment, or user:pass@ in the URL.

How auto identity detection works in sidecar mode: on every invocation the CLI asks the sidecar to look up the logged-in user's open_id via /open-apis/authen/v1/user_info. If that succeeds, --as defaults to user; if it fails (trusted side has no valid user login, or the call errors out), it falls back to bot. Setting LARKSUITE_CLI_DEFAULT_AS=user lets you short-circuit this and always default to user regardless of the lookup result; set it to bot for the opposite.

Note: LARKSUITE_CLI_STRICT_MODE and the server's identity allowlist are two separate enforcement points:

  • STRICT_MODE is interpreted locally by the sandbox CLI — it rejects --as values the sandbox itself disallows, before any request goes out.
  • The server's allowlist is built from the trusted-side config's SupportedIdentities (sidecar/server-demo/allowlist.go). The sandbox cannot override it.

A well-configured deployment aligns both (e.g. both set to user when the app only supports user tokens), but they are computed independently.

Graceful shutdown

Send SIGINT (Ctrl+C) or SIGTERM to stop the server. The demo drains in-flight requests with a 5-second timeout before exiting.

Wire protocol

See the sidecar package on pkg.go.dev for protocol constants, HMAC signing/verification, and address validation utilities.

Headers (client → server):

Header Purpose
X-Lark-Proxy-Version Wire-protocol version (currently "v1"). Server rejects unknown values with 400.
X-Lark-Proxy-Target Original target scheme + host only (e.g. https://open.feishu.cn). Must be https://; any path/query/fragment/userinfo in this header is rejected. The path and query come from the request line itself; the server reconstructs the upstream URL as https://<host> + requestURI.
X-Lark-Proxy-Identity "user" or "bot". Covered by the signature.
X-Lark-Proxy-Auth-Header Which header the server should inject real token into. Covered by the signature.
X-Lark-Proxy-Signature hex-encoded HMAC-SHA256
X-Lark-Proxy-Timestamp Unix seconds (drift ≤ 60s)
X-Lark-Body-SHA256 hex-encoded SHA-256 of the request body

Signing material (newline-separated, in order):

version
method
host
pathAndQuery
bodySHA256
timestamp
identity
authHeader

Every field above is part of the canonical string. In particular, identity and authHeader are covered so a captured request cannot be replayed with its identity flipped (bot↔user) or its auth-header redirected (e.g. into Cookie) inside the 60s drift window.

Source layout

File Purpose
main.go Entry point: flag parsing, server lifecycle
handler.go proxyHandler.ServeHTTP — main request flow
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 for all of the above

See also

  • server-multi-tenant-demo — extends this demo with per-client HMAC key isolation, OAuth device-flow login, and persistent client → user mapping for multi-tenant deployments