From 99830f4d6cebbb413bc44e3c32aa8de6d2fdd8ad Mon Sep 17 00:00:00 2001 From: AlbertSun Date: Wed, 17 Jun 2026 20:04:33 +0800 Subject: [PATCH] fix(auth): reject private_key_jwt config when no signing key was bound --- cmd/config/init_auth_method_test.go | 15 +++++++++++++++ cmd/config/init_interactive.go | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/cmd/config/init_auth_method_test.go b/cmd/config/init_auth_method_test.go index 65562940f..bb110c3ca 100644 --- a/cmd/config/init_auth_method_test.go +++ b/cmd/config/init_auth_method_test.go @@ -57,6 +57,21 @@ func TestResolveRegisterAuthMethod(t *testing.T) { } } +// TestValidatePKJWTKeyBinding covers the guard that rejects a registration +// resolving to private_key_jwt with no signing key bound (e.g. an existing +// secret-based app was selected on the confirm page). +func TestValidatePKJWTKeyBinding(t *testing.T) { + if err := validatePKJWTKeyBinding(core.AuthMethodPrivateKeyJWT, ""); err == nil { + t.Error("pkjwt with empty keyLabel: expected error") + } + if err := validatePKJWTKeyBinding(core.AuthMethodPrivateKeyJWT, "agent-key"); err != nil { + t.Errorf("pkjwt with keyLabel: expected nil, got %v", err) + } + if err := validatePKJWTKeyBinding(core.AuthMethodClientSecret, ""); err != nil { + t.Errorf("client_secret: expected nil, got %v", err) + } +} + // TestResolveFinalAuthMethod locks the authoritative-method logic. The 2nd case // is the real bug: we requested private_key_jwt but the server resolved to an // existing client_secret app — we must persist client_secret, not pkjwt. diff --git a/cmd/config/init_interactive.go b/cmd/config/init_interactive.go index b5a8cdb13..f26ca7d33 100644 --- a/cmd/config/init_interactive.go +++ b/cmd/config/init_interactive.go @@ -350,6 +350,9 @@ func runCreateAppFlow(ctx context.Context, f *cmdutil.Factory, brandOverride cor if finalMethod == core.AuthMethodPrivateKeyJWT { keyToStore = keyLabel } + if err := validatePKJWTKeyBinding(finalMethod, keyToStore); err != nil { + return nil, err + } return &configInitResult{ Mode: "create", Brand: finalBrand, @@ -360,6 +363,21 @@ func runCreateAppFlow(ctx context.Context, f *cmdutil.Factory, brandOverride cor }, nil } +// validatePKJWTKeyBinding rejects a registration that resolved to +// private_key_jwt without a signing key bound to it. keyLabel is non-empty only +// when the local flow chose private_key_jwt and signed a TEE attestation; a +// resolved method of private_key_jwt with no key handle would save an unusable +// config (rejected later at config load, surfacing as "saved OK, fails on first +// use"), so it is caught here at registration time instead. +func validatePKJWTKeyBinding(finalMethod, keyLabel string) error { + if finalMethod == core.AuthMethodPrivateKeyJWT && keyLabel == "" { + return errs.NewConfigError(errs.SubtypeInvalidClient, + "registration resolved to private_key_jwt but no signing key was bound to this app (an existing secret-based app may have been selected)"). + WithHint("re-register with: lark-cli config init --new --auth-method private_key_jwt") + } + return nil +} + // resolveFinalAuthMethod picks the authoritative method from the poll result, // preferring private_key_jwt, then client_secret. It falls back to the requested // method when the server returns nothing (older servers).