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