108 lines
2.9 KiB
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
|
||
|
|
}
|