axe/internal/auth/jwt.go
2026-10-18 22:07:25 +03:00

86 lines
3.4 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 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
}