// ABOUTME: Authentication providers for EdgeXR Master Controller API // ABOUTME: Supports Bearer token authentication with pluggable provider interface package v2 import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "sync" "time" ) // AuthProvider interface for attaching authentication to requests type AuthProvider interface { // Attach adds authentication headers to the request Attach(ctx context.Context, req *http.Request) error } // StaticTokenProvider implements Bearer token authentication with a fixed token type StaticTokenProvider struct { Token string } // NewStaticTokenProvider creates a new static token provider func NewStaticTokenProvider(token string) *StaticTokenProvider { return &StaticTokenProvider{Token: token} } // Attach adds the Bearer token to the request Authorization header func (s *StaticTokenProvider) Attach(ctx context.Context, req *http.Request) error { if s.Token != "" { req.Header.Set("Authorization", "Bearer "+s.Token) } return nil } // UsernamePasswordProvider implements dynamic token retrieval using username/password // This matches the existing client/client.go RetrieveToken implementation type UsernamePasswordProvider struct { BaseURL string Username string Password string HTTPClient *http.Client // Token caching mu sync.RWMutex cachedToken string tokenExpiry time.Time } // NewUsernamePasswordProvider creates a new username/password auth provider func NewUsernamePasswordProvider(baseURL, username, password string, httpClient *http.Client) *UsernamePasswordProvider { if httpClient == nil { httpClient = &http.Client{Timeout: 30 * time.Second} } return &UsernamePasswordProvider{ BaseURL: strings.TrimRight(baseURL, "/"), Username: username, Password: password, HTTPClient: httpClient, } } // Attach retrieves a token (with caching) and adds it to the Authorization header func (u *UsernamePasswordProvider) Attach(ctx context.Context, req *http.Request) error { token, err := u.getToken(ctx) if err != nil { return fmt.Errorf("failed to get token: %w", err) } if token != "" { req.Header.Set("Authorization", "Bearer "+token) } return nil } // getToken retrieves a token, using cache if valid func (u *UsernamePasswordProvider) getToken(ctx context.Context) (string, error) { // Check cache first u.mu.RLock() if u.cachedToken != "" && time.Now().Before(u.tokenExpiry) { token := u.cachedToken u.mu.RUnlock() return token, nil } u.mu.RUnlock() // Need to retrieve new token u.mu.Lock() defer u.mu.Unlock() // Double-check after acquiring write lock if u.cachedToken != "" && time.Now().Before(u.tokenExpiry) { return u.cachedToken, nil } // Retrieve token using existing RetrieveToken logic token, err := u.retrieveToken(ctx) if err != nil { return "", err } // Cache token with reasonable expiry (assume 1 hour, can be configurable) u.cachedToken = token u.tokenExpiry = time.Now().Add(1 * time.Hour) return token, nil } // retrieveToken implements the same logic as the existing client/client.go RetrieveToken method func (u *UsernamePasswordProvider) retrieveToken(ctx context.Context) (string, error) { // Marshal credentials - same as existing implementation jsonData, err := json.Marshal(map[string]string{ "username": u.Username, "password": u.Password, }) if err != nil { return "", err } // Create request - same as existing implementation loginURL := u.BaseURL + "/api/v1/login" request, err := http.NewRequestWithContext(ctx, "POST", loginURL, bytes.NewBuffer(jsonData)) if err != nil { return "", err } request.Header.Set("Content-Type", "application/json") // Execute request resp, err := u.HTTPClient.Do(request) if err != nil { return "", err } defer func() { _ = resp.Body.Close() }() // Read response body - same as existing implementation body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("error reading response body: %v", err) } if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("login failed with status %d: %s", resp.StatusCode, string(body)) } // Parse JSON response - same as existing implementation var respData struct { Token string `json:"token"` } err = json.Unmarshal(body, &respData) if err != nil { return "", fmt.Errorf("error parsing JSON (status %d): %v", resp.StatusCode, err) } return respData.Token, nil } // InvalidateToken clears the cached token, forcing a new login on next request func (u *UsernamePasswordProvider) InvalidateToken() { u.mu.Lock() defer u.mu.Unlock() u.cachedToken = "" u.tokenExpiry = time.Time{} } // NoAuthProvider implements no authentication (for testing or public endpoints) type NoAuthProvider struct{} // NewNoAuthProvider creates a new no-auth provider func NewNoAuthProvider() *NoAuthProvider { return &NoAuthProvider{} } // Attach does nothing (no authentication) func (n *NoAuthProvider) Attach(ctx context.Context, req *http.Request) error { return nil }