package auth import ( "crypto/sha256" "crypto/subtle" "encoding/hex" "errors" "fmt" "os" "path/filepath" "strings" ) const EnvTokenHashes = "AUTH_PROXY_TOKEN_HASHES" var ( ErrMissingAuthHeader = errors.New("missing authorization header") ErrInvalidAuthHeader = errors.New("invalid authorization header format") ErrEmptyTokenSet = errors.New("no token hashes configured") ErrTokenDirUnreadable = errors.New("unable to read token hash directory") ErrInvalidTokenHash = errors.New("invalid token hash format") ) type Validator struct { tokenDigests [][sha256.Size]byte } func NewValidator(tokenHashes []string) (*Validator, error) { if len(tokenHashes) == 0 { return nil, ErrEmptyTokenSet } unique := make(map[[sha256.Size]byte]struct{}, len(tokenHashes)) digests := make([][sha256.Size]byte, 0, len(tokenHashes)) for _, hashValue := range tokenHashes { hashValue = normalizeHash(hashValue) if hashValue == "" { continue } digest, err := decodeHash(hashValue) if err != nil { return nil, err } if _, exists := unique[digest]; exists { continue } unique[digest] = struct{}{} digests = append(digests, digest) } if len(digests) == 0 { return nil, ErrEmptyTokenSet } return &Validator{tokenDigests: digests}, nil } func (v *Validator) IsValid(token string) bool { token = strings.TrimSpace(token) if token == "" || v == nil { return false } provided := sha256.Sum256([]byte(token)) matched := 0 for _, allowed := range v.tokenDigests { matched |= subtle.ConstantTimeCompare(provided[:], allowed[:]) } return matched == 1 } func (v *Validator) TokenCount() int { if v == nil { return 0 } return len(v.tokenDigests) } func ParseBearerToken(authHeader string) (string, error) { authHeader = strings.TrimSpace(authHeader) if authHeader == "" { return "", ErrMissingAuthHeader } parts := strings.Fields(authHeader) if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || strings.TrimSpace(parts[1]) == "" { return "", ErrInvalidAuthHeader } return parts[1], nil } func LoadTokenHashes(hashesDir, hashesCSV string) ([]string, error) { hashes := make([]string, 0) dirHashes, err := readHashesFromDir(hashesDir) if err != nil { return nil, err } hashes = append(hashes, dirHashes...) hashes = append(hashes, parseHashesCSV(hashesCSV)...) cleaned := make([]string, 0, len(hashes)) for _, hashValue := range hashes { h := normalizeHash(hashValue) if h != "" { cleaned = append(cleaned, h) } } if len(cleaned) == 0 { return nil, ErrEmptyTokenSet } return cleaned, nil } func readHashesFromDir(hashesDir string) ([]string, error) { if strings.TrimSpace(hashesDir) == "" { return nil, nil } entries, err := os.ReadDir(hashesDir) if err != nil { return nil, fmt.Errorf("%w %q: %v", ErrTokenDirUnreadable, hashesDir, err) } hashes := make([]string, 0, len(entries)) for _, entry := range entries { if entry.IsDir() { continue } path := filepath.Join(hashesDir, entry.Name()) content, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read hash file %q: %w", path, err) } hashes = append(hashes, strings.TrimSpace(string(content))) } return hashes, nil } func parseHashesCSV(hashesCSV string) []string { if strings.TrimSpace(hashesCSV) == "" { return nil } parts := strings.Split(hashesCSV, ",") hashes := make([]string, 0, len(parts)) for _, part := range parts { h := normalizeHash(part) if h != "" { hashes = append(hashes, h) } } return hashes } func normalizeHash(value string) string { value = strings.TrimSpace(value) if strings.HasPrefix(strings.ToLower(value), "sha256:") { value = strings.TrimSpace(value[len("sha256:"):]) } return strings.ToLower(value) } func decodeHash(value string) ([sha256.Size]byte, error) { var digest [sha256.Size]byte if len(value) != sha256.Size*2 { return digest, fmt.Errorf("%w: expected %d hex chars, got %d", ErrInvalidTokenHash, sha256.Size*2, len(value)) } decoded, err := hex.DecodeString(value) if err != nil { return digest, fmt.Errorf("%w: %v", ErrInvalidTokenHash, err) } copy(digest[:], decoded) return digest, nil } func TokenFingerprint(token string) string { token = strings.TrimSpace(token) if token == "" { return "" } sum := sha256.Sum256([]byte(token)) return fmt.Sprintf("sha256:%x", sum[:6]) }