Files
go-lcg/serve/auth.go

279 lines
8.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package serve
import (
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/golang-jwt/jwt/v5"
)
// JWTClaims представляет claims для JWT токена
type JWTClaims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
// AuthRequest представляет запрос на аутентификацию
type AuthRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// AuthResponse представляет ответ на аутентификацию
type AuthResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Error string `json:"error,omitempty"`
}
// JWTSecretKey генерирует или загружает секретный ключ для JWT
func getJWTSecretKey() ([]byte, error) {
// Пытаемся загрузить из переменной окружения
if secret := os.Getenv("LCG_JWT_SECRET"); secret != "" {
return []byte(secret), nil
}
// Пытаемся загрузить из файла
secretFile := fmt.Sprintf("%s/server/jwt_secret", config.AppConfig.Server.ConfigFolder)
if data, err := os.ReadFile(secretFile); err == nil {
return data, nil
}
// Генерируем новый секретный ключ
secret := make([]byte, 32)
if _, err := rand.Read(secret); err != nil {
return nil, fmt.Errorf("failed to generate JWT secret: %v", err)
}
// Создаем директорию если не существует
if err := os.MkdirAll(fmt.Sprintf("%s/server", config.AppConfig.Server.ConfigFolder), 0755); err != nil {
return nil, fmt.Errorf("failed to create config directory: %v", err)
}
// Сохраняем секретный ключ в файл
if err := os.WriteFile(secretFile, secret, 0600); err != nil {
return nil, fmt.Errorf("failed to save JWT secret: %v", err)
}
return secret, nil
}
// generateJWTToken создает JWT токен для пользователя
func generateJWTToken(username string) (string, error) {
secret, err := getJWTSecretKey()
if err != nil {
return "", err
}
// Создаем claims
claims := JWTClaims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), // Токен действителен 24 часа
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "lcg-server",
},
}
// Создаем токен
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secret)
}
// validateJWTToken проверяет JWT токен
func validateJWTToken(tokenString string) (*JWTClaims, error) {
secret, err := getJWTSecretKey()
if err != nil {
return nil, err
}
// Парсим токен
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
// Проверяем метод подписи
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return secret, nil
})
if err != nil {
return nil, err
}
// Проверяем валидность токена
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
// getTokenFromCookie извлекает JWT токен из cookies
func getTokenFromCookie(r *http.Request) (string, error) {
cookie, err := r.Cookie("auth_token")
if err != nil {
return "", err
}
return cookie.Value, nil
}
// setAuthCookie устанавливает HTTP-only cookie с JWT токеном
func setAuthCookie(w http.ResponseWriter, token string) {
cookie := &http.Cookie{
Name: "auth_token",
Value: token,
Path: config.AppConfig.Server.CookiePath,
HttpOnly: true,
Secure: config.AppConfig.Server.CookieSecure,
SameSite: http.SameSiteLaxMode, // Более мягкий режим для reverse proxy
MaxAge: config.AppConfig.Server.CookieTTLHours * 60 * 60,
}
// Устанавливаем Domain только если это не IP адрес и не 0.0.0.0
// При доступе по IP адресу не устанавливаем Domain, иначе cookie не будет работать
if config.AppConfig.Server.Domain != "" {
domain := config.AppConfig.Server.Domain
// Проверяем, не является ли домен IP адресом или 0.0.0.0
if !isIPAddress(domain) && domain != "0.0.0.0" && domain != "::" && domain != "::1" {
cookie.Domain = domain
}
// Если domain пустой, 0.0.0.0 или IP адрес - не устанавливаем Domain
// Браузер автоматически применит cookie к текущему хосту
}
http.SetCookie(w, cookie)
}
// clearAuthCookie удаляет cookie с токеном
func clearAuthCookie(w http.ResponseWriter) {
cookie := &http.Cookie{
Name: "auth_token",
Value: "",
Path: config.AppConfig.Server.CookiePath,
HttpOnly: true,
Secure: config.AppConfig.Server.CookieSecure,
SameSite: http.SameSiteLaxMode,
MaxAge: -1, // Удаляем cookie
}
// Устанавливаем Domain только если это не IP адрес
if config.AppConfig.Server.Domain != "" {
domain := config.AppConfig.Server.Domain
if !isIPAddress(domain) && domain != "0.0.0.0" && domain != "::" && domain != "::1" {
cookie.Domain = domain
}
}
http.SetCookie(w, cookie)
}
// handleLogin обрабатывает запрос на вход
func handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req AuthRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
apiJsonResponse(w, AuthResponse{
Success: false,
Error: "Invalid request body",
})
return
}
// Проверяем пароль
if req.Password != config.AppConfig.Server.Password {
apiJsonResponse(w, AuthResponse{
Success: false,
Error: "Неверный пароль",
})
return
}
// Генерируем JWT токен
token, err := generateJWTToken(req.Username)
if err != nil {
apiJsonResponse(w, AuthResponse{
Success: false,
Error: "Failed to generate token",
})
return
}
// Устанавливаем cookie
setAuthCookie(w, token)
apiJsonResponse(w, AuthResponse{
Success: true,
Message: "Успешная авторизация",
})
}
// handleLogout обрабатывает запрос на выход
func handleLogout(w http.ResponseWriter, r *http.Request) {
clearAuthCookie(w)
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
// handleValidateToken обрабатывает проверку валидности токена
func handleValidateToken(w http.ResponseWriter, r *http.Request) {
token, err := getTokenFromCookie(r)
if err != nil {
apiJsonResponse(w, AuthResponse{
Success: false,
Error: "Token not found",
})
return
}
_, err = validateJWTToken(token)
if err != nil {
apiJsonResponse(w, AuthResponse{
Success: false,
Error: "Invalid token",
})
return
}
apiJsonResponse(w, AuthResponse{
Success: true,
Message: "Token is valid",
})
}
// requireAuth middleware проверяет аутентификацию
func requireAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Проверяем, требуется ли аутентификация
if !config.AppConfig.Server.RequireAuth {
next(w, r)
return
}
// Получаем токен из cookie
token, err := getTokenFromCookie(r)
if err != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Проверяем валидность токена
_, err = validateJWTToken(token)
if err != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Токен валиден, продолжаем
next(w, r)
}
}