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 }