// Package traefikoidc provides OIDC authentication middleware for Traefik. // This file contains utility/helper methods extracted from main.go for better code organization. package traefikoidc import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "html" "net/http" "runtime" "strings" "time" ) // safeLogDebug provides nil-safe logging for debug messages func (t *TraefikOidc) safeLogDebug(msg string) { if t.logger != nil { t.logger.Debug("%s", msg) } } // safeLogDebugf provides nil-safe logging for formatted debug messages func (t *TraefikOidc) safeLogDebugf(format string, args ...interface{}) { if t.logger != nil { t.logger.Debugf(format, args...) } } // safeLogError provides nil-safe logging for error messages func (t *TraefikOidc) safeLogError(msg string) { if t.logger != nil { t.logger.Error("%s", msg) } } // safeLogErrorf provides nil-safe logging for formatted error messages func (t *TraefikOidc) safeLogErrorf(format string, args ...interface{}) { if t.logger != nil { t.logger.Errorf(format, args...) } } // safeLogInfo provides nil-safe logging for info messages func (t *TraefikOidc) safeLogInfo(msg string) { if t.logger != nil { t.logger.Info("%s", msg) } } // isAllowedUser checks if a user identifier is authorized based on the configured user identifier claim. // When using email as the identifier (default), it validates against allowedUsers and allowedUserDomains. // When using non-email identifiers (sub, oid, upn, etc.), it only validates against allowedUsers // since domain-based validation doesn't apply to non-email identifiers. // // Parameters: // - userIdentifier: The user identifier to validate (email, sub, oid, upn, etc.). // // Returns: // - true if the user is authorized, false otherwise. func (t *TraefikOidc) isAllowedUser(userIdentifier string) bool { // If no restrictions are configured, allow all authenticated users if len(t.allowedUserDomains) == 0 && len(t.allowedUsers) == 0 { return true } // Check if user is explicitly allowed if len(t.allowedUsers) > 0 { _, userAllowed := t.allowedUsers[strings.ToLower(userIdentifier)] if userAllowed { t.logger.Debugf("User identifier %s is explicitly allowed in allowedUsers", userIdentifier) return true } } // For email-based identifiers, also check domain restrictions // Only apply domain validation if using email as identifier AND identifier looks like an email if t.userIdentifierClaim == "email" && strings.Contains(userIdentifier, "@") { return t.isAllowedDomain(userIdentifier) } // For non-email identifiers with allowedUserDomains configured, log a warning if len(t.allowedUserDomains) > 0 && t.userIdentifierClaim != "email" { t.logger.Debugf("AllowedUserDomains is configured but userIdentifierClaim is '%s', not 'email'. Domain validation skipped for: %s", t.userIdentifierClaim, userIdentifier) } // User not found in allowedUsers list if len(t.allowedUsers) > 0 { t.logger.Debugf("User identifier %s is not in the allowed users list", userIdentifier) } return false } // isAllowedDomain checks if an email address is authorized based on domain or user whitelist. // It validates against both allowed user domains and specific allowed users. // Parameters: // - email: The email address to validate. // // Returns: // - true if the email is authorized (domain or user allowed), false if not authorized // or if the email format is invalid. func (t *TraefikOidc) isAllowedDomain(email string) bool { if len(t.allowedUserDomains) == 0 && len(t.allowedUsers) == 0 { return true } if len(t.allowedUsers) > 0 { _, userAllowed := t.allowedUsers[strings.ToLower(email)] if userAllowed { t.logger.Debugf("Email %s is explicitly allowed in allowedUsers", email) return true } } if len(t.allowedUserDomains) > 0 { parts := strings.Split(email, "@") if len(parts) != 2 { t.logger.Errorf("Invalid email format encountered: %s", email) return false } domain := parts[1] _, domainAllowed := t.allowedUserDomains[domain] if domainAllowed { t.logger.Debugf("Email domain %s is allowed", domain) return true } else { t.logger.Debugf("Email domain %s is NOT allowed. Allowed domains: %v", domain, keysFromMap(t.allowedUserDomains)) } } else if len(t.allowedUsers) > 0 { t.logger.Debugf("Email %s is not in the allowed users list: %v", email, keysFromMap(t.allowedUsers)) } return false } // keysFromMap extracts string keys from a map for logging purposes. // Helper function to get keys from a map for logging. // Parameters: // - m: The map to extract keys from. // // Returns: // - A slice of string keys. func keysFromMap(m map[string]struct{}) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } // sendErrorResponse sends an appropriate error response based on the request's Accept header. // It sends JSON responses for clients that accept JSON, otherwise sends HTML error pages. // Parameters: // - rw: The HTTP response writer. // - req: The HTTP request (used to check Accept header). // - message: The error message to display. // - code: The HTTP status code to set for the response. func (t *TraefikOidc) sendErrorResponse(rw http.ResponseWriter, req *http.Request, message string, code int) { acceptHeader := req.Header.Get("Accept") if strings.Contains(acceptHeader, "application/json") { t.logger.Debugf("Sending JSON error response (code %d): %s", code, message) rw.Header().Set("Content-Type", "application/json") rw.WriteHeader(code) _ = json.NewEncoder(rw).Encode(map[string]interface{}{ "error": http.StatusText(code), "error_description": message, "status_code": code, }) // Safe to ignore: error response write return } t.logger.Debugf("Sending HTML error response (code %d): %s", code, message) returnURL := "/" // Escape message to prevent XSS attacks escapedMessage := html.EscapeString(message) htmlBody := fmt.Sprintf(`