// Изменено: 2026-03-11 // Auth middleware — проверяет Bearer JWT-токен из заголовка Authorization. // // Архитектура аутентификации: // - В v1 токен — это JWT облака (nubes), выданный при логине. // - Оператор НЕ проверяет подпись JWT (публичный ключ nubes недоступен внутри кластера). // - Проверяется только структура JWT и claim "sub" (не пустой) и "exp" (не истёк). // - Полная проверка подлинности токена происходит в провайдере terraform через PingNubesAPI. // - Такой подход называют "trusted perimeter": оператор доступен только внутри кластера, // внешний доступ — через Ingress, где токен уже проверен на уровне API-шлюза. // // TODO v2: получать публичный ключ из nubes JWKS endpoint и проверять подпись RS256. package middleware import ( "encoding/base64" "encoding/json" "log/slog" "net/http" "strings" "time" ) // Auth возвращает middleware которое требует заголовок: // // Authorization: Bearer // // Проверяет: структура JWT (3 части), наличие "sub", отсутствие истечения "exp". func Auth(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 } if err := validateJWT(parts[1]); 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) }) } // validateJWT проверяет структуру JWT и claim "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] // JWT использует base64url без padding 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 int64 `json:"exp"` } if err := json.Unmarshal(decoded, &claims); err != nil { return &jwtError{"cannot parse JWT claims"} } if claims.Sub == "" { return &jwtError{"missing sub claim"} } if claims.Exp > 0 && claims.Exp < time.Now().Unix() { return &jwtError{"token expired"} } return nil } type jwtError struct{ msg string } func (e *jwtError) Error() string { return e.msg } // verifySignature — точка вставки для проверки подписи JWT (v2). // // Текущее состояние (v1): подпись НЕ проверяется. // Причина: публичный ключ nubes недоступен внутри кластера без JWKS endpoint. // Безопасность обеспечивается "trusted perimeter" — оператор доступен только изнутри кластера. // // Когда nubes предоставит JWKS endpoint, реализация: // // func verifySignature(token string) error { // // 1. Получить JWKS: GET {NUBES_JWKS_URL}/.well-known/jwks.json // // 2. Найти ключ по "kid" из JWT header // // 3. Проверить подпись RS256/ES256 // // Пример: github.com/lestrrat-go/jwx/v2/jwk + jwt.Parse // return nil // } // // После реализации добавить вызов в validateJWT после проверки структуры: // // if err := verifySignature(token); err != nil { // return &jwtError{"signature verification failed: " + err.Error()} // }