Skip to content

feat(token): mint TAT via unified OAuth v3 Token Endpoint#1408

Open
albertnusouo wants to merge 5 commits into
mainfrom
feat/token_endpoint_oauth_v3
Open

feat(token): mint TAT via unified OAuth v3 Token Endpoint#1408
albertnusouo wants to merge 5 commits into
mainfrom
feat/token_endpoint_oauth_v3

Conversation

@albertnusouo

@albertnusouo albertnusouo commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

What

Migrate tenant-access-token (TAT) minting from the legacy per-token endpoint (/open-apis/auth/v3/tenant_access_token/internal) to the unified OAuth 2.0 Token Endpoint (accounts.<brand>/oauth/v3/token). This swaps only the TAT request interface — no identity, caching, or scope behavior changes.

Why

/oauth/v3/token is the platform direction for all grant types and the path the server now expects for client_credentials.

Changes

  • core — add OAuthTokenV3Path = "/oauth/v3/token".
  • credential/tat_fetchFetchTAT POSTs a form-encoded grant_type=client_credentials + client_id + client_secret (client_secret_post) to the accounts domain; parses the OAuth {code, access_token, error, error_description} response; transient 5xx / server_error stay untyped so the post-config init probe stays silent.
  • credential/default_provider — OAuth error classification: invalid_client / unauthorized_clientCategoryConfig/InvalidClient; every other deterministic error → typed via BuildAPIError, with a code-0 backstop so an OAuth error carrying no numeric code (e.g. invalid_scope) is never swallowed into a ("", nil) empty-token "success".
  • config/init_probe — comment-only refresh for the OAuth2 error model.

Not in scope (explored then removed on this branch)

The Agent Employee (hybrid TAT) missing-scope auto-retry / scope-bound TAT minting was added in earlier commits and then removed (revert(token): drop Agent Employee hybrid-TAT scope chain). Net effect of the branch is the endpoint swap + error handling only — internal/client/client.go and internal/credential/types.go are unchanged vs the base.

Testing

  • go build ./..., go vet, gofmt -l clean.
  • Unit tests: v3 URL/form/response shape, OAuth error classification (invalid_client / unauthorized_client / other, including the code-0 path), transient-untyped handling, and the post-config init probe propagation. All green.
  • Verified live: config init / --as bot calls hit POST accounts.feishu.cn/oauth/v3/token with grant_type=client_credentials + client_id + client_secret (captured via proxy).

Notes

  • Default brand endpoints remain production (accounts.feishu.cn / accounts.larksuite.com); no BOE/staging hardcoding.
  • private_key_jwt auth is out of scope (separate follow-up); this uses client_secret_post.

Switch tenant-access-token minting from the legacy
/open-apis/auth/v3/tenant_access_token/internal endpoint to the unified
OAuth 2.0 Token Endpoint (accounts.<brand>/oauth/v3/token) using the
client_credentials grant with client_secret_post auth.

- core: add OAuthTokenV3Path constant
- credential: FetchTAT/fetchTAT POST a form-encoded
  grant_type/client_id/client_secret to the accounts domain; parse the
  OAuth {code, access_token, error, error_description} response; keep
  5xx/server_error untyped so probe callers stay silent; classify
  invalid_client/unauthorized_client as CategoryConfig/InvalidClient
- credential: scope-bound TAT (TokenSpec.Scopes); scoped mints bypass the
  unscoped tatOnce cache
- client: Agent Employee (hybrid TAT) missing-scope auto-retry — on
  99991679, re-mint a TAT carrying the app's full granted scopes and
  retry once
- config: refresh init-probe wording for the OAuth2 error model

Tests cover the v3 URL/form/response shape, OAuth error classification,
scope-bound vs unscoped mint, and the missing-scope retry.
@github-actions github-actions Bot added the size/L Large or sensitive change across domains or core paths label Jun 11, 2026
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Migrates tenant access token acquisition to OAuth 2.0 v3 token endpoint with form-encoded client_credentials, updates deterministic error classification to use OAuth2 error strings (e.g., invalid_client/unauthorized_client → ConfigError/SubtypeInvalidClient), and updates probe integration and tests accordingly.

Changes

OAuth v3 TAT Endpoint Migration

Layer / File(s) Summary
OAuth v3 endpoint path contract
internal/core/types.go, internal/core/types_test.go
Introduces OAuthTokenV3Path constant for the unified OAuth token endpoint and verifies endpoint resolution for the empty-brand default.
TAT fetch: form-encoded OAuth v3 POST
internal/credential/tat_fetch.go
Rewrites FetchTAT to POST to /oauth/v3/token with form-encoded client_credentials, reads a bounded response, unmarshals OAuth token fields (code, access_token, error, error_description, msg), returns token only on code==0 with access_token, treats transient/malformed/unparseable cases as untyped errors, and classifies deterministic rejections via classifyTATResponseCode.
FetchTAT test updates and brand routing
internal/credential/tat_fetch_test.go
Updates tests to assert request URL, application/x-www-form-urlencoded body fields, OAuth-style success and error payloads (invalid_client, invalid_scope, server_error), gateway {code,msg} fallback, and Feishu/Lark routing to /oauth/v3/token.
OAuth error classification logic and tests
internal/credential/default_provider.go, internal/credential/default_provider_test.go
Refactors classifyTATResponseCode to accept OAuth2 error/error_description, prefer error_description for messages, map invalid_client/unauthorized_client to errs.ConfigError{SubtypeInvalidClient} with a hint, route other deterministic failures via errclass.BuildAPIError with a SubtypeUnknown fallback, and add OAuth2-string-based test coverage.
Probe command OAuth2 credential-rejection detection
cmd/config/init_probe.go, cmd/config/init_probe_test.go
Updates runProbe doc comment to reference OAuth2-classification behavior, adapts test fakeRT to stub /oauth/v3/token, simplifies assertConfigRejection to check CategoryConfig/SubtypeInvalidClient, replaces numeric-code tests with OAuth2 error-string variants, and ensures probe endpoint isn't called when TAT fails.
Compatibility comments and legacy notes
internal/errclass/codemeta.go, internal/output/lark_errors.go
Adjusts inline documentation for legacy numeric code 10014 and Lark error constant to describe compatibility and defensive fallback behavior after OAuth v3 migration.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • larksuite/cli#1151: The prior PR's TAT/FetchTAT typed error contract is related and similarly touches credential-rejection classification.

Suggested reviewers

  • MaxHuang22
  • liangshuo-1

Poem

🐰 From JSON tunnels to OAuth's new gate,
I hopped and changed the token's fate.
Forms now whisper client and secret,
Errors sorted, typed and neat.
I nibble bugs and bounce elate.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: migrating TAT minting to the OAuth v3 Token Endpoint, which is the central focus of all file modifications.
Docstring Coverage ✅ Passed Docstring coverage is 81.48% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description comprehensively covers all required template sections with detailed context, motivation, changes, and testing verification.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/token_endpoint_oauth_v3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@b7db0f7cdc7a057ce0a28ea34532ddda53526eea

🧩 Skill update

npx skills add larksuite/cli#feat/token_endpoint_oauth_v3 -y -g

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.83%. Comparing base (e64610f) to head (b7db0f7).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
internal/credential/tat_fetch.go 86.66% 2 Missing and 2 partials ⚠️
internal/credential/default_provider.go 80.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1408      +/-   ##
==========================================
+ Coverage   72.75%   72.83%   +0.07%     
==========================================
  Files         730      732       +2     
  Lines       69034    69157     +123     
==========================================
+ Hits        50228    50369     +141     
+ Misses      15034    15005      -29     
- Partials     3772     3783      +11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/credential/default_provider_test.go (1)

28-73: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use errs.ProblemOf for typed error assertions in these error-path tests.

These tests currently rely on errors.As (and in Line 66 path, only “not ConfigError”), which can miss regressions where the error stops being typed. Please assert typed metadata via errs.ProblemOf and then check category/subtype from the returned problem.

As per coding guidelines: “Error-path tests must assert typed metadata via errs.ProblemOf (category/subtype/param) and cause preservation, not message substrings alone.”

Suggested assertion pattern update
 func TestClassifyTATResponseCode_InvalidClient_MapsToInvalidClient(t *testing.T) {
 	err := classifyTATResponseCode(0, "invalid_client", "client authentication failed", "feishu", "cli_app_x")
 	if err == nil {
 		t.Fatal("expected non-nil error for invalid_client")
 	}
-	var cfgErr *errs.ConfigError
-	if !errors.As(err, &cfgErr) {
-		t.Fatalf("expected *errs.ConfigError, got %T: %v", err, err)
-	}
-	if cfgErr.Category != errs.CategoryConfig {
-		t.Errorf("Category = %q, want %q", cfgErr.Category, errs.CategoryConfig)
-	}
-	if cfgErr.Subtype != errs.SubtypeInvalidClient {
-		t.Errorf("Subtype = %q, want %q", cfgErr.Subtype, errs.SubtypeInvalidClient)
+	p, ok := errs.ProblemOf(err)
+	if !ok {
+		t.Fatalf("expected typed errs.* error, got %T: %v", err, err)
+	}
+	if p.Category != errs.CategoryConfig {
+		t.Errorf("Category = %q, want %q", p.Category, errs.CategoryConfig)
+	}
+	if p.Subtype != errs.SubtypeInvalidClient {
+		t.Errorf("Subtype = %q, want %q", p.Subtype, errs.SubtypeInvalidClient)
 	}
-	if cfgErr.Hint == "" {
+	if p.Hint == "" {
 		t.Error("Hint must be non-empty so the user gets a recovery action")
 	}
 }

 func TestClassifyTATResponseCode_OtherErrorFallsThrough(t *testing.T) {
 	err := classifyTATResponseCode(20068, "invalid_scope", "unauthorized scope", "feishu", "cli_app_x")
 	if err == nil {
 		t.Fatal("expected non-nil error for invalid_scope")
 	}
-	var cfgErr *errs.ConfigError
-	if errors.As(err, &cfgErr) {
-		t.Fatalf("invalid_scope must not be classified as ConfigError, got %T", err)
+	p, ok := errs.ProblemOf(err)
+	if !ok {
+		t.Fatalf("expected typed errs.* error, got %T: %v", err, err)
+	}
+	if p.Category == errs.CategoryConfig && p.Subtype == errs.SubtypeInvalidClient {
+		t.Fatalf("invalid_scope must not be classified as ConfigError/InvalidClient")
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/credential/default_provider_test.go` around lines 28 - 73, Replace
the current errors.As-based assertions in the three tests that call
classifyTATResponseCode with calls to errs.ProblemOf: call p :=
errs.ProblemOf(err) and assert p is non-nil for cases that should yield a typed
problem (TestClassifyTATResponseCode_InvalidClient_MapsToInvalidClient and
TestClassifyTATResponseCode_UnauthorizedClient_MapsToInvalidClient), then check
p.Category == errs.CategoryConfig and p.Subtype == errs.SubtypeInvalidClient;
for TestClassifyTATResponseCode_OtherErrorFallsThrough assert
errs.ProblemOf(err) is nil or that p.Category != errs.CategoryConfig to ensure
it isn’t classified as a ConfigError; also ensure the original error is still
the cause (err wraps the problem) when applicable.

Source: Coding guidelines

🧹 Nitpick comments (2)
internal/credential/tat_fetch_test.go (1)

89-98: ⚡ Quick win

Use errs.ProblemOf for typed metadata assertions in error-path tests.

These assertions currently validate typed errors via errors.As / errs.IsTyped, but the repo test rule requires asserting category/subtype through errs.ProblemOf on error paths.

Suggested test-assertion adjustment
-	var cfgErr *errs.ConfigError
-	if !errors.As(err, &cfgErr) {
-		t.Fatalf("error not *errs.ConfigError: %T %v", err, err)
-	}
-	if cfgErr.Category != errs.CategoryConfig {
-		t.Errorf("Category = %q, want %q", cfgErr.Category, errs.CategoryConfig)
-	}
-	if cfgErr.Subtype != errs.SubtypeInvalidClient {
-		t.Errorf("Subtype = %q, want %q", cfgErr.Subtype, errs.SubtypeInvalidClient)
-	}
+	prob, ok := errs.ProblemOf(err)
+	if !ok {
+		t.Fatalf("expected typed problem, got %T %v", err, err)
+	}
+	if prob.Category != errs.CategoryConfig {
+		t.Errorf("Category = %q, want %q", prob.Category, errs.CategoryConfig)
+	}
+	if prob.Subtype != errs.SubtypeInvalidClient {
+		t.Errorf("Subtype = %q, want %q", prob.Subtype, errs.SubtypeInvalidClient)
+	}
@@
-	if !errs.IsTyped(err) {
-		t.Fatalf("expected a typed errs.* error, got %T %v", err, err)
-	}
+	prob, ok := errs.ProblemOf(err)
+	if !ok {
+		t.Fatalf("expected typed problem, got %T %v", err, err)
+	}
+	if prob.Category == errs.CategoryConfig && prob.Subtype == errs.SubtypeInvalidClient {
+		t.Errorf("invalid_scope must not map to Config/InvalidClient")
+	}

As per coding guidelines: "**/*_test.go: Error-path tests must assert typed metadata via errs.ProblemOf (category/subtype/param) and cause preservation, not message substrings alone."

Also applies to: 113-119

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/credential/tat_fetch_test.go` around lines 89 - 98, Replace the
current errors.As based assertions in the test that extract cfgErr and compare
Category/Subtype with direct usage of errs.ProblemOf on the returned error;
specifically, stop using errors.As/errs.IsTyped with cfgErr and instead call
errs.ProblemOf(err) to retrieve the Problem value and assert Problem.Category ==
errs.CategoryConfig and Problem.Subtype == errs.SubtypeInvalidClient (and assert
the preserved cause if applicable). Update both the block referencing cfgErr
(the errors.As call and subsequent Category/Subtype checks) and the similar
assertions at lines 113-119 to use errs.ProblemOf for typed metadata assertions
and verify cause preservation.

Source: Coding guidelines

cmd/config/init_probe_test.go (1)

92-106: ⚡ Quick win

Align typed error assertions with errs.ProblemOf in probe error-path tests.

For these error-path tests, prefer asserting category/subtype through errs.ProblemOf instead of only concrete type checks (errors.As) or errs.IsTyped presence checks.

As per coding guidelines: "**/*_test.go: Error-path tests must assert typed metadata via errs.ProblemOf (category/subtype/param) and cause preservation, not message substrings alone."

Also applies to: 165-167

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/config/init_probe_test.go` around lines 92 - 106, Replace the
concrete-type assertions in assertConfigRejection to use errs.ProblemOf to
validate the error's typed metadata (category, subtype and params) and ensure
the underlying cause is preserved: call errs.ProblemOf(err) and assert the
returned Problem has Category == errs.CategoryConfig and Subtype ==
errs.SubtypeInvalidClient and that Problem.Cause matches the original cause (or
use errors.Is on the cause); remove or keep a minimal errors.As check only if
you still need the concrete type but rely on errs.ProblemOf for metadata checks;
apply the same change to the similar assertions around lines 165-167.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/client/client_test.go`:
- Around line 667-690: Add tests that exercise the actual retry behavior in
DoSDKRequest instead of only respMissingScope: write one test that simulates an
initial API response with code 99991679, verifies DoSDKRequest detects
respMissingScope, triggers a scoped bot TAT mint, retries the request
successfully, and then reuses that scoped token for a subsequent DoSDKRequest
call (assert the mint was called only once and the second call used the scoped
token). Also add a test where the retry also returns 99991679 to assert
DoSDKRequest does not loop (only one retry) and surfaces the error; use the same
test harness/mocks used elsewhere in client_test.go to stub responses and count
minting attempts, referencing DoSDKRequest and respMissingScope to locate the
logic to exercise.

In `@internal/client/client.go`:
- Around line 50-54: The botScopedTok cache currently returns a stored scoped
TAT forever; modify resolveAccessToken (and related code paths that set
fullScopeBotToken / botScopedTok) to store token expiry/issued-at metadata when
caching, or else clear/invalidate botScopedTok under botScopedMu when API
responses indicate token invalid/expired (e.g., token-invalid, 401/403 or the
99991679 missing_scope flow), so subsequent calls re-run
Credential.ResolveToken; ensure both the lazy-set site (where
fullScopeBotToken/botScopedTok is populated) and the DoSDKRequest error handling
paths that detect invalid tokens clear the cached token under the same mutex so
the process can recover without restart.

---

Outside diff comments:
In `@internal/credential/default_provider_test.go`:
- Around line 28-73: Replace the current errors.As-based assertions in the three
tests that call classifyTATResponseCode with calls to errs.ProblemOf: call p :=
errs.ProblemOf(err) and assert p is non-nil for cases that should yield a typed
problem (TestClassifyTATResponseCode_InvalidClient_MapsToInvalidClient and
TestClassifyTATResponseCode_UnauthorizedClient_MapsToInvalidClient), then check
p.Category == errs.CategoryConfig and p.Subtype == errs.SubtypeInvalidClient;
for TestClassifyTATResponseCode_OtherErrorFallsThrough assert
errs.ProblemOf(err) is nil or that p.Category != errs.CategoryConfig to ensure
it isn’t classified as a ConfigError; also ensure the original error is still
the cause (err wraps the problem) when applicable.

---

Nitpick comments:
In `@cmd/config/init_probe_test.go`:
- Around line 92-106: Replace the concrete-type assertions in
assertConfigRejection to use errs.ProblemOf to validate the error's typed
metadata (category, subtype and params) and ensure the underlying cause is
preserved: call errs.ProblemOf(err) and assert the returned Problem has Category
== errs.CategoryConfig and Subtype == errs.SubtypeInvalidClient and that
Problem.Cause matches the original cause (or use errors.Is on the cause); remove
or keep a minimal errors.As check only if you still need the concrete type but
rely on errs.ProblemOf for metadata checks; apply the same change to the similar
assertions around lines 165-167.

In `@internal/credential/tat_fetch_test.go`:
- Around line 89-98: Replace the current errors.As based assertions in the test
that extract cfgErr and compare Category/Subtype with direct usage of
errs.ProblemOf on the returned error; specifically, stop using
errors.As/errs.IsTyped with cfgErr and instead call errs.ProblemOf(err) to
retrieve the Problem value and assert Problem.Category == errs.CategoryConfig
and Problem.Subtype == errs.SubtypeInvalidClient (and assert the preserved cause
if applicable). Update both the block referencing cfgErr (the errors.As call and
subsequent Category/Subtype checks) and the similar assertions at lines 113-119
to use errs.ProblemOf for typed metadata assertions and verify cause
preservation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 75da74f1-057f-4193-8b30-c04c283c6a81

📥 Commits

Reviewing files that changed from the base of the PR and between e53f9d9 and e6fda52.

📒 Files selected for processing (11)
  • cmd/config/init_probe.go
  • cmd/config/init_probe_test.go
  • internal/client/client.go
  • internal/client/client_test.go
  • internal/core/types.go
  • internal/core/types_test.go
  • internal/credential/default_provider.go
  • internal/credential/default_provider_test.go
  • internal/credential/tat_fetch.go
  • internal/credential/tat_fetch_test.go
  • internal/credential/types.go

Comment thread internal/client/client_test.go Outdated
Comment thread internal/client/client.go Outdated
Keep only the core TAT → unified OAuth v3 Token Endpoint swap; remove the
Agent Employee (hybrid TAT) missing-scope auto-retry, which is out of scope
for this change.

Removed:
- client: botScopedTok cache, resolveAccessToken cache check, DoSDKRequest
  99991679 auto-retry, respMissingScope / fullScopeBotToken / appGrantedScopes
- credential: TokenSpec.Scopes; scope parameter on FetchTAT / resolveTAT /
  doResolveTAT (fetchTAT folded back into FetchTAT)
- tests: missing-scope retry tests, scope-bound mint tests

Retained: v3 endpoint (client_credentials / client_secret_post / form-encoded),
OAuth error classification (invalid_client / unauthorized_client -> InvalidClient)
with the code-0 backstop, and init-probe wording.
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