“Our auth module is 5,000 lines of code and growing. Every new supplier adds 200-300 lines of auth logic.” — Senior Developer

“We have 15 different error types to handle. Developers spend more time parsing errors than writing business logic.” — Tech Lead

If this sounds familiar, you need Supplier Proxy’s authentication and error handling capabilities.

Part 1: Authentication Management

The Authentication Explosion

Hotel suppliers use many different authentication methods:

Auth Type Suppliers Using Complexity
Basic Auth 15% Low
API Key (Header) 25% Low
HMAC-SHA256 20% Medium
HMAC-SHA512 10% Medium-High
OAuth 1.0 5% Very High
OAuth 2.0 15% High
JWT 5% High
WSSecurity (XML) 3% Extreme
Custom 2% Extreme

Total: 50 suppliers, 9 different auth types

Supplier Proxy’s Authentication Architecture

┌────────────────────────────────────────────────────────────┐
│              Your Application                                │
│                                                              │
│  hotelbyte.SearchHotels({                                    │
│    apiKey: "HB-123456",                                      │
│  })                                                          │
└────────────────────────────────────────────────────────────┘
                          │
                          │ Single API Key
                          ▼
┌────────────────────────────────────────────────────────────┐
│           Supplier Proxy - Authentication Manager          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐    │
│  │  Auth Router │  │   Token     │  │    Cache     │    │
│  │              │  │  Manager     │  │              │    │
│  └──────────────┘  └──────────────┘  └──────────────┘    │
└────────────────────────────────────────────────────────────┘
                          │
          ┌───────────────┼───────────────┐
          │               │               │
          ▼               ▼               ▼
    ┌───────────┐   ┌───────────┐   ┌───────────┐
    │Basic Auth │   │   HMAC    │   │  OAuth2   │
    └───────────┘   └───────────┘   └───────────┘

Authentication Protocol Implementations

1. Basic Auth + API Key (Simple)

type BasicAuthConfig struct {
    Username string
    Password string
    APIKey   string
}

func (a *BasicAuthConfig) Apply(req *http.Request) {
    // Set API key
    if a.APIKey != "" {
        req.Header.Set("X-API-Key", a.APIKey)
        req.Header.Set("X-API-Secret", a.Password)
        return
    }

    // Set Basic Auth
    req.SetBasicAuth(a.Username, a.Password)
}

Configuration:

authConfigs := map[string]AuthConfig{
    "hotelbeds": {
        Type:     AuthTypeBasic,
        Username: "hotelbyte_api",
        Password: "secret_from_vault",
    },
    "agoda": {
        Type:   AuthTypeAPIKey,
        APIKey: "agoda_key_123",
    },
}

2. HMAC-SHA256 (Complex)

type HMACAuthConfig struct {
    APIKey    string
    APISecret string
    Algorithm string
    Headers   []string
}

func (h *HMACAuthConfig) Apply(req *http.Request) error {
    timestamp := time.Now().Unix()
    nonce := generateNonce()

    // Build signature string
    parts := []string{
        req.Method,
        req.URL.Path,
        strconv.FormatInt(timestamp, 10),
        nonce,
    }

    // Add specified headers
    for _, header := range h.Headers {
        parts = append(parts, req.Header.Get(header))
    }

    signatureString := strings.Join(parts, "\n")

    // Generate HMAC
    var hash hash.Hash
    switch h.Algorithm {
    case "SHA256":
        hash = hmac.New(sha256.New, []byte(h.APISecret))
    case "SHA512":
        hash = hmac.New(sha512.New, []byte(h.APISecret))
    default:
        return fmt.Errorf("unsupported algorithm: %s", h.Algorithm)
    }

    hash.Write([]byte(signatureString))
    signature := hex.EncodeToString(hash.Sum(nil))

    // Set headers
    req.Header.Set("X-API-Key", h.APIKey)
    req.Header.Set("X-Timestamp", strconv.FormatInt(timestamp, 10))
    req.Header.Set("X-Nonce", nonce)
    req.Header.Set("X-Signature", signature)

    return nil
}

Configuration:

authConfigs := map[string]AuthConfig{
    "dida": {
        Type:      AuthTypeHMAC,
        APIKey:    "dida_key",
        APISecret: "dida_secret",
        Algorithm: "SHA256",
        Headers:   []string{"Content-Type", "Accept"},
    },
    "derbysoft": {
        Type:      AuthTypeHMAC,
        APIKey:    "derby_key",
        APISecret: "derby_secret",
        Algorithm: "SHA512",
        Headers:   []string{"Content-Type"},
    },
}

3. OAuth 2.0 (Complex)

type OAuth2Config struct {
    ClientID     string
    ClientSecret string
    TokenURL     string
    Scopes       []string
}

type OAuth2TokenManager struct {
    configs map[string]*OAuth2Config
    tokens  map[string]*oauth2.Token
    mu      sync.RWMutex
    client  *http.Client
}

func (tm *OAuth2TokenManager) GetToken(supplier string) (string, error) {
    tm.mu.RLock()
    token, exists := tm.tokens[supplier]
    tm.mu.RUnlock()

    if exists && token.Valid() {
        return token.AccessToken, nil
    }

    tm.mu.Lock()
    defer tm.mu.Unlock()

    config := tm.configs[supplier]
    oauthConfig := &oauth2.Config{
        ClientID:     config.ClientID,
        ClientSecret: config.ClientSecret,
        Endpoint: oauth2.Endpoint{
            TokenURL: config.TokenURL,
        },
        Scopes: config.Scopes,
    }

    ctx := context.Background()
    newToken, err := oauthConfig.TokenSource(ctx, &oauth2.Token{}).Token()
    if err != nil {
        return "", fmt.Errorf("failed to get token: %w", err)
    }

    tm.tokens[supplier] = newToken
    return newToken.AccessToken, nil
}

func (tm *OAuth2TokenManager) Apply(req *http.Request, supplier string) error {
    token, err := tm.GetToken(supplier)
    if err != nil {
        return err
    }

    req.Header.Set("Authorization", "Bearer "+token)
    return nil
}

Configuration:

authConfigs := map[string]AuthConfig{
    "expedia": {
        Type:         AuthTypeOAuth2,
        ClientID:     "expedia_client_id",
        ClientSecret: "expedia_client_secret",
        TokenURL:     "https://api.expedia.com/v2/oauth/token",
        Scopes:       []string{"hotel.read", "hotel.write"},
    },
    "travelgds": {
        Type:         AuthTypeOAuth2,
        ClientID:     "gds_client_id",
        ClientSecret: "gds_client_secret",
        TokenURL:     "https://api.travelgds.com/oauth2/token",
        Scopes:       []string{"read:hotels"},
    },
}

4. JWT (Complex)

type JWTConfig struct {
    Issuer     string
    Subject    string
    PrivateKey []byte
    Algorithm  string
    Expiry     time.Duration
}

type JWTTokenManager struct {
    configs map[string]*JWTConfig
    tokens  map[string]*jwt.Token
    mu      sync.RWMutex
}

func (jtm *JWTTokenManager) GenerateToken(supplier string) (string, error) {
    jtm.mu.Lock()
    defer jtm.mu.Unlock()

    config := jtm.configs[supplier]

    claims := &jwt.StandardClaims{
        Issuer:    config.Issuer,
        Subject:   config.Subject,
        ExpiresAt: time.Now().Add(config.Expiry).Unix(),
        IssuedAt:  time.Now().Unix(),
    }

    var token *jwt.Token

    switch config.Algorithm {
    case "RS256":
        privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(config.PrivateKey)
        if err != nil {
            return "", err
        }
        token = jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
        return token.SignedString(privateKey)
    case "HS256":
        token = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
        return token.SignedString(config.PrivateKey)
    default:
        return "", fmt.Errorf("unsupported algorithm: %s", config.Algorithm)
    }
}

func (jtm *JWTTokenManager) Apply(req *http.Request, supplier string) error {
    token, err := jtm.GenerateToken(supplier)
    if err != nil {
        return err
    }

    req.Header.Set("Authorization", "Bearer "+token)
    return nil
}

Configuration:

authConfigs := map[string]AuthConfig{
    "bedbank": {
        Type:       AuthTypeJWT,
        Issuer:     "hotelbyte",
        Subject:    "api-client",
        PrivateKey: []byte(privateKeyPEM),
        Algorithm:  "RS256",
        Expiry:     time.Hour,
    },
}

Authentication Router

type AuthRouter struct {
    managers map[string]AuthManager
    config   map[string]AuthConfig
    mu       sync.RWMutex
}

func (ar *AuthRouter) Apply(req *http.Request, supplier string) error {
    ar.mu.RLock()
    config, exists := ar.config[supplier]
    ar.mu.RUnlock()

    if !exists {
        return fmt.Errorf("no auth config for supplier: %s", supplier)
    }

    switch config.Type {
    case AuthTypeBasic, AuthTypeAPIKey:
        manager := &BasicAuthManager{Config: config}
        return manager.Apply(req)
    case AuthTypeHMAC:
        manager := &HMACAuthManager{Config: config}
        return manager.Apply(req)
    case AuthTypeOAuth2:
        manager := &OAuth2TokenManager{Config: config}
        return manager.Apply(req, supplier)
    case AuthTypeJWT:
        manager := &JWTTokenManager{Config: config}
        return manager.Apply(req, supplier)
    default:
        return fmt.Errorf("unsupported auth type: %s", config.Type)
    }
}

Part 2: Error Handling

The Error Fragmentation Problem

Suppliers use different error formats:

Supplier A (HTTP Status + JSON):

{
  "error": {
    "code": "AUTH_001",
    "message": "Invalid API credentials",
    "details": "The provided API key is expired"
  }
}

Supplier B (HTTP Status + XML):

<Error>
  <Code>AUTHENTICATION_FAILED</Code>
  <Message>Invalid signature</Message>
</Error>

Supplier C (HTTP Status Only):

HTTP/1.1 401 Unauthorized
X-Error-Code: ERR_AUTH_1001

Supplier D (200 OK + Error in Body):

{
  "status": "error",
  "error": {
    "type": "authentication_error",
    "code": "invalid_credentials"
  }
}

Supplier Proxy’s Unified Error Model

// Standardized error structure
type StandardError struct {
    Code        string            `json:"code"`
    Message     string            `json:"message"`
    Category    ErrorCategory     `json:"category"`
    Supplier    string            `json:"supplier"`
    Details     map[string]string `json:"details,omitempty"`
    Timestamp   time.Time         `json:"timestamp"`
    RequestID   string            `json:"request_id"`
    Retryable   bool              `json:"retryable"`
}

type ErrorCategory string

const (
    ErrCategoryAuthentication ErrorCategory = "authentication"
    ErrCategoryAuthorization  ErrorCategory = "authorization"
    ErrCategoryRateLimit      ErrorCategory = "rate_limit"
    ErrCategoryValidation     ErrorCategory = "validation"
    ErrCategoryBusiness       ErrorCategory = "business"
    ErrCategorySystem         ErrorCategory = "system"
)

Error Translation Layer

type ErrorTranslator struct {
    translators map[string]ErrorMap
}

type ErrorMap struct {
    CodeMap       map[string]string          // Supplier code → Standard code
    CategoryMap   map[string]ErrorCategory   // Supplier code → Category
    RetryableMap  map[string]bool            // Supplier code → Retryable
}

var globalErrorMap = map[string]ErrorMap{
    "hotelbeds": {
        CodeMap: map[string]string{
            "AUTH_001":        "ERR_AUTH_INVALID_CREDENTIALS",
            "AUTH_002":        "ERR_AUTH_EXPIRED_TOKEN",
            "RATE_LIMIT_001":  "ERR_RATE_LIMIT_EXCEEDED",
            "VALIDATION_001":  "ERR_VALIDATION_INVALID_PARAMETER",
        },
        CategoryMap: map[string]ErrorCategory{
            "AUTH_001":       ErrCategoryAuthentication,
            "AUTH_002":       ErrCategoryAuthentication,
            "RATE_LIMIT_001": ErrCategoryRateLimit,
            "VALIDATION_001": ErrCategoryValidation,
        },
        RetryableMap: map[string]bool{
            "AUTH_001":        false,
            "AUTH_002":        true,
            "RATE_LIMIT_001":  true,
            "VALIDATION_001":  false,
        },
    },
    "dida": {
        CodeMap: map[string]string{
            "ERR_AUTH_1001":   "ERR_AUTH_INVALID_CREDENTIALS",
            "ERR_RATE_2001":   "ERR_RATE_LIMIT_EXCEEDED",
        },
        CategoryMap: map[string]ErrorCategory{
            "ERR_AUTH_1001":  ErrCategoryAuthentication,
            "ERR_RATE_2001":  ErrCategoryRateLimit,
        },
        RetryableMap: map[string]bool{
            "ERR_AUTH_1001":  false,
            "ERR_RATE_2001":  true,
        },
    },
    // ... 50 suppliers
}

func (et *ErrorTranslator) Translate(supplier string, statusCode int, body []byte) *StandardError {
    errorMap, exists := et.translators[supplier]
    if !exists {
        return et.fallbackError(supplier, statusCode)
    }

    // Parse supplier-specific error
    supplierError := et.parseSupplierError(supplier, body)

    // Translate to standard format
    standardCode := errorMap.CodeMap[supplierError.Code]
    if standardCode == "" {
        standardCode = "ERR_UNKNOWN"
    }

    category := errorMap.CategoryMap[supplierError.Code]
    if category == "" {
        category = ErrCategorySystem
    }

    retryable := errorMap.RetryableMap[supplierError.Code]

    return &StandardError{
        Code:       standardCode,
        Message:    supplierError.Message,
        Category:   category,
        Supplier:   supplier,
        Details:    supplierError.Details,
        Timestamp:  time.Now(),
        RequestID:  generateRequestID(),
        Retryable:  retryable,
    }
}

Error Parsing by Supplier

func (et *ErrorTranslator) parseSupplierError(supplier string, body []byte) *SupplierError {
    var err error
    var supplierError SupplierError

    switch supplier {
    case "hotelbeds", "dida", "expedia", "agoda":
        // JSON errors
        var jsonErr struct {
            Error struct {
                Code    string `json:"code"`
                Message string `json:"message"`
                Details string `json:"details"`
            } `json:"error"`
        }
        err = json.Unmarshal(body, &jsonErr)
        if err == nil {
            supplierError.Code = jsonErr.Error.Code
            supplierError.Message = jsonErr.Error.Message
            supplierError.Details = map[string]string{"details": jsonErr.Error.Details}
        }

    case "travelgds":
        // XML errors
        var xmlErr struct {
            Code    string `xml:"Code"`
            Message string `xml:"Message"`
        }
        err = xml.Unmarshal(body, &xmlErr)
        if err == nil {
            supplierError.Code = xmlErr.Code
            supplierError.Message = xmlErr.Message
        }

    case "bedbank":
        // 200 OK with error in body
        var jsonErr struct {
            Status string `json:"status"`
            Error  struct {
                Type  string `json:"type"`
                Code  string `json:"code"`
                Message string `json:"message"`
            } `json:"error"`
        }
        err = json.Unmarshal(body, &jsonErr)
        if err == nil && jsonErr.Status == "error" {
            supplierError.Code = jsonErr.Error.Code
            supplierError.Message = jsonErr.Error.Message
        }
    }

    if err != nil {
        return et.fallbackSupplierError(supplier, body)
    }

    return &supplierError
}

Error Handling Strategy

type ErrorHandler struct {
    translator *ErrorTranslator
    logger     *zap.Logger
}

func (eh *ErrorHandler) Handle(supplier string, resp *http.Response, body []byte) error {
    // Translate error
    stdErr := eh.translator.Translate(supplier, resp.StatusCode, body)

    // Log error
    eh.logger.Error("Supplier API error",
        zap.String("supplier", supplier),
        zap.String("code", stdErr.Code),
        zap.String("message", stdErr.Message),
        zap.String("category", string(stdErr.Category)),
        zap.Bool("retryable", stdErr.Retryable),
    )

    // Handle by category
    switch stdErr.Category {
    case ErrCategoryAuthentication:
        return &AuthenticationError{StandardError: stdErr}
    case ErrCategoryRateLimit:
        return &RateLimitError{StandardError: stdErr}
    case ErrCategoryValidation:
        return &ValidationError{StandardError: stdErr}
    case ErrCategoryBusiness:
        return &BusinessError{StandardError: stdErr}
    default:
        return &SystemError{StandardError: stdErr}
    }
}

// Usage in application
func SearchHotels(req *SearchRequest) (*Response, error) {
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }

    if resp.StatusCode >= 400 {
        body, _ := io.ReadAll(resp.Body)
        resp.Body.Close()

        // Use error handler
        err := errorHandler.Handle("hotelbeds", resp, body)

        // Check error type
        if rateLimitErr, ok := err.(*RateLimitError); ok {
            // Retry with backoff
            return RetryWithBackoff(req, rateLimitErr)
        }

        if authErr, ok := err.(*AuthenticationError); ok {
            // Refresh auth and retry
            return RetryWithAuthRefresh(req, authErr)
        }

        return nil, err
    }

    // Process success response
    return processResponse(resp)
}

Real-World Impact

Case Study: Authentication Simplification

Before Supplier Proxy:

Authentication code: 5,200 lines
Code complexity: High
Maintenance burden: Very High
New supplier auth time: 2-3 days
Auth-related bugs: 8-12 per month

After Supplier Proxy:

Authentication code: 1,800 lines (centralized)
Code complexity: Low
Maintenance burden: Low
New supplier auth time: 1-2 hours
Auth-related bugs: 0 (handled in proxy)

Impact:

  • 65% reduction in authentication code
  • 90% reduction in new supplier auth time
  • 100% reduction in auth-related bugs
  • Centralized management of 50 suppliers’ auth

Case Study: Error Handling Simplification

Before Supplier Proxy:

Error handling code: 3,400 lines
Error types: 15-20
Developer time on errors: 30% of total
User-facing error messages: Inconsistent

After Supplier Proxy:

Error handling code: 800 lines (unified)
Error types: 6 standard categories
Developer time on errors: 5% of total
User-facing error messages: Consistent

Impact:

  • 76% reduction in error handling code
  • 83% reduction in developer time on errors
  • Consistent error experience across all suppliers

Best Practices

Authentication Best Practices

  1. Secure credential storage - Use vaults, environment variables
  2. Token caching - Don’t fetch tokens on every request
  3. Refresh proactively - Don’t wait for 401 errors
  4. Rotate credentials - Regular credential rotation
  5. Monitor auth failures - Alert on high auth error rates

Error Handling Best Practices

  1. Standardize error codes - One set of codes for all suppliers
  2. Categorize errors - Auth, rate limit, validation, business
  3. Mark retryable errors - Don’t retry on client errors
  4. Log everything - Full error context for debugging
  5. Translate messages - User-friendly error messages

Summary

Supplier Proxy’s authentication and error handling:

Authentication:

  • 9 different auth protocols supported
  • Automatic token management
  • Centralized configuration
  • Secure credential handling

Error Handling:

  • Unified error model
  • Translation for 50+ suppliers
  • Categorized error types
  • Retry logic built-in

Result: 65% less code, 90% faster supplier onboarding, consistent error experience.

Next: Supplier Proxy Architecture and Scalability



Series Navigation

Supplier Proxy Series:

  1. What is Supplier Proxy and Its Role in Hotel API Aggregation
  2. How Supplier Proxy Handles Authentication and Error Handling ← You are here
  3. Supplier Proxy Architecture and Scalability
  4. Cost Benefits and ROI Analysis