// Package middleware — HTTP-промежуточные слои API. package middleware import ( "encoding/base64" "log/slog" "net/http" "strings" ) // Auth — проверка Bearer-токена. // // authTestMode (env AUTH_TEST_MODE, дефолт false): // - true — принимается любая строка без пробелов (для локальных тестов); // - false — структурная проверка JWT (sub + exp), как в старом коде. // // В новой архитектуре подпись JWT не проверяется (как и раньше): внешний // периметр обеспечивает платформа, полная валидация — на стороне шлюза. func Auth(authTestMode bool, log *slog.Logger, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { header := r.Header.Get("Authorization") if header == "" { log.Warn("auth: missing authorization header", "remote", r.RemoteAddr, "path", r.URL.Path) http.Error(w, `{"error":"authorization required"}`, http.StatusUnauthorized) return } parts := strings.SplitN(header, " ", 2) if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") { log.Warn("auth: invalid authorization format", "remote", r.RemoteAddr) http.Error(w, `{"error":"invalid authorization format, use Bearer "}`, http.StatusUnauthorized) return } token := parts[1] if authTestMode && isPlainToken(token) { log.Info("auth: test mode — plain token accepted", "remote", r.RemoteAddr, "path", r.URL.Path) next.ServeHTTP(w, r) return } if err := validateJWT(token); err != nil { log.Warn("auth: invalid token", "remote", r.RemoteAddr, "path", r.URL.Path, "reason", err.Error()) http.Error(w, `{"error":"invalid token"}`, http.StatusForbidden) return } next.ServeHTTP(w, r) }) } // isPlainToken — непустая строка без пробелов, не похожая на JWT (три части через точку). func isPlainToken(token string) bool { if token == "" || strings.ContainsAny(token, " \t\n\r") { return false } return len(strings.Split(token, ".")) != 3 } // jwtError — структурная ошибка JWT. type jwtError struct{ msg string } func (e *jwtError) Error() string { return e.msg } // validateJWT проверяет структуру JWT: три части, корректный payload, sub и exp. // Подпись НЕ проверяется — см. комментарий пакета. func validateJWT(token string) error { jwtParts := strings.Split(token, ".") if len(jwtParts) != 3 { return &jwtError{"not a JWT: expected 3 parts"} } payload := jwtParts[1] switch len(payload) % 4 { case 2: payload += "==" case 3: payload += "=" } decoded, err := base64.URLEncoding.DecodeString(payload) if err != nil { decoded, err = base64.StdEncoding.DecodeString(payload) } if err != nil { return &jwtError{"cannot decode JWT payload"} } var claims struct { Sub string `json:"sub"` Exp float64 `json:"exp"` } if err := jsonUnmarshal(decoded, &claims); err != nil { return &jwtError{"cannot parse JWT payload"} } if claims.Sub == "" { return &jwtError{"JWT missing sub claim"} } if claims.Exp > 0 { if now := float64(timeNow().Unix()); claims.Exp < now { return &jwtError{"JWT expired"} } } return nil }