Supplier Proxy Series (2): How Supplier Proxy Handles Authentication and Error Handling
Deep dive into Supplier Proxy's authentication management and error handling strategies. Learn how it handles multiple auth protocols (Basic Auth, HMAC, OAuth2, JWT) and standardizes error handling across 50+ hotel suppliers.
“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
- Secure credential storage - Use vaults, environment variables
- Token caching - Don’t fetch tokens on every request
- Refresh proactively - Don’t wait for 401 errors
- Rotate credentials - Regular credential rotation
- Monitor auth failures - Alert on high auth error rates
Error Handling Best Practices
- Standardize error codes - One set of codes for all suppliers
- Categorize errors - Auth, rate limit, validation, business
- Mark retryable errors - Don’t retry on client errors
- Log everything - Full error context for debugging
- 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
Recommended Reading
Series Navigation
Supplier Proxy Series:
- What is Supplier Proxy and Its Role in Hotel API Aggregation
- How Supplier Proxy Handles Authentication and Error Handling ← You are here
- Supplier Proxy Architecture and Scalability
- Cost Benefits and ROI Analysis
Comments