thread-master/apps/backend/internal/module/token/usecase/jwt.go

108 lines
2.9 KiB
Go

package usecase
import (
"crypto/rand"
"encoding/hex"
"fmt"
"time"
"apps/backend/internal/module/token/domain"
"github.com/golang-jwt/jwt/v5"
)
// JWTIssuer implements domain.Issuer (stand-alone token/usecase JWT spirit; HS256 dual secret).
type JWTIssuer struct {
AccessSecret []byte
RefreshSecret []byte
AccessTTL time.Duration
RefreshTTL time.Duration
}
func NewJWTIssuer(accessSecret, refreshSecret string, accessExpireSec, refreshExpireSec int64) *JWTIssuer {
if accessExpireSec <= 0 {
accessExpireSec = 86400
}
if refreshExpireSec <= 0 {
refreshExpireSec = 604800
}
return &JWTIssuer{
AccessSecret: []byte(accessSecret),
RefreshSecret: []byte(refreshSecret),
AccessTTL: time.Duration(accessExpireSec) * time.Second,
RefreshTTL: time.Duration(refreshExpireSec) * time.Second,
}
}
func newJTI() string {
var b [16]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}
func (i *JWTIssuer) Issue(uid int64, roles []string) (*domain.TokenPair, string, time.Time, error) {
now := time.Now()
accessExp := now.Add(i.AccessTTL)
refreshExp := now.Add(i.RefreshTTL)
refreshJTI := newJTI()
access := jwt.NewWithClaims(jwt.SigningMethodHS256, domain.Claims{
UID: uid, Roles: roles, Kind: "access",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(accessExp),
IssuedAt: jwt.NewNumericDate(now),
Subject: fmt.Sprintf("%d", uid),
},
})
accessStr, err := access.SignedString(i.AccessSecret)
if err != nil {
return nil, "", time.Time{}, err
}
refresh := jwt.NewWithClaims(jwt.SigningMethodHS256, domain.Claims{
UID: uid, Roles: roles, JTI: refreshJTI, Kind: "refresh",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(refreshExp),
IssuedAt: jwt.NewNumericDate(now),
Subject: fmt.Sprintf("%d", uid),
ID: refreshJTI,
},
})
refreshStr, err := refresh.SignedString(i.RefreshSecret)
if err != nil {
return nil, "", time.Time{}, err
}
return &domain.TokenPair{
AccessToken: accessStr,
RefreshToken: refreshStr,
TokenType: "Bearer",
ExpiresIn: int64(i.AccessTTL.Seconds()),
}, refreshJTI, refreshExp, nil
}
func (i *JWTIssuer) ParseAccess(tokenStr string) (*domain.Claims, error) {
return i.parse(tokenStr, i.AccessSecret, "access")
}
func (i *JWTIssuer) ParseRefresh(tokenStr string) (*domain.Claims, error) {
return i.parse(tokenStr, i.RefreshSecret, "refresh")
}
func (i *JWTIssuer) parse(tokenStr string, secret []byte, kind string) (*domain.Claims, error) {
tok, err := jwt.ParseWithClaims(tokenStr, &domain.Claims{}, func(t *jwt.Token) (interface{}, error) {
if t.Method != jwt.SigningMethodHS256 {
return nil, fmt.Errorf("unexpected signing method")
}
return secret, nil
})
if err != nil {
return nil, domain.ErrInvalidToken
}
claims, ok := tok.Claims.(*domain.Claims)
if !ok || !tok.Valid || claims.Kind != kind {
return nil, domain.ErrInvalidToken
}
return claims, nil
}