diff --git a/go.mod b/go.mod index cc11966..2e12f11 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( require github.com/vearutop/statigz v1.4.0 // indirect require ( + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.10.0 diff --git a/internal/modules/processor_jwt/interface.go b/internal/modules/processor_jwt/interface.go new file mode 100644 index 0000000..39705da --- /dev/null +++ b/internal/modules/processor_jwt/interface.go @@ -0,0 +1,24 @@ +package processor_jwt + +import ( + "slices" + + "github.com/golang-jwt/jwt/v5" +) + +type JWTClaims struct { + UserID int `json:"user_id"` + UserEmail string `json:"user_email"` + Roles []string `json:"roles"` + jwt.RegisteredClaims +} + +func (c *JWTClaims) HasRole(role string) bool { + return slices.Contains(c.Roles, role) +} + +type IProcessorJWT interface { + GenerateAccessToken(userId int, userEmail string, userRoles []string) (string, error) + GenerateRefreshToken(userId int) (string, error) + GetClaims(token string) (*JWTClaims, error) +} diff --git a/internal/modules/processor_jwt/processor.go b/internal/modules/processor_jwt/processor.go new file mode 100644 index 0000000..00a37ec --- /dev/null +++ b/internal/modules/processor_jwt/processor.go @@ -0,0 +1,75 @@ +package processor_jwt + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type processor struct { + secret string +} + +func NewProcessor(secret string) IProcessorJWT { + return &processor{ + secret: secret, + } +} + +func (p *processor) GenerateAccessToken(userId int, userEmail string, userRoles []string) (string, error) { + now := time.Now() + + claims := &JWTClaims{ + UserID: userId, + UserEmail: userEmail, + Roles: userRoles, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(3 * time.Hour)), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(p.secret) +} + +func (p *processor) GenerateRefreshToken(userId int) (string, error) { + now := time.Now() + + claims := &JWTClaims{ + UserID: userId, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(7 * 24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(p.secret) +} + +func (p *processor) GetClaims(token string) (*JWTClaims, error) { + tokenWithClaims, err := jwt.ParseWithClaims( + token, + &JWTClaims{}, + func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return p.secret, nil + }, + ) + if err != nil || !tokenWithClaims.Valid { + return nil, errors.New("invalid or expired token") + } + + claims, ok := tokenWithClaims.Claims.(*JWTClaims) + if !ok { + return nil, errors.New("invalid claims") + } + + return claims, nil +}