// Package auth — выпуск и проверка JWT, хеши паролей и middleware // авторизации. Пакет ничего не знает о базе: работает только со строками. package auth import ( "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" ) // allowedAlgorithms — алгоритмы, которыми могут быть подписаны токены. // Раньше ключевая функция возвращала секрет для любого t.Method: подделанный // заголовок alg проходил бы без проверки. Теперь и алгоритм, и ключ // сверяются явно, а сама библиотека сравнивает подпись за постоянное время. var allowedAlgorithms = map[string]bool{"HS256": true} const ( // AccessTTL — жизнь короткого access-токена. Дальше клиент идёт // на /auth/refresh с refresh-токеном, который отозвать можно. AccessTTL = time.Hour // RefreshTTL — жизнь refresh-токена; при каждом обновлении он // меняется на новый (ротация), старый гасится. RefreshTTL = 30 * 24 * time.Hour ) // Claims — полезная нагрузка нашего токена: идентификатор пользователя, // роль и стандартные временные поля (exp, iat). type Claims struct { UserID string `json:"user_id"` Role string `json:"role"` jwt.RegisteredClaims } // GenerateToken подписывает access-токен на AccessTTL. Пустой секрет — // ошибка, а не подпись пустой строкой: раньше так можно было войти, // подставив любой ключ. func GenerateToken(secret, userID, role string) (string, error) { if secret == "" { return "", errors.New("jwt secret is empty") } claims := Claims{ UserID: userID, Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTTL)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(secret)) } // ParseToken проверяет подпись, алгоритм и срок жизни и возвращает // claims. Любой спорный случай (чужой секрет, alg≠HS256, просрочка) — // ошибка: наружу она не уходит, middleware отдаёт 401. func ParseToken(secret, tokenStr string) (*Claims, error) { if secret == "" { return nil, errors.New("jwt secret is empty") } claims := &Claims{} token, err := jwt.ParseWithClaims( tokenStr, claims, func(t *jwt.Token) (interface{}, error) { if !allowedAlgorithms[t.Method.Alg()] { return nil, fmt.Errorf("unexpected signing method: %s", t.Method.Alg()) } return []byte(secret), nil }, // belt & braces: WithValidMethods повторяет проверку выше, // WithExpirationRequired не пускает токены без exp. jwt.WithValidMethods([]string{"HS256"}), jwt.WithExpirationRequired(), ) if err != nil { return nil, err } if !token.Valid { return nil, jwt.ErrTokenInvalidClaims } return claims, nil }