diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/dto.go b/main_dc/yalarba/api_yal/internal/domain/auth/dto.go index c3ac00d..a5d2517 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/dto.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/dto.go @@ -1,3 +1,4 @@ +// dto.go package auth import ( @@ -21,10 +22,16 @@ type LoginRequest struct { // AuthResponse структура ответа при успешной аутентификации type AuthResponse struct { - Token string `json:"token"` - RefreshToken string `json:"refresh_token,omitempty"` - ExpiresAt time.Time `json:"expires_at"` - User UserInfo `json:"user"` + Token string `json:"token"` // Access token для Bearer авторизации + ExpiresAt time.Time `json:"expires_at"` // Время истечения access token + User UserInfo `json:"user"` +} + +// RefreshTokenResponse структура ответа при обновлении токена +type RefreshTokenResponse struct { + Token string `json:"token"` // Новый access token + ExpiresAt time.Time `json:"expires_at"` // Время истечения нового access token + User UserInfo `json:"user"` } // ResetPasswordRequest - запрос на сброс пароля @@ -32,7 +39,7 @@ type ResetPasswordRequest struct { Email string `json:"email" validate:"required,email"` } -// RefreshTokenRequest - запрос на обновление токена +// RefreshTokenRequest - запрос на обновление токена (только для мобильных приложений) type RefreshTokenRequest struct { RefreshToken string `json:"refresh_token" validate:"required"` } @@ -57,4 +64,6 @@ var ( ErrUserNotFound = errors.New("user not found") ErrInvalidPassword = errors.New("invalid password") ErrUserAlreadyExists = errors.New("user with this email already exists") -) + ErrInvalidToken = errors.New("invalid token") + ErrTokenExpired = errors.New("token expired") +) \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/handler.go b/main_dc/yalarba/api_yal/internal/domain/auth/handler.go index 037a6ac..2d0aa08 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/handler.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/handler.go @@ -1,3 +1,4 @@ +// handler.go package auth import ( @@ -5,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "time" "api_yal/internal/logger" "api_yal/internal/middleware" @@ -13,6 +15,12 @@ import ( "go.uber.org/zap" ) +// Cookie константы +const ( + RefreshTokenCookieName = "refresh_token" + RefreshTokenExpiration = 7 * 24 * time.Hour // 7 дней +) + // AuthHandler обработчик для аутентификации type AuthHandler struct { authService AuthService @@ -20,13 +28,39 @@ type AuthHandler struct { } // NewAuthHandler создает новый экземпляр AuthHandler -func NewAuthHandler(authService *AuthService) *AuthHandler { +func NewAuthHandler(authService AuthService) *AuthHandler { return &AuthHandler{ - authService: *authService, + authService: authService, validator: validator.New(), } } +// setRefreshTokenCookie устанавливает HttpOnly cookie с refresh token +func (h *AuthHandler) setRefreshTokenCookie(w http.ResponseWriter, refreshToken string) { + http.SetCookie(w, &http.Cookie{ + Name: RefreshTokenCookieName, + Value: refreshToken, + Path: "/", + HttpOnly: true, + Secure: true, // Всегда true в production + SameSite: http.SameSiteStrictMode, + MaxAge: int(RefreshTokenExpiration.Seconds()), + }) +} + +// clearRefreshTokenCookie удаляет refresh token cookie +func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{ + Name: RefreshTokenCookieName, + Value: "", + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + MaxAge: -1, + }) +} + // Register регистрация аккаунта пользователя func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { l := logger.Get() @@ -110,7 +144,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { return } - response, err := h.authService.Login(req) + response, refreshToken, err := h.authService.Login(req) if err != nil { l.Error("Ошибка входа: %v", zap.Error(err)) @@ -127,39 +161,81 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { return } + // Устанавливаем refresh token в HttpOnly cookie + h.setRefreshTokenCookie(w, refreshToken) + l.Debug("Завершение обработки запроса входа") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } // RefreshToken обновление токена +// Поддерживает два способа получения refresh token: +// 1. Из HttpOnly cookie (для web приложений) +// 2. Из тела запроса (для мобильных приложений) func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { l := logger.Get() l.Info("Начало обработки запроса обновления токена") - var req RefreshTokenRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + var refreshToken string + + // Пытаемся получить refresh token из cookie + cookie, err := r.Cookie(RefreshTokenCookieName) + if err == nil && cookie != nil { + refreshToken = cookie.Value + } + + // Если в cookie нет, пробуем получить из тела запроса (для мобильных приложений) + if refreshToken == "" { + var req RefreshTokenRequest + if err := json.NewDecoder(r.Body).Decode(&req); err == nil { + refreshToken = req.RefreshToken + } + } + + if refreshToken == "" { + http.Error(w, "Refresh token required", http.StatusBadRequest) return } - if err := h.validator.Struct(req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) - return - } - - response, err := h.authService.RefreshToken(req.RefreshToken) + response, err := h.authService.RefreshToken(refreshToken) if err != nil { l.Error("Ошибка обновления токена", zap.Error(err)) + + if errors.Is(err, ErrInvalidToken) || errors.Is(err, ErrTokenExpired) { + // Очищаем невалидный refresh token + h.clearRefreshTokenCookie(w) + http.Error(w, "Invalid or expired refresh token", http.StatusUnauthorized) + return + } + http.Error(w, "Token refresh failed", http.StatusUnauthorized) return } + // Генерируем новый refresh token + newRefreshToken, err := h.generateNewRefreshTokenFromUser(response.User.ID) + if err != nil { + l.Error("Ошибка генерации нового refresh token", zap.Error(err)) + http.Error(w, "Failed to generate refresh token", http.StatusInternalServerError) + return + } + + // Обновляем refresh token в cookie + h.setRefreshTokenCookie(w, newRefreshToken) + l.Info("Завершение обработки запроса обновления токена") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } +// generateNewRefreshTokenFromUser генерирует новый refresh token для пользователя +// Вспомогательная функция для обновления refresh token +func (h *AuthHandler) generateNewRefreshTokenFromUser(userID uint) (string, error) { + // Используем сервис для генерации refresh token + return h.authService.GenerateNewRefreshToken(userID) +} + // Logout выход пользователя func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { l := logger.Get() @@ -178,6 +254,9 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { return } + // Очищаем refresh token cookie + h.clearRefreshTokenCookie(w) + l.Info("Завершение обработки запроса выхода") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ @@ -247,4 +326,4 @@ func (h *AuthHandler) ChangePassword(w http.ResponseWriter, r *http.Request) { "user_id": userID, "message": "Change password endpoint - to be implemented", }) -} +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/router.go b/main_dc/yalarba/api_yal/internal/domain/auth/router.go index 46b8c3c..568812e 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/router.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/router.go @@ -1,9 +1,11 @@ +// router.go package auth import ( "api_yal/internal/logger" "api_yal/internal/middleware" "api_yal/internal/repository" + "time" "github.com/go-chi/chi/v5" "gorm.io/gorm" @@ -13,8 +15,16 @@ import ( func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { // Создаем репозиторий и сервис accountRepo := repository.NewAccountRepository(db) - authService := NewAuthService(accountRepo, jwtSecret) - handler := NewAuthHandler(&authService) + + // Конфигурация токенов + authConfig := AuthServiceConfig{ + JWTSecret: jwtSecret, + AccessTokenTTL: 15 * time.Minute, // Access token живет 15 минут + RefreshTokenTTL: 7 * 24 * time.Hour, // Refresh token живет 7 дней + } + + authService := NewAuthService(accountRepo, authConfig) + handler := NewAuthHandler(authService) l := logger.Get() l.Debug("Регистрация маршрутов аутентификации") diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go b/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go index b3ddb40..f1def9a 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go @@ -1,3 +1,4 @@ +// service.go package auth import ( @@ -16,25 +17,73 @@ import ( // AuthService интерфейс сервиса аутентификации type AuthService interface { Register(req RegisterRequest) (*AuthResponse, error) - Login(req LoginRequest) (*AuthResponse, error) - RefreshToken(refreshToken string) (*AuthResponse, error) + Login(req LoginRequest) (*AuthResponse, string, error) // Возвращает refresh token отдельно + RefreshToken(refreshToken string) (*RefreshTokenResponse, error) Logout(userID uint) error + ValidateAccessToken(tokenString string) (*jwt.MapClaims, error) + GetUserFromToken(claims *jwt.MapClaims) (*models.Account, error) + GenerateNewRefreshToken(userID uint) (string, error) } // authServiceImpl реализация сервиса аутентификации type authServiceImpl struct { - accountRepo repository.AccountRepository - jwtSecret []byte + accountRepo repository.AccountRepository + jwtSecret []byte + accessTokenTTL time.Duration + refreshTokenTTL time.Duration +} + +// AuthServiceConfig конфигурация для сервиса аутентификации +type AuthServiceConfig struct { + JWTSecret string + AccessTokenTTL time.Duration // Рекомендуется 15-30 минут + RefreshTokenTTL time.Duration // Рекомендуется 7-30 дней } // NewAuthService создает новый экземпляр сервиса аутентификации -func NewAuthService(accountRepo repository.AccountRepository, jwtSecret string) AuthService { +func NewAuthService(accountRepo repository.AccountRepository, config AuthServiceConfig) AuthService { return &authServiceImpl{ - accountRepo: accountRepo, - jwtSecret: []byte(jwtSecret), + accountRepo: accountRepo, + jwtSecret: []byte(config.JWTSecret), + accessTokenTTL: config.AccessTokenTTL, + refreshTokenTTL: config.RefreshTokenTTL, } } +// GenerateNewRefreshToken генерирует новый refresh token для пользователя по ID +func (s *authServiceImpl) GenerateNewRefreshToken(userID uint) (string, error) { + l := logger.Get() + l.Info("Генерация нового refresh token", zap.Uint("userID", userID)) + + // Получаем пользователя из базы данных + account, err := s.accountRepo.GetByID(userID) + if err != nil { + l.Error("Пользователь не найден при генерации refresh token", + zap.Uint("userID", userID), + zap.Error(err)) + return "", ErrUserNotFound + } + + // Проверяем, активен ли аккаунт + if !account.IsActive { + l.Error("Попытка генерации refresh token для деактивированного аккаунта", + zap.Uint("userID", userID)) + return "", errors.New("account is deactivated") + } + + // Генерируем новый refresh token + refreshToken, err := s.generateRefreshToken(account) + if err != nil { + l.Error("Ошибка генерации refresh token", + zap.Uint("userID", userID), + zap.Error(err)) + return "", err + } + + l.Info("Refresh token успешно сгенерирован", zap.Uint("userID", userID)) + return refreshToken, nil +} + // Register регистрирует нового пользователя func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) { l := logger.Get() @@ -74,17 +123,16 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) { return nil, err } - // Генерируем JWT токены - accessToken, refreshToken, expiresAt, err := s.generateTokens(newAcc) + // Генерируем access token + accessToken, expiresAt, err := s.generateAccessToken(newAcc) if err != nil { return nil, err } // Формируем ответ response := &AuthResponse{ - Token: accessToken, - RefreshToken: refreshToken, - ExpiresAt: expiresAt, + Token: accessToken, + ExpiresAt: expiresAt, User: UserInfo{ ID: newAcc.Base.ID, Email: newAcc.Email, @@ -99,8 +147,8 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) { return response, nil } -// Login аутентифицирует пользователя -func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) { +// Login аутентифицирует пользователя и возвращает access и refresh токены +func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, string, error) { l := logger.Get() l.Info("Начало входа пользователя", zap.String("email", req.Email)) @@ -108,32 +156,36 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) { account, err := s.accountRepo.GetByEmail(req.Email) if err != nil { l.Error("Пользователь не найден", zap.String("email", req.Email), zap.Error(err)) - return nil, ErrUserNotFound + return nil, "", ErrUserNotFound } // Проверяем, активен ли аккаунт if !account.IsActive { l.Error("Аккаунт деактивирован", zap.String("email", req.Email)) - return nil, errors.New("account is deactivated") + return nil, "", errors.New("account is deactivated") } // Сравниваем пароли if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(req.Password)); err != nil { l.Error("Неверный пароль для пользователя", zap.String("email", req.Email)) - return nil, ErrInvalidPassword + return nil, "", ErrInvalidPassword } - // Генерируем JWT токены - accessToken, refreshToken, expiresAt, err := s.generateTokens(account) + // Генерируем токены + accessToken, expiresAt, err := s.generateAccessToken(account) if err != nil { - return nil, err + return nil, "", err + } + + refreshToken, err := s.generateRefreshToken(account) + if err != nil { + return nil, "", err } // Формируем ответ response := &AuthResponse{ - Token: accessToken, - RefreshToken: refreshToken, - ExpiresAt: expiresAt, + Token: accessToken, + ExpiresAt: expiresAt, User: UserInfo{ ID: account.Base.ID, Email: account.Email, @@ -145,53 +197,33 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) { } l.Info("Пользователь успешно вошел", zap.String("email", req.Email)) - return response, nil + return response, refreshToken, nil } // RefreshToken обновляет JWT токен по refresh token -func (s *authServiceImpl) RefreshToken(refreshToken string) (*AuthResponse, error) { +func (s *authServiceImpl) RefreshToken(refreshToken string) (*RefreshTokenResponse, error) { l := logger.Get() l.Info("Начало обновления токена") // Парсим и валидируем refresh token - token, err := jwt.Parse(refreshToken, func(t *jwt.Token) (interface{}, error) { - // Проверяем метод подписи - if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) - } - return s.jwtSecret, nil - }) - - if err != nil || !token.Valid { - l.Error("Невалидный refresh token", zap.Error(err)) - return nil, errors.New("invalid refresh token") - } - - // Получаем claims - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { - l.Error("Не удалось получить claims из токена") - return nil, errors.New("invalid token claims") - } - - // Проверяем тип токена (должен быть refresh) - if tokenType, exists := claims["type"]; !exists || tokenType != "refresh" { - l.Error("Токен не является refresh токеном") - return nil, errors.New("invalid token type") + claims, err := s.validateRefreshToken(refreshToken) + if err != nil { + l.Error("Ошибка валидации refresh token", zap.Error(err)) + return nil, err } // Получаем ID пользователя из claims - userIDStr, ok := claims["sub"].(string) + userIDStr, ok := (*claims)["sub"].(string) if !ok { l.Error("Не удалось получить subject из токена") - return nil, errors.New("invalid token claims") + return nil, ErrInvalidToken } // Конвертируем string в uint var userID uint if _, err := fmt.Sscan(userIDStr, &userID); err != nil { l.Error("Ошибка конвертации user ID", zap.Error(err)) - return nil, errors.New("invalid user id in token") + return nil, ErrInvalidToken } // Получаем пользователя из базы по ID @@ -207,18 +239,17 @@ func (s *authServiceImpl) RefreshToken(refreshToken string) (*AuthResponse, erro return nil, errors.New("account is deactivated") } - // Генерируем новую пару токенов - accessToken, newRefreshToken, expiresAt, err := s.generateTokens(account) + // Генерируем новый access token + accessToken, expiresAt, err := s.generateAccessToken(account) if err != nil { - l.Error("Ошибка генерации токенов", zap.Error(err)) + l.Error("Ошибка генерации access token", zap.Error(err)) return nil, err } l.Info("Обновление токена успешно завершено", zap.Uint("userID", userID)) - return &AuthResponse{ - Token: accessToken, - RefreshToken: newRefreshToken, - ExpiresAt: expiresAt, + return &RefreshTokenResponse{ + Token: accessToken, + ExpiresAt: expiresAt, User: UserInfo{ ID: account.Base.ID, Email: account.Email, @@ -235,48 +266,132 @@ func (s *authServiceImpl) Logout(userID uint) error { l := logger.Get() l.Info("Выход пользователя", zap.Uint("userID", userID)) // Здесь можно добавить логику инвалидации токенов (например, в Redis) + // Для базовой реализации достаточно удалить cookie на клиенте l.Info("Выход успешно завершен", zap.Uint("userID", userID)) return nil } -// generateTokens генерирует пару токенов: access и refresh -func (s *authServiceImpl) generateTokens(account *models.Account) (accessToken, refreshToken string, expiresAt time.Time, err error) { - // Access token expires in 15 minutes (более безопасно для access token) - accessExpiresAt := time.Now().Add(15 * time.Minute) - - // Refresh token expires in 7 days - refreshExpiresAt := time.Now().Add(7 * 24 * time.Hour) +// ValidateAccessToken валидирует access token +func (s *authServiceImpl) ValidateAccessToken(tokenString string) (*jwt.MapClaims, error) { + token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { + // Проверяем метод подписи + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) + } + return s.jwtSecret, nil + }) - // Создаем access token - accessClaims := jwt.MapClaims{ + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, ErrInvalidToken + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, ErrInvalidToken + } + + // Проверяем тип токена + if tokenType, exists := claims["type"]; !exists || tokenType != "access" { + return nil, ErrInvalidToken + } + + return &claims, nil +} + +// GetUserFromToken получает пользователя из claims +func (s *authServiceImpl) GetUserFromToken(claims *jwt.MapClaims) (*models.Account, error) { + userIDStr, ok := (*claims)["sub"].(string) + if !ok { + return nil, ErrInvalidToken + } + + var userID uint + if _, err := fmt.Sscan(userIDStr, &userID); err != nil { + return nil, ErrInvalidToken + } + + return s.accountRepo.GetByID(userID) +} + +// generateAccessToken генерирует access token +func (s *authServiceImpl) generateAccessToken(account *models.Account) (string, time.Time, error) { + expiresAt := time.Now().Add(s.accessTokenTTL) + + claims := jwt.MapClaims{ "sub": fmt.Sprintf("%d", account.Base.ID), "email": account.Email, "role": account.Role, - "exp": accessExpiresAt.Unix(), + "exp": expiresAt.Unix(), "iat": time.Now().Unix(), "type": "access", } - accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) - accessToken, err = accessTokenObj.SignedString(s.jwtSecret) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString(s.jwtSecret) if err != nil { - return "", "", time.Time{}, err + return "", time.Time{}, err } - // Создаем refresh token - refreshClaims := jwt.MapClaims{ + return tokenString, expiresAt, nil +} + +// generateRefreshToken генерирует refresh token +func (s *authServiceImpl) generateRefreshToken(account *models.Account) (string, error) { + expiresAt := time.Now().Add(s.refreshTokenTTL) + + claims := jwt.MapClaims{ "sub": fmt.Sprintf("%d", account.Base.ID), - "exp": refreshExpiresAt.Unix(), + "exp": expiresAt.Unix(), "iat": time.Now().Unix(), "type": "refresh", "jti": fmt.Sprintf("refresh-%d-%d", account.Base.ID, time.Now().Unix()), } - refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) - refreshToken, err = refreshTokenObj.SignedString(s.jwtSecret) + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString(s.jwtSecret) if err != nil { - return "", "", time.Time{}, err + return "", err } - return accessToken, refreshToken, accessExpiresAt, nil + return tokenString, nil +} + +// validateRefreshToken валидирует refresh token +func (s *authServiceImpl) validateRefreshToken(tokenString string) (*jwt.MapClaims, error) { + token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) + } + return s.jwtSecret, nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, ErrInvalidToken + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, ErrInvalidToken + } + + // Проверяем тип токена + if tokenType, exists := claims["type"]; !exists || tokenType != "refresh" { + return nil, ErrInvalidToken + } + + // Проверяем время истечения + exp, ok := claims["exp"].(float64) + if ok && int64(exp) < time.Now().Unix() { + return nil, ErrTokenExpired + } + + return &claims, nil } \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/middleware/auth.go b/main_dc/yalarba/api_yal/internal/middleware/auth.go index 50db5c7..b944f31 100644 --- a/main_dc/yalarba/api_yal/internal/middleware/auth.go +++ b/main_dc/yalarba/api_yal/internal/middleware/auth.go @@ -1,7 +1,9 @@ +// middleware/auth.go (обновленная версия с логированием) package middleware import ( "context" + "fmt" "net/http" "strings" @@ -14,12 +16,9 @@ import ( type contextKey string const ( - // UserIDKey ключ для хранения ID пользователя в контексте - UserIDKey contextKey = "userID" - // UserEmailKey ключ для хранения email пользователя в контексте + UserIDKey contextKey = "userID" UserEmailKey contextKey = "userEmail" - // UserRoleKey ключ для хранения роли пользователя в контексте - UserRoleKey contextKey = "userRole" + UserRoleKey contextKey = "userRole" ) // AuthMiddleware создает middleware для проверки JWT токена @@ -28,10 +27,15 @@ func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { l := logger.Get() + l.Info("=== AUTH MIDDLEWARE START ===") + l.Info("Request path", zap.String("path", r.URL.Path)) + // Получаем токен из заголовка Authorization authHeader := r.Header.Get("Authorization") + l.Info("Authorization header", zap.String("header", authHeader)) + if authHeader == "" { - l.Debug("Отсутствует заголовок Authorization") + l.Warn("Отсутствует заголовок Authorization") http.Error(w, "Authorization header required", http.StatusUnauthorized) return } @@ -39,63 +43,101 @@ func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler { // Ожидаем формат "Bearer " parts := strings.Split(authHeader, " ") if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { - l.Debug("Неверный формат заголовка Authorization") + l.Warn("Неверный формат заголовка Authorization", + zap.Int("parts_count", len(parts)), + zap.String("first_part", parts[0])) http.Error(w, "Invalid authorization header format", http.StatusUnauthorized) return } tokenString := parts[1] + l.Info("Token extracted", zap.String("token_preview", tokenString[:min(20, len(tokenString))]+"...")) // Парсим и валидируем токен token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Проверяем метод подписи if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + l.Error("Unexpected signing method", + zap.String("method", token.Method.Alg())) return nil, jwt.ErrSignatureInvalid } return []byte(jwtSecret), nil }) - if err != nil || !token.Valid { - l.Debug("Невалидный токен: %v", zap.Error(err)) + if err != nil { + l.Error("Token parse error", zap.Error(err)) + http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized) + return + } + + if !token.Valid { + l.Error("Token is not valid") http.Error(w, "Invalid token", http.StatusUnauthorized) return } + l.Info("Token is valid") + // Извлекаем claims claims, ok := token.Claims.(jwt.MapClaims) if !ok { - l.Debug("Не удалось извлечь claims из токена") + l.Error("Failed to extract claims") http.Error(w, "Invalid token claims", http.StatusUnauthorized) return } + l.Info("Claims extracted", zap.Any("claims", claims)) + + // Проверяем тип токена (должен быть access) + if tokenType, exists := claims["type"]; exists { + l.Info("Token type", zap.String("type", tokenType.(string))) + if tokenType != "access" { + l.Error("Wrong token type, expected access", zap.String("type", tokenType.(string))) + http.Error(w, "Invalid token type", http.StatusUnauthorized) + return + } + } + // Добавляем информацию о пользователе в контекст ctx := r.Context() // Извлекаем userID из sub (subject) - if userID, ok := claims["sub"].(float64); ok { - ctx = context.WithValue(ctx, UserIDKey, uint(userID)) + // В claims sub хранится как string, а не float64 + if userID, ok := claims["sub"].(string); ok { + l.Info("User ID from claims", zap.String("user_id_str", userID)) + // Конвертируем string в uint + var userIDUint uint + if _, err := fmt.Sscan(userID, &userIDUint); err == nil { + ctx = context.WithValue(ctx, UserIDKey, userIDUint) + l.Info("User ID added to context", zap.Uint("user_id", userIDUint)) + } + } else { + l.Error("sub claim not found or wrong type") } // Извлекаем email if email, ok := claims["email"].(string); ok { ctx = context.WithValue(ctx, UserEmailKey, email) + l.Info("Email added to context", zap.String("email", email)) } // Извлекаем роль if role, ok := claims["role"].(string); ok { ctx = context.WithValue(ctx, UserRoleKey, role) + l.Info("Role added to context", zap.String("role", role)) } + l.Info("=== AUTH MIDDLEWARE END ===") + // Передаем управление дальше с обновленным контекстом next.ServeHTTP(w, r.WithContext(ctx)) }) } } -// AuthMiddlewareWithContext (для обратной совместимости) -func AuthMiddlewareWithContext(next http.Handler) http.Handler { - // Эта функция должна быть реализована в основном приложении - // с передачей jwtSecret из конфигурации - return next +func min(a, b int) int { + if a < b { + return a + } + return b } \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/router/router.go b/main_dc/yalarba/api_yal/internal/router/router.go index 224abf1..e162493 100644 --- a/main_dc/yalarba/api_yal/internal/router/router.go +++ b/main_dc/yalarba/api_yal/internal/router/router.go @@ -4,7 +4,6 @@ import ( "api_yal/internal/config" "api_yal/internal/logger" "api_yal/internal/domain/auth" - CastomMiddleware "api_yal/internal/middleware" "time" "encoding/json" @@ -29,7 +28,6 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { addProductionMiddleware(r, config) // 2. Затем добавляем middleware аутентификации (он тоже применяется ко всем маршрутам) - r.Use(CastomMiddleware.AuthMiddlewareWithContext) zapLogger.Debug("Auth middleware применён") // 3. И только потом регистрируем маршруты