From a815ce85b079d262aa8e71a2033589fa00adc3ce Mon Sep 17 00:00:00 2001 From: Zhang-986 <3225483474@qq.com> Date: Sun, 21 Jun 2026 11:09:21 +0800 Subject: [PATCH] fix(core): isolate token cache by app secret --- core/tokenmanager.go | 27 +++++--- core/tokenmanager_test.go | 125 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 138 insertions(+), 14 deletions(-) diff --git a/core/tokenmanager.go b/core/tokenmanager.go index e9cb1827..20dd0b34 100644 --- a/core/tokenmanager.go +++ b/core/tokenmanager.go @@ -14,6 +14,8 @@ package larkcore import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -33,7 +35,7 @@ func (m *TokenManager) getAppAccessToken(ctx context.Context, config *Config, ap return "", &CodeError{Code: ErrCodeClientAssertionProviderNotConfigured, Msg: "AppAccessToken is not available in ClientAssertion mode"} } - token, err := m.get(ctx, appAccessTokenKey(config.AppId)) + token, err := m.get(ctx, appAccessTokenKey(config.AppId, config.AppSecret)) if err != nil { return "", err } @@ -62,7 +64,7 @@ func (m *TokenManager) getTenantAccessToken(ctx context.Context, config *Config, return m.getTenantTokenByClientAssertion(ctx, config, tenantKey) } - token, err := m.get(ctx, tenantAccessTokenKey(config.AppId, tenantKey)) + token, err := m.get(ctx, tenantAccessTokenKey(config.AppId, config.AppSecret, tenantKey)) if err != nil { return "", err } @@ -155,18 +157,23 @@ type MarketplaceTenantAccessTokenReq struct { TenantKey string `json:"tenant_key"` } -func appAccessTokenKey(appID string) string { - return fmt.Sprintf("%s-%s", appAccessTokenKeyPrefix, appID) +func appAccessTokenKey(appID, appSecret string) string { + return fmt.Sprintf("%s:app_secret:%s:%s", appAccessTokenKeyPrefix, appID, appSecretFingerprint(appSecret)) } -func tenantAccessTokenKey(appID, tenantKey string) string { - return fmt.Sprintf("%s:app_secret:%s:%s", tenantAccessTokenKeyPrefix, appID, tenantKey) +func tenantAccessTokenKey(appID, appSecret, tenantKey string) string { + return fmt.Sprintf("%s:app_secret:%s:%s:%s", tenantAccessTokenKeyPrefix, appID, appSecretFingerprint(appSecret), tenantKey) } func clientAssertionTenantAccessTokenKey(appID, tenantKey, aud string) string { return fmt.Sprintf("%s:client_assertion:%s:%s:%s", tenantAccessTokenKeyPrefix, appID, tenantKey, aud) } +func appSecretFingerprint(appSecret string) string { + sum := sha256.Sum256([]byte(appSecret)) + return hex.EncodeToString(sum[:]) +} + func (m *TokenManager) getTenantTokenByClientAssertion(ctx context.Context, config *Config, tenantKey string) (string, error) { oauthBaseUrl, err := ResolveOAuthBaseUrl(config) if err != nil { @@ -282,7 +289,7 @@ func (m *TokenManager) getCustomAppAccessTokenThenCache(ctx context.Context, con return "", resp.CodeError } expire := time.Duration(resp.Expire)*time.Second - expiryDelta - err = m.set(ctx, appAccessTokenKey(config.AppId), resp.AppAccessToken, expire) + err = m.set(ctx, appAccessTokenKey(config.AppId, config.AppSecret), resp.AppAccessToken, expire) if err != nil { config.Logger.Warn(ctx, fmt.Sprintf("custom app appAccessToken save cache, err:%v", err)) } @@ -313,7 +320,7 @@ func (m *TokenManager) getCustomTenantAccessTokenThenCache(ctx context.Context, return "", tenantAccessTokenResp.CodeError } expire := time.Duration(tenantAccessTokenResp.Expire)*time.Second - expiryDelta - err = m.cache.Set(ctx, tenantAccessTokenKey(config.AppId, tenantKey), tenantAccessTokenResp.TenantAccessToken, expire) + err = m.cache.Set(ctx, tenantAccessTokenKey(config.AppId, config.AppSecret, tenantKey), tenantAccessTokenResp.TenantAccessToken, expire) if err != nil { config.Logger.Warn(ctx, fmt.Sprintf("custom app tenantAccessToken save cache, err:%v", err)) } @@ -357,7 +364,7 @@ func (m *TokenManager) getMarketplaceAppAccessTokenThenCache(ctx context.Context return "", appAccessTokenResp.CodeError } expire := time.Duration(appAccessTokenResp.Expire)*time.Second - expiryDelta - err = m.set(ctx, appAccessTokenKey(config.AppId), appAccessTokenResp.AppAccessToken, expire) + err = m.set(ctx, appAccessTokenKey(config.AppId, config.AppSecret), appAccessTokenResp.AppAccessToken, expire) if err != nil { config.Logger.Warn(ctx, fmt.Sprintf("marketplace app appAccessToken save cache, err:%v", err)) } @@ -393,7 +400,7 @@ func (m *TokenManager) getMarketplaceTenantAccessTokenThenCache(ctx context.Cont return "", tenantAccessTokenResp.CodeError } expire := time.Duration(tenantAccessTokenResp.Expire)*time.Second - expiryDelta - err = m.set(ctx, tenantAccessTokenKey(config.AppId, tenantKey), tenantAccessTokenResp.TenantAccessToken, expire) + err = m.set(ctx, tenantAccessTokenKey(config.AppId, config.AppSecret, tenantKey), tenantAccessTokenResp.TenantAccessToken, expire) if err != nil { config.Logger.Warn(ctx, fmt.Sprintf("market app tenantAccessToken save cache, err:%v", err)) } diff --git a/core/tokenmanager_test.go b/core/tokenmanager_test.go index ecf966cd..7fb0f381 100644 --- a/core/tokenmanager_test.go +++ b/core/tokenmanager_test.go @@ -17,6 +17,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strings" "testing" "time" ) @@ -39,7 +40,7 @@ func TestTokenManagerSetAndGet(t *testing.T) { cache := &localCache{} tokenManager := TokenManager{cache: cache} - err := tokenManager.set(context.Background(), tenantAccessTokenKey(config.AppId, "tenantKey"), "tokenValue", time.Minute) + err := tokenManager.set(context.Background(), tenantAccessTokenKey(config.AppId, config.AppSecret, "tenantKey"), "tokenValue", time.Minute) if err != nil { t.Errorf("set key failed ,%v", err) } @@ -56,14 +57,130 @@ func TestTokenManagerSetAndGet(t *testing.T) { } func TestTenantAccessTokenCacheKeysIncludeCredentialMode(t *testing.T) { - if got, want := tenantAccessTokenKey("cli_a", "tenantKey"), "tenant_access_token:app_secret:cli_a:tenantKey"; got != want { - t.Fatalf("unexpected app secret tenant token key: %s", got) + appKey := appAccessTokenKey("cli_a", "secret-a") + if appKey == appAccessTokenKey("cli_a", "secret-b") { + t.Fatalf("app token cache key must change when app secret changes") + } + if strings.Contains(appKey, "secret-a") { + t.Fatalf("app token cache key must not contain raw app secret: %s", appKey) + } + + tenantKey := tenantAccessTokenKey("cli_a", "secret-a", "tenantKey") + if tenantKey == tenantAccessTokenKey("cli_a", "secret-b", "tenantKey") { + t.Fatalf("tenant token cache key must change when app secret changes") + } + if strings.Contains(tenantKey, "secret-a") { + t.Fatalf("tenant token cache key must not contain raw app secret: %s", tenantKey) } if got, want := clientAssertionTenantAccessTokenKey("cli_a", "tenantKey", "accounts.feishu.cn"), "tenant_access_token:client_assertion:cli_a:tenantKey:accounts.feishu.cn"; got != want { t.Fatalf("unexpected client assertion tenant token key: %s", got) } } +func TestGetAppAccessTokenDoesNotReuseCacheAcrossAppSecrets(t *testing.T) { + var calls int + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + if r.URL.Path != AppAccessTokenInternalUrlPath { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var req SelfBuiltAppAccessTokenReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("decode body failed: %v", err) + } + if req.AppSecret != "correct-secret" { + _ = json.NewEncoder(w).Encode(&AppAccessTokenResp{ + CodeError: CodeError{Code: 19002, Msg: "invalid app secret"}, + }) + return + } + _ = json.NewEncoder(w).Encode(&AppAccessTokenResp{ + CodeError: CodeError{Code: 0}, + Expire: 7200, + AppAccessToken: "app-token", + }) + })) + defer server.Close() + + config := mockConfig() + config.BaseUrl = server.URL + config.HttpClient = server.Client() + config.EnableTokenCache = true + config.AppId = "cli_a" + config.AppSecret = "correct-secret" + + manager := TokenManager{cache: &localCache{}} + token, err := manager.getAppAccessToken(context.Background(), config, "") + if err != nil { + t.Fatalf("get app access token failed: %v", err) + } + if token != "app-token" { + t.Fatalf("unexpected token: %s", token) + } + + wrongSecretConfig := *config + wrongSecretConfig.AppSecret = "wrong-secret" + _, err = manager.getAppAccessToken(context.Background(), &wrongSecretConfig, "") + if err == nil { + t.Fatalf("expected wrong app secret to miss cache and return an auth error") + } + if calls != 2 { + t.Fatalf("expected wrong app secret to trigger a second token request, got %d calls", calls) + } +} + +func TestGetTenantAccessTokenDoesNotReuseCacheAcrossAppSecrets(t *testing.T) { + var calls int + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + if r.URL.Path != TenantAccessTokenInternalUrlPath { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var req SelfBuiltTenantAccessTokenReq + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("decode body failed: %v", err) + } + if req.AppSecret != "correct-secret" { + _ = json.NewEncoder(w).Encode(&TenantAccessTokenResp{ + CodeError: CodeError{Code: 19002, Msg: "invalid app secret"}, + }) + return + } + _ = json.NewEncoder(w).Encode(&TenantAccessTokenResp{ + CodeError: CodeError{Code: 0}, + Expire: 7200, + TenantAccessToken: "tenant-token", + }) + })) + defer server.Close() + + config := mockConfig() + config.BaseUrl = server.URL + config.HttpClient = server.Client() + config.EnableTokenCache = true + config.AppId = "cli_a" + config.AppSecret = "correct-secret" + + manager := TokenManager{cache: &localCache{}} + token, err := manager.getTenantAccessToken(context.Background(), config, "tenantKey", "") + if err != nil { + t.Fatalf("get tenant access token failed: %v", err) + } + if token != "tenant-token" { + t.Fatalf("unexpected token: %s", token) + } + + wrongSecretConfig := *config + wrongSecretConfig.AppSecret = "wrong-secret" + _, err = manager.getTenantAccessToken(context.Background(), &wrongSecretConfig, "tenantKey", "") + if err == nil { + t.Fatalf("expected wrong app secret to miss cache and return an auth error") + } + if calls != 2 { + t.Fatalf("expected wrong app secret to trigger a second token request, got %d calls", calls) + } +} + func TestGetTenantAccessTokenByClientAssertionIgnoresAppSecretCache(t *testing.T) { provider := &mockClientAssertionProvider{token: &Token{Value: "client-assertion"}} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -83,7 +200,7 @@ func TestGetTenantAccessTokenByClientAssertionIgnoresAppSecretCache(t *testing.T config.ClientAssertionProvider = provider cache := &localCache{} - if err := cache.Set(context.Background(), tenantAccessTokenKey(config.AppId, "tenantKey"), "cached-appsecret-token", time.Hour); err != nil { + if err := cache.Set(context.Background(), tenantAccessTokenKey(config.AppId, config.AppSecret, "tenantKey"), "cached-appsecret-token", time.Hour); err != nil { t.Fatalf("seed app secret cache failed: %v", err) }