feat(auth): add edition overlay auth credentials and token marker for embedded mode
- Add AuthClientID and AuthClientFromMCP fields to edition Hooks, allowing overlays to override the default OAuth client ID and route auth through MCP endpoints. - Introduce token marker file (token.json) mechanism so the host application in embedded mode can detect authentication state without accessing the keychain. - Update SaveTokenData/DeleteTokenData to write/remove the marker file when running in embedded mode. - Update ClientID() and IsClientIDFromMCP() to respect edition overrides.
This commit is contained in:
1 parent
953b422295
commit
aec852f971
3 files changed
+56
-8
No files matched your search
@@ -18,6 +18,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -110,7 +112,7 @@ func SetClientIDFromMCP(id string) {
|
||||
func IsClientIDFromMCP() bool {
|
||||
clientMu.RLock()
|
||||
defer clientMu.RUnlock()
|
||||
return clientIDFromMCP
|
||||
return clientIDFromMCP || edition.Get().AuthClientFromMCP
|
||||
}
|
||||
|
||||
// GetUserAccessTokenURL returns the appropriate token exchange URL.
|
||||
@@ -189,6 +191,9 @@ func ClientID() string {
|
||||
if override != "" {
|
||||
return override
|
||||
}
|
||||
if id := edition.Get().AuthClientID; id != "" {
|
||||
return id
|
||||
}
|
||||
// Try loading from persisted app config
|
||||
if id, _ := ResolveAppCredentials(getDefaultConfigDir()); id != "" {
|
||||
return id
|
||||
|
||||
+46
-7
@@ -20,7 +20,11 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||
)
|
||||
|
||||
// TokenData holds the OAuth token set persisted to disk.
|
||||
@@ -61,10 +65,46 @@ func (t *TokenData) HasPersistentCode() bool {
|
||||
return t != nil && t.PersistentCode != ""
|
||||
}
|
||||
|
||||
const tokenJSONFile = "token.json"
|
||||
|
||||
// TokenMarker is a lightweight file the host application reads to detect
|
||||
// whether the CLI has a valid token without accessing the keychain.
|
||||
type TokenMarker struct {
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// WriteTokenMarker writes a token.json marker containing only an updated_at
|
||||
// timestamp. The host application uses this file's presence and mtime to
|
||||
// decide whether it needs to trigger a new auth exchange.
|
||||
func WriteTokenMarker(configDir string) error {
|
||||
marker := TokenMarker{UpdatedAt: time.Now().Format(time.RFC3339)}
|
||||
data, _ := json.MarshalIndent(marker, "", " ")
|
||||
if err := os.MkdirAll(configDir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := filepath.Join(configDir, tokenJSONFile+".tmp")
|
||||
if err := os.WriteFile(tmp, data, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, filepath.Join(configDir, tokenJSONFile))
|
||||
}
|
||||
|
||||
// DeleteTokenMarker removes the token.json marker file.
|
||||
func DeleteTokenMarker(configDir string) error {
|
||||
return os.Remove(filepath.Join(configDir, tokenJSONFile))
|
||||
}
|
||||
|
||||
// SaveTokenData saves TokenData to the platform keychain.
|
||||
// Uses the new keychain-based storage with random master key for better security.
|
||||
// In embedded mode, it also writes a token.json marker file so the host
|
||||
// application can detect authentication state without accessing the keychain.
|
||||
func SaveTokenData(configDir string, data *TokenData) error {
|
||||
return SaveTokenDataKeychain(data)
|
||||
if err := SaveTokenDataKeychain(data); err != nil {
|
||||
return err
|
||||
}
|
||||
if edition.Get().IsEmbedded {
|
||||
_ = WriteTokenMarker(configDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadTokenData reads TokenData from the platform keychain.
|
||||
@@ -91,14 +131,13 @@ func LoadTokenData(configDir string) (*TokenData, error) {
|
||||
}
|
||||
|
||||
// DeleteTokenData removes token data from both keychain and legacy storage.
|
||||
// In embedded mode, it also removes the token.json marker file.
|
||||
func DeleteTokenData(configDir string) error {
|
||||
// Delete from keychain
|
||||
keychainErr := DeleteTokenDataKeychain()
|
||||
|
||||
// Also clean up any legacy .data file
|
||||
legacyErr := DeleteSecureData(configDir)
|
||||
|
||||
// Return keychain error if any, otherwise legacy error
|
||||
if edition.Get().IsEmbedded {
|
||||
_ = DeleteTokenMarker(configDir)
|
||||
}
|
||||
if keychainErr != nil {
|
||||
return keychainErr
|
||||
}
|
||||
|
||||
@@ -77,6 +77,10 @@ type Hooks struct {
|
||||
OnAuthError func(configDir string, err error) error
|
||||
TokenProvider func(ctx context.Context, fallback func() (string, error)) (string, error)
|
||||
|
||||
// --- auth credentials (overlay-only) ---
|
||||
AuthClientID string // non-empty overrides DefaultClientID
|
||||
AuthClientFromMCP bool // true routes OAuth through MCP endpoints
|
||||
|
||||
// --- product & endpoint ---
|
||||
StaticServers func() []ServerInfo // non-nil → skip Market discovery
|
||||
VisibleProducts func() []string // non-nil → override help visibility
|
||||
|
||||
Reference in new issue
Block a user