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
|
|||
|
|
}
|