feat(auth): support LARK_ACCESS_TOKEN env var for external token injection#238
feat(auth): support LARK_ACCESS_TOKEN env var for external token injection#238xzilong wants to merge 2 commits intolarksuite:mainfrom
Conversation
…ction Allows external OAuth managers (e.g. a centralized server) to inject a user access token via the LARK_ACCESS_TOKEN environment variable, so lark-cli can operate without its own keychain or config file. When LARK_ACCESS_TOKEN is set: - GetValidAccessToken() returns it immediately, bypassing keychain lookup - RequireConfig() returns a minimal env-based config (LARK_BRAND defaults to feishu) - RequireAuth() skips the UserOpenId check - autoDetectIdentity() returns "user" without checking stored tokens - DoSDKRequest() allows empty UserOpenId This enables use cases such as AI agent frameworks that manage OAuth centrally (e.g. daedalus → cowork → lark-cli). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The Lark SDK validates AppID and AppSecret are non-empty even when a UserAccessToken is provided directly (via LARK_ACCESS_TOKEN env var). Since AppID/AppSecret are only used for bot (tenant) token acquisition, which is never triggered in user-token injection mode, we use a placeholder value to pass the SDK validation. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughChanges introduce environment variable-based authentication via Changes
Sequence DiagramsequenceDiagram
actor User
participant Config as Config Loader
participant Factory as Identity Factory
participant Auth as Auth Service
participant Client as API Client
User->>Config: Start app with LARK_ACCESS_TOKEN env var
Config->>Config: Check LARK_ACCESS_TOKEN
alt Token set
Config->>Config: Return minimal env-based config
else Token unset
Config->>Config: Load from disk config files
end
Config-->>Factory: Provide config
Factory->>Factory: Check LARK_ACCESS_TOKEN for identity
alt Token set
Factory->>Factory: Return AsUser identity
else Token unset
Factory->>Factory: Use config-based identity detection
end
Factory-->>Auth: Provide identity + config
Auth->>Auth: Check LARK_ACCESS_TOKEN
alt Token set
Auth-->>Client: Return env token directly
else Token unset
Auth->>Auth: Lookup/refresh stored token
Auth-->>Client: Return stored token
end
Client->>Client: Use token for API requests
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
企业开发里,auth token 和 AppSecret 集中管理。需要保留环境变量的注入能力 |
Greptile SummaryThis PR introduces env-var-based access token injection (
Confidence Score: 4/5Safe to merge after fixing the unconditional placeholder logic in factory_default.go; all other env-token bypass points are consistent. One P1 finding remains: the AppID/AppSecret placeholder is applied to any empty credential, not just env-token mode, which can silently mask non-env-token misconfigurations with unhelpful SDK errors. The P2 brand-inheritance issue is a UX concern but not a correctness blocker. internal/cmdutil/factory_default.go — placeholder guard needs to be conditioned on LARK_ACCESS_TOKEN Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[CLI command invoked] --> B{LARK_ACCESS_TOKEN set?}
B -- Yes --> C[configFromEnv
Brand from LARK_BRAND
defaults to feishu]
B -- No --> D[RequireConfig
load from config file]
C --> E[RequireAuth
skips UserOpenId check]
D --> F[RequireAuth
checks UserOpenId != empty]
E --> G[autoDetectIdentity
returns AsUser immediately]
F --> H[autoDetectIdentity
checks stored token]
G --> I[DoSDKRequest
skips login-required guard]
H --> I
I --> J{as.IsBot?}
J -- No --> K[GetValidAccessToken]
J -- Yes --> L[Use tenant token]
K --> M{LARK_ACCESS_TOKEN set?}
M -- Yes --> N[Return env token directly]
M -- No --> O[Read/refresh stored token]
N --> P[SDK.Do with user token]
O --> P
L --> P
Reviews (1): Last reviewed commit: "fix(auth): use placeholder AppID/AppSecr..." | Re-trigger Greptile |
| func configFromEnv() *CliConfig { | ||
| if os.Getenv("LARK_ACCESS_TOKEN") == "" { | ||
| return nil | ||
| } | ||
| brand := LarkBrand(os.Getenv("LARK_BRAND")) | ||
| if brand == "" { | ||
| brand = BrandFeishu | ||
| } | ||
| return &CliConfig{ | ||
| Brand: brand, | ||
| DefaultAs: "user", | ||
| } | ||
| } |
There was a problem hiding this comment.
Existing config brand ignored when LARK_ACCESS_TOKEN is set
configFromEnv() returns a minimal config that derives Brand solely from LARK_BRAND, falling back to feishu. If a user already has a config file with brand: "lark" (international) and injects LARK_ACCESS_TOKEN without also setting LARK_BRAND=lark, all API calls will silently hit the feishu endpoints (open.feishu.cn) instead of the expected open.larksuite.com. The PR description notes this as a known requirement, so at minimum it should be surfaced in a warning or documented in the hint of the existing auth error messages.
| appID, appSecret := cfg.AppID, cfg.AppSecret | ||
| if appID == "" { | ||
| appID = "env_token_mode" | ||
| } | ||
| if appSecret == "" { | ||
| appSecret = "env_token_mode" | ||
| } | ||
| client := lark.NewClient(appID, appSecret, opts...) |
There was a problem hiding this comment.
Placeholder applied unconditionally to any empty AppID/AppSecret
The empty-string guard is not gated on LARK_ACCESS_TOKEN being set. If a user has a misconfigured config file with an empty AppId (outside of env-token mode), the SDK will be silently initialized with a fake credential and will produce a cryptic Lark API error rather than a clear "AppID is missing" diagnostic.
The fix is to wrap each fallback in a LARK_ACCESS_TOKEN check: return an explicit fmt.Errorf("config error: AppID is empty") when the env var is absent, and only apply the placeholder when env-token mode is confirmed active.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
internal/core/config.go (1)
107-122: Consider validatingLARK_BRANDagainst known values.The function accepts any string for
LARK_BRANDand only defaults toBrandFeishuwhen empty. An invalid brand value (e.g., typo like "feisuh") could cause issues downstream in endpoint resolution.♻️ Optional: Add brand validation
func configFromEnv() *CliConfig { if os.Getenv("LARK_ACCESS_TOKEN") == "" { return nil } brand := LarkBrand(os.Getenv("LARK_BRAND")) if brand == "" { brand = BrandFeishu + } else if brand != BrandFeishu && brand != BrandLark { + // Log warning but continue with provided value + fmt.Fprintf(os.Stderr, "warning: unrecognized LARK_BRAND %q, expected 'feishu' or 'lark'\n", brand) } return &CliConfig{ Brand: brand, DefaultAs: "user", } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/core/config.go` around lines 107 - 122, configFromEnv currently accepts any LARK_BRAND string which can produce invalid brands; update configFromEnv to validate the parsed LarkBrand against the known/allowed brand constants (e.g., BrandFeishu and any other declared Brand* constants) and fall back to BrandFeishu when the env value is empty or not one of the allowed values. Implement the check either inline (switch/if against allowed Brand constants) or via a small helper like isValidBrand(LarkBrand) and use that to decide the Brand field on the returned CliConfig.internal/auth/uat_client.go (1)
70-71: Optional: Consider extracting the env var name to a shared constant.The string
"LARK_ACCESS_TOKEN"appears in 5 files across this PR. While unlikely to change, extracting it to a constant incorepackage would provide a single source of truth and enable IDE-assisted refactoring.♻️ Example constant definition
In
internal/core/constants.go(or similar):// EnvAccessToken is the environment variable for external token injection. const EnvAccessToken = "LARK_ACCESS_TOKEN"Then use
core.EnvAccessTokeninstead of the hardcoded string.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/auth/uat_client.go` around lines 70 - 71, String literal "LARK_ACCESS_TOKEN" is repeated across files; create a single exported constant (e.g., EnvAccessToken) in the core package (for example add EnvAccessToken = "LARK_ACCESS_TOKEN" with a short doc comment in internal/core/constants.go), then replace all hardcoded occurrences (including the getenv call in internal/auth/uat_client.go and the other four usages) with core.EnvAccessToken and update imports where necessary; ensure the constant is exported so other packages can reference it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@internal/auth/uat_client.go`:
- Around line 70-71: String literal "LARK_ACCESS_TOKEN" is repeated across
files; create a single exported constant (e.g., EnvAccessToken) in the core
package (for example add EnvAccessToken = "LARK_ACCESS_TOKEN" with a short doc
comment in internal/core/constants.go), then replace all hardcoded occurrences
(including the getenv call in internal/auth/uat_client.go and the other four
usages) with core.EnvAccessToken and update imports where necessary; ensure the
constant is exported so other packages can reference it.
In `@internal/core/config.go`:
- Around line 107-122: configFromEnv currently accepts any LARK_BRAND string
which can produce invalid brands; update configFromEnv to validate the parsed
LarkBrand against the known/allowed brand constants (e.g., BrandFeishu and any
other declared Brand* constants) and fall back to BrandFeishu when the env value
is empty or not one of the allowed values. Implement the check either inline
(switch/if against allowed Brand constants) or via a small helper like
isValidBrand(LarkBrand) and use that to decide the Brand field on the returned
CliConfig.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0a62992d-744d-471c-9711-3169b63237a4
📒 Files selected for processing (5)
internal/auth/uat_client.gointernal/client/client.gointernal/cmdutil/factory.gointernal/cmdutil/factory_default.gointernal/core/config.go
Summary
LARK_ACCESS_TOKENenvironment variable to inject an external user access token, bypassing the local auth flowGetValidAccessTokenreturns the env token directly; the config/auth checks are skippedLARK_BRANDcan optionally specify the brand (defaults tofeishu)AppID/AppSecretare absent (env-token mode), placeholder values are used so the Lark SDK initializes without panic — bot-token acquisition is never triggered in this pathMotivation
Enables integration scenarios where a centralized OAuth manager supplies the user access token at runtime (e.g. CI pipelines, multi-tenant proxies), without requiring each caller to run
lark-cli auth login.Changes
internal/core/config.goconfigFromEnv()builds a minimal config from env;RequireConfig/RequireAuthshort-circuit when token is injectedinternal/auth/uat_client.goGetValidAccessTokenreadsLARK_ACCESS_TOKENfirstinternal/client/client.gointernal/cmdutil/factory.goIdentityType()returnsAsUserwhen env token is setinternal/cmdutil/factory_default.goTest plan
LARK_ACCESS_TOKEN=<token> lark-cli message send ...works without priorauth loginLARK_ACCESS_TOKEN, existing auth flow is unaffectedLARK_BRAND=lark LARK_ACCESS_TOKEN=<token> lark-cli ...picks up Lark brand correctly🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
LARK_ACCESS_TOKENenvironment variable as an alternative authentication method. Users can now provide credentials directly via environment variable, simplifying credential management and integration with automated systems, CI/CD pipelines, and containerized deployments.