axe/internal/auth/jwt.go

86 lines
3.4 KiB
Go
Raw Permalink Normal View History

2026-10-18 19:07:25 +00:00
// 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
}