184 lines
4.9 KiB
Go
184 lines
4.9 KiB
Go
// ABOUTME: Authentication providers for EdgeXR Master Controller API
|
|
// ABOUTME: Supports Bearer token authentication with pluggable provider interface
|
|
|
|
package edgeconnect
|
|
|
|
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 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
|
|
}
|