Skip to content

feat(auth): support LARK_ACCESS_TOKEN env var for external token injection#238

Open
xzilong wants to merge 2 commits intolarksuite:mainfrom
hongshancapital:feat/lark-access-token-upstream
Open

feat(auth): support LARK_ACCESS_TOKEN env var for external token injection#238
xzilong wants to merge 2 commits intolarksuite:mainfrom
hongshancapital:feat/lark-access-token-upstream

Conversation

@xzilong
Copy link
Copy Markdown

@xzilong xzilong commented Apr 2, 2026

Summary

  • Add support for LARK_ACCESS_TOKEN environment variable to inject an external user access token, bypassing the local auth flow
  • When set, GetValidAccessToken returns the env token directly; the config/auth checks are skipped
  • LARK_BRAND can optionally specify the brand (defaults to feishu)
  • When AppID/AppSecret are absent (env-token mode), placeholder values are used so the Lark SDK initializes without panic — bot-token acquisition is never triggered in this path

Motivation

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

File Change
internal/core/config.go configFromEnv() builds a minimal config from env; RequireConfig/RequireAuth short-circuit when token is injected
internal/auth/uat_client.go GetValidAccessToken reads LARK_ACCESS_TOKEN first
internal/client/client.go Skip "login required" error when env token is present
internal/cmdutil/factory.go IdentityType() returns AsUser when env token is set
internal/cmdutil/factory_default.go Use placeholder AppID/AppSecret when values are empty

Test plan

  • LARK_ACCESS_TOKEN=<token> lark-cli message send ... works without prior auth login
  • Without LARK_ACCESS_TOKEN, existing auth flow is unaffected
  • LARK_BRAND=lark LARK_ACCESS_TOKEN=<token> lark-cli ... picks up Lark brand correctly

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for the LARK_ACCESS_TOKEN environment 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.

xzilong and others added 2 commits April 2, 2026 23:01
…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>
@github-actions github-actions bot added the size/L Large or sensitive change across domains or core paths label Apr 2, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Changes introduce environment variable-based authentication via LARK_ACCESS_TOKEN. Multiple components now check this environment variable early in their execution paths and bypass normal stored-credential and configuration flows when it is set.

Changes

Cohort / File(s) Summary
Environment-variable auth shortcuts
internal/auth/uat_client.go, internal/client/client.go, internal/cmdutil/factory.go, internal/core/config.go
Added early checks for LARK_ACCESS_TOKEN environment variable that bypass stored token lookup, config file loading, and identity detection flows when present. New configFromEnv() helper creates minimal config from env var.
Client initialization
internal/cmdutil/factory_default.go
Updated cachedLarkClientFunc to substitute "env_token_mode" placeholder values for empty appID and appSecret credentials, enabling token-mode initialization without stored app credentials.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A token floats in the air so bright,
No configs needed, just set it right!
Environment speaks its secret code,
Shortcuts light the auth-bound road,
Hopping faster through the gate! 🔐✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding support for the LARK_ACCESS_TOKEN environment variable to enable external token injection, bypassing local auth flow.
Description check ✅ Passed The description provides a comprehensive summary, clear motivation, detailed changes table, and test plan items. All required template sections are covered with substantive content.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@xzilong
Copy link
Copy Markdown
Author

xzilong commented Apr 2, 2026

企业开发里,auth token 和 AppSecret 集中管理。需要保留环境变量的注入能力

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 2, 2026

Greptile Summary

This PR introduces env-var-based access token injection (LARK_ACCESS_TOKEN) as a bypass for the normal OAuth flow, targeting CI/multi-tenant scenarios. The overall approach is sound — short-circuit points in RequireConfig, RequireAuth, GetValidAccessToken, and DoSDKRequest are all consistent — but there is one implementation gap worth addressing before merging.

  • P1 (factory_default.go): The AppID/AppSecret placeholder fallback is not gated on LARK_ACCESS_TOKEN. Any config with an empty AppId (even outside env-token mode) silently receives a fake credential, masking the real misconfiguration with a cryptic Lark SDK error.
  • P2 (config.go): When LARK_ACCESS_TOKEN is set alongside an existing config file, the config's Brand is ignored; only LARK_BRAND env var determines the endpoint. Users with a brand: \"lark\" config who don't also set LARK_BRAND=lark will silently hit feishu endpoints.

Confidence Score: 4/5

Safe 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

Filename Overview
internal/cmdutil/factory_default.go Adds placeholder AppID/AppSecret for env-token mode, but the guard is not tied to LARK_ACCESS_TOKEN — any config with an empty AppID/AppSecret silently gets the placeholder, masking misconfiguration.
internal/core/config.go Adds configFromEnv() for minimal env-based config; RequireConfig/RequireAuth short-circuit when LARK_ACCESS_TOKEN is set. Brand silently defaults to feishu, ignoring any existing config file brand.
internal/auth/uat_client.go GetValidAccessToken now returns the env token directly when LARK_ACCESS_TOKEN is set, bypassing stored-token lookup and refresh logic cleanly.
internal/client/client.go Skips the login-required guard when LARK_ACCESS_TOKEN is present, allowing DoSDKRequest to proceed to GetValidAccessToken where the env token is returned.
internal/cmdutil/factory.go autoDetectIdentity returns AsUser immediately when LARK_ACCESS_TOKEN is set; in practice this path is only reached via --as auto since configFromEnv already sets DefaultAs=user.

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
Loading

Reviews (1): Last reviewed commit: "fix(auth): use placeholder AppID/AppSecr..." | Re-trigger Greptile

Comment on lines +110 to +122
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",
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +113 to +120
appID, appSecret := cfg.AppID, cfg.AppSecret
if appID == "" {
appID = "env_token_mode"
}
if appSecret == "" {
appSecret = "env_token_mode"
}
client := lark.NewClient(appID, appSecret, opts...)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
internal/core/config.go (1)

107-122: Consider validating LARK_BRAND against known values.

The function accepts any string for LARK_BRAND and only defaults to BrandFeishu when 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 in core package 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.EnvAccessToken instead 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

📥 Commits

Reviewing files that changed from the base of the PR and between f67f569 and b990c93.

📒 Files selected for processing (5)
  • internal/auth/uat_client.go
  • internal/client/client.go
  • internal/cmdutil/factory.go
  • internal/cmdutil/factory_default.go
  • internal/core/config.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant