From d7a51a0f60cfd195f3a22715fa42436a5f284898 Mon Sep 17 00:00:00 2001 From: Turbootzz Date: Fri, 24 Oct 2025 18:28:52 +0200 Subject: [PATCH] changed auth method --- .env.example | 8 +-- cmd/api/main.go | 24 ++++---- internal/vaultwarden/client.go | 14 ++++- internal/vaultwarden/init.go | 43 ++++++++++++++ internal/vaultwarden/session.go | 100 ++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 18 deletions(-) create mode 100644 internal/vaultwarden/init.go create mode 100644 internal/vaultwarden/session.go diff --git a/.env.example b/.env.example index 807ff4d..4812dcc 100644 --- a/.env.example +++ b/.env.example @@ -13,12 +13,8 @@ CORS_ALLOWED_ORIGINS=http://localhost:3000 # URL of your Vaultwarden instance (no trailing slash) VAULTWARDEN_URL=https://vault.yourdomain.com -# Authentication Method 1: API Keys (RECOMMENDED - doesn't expire) -VAULTWARDEN_CLIENT_ID=user.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -VAULTWARDEN_CLIENT_SECRET=your-long-client-secret-here - -# Authentication Method 2: Session Token (LEGACY - expires after some time) -# VAULTWARDEN_ACCESS_TOKEN=your-session-token-here +# Authentication +VAULTWARDEN_ACCESS_TOKEN=your-session-token-here # Performance & Caching # How long to cache secrets in memory (reduces API calls) diff --git a/cmd/api/main.go b/cmd/api/main.go index 74ed204..0da6856 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -30,18 +30,22 @@ func main() { // Initialize Vaultwarden client var vaultClient *vaultwarden.Client - if cfg.VaultwardenClientID != "" || cfg.VaultwardenSecret != "" { - if cfg.VaultwardenClientID == "" || cfg.VaultwardenSecret == "" { - logger.Error.Fatal("Both VaultwardenClientID and VaultwardenSecret must be provided for API key authentication") + + vaultwardenEmail := os.Getenv("VAULTWARDEN_EMAIL") + vaultwardenPassword := os.Getenv("VAULTWARDEN_PASSWORD") + + if vaultwardenEmail != "" && vaultwardenPassword != "" { + logger.Info.Println("Initializing Bitwarden CLI with credentials") + sessionToken, err := vaultwarden.InitializeBitwardenCLI(cfg.VaultwardenURL, vaultwardenEmail, vaultwardenPassword) + if err != nil { + logger.Error.Fatalf("Failed to initialize Bitwarden CLI: %v", err) } - logger.Info.Println("Using API key authentication (recommended)") - vaultClient = vaultwarden.NewClientWithAuth(cfg.VaultwardenURL, cfg.VaultwardenClientID, cfg.VaultwardenSecret, cfg.CacheTTL) - } else { - if cfg.VaultwardenToken == "" { - logger.Error.Fatal("VaultwardenToken must be provided for legacy session token authentication") - } - logger.Warn.Println("Using session token authentication (legacy - token will expire!)") + vaultClient = vaultwarden.NewClient(cfg.VaultwardenURL, sessionToken, cfg.CacheTTL) + } else if cfg.VaultwardenToken != "" { + logger.Warn.Println("Using provided session token (will expire)") vaultClient = vaultwarden.NewClient(cfg.VaultwardenURL, cfg.VaultwardenToken, cfg.CacheTTL) + } else { + logger.Error.Fatal("No authentication method configured. Set VAULTWARDEN_EMAIL+VAULTWARDEN_PASSWORD or VAULTWARDEN_ACCESS_TOKEN") } // Initialize handlers diff --git a/internal/vaultwarden/client.go b/internal/vaultwarden/client.go index 903ddc8..656abc4 100644 --- a/internal/vaultwarden/client.go +++ b/internal/vaultwarden/client.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "net/http" + "os" + "strings" "sync" "time" @@ -66,11 +68,17 @@ func NewClient(baseURL, token string, cacheTTL time.Duration) *Client { enabled: cacheTTL > 0, } - // Start cache cleanup goroutine if caching is enabled if cacheTTL > 0 { go cache.startCleanup(cacheTTL) } + if token == "" { + if sessionData, err := os.ReadFile("/tmp/bw_session"); err == nil { + token = strings.TrimSpace(string(sessionData)) + logger.Info.Println("Loaded session token from /tmp/bw_session") + } + } + return &Client{ baseURL: baseURL, token: token, @@ -195,8 +203,10 @@ func (c *Client) fetchSecret(name string) (string, error) { return "", fmt.Errorf("failed to decode response: %w", err) } - // Search for the secret by name + logger.Info.Printf("Found %d ciphers from Vaultwarden API", len(cipherResp.Data)) + for _, cipher := range cipherResp.Data { + logger.Info.Printf("Cipher name: '%s' (looking for: '%s')", cipher.Name, name) if cipher.Name == name { return c.extractSecretValue(cipher) } diff --git a/internal/vaultwarden/init.go b/internal/vaultwarden/init.go new file mode 100644 index 0000000..9febae1 --- /dev/null +++ b/internal/vaultwarden/init.go @@ -0,0 +1,43 @@ +package vaultwarden + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/thijsherman/vaultwarden-api/pkg/logger" +) + +func InitializeBitwardenCLI(serverURL, email, password string) (string, error) { + logger.Info.Println("Initializing Bitwarden CLI...") + + cmd := exec.Command("bw", "config", "server", serverURL) + if output, err := cmd.CombinedOutput(); err != nil { + return "", fmt.Errorf("config server failed: %w - %s", err, output) + } + + cmd = exec.Command("bw", "login", email, "--passwordenv", "BW_PASSWORD", "--raw") + cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", password)) + output, err := cmd.CombinedOutput() + + outputStr := strings.TrimSpace(string(output)) + if err != nil && !strings.Contains(outputStr, "already logged in") { + return "", fmt.Errorf("login failed: %w - %s", err, outputStr) + } + + cmd = exec.Command("bw", "unlock", "--passwordenv", "BW_PASSWORD", "--raw") + cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", password)) + sessionToken, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("unlock failed: %w", err) + } + + token := strings.TrimSpace(string(sessionToken)) + if token == "" { + return "", fmt.Errorf("received empty session token") + } + + logger.Info.Println("Bitwarden CLI initialized successfully") + return token, nil +} diff --git a/internal/vaultwarden/session.go b/internal/vaultwarden/session.go new file mode 100644 index 0000000..d41353f --- /dev/null +++ b/internal/vaultwarden/session.go @@ -0,0 +1,100 @@ +package vaultwarden + +import ( + "fmt" + "os" + "os/exec" + "sync" + "time" + + "github.com/thijsherman/vaultwarden-api/pkg/logger" +) + +type SessionManager struct { + mu sync.RWMutex + sessionToken string + serverURL string + email string + password string + refreshTicker *time.Ticker + stopChan chan bool +} + +func NewSessionManager(serverURL, email, password string) *SessionManager { + sm := &SessionManager{ + serverURL: serverURL, + email: email, + password: password, + stopChan: make(chan bool), + } + + if err := sm.login(); err != nil { + logger.Warn.Printf("Initial login failed: %v - will retry", err) + } + + sm.startAutoRefresh() + return sm +} + +func (sm *SessionManager) GetToken() string { + sm.mu.RLock() + defer sm.mu.RUnlock() + return sm.sessionToken +} + +func (sm *SessionManager) login() error { + sm.mu.Lock() + defer sm.mu.Unlock() + + cmd := exec.Command("bw", "config", "server", sm.serverURL) + if err := cmd.Run(); err != nil { + return fmt.Errorf("config server failed: %w", err) + } + + if sm.password != "" { + cmd = exec.Command("bw", "login", sm.email, "--passwordenv", "BW_PASSWORD") + cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", sm.password)) + output, err := cmd.CombinedOutput() + if err != nil && string(output) != "" && !contains(string(output), "already logged in") { + return fmt.Errorf("login failed: %w - %s", err, output) + } + } + + cmd = exec.Command("bw", "unlock", "--passwordenv", "BW_PASSWORD", "--raw") + cmd.Env = append(os.Environ(), fmt.Sprintf("BW_PASSWORD=%s", sm.password)) + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("unlock failed: %w", err) + } + + sm.sessionToken = string(output) + logger.Info.Println("Successfully obtained Bitwarden session token") + return nil +} + +func (sm *SessionManager) startAutoRefresh() { + sm.refreshTicker = time.NewTicker(6 * time.Hour) + + go func() { + for { + select { + case <-sm.refreshTicker.C: + logger.Info.Println("Refreshing Bitwarden session...") + if err := sm.login(); err != nil { + logger.Error.Printf("Session refresh failed: %v", err) + } + case <-sm.stopChan: + return + } + } + }() +} + +func (sm *SessionManager) Stop() { + sm.refreshTicker.Stop() + sm.stopChan <- true +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && s[:len(substr)] == substr +}