157 lines
4.9 KiB
Go
157 lines
4.9 KiB
Go
package auth
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"log"
|
||
"strconv"
|
||
"tailly_back_v2/internal/domain"
|
||
"time"
|
||
|
||
"github.com/golang-jwt/jwt/v5"
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
var (
|
||
ErrInvalidToken = errors.New("invalid token")
|
||
)
|
||
|
||
type TokenAuth struct {
|
||
accessTokenSecret string
|
||
refreshTokenSecret string
|
||
accessTokenExpiry time.Duration
|
||
refreshTokenExpiry time.Duration
|
||
}
|
||
|
||
func NewTokenAuth(accessSecret, refreshSecret string, accessExpiry, refreshExpiry time.Duration) *TokenAuth {
|
||
return &TokenAuth{
|
||
accessTokenSecret: accessSecret,
|
||
refreshTokenSecret: refreshSecret,
|
||
accessTokenExpiry: accessExpiry,
|
||
refreshTokenExpiry: refreshExpiry,
|
||
}
|
||
}
|
||
|
||
// GenerateTokens создает пару access и refresh токенов
|
||
func (a *TokenAuth) GenerateTokens(userID int, existingRefreshToken ...string) (*domain.Tokens, error) {
|
||
accessExpires := time.Now().UTC().Add(a.accessTokenExpiry)
|
||
refreshExpires := time.Now().UTC().Add(a.refreshTokenExpiry)
|
||
|
||
// Генерируем новый access token
|
||
accessToken, err := a.generateAccessToken(userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Используем существующий refresh token, если он передан и валиден
|
||
var refreshToken string
|
||
if len(existingRefreshToken) > 0 && existingRefreshToken[0] != "" {
|
||
// Проверяем, что существующий токен еще действителен
|
||
if _, err := a.ValidateRefreshToken(existingRefreshToken[0]); err == nil {
|
||
refreshToken = existingRefreshToken[0]
|
||
} else {
|
||
log.Printf("Existing refresh token is invalid, generating new one: %v", err)
|
||
refreshToken, err = a.generateRefreshToken(userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
} else {
|
||
// Генерируем новый refresh token
|
||
refreshToken, err = a.generateRefreshToken(userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
return &domain.Tokens{
|
||
AccessToken: accessToken,
|
||
RefreshToken: refreshToken,
|
||
AccessTokenExpires: accessExpires,
|
||
RefreshTokenExpires: refreshExpires,
|
||
}, nil
|
||
}
|
||
|
||
// generateAccessToken создает access токен (короткоживущий)
|
||
func (a *TokenAuth) generateAccessToken(userID int) (string, error) {
|
||
claims := jwt.RegisteredClaims{
|
||
Subject: fmt.Sprintf("%d", userID),
|
||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(a.accessTokenExpiry)),
|
||
}
|
||
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||
return token.SignedString([]byte(a.accessTokenSecret))
|
||
}
|
||
|
||
// generateRefreshToken создает refresh токен (долгоживущий)
|
||
func (a *TokenAuth) generateRefreshToken(userID int) (string, error) {
|
||
claims := jwt.RegisteredClaims{
|
||
Subject: fmt.Sprintf("%d", userID),
|
||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(a.refreshTokenExpiry)),
|
||
}
|
||
|
||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||
return token.SignedString([]byte(a.refreshTokenSecret))
|
||
}
|
||
|
||
// ValidateAccessToken проверяет access токен и возвращает userID
|
||
func (a *TokenAuth) ValidateAccessToken(tokenString string) (int, error) {
|
||
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||
}
|
||
return []byte(a.accessTokenSecret), nil
|
||
})
|
||
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid {
|
||
var userID int
|
||
_, err := fmt.Sscanf(claims.Subject, "%d", &userID)
|
||
if err != nil {
|
||
return 0, ErrInvalidToken
|
||
}
|
||
return userID, nil
|
||
}
|
||
|
||
return 0, ErrInvalidToken
|
||
}
|
||
|
||
// ValidateRefreshToken проверяет refresh токен и возвращает userID
|
||
func (a *TokenAuth) ValidateRefreshToken(tokenString string) (int, error) {
|
||
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||
}
|
||
return []byte(a.refreshTokenSecret), nil
|
||
})
|
||
log.Printf("ValidateRefreshToken валидация токена: %v", token)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("token validation failed: %w", err)
|
||
}
|
||
|
||
if claims, ok := token.Claims.(*jwt.RegisteredClaims); ok && token.Valid {
|
||
userID, err := strconv.Atoi(claims.Subject)
|
||
if err != nil {
|
||
return 0, errors.New("invalid user ID in token")
|
||
}
|
||
return userID, nil
|
||
}
|
||
|
||
return 0, errors.New("invalid token claims")
|
||
}
|
||
|
||
// HashPassword хеширует пароль
|
||
func HashPassword(password string) (string, error) {
|
||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||
return string(bytes), err
|
||
}
|
||
|
||
// CheckPasswordHash проверяет пароль с хешем
|
||
func CheckPasswordHash(password, hash string) bool {
|
||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||
return err == nil
|
||
}
|