From 597000f22208137d3c948add7a8c458b9c411f2f Mon Sep 17 00:00:00 2001 From: Alexey Date: Thu, 18 Dec 2025 10:48:27 +0200 Subject: [PATCH] add jwt creating and parsing --- internal/jwt/parse.go | 28 +++++++++++++++++++++ internal/jwt/service.go | 49 ++++++++++++++++++++++++++++++++++++ internal/jwt/signer.go | 8 ++++++ internal/jwt/signer_HS256.go | 20 +++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 internal/jwt/parse.go create mode 100644 internal/jwt/service.go create mode 100644 internal/jwt/signer.go create mode 100644 internal/jwt/signer_HS256.go diff --git a/internal/jwt/parse.go b/internal/jwt/parse.go new file mode 100644 index 0000000..78e9a1c --- /dev/null +++ b/internal/jwt/parse.go @@ -0,0 +1,28 @@ +package jwt + +import ( + "fmt" + + "github.com/golang-jwt/jwt/v5" +) + +func parse( + tokenStr string, + method jwt.SigningMethod, + key any, +) (jwt.Claims, error) { + t, err := jwt.Parse(tokenStr, func(tok *jwt.Token) (any, error) { + if tok.Method.Alg() != method.Alg() { + return nil, fmt.Errorf("unexpected signing method") + } + return key, nil + }) + if err != nil { + return nil, err + } + // check validity twice: invalid token may return nil error + if !t.Valid { + return nil, fmt.Errorf("invalid token") + } + return t.Claims, nil +} diff --git a/internal/jwt/service.go b/internal/jwt/service.go new file mode 100644 index 0000000..6a6f80b --- /dev/null +++ b/internal/jwt/service.go @@ -0,0 +1,49 @@ +package jwt + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +type Service struct { + signer Signer + expiry time.Duration +} + +func NewService(signer Signer, exp time.Duration) *Service { + return &Service{ + signer: signer, + expiry: exp, + } +} + +// Generate creates a new JWT token for a given user ID and +// returns the token string along with its JTI(JWT IDentifier). +func (s *Service) Generate(userID int) (string, string, error) { + jti := uuid.NewString() + + claims := jwt.MapClaims{ + "sub": userID, + "jti": jti, + "exp": time.Now().Add(s.expiry).Unix(), + "iat": time.Now().Unix(), + } + + token, err := s.signer.Sign(claims) + return token, jti, err +} + +// Validate verifies the JWT token and extracts the user ID and JTI(JWT IDentifier). +func (s *Service) Validate(token string) (int, string, error) { + claims, err := s.signer.Verify(token) + if err != nil { + return 0, "", err + } + + sub := int(claims.(jwt.MapClaims)["sub"].(float64)) + jti := claims.(jwt.MapClaims)["jti"].(string) + + return sub, jti, nil +} diff --git a/internal/jwt/signer.go b/internal/jwt/signer.go new file mode 100644 index 0000000..65f30db --- /dev/null +++ b/internal/jwt/signer.go @@ -0,0 +1,8 @@ +package jwt + +import "github.com/golang-jwt/jwt/v5" + +type Signer interface { + Sign(claims jwt.Claims) (string, error) + Verify(token string) (jwt.Claims, error) +} diff --git a/internal/jwt/signer_HS256.go b/internal/jwt/signer_HS256.go new file mode 100644 index 0000000..0a1bc5c --- /dev/null +++ b/internal/jwt/signer_HS256.go @@ -0,0 +1,20 @@ +package jwt + +import "github.com/golang-jwt/jwt/v5" + +type HMACSigner struct { + secret []byte +} + +func NewHMACSigner(secret []byte) *HMACSigner { + return &HMACSigner{secret: secret} +} + +func (s *HMACSigner) Sign(claims jwt.Claims) (string, error) { + t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return t.SignedString(s.secret) +} + +func (s *HMACSigner) Verify(tokenStr string) (jwt.Claims, error) { + return parse(tokenStr, jwt.SigningMethodHS256, s.secret) +}