package core import ( "encoding/json" "errors" "time" "github.com/golang-jwt/jwt/v5" ) type JWT struct { signingKey []byte lifetimeMinutes int } type JWTOptions struct { SigningKey string LifetimeMinutes int } var j *JWT func newJWT(opts JWTOptions) *JWT { j = &JWT{ signingKey: []byte(opts.SigningKey), lifetimeMinutes: opts.LifetimeMinutes, } return j } func resolveJWT() *JWT { return j } // LifetimeMinutes returns the configured token lifetime in minutes. func (j *JWT) LifetimeMinutes() int { return j.lifetimeMinutes } type claims struct { J []byte jwt.RegisteredClaims } func (j *JWT) GenerateToken(payload map[string]interface{}) (string, error) { expiresAt := time.Now().Add(time.Duration(j.lifetimeMinutes) * time.Minute) claims, err := mapClaims(payload, expiresAt) if err != nil { return "", err } t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token, err := t.SignedString(j.signingKey) if err != nil { return "", err } return token, nil } // DecodeTokenIgnoreExpiry decodes a token's payload without validating its expiration. // It is used to inspect tokens that may already be expired (e.g. sliding session renewal), // while still verifying the signature and the token structure. func (j *JWT) DecodeTokenIgnoreExpiry(token string) (payload map[string]interface{}, err error) { t, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) { return j.signingKey, nil }, jwt.WithoutClaimsValidation()) if err != nil { return nil, err } c, ok := t.Claims.(*claims) if !ok { return nil, errors.New("error decoding token") } err = json.Unmarshal(c.J, &payload) if err != nil { return nil, err } return payload, nil } // ExpiresAtIgnoreExpiry returns the expiration time carried by the token, without // validating it. It verifies the signature and returns the token's "exp" claim. func (j *JWT) ExpiresAtIgnoreExpiry(token string) (time.Time, error) { t, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) { return j.signingKey, nil }, jwt.WithoutClaimsValidation()) if err != nil { return time.Time{}, err } c, ok := t.Claims.(*claims) if !ok { return time.Time{}, errors.New("error decoding token") } if c.ExpiresAt == nil { return time.Time{}, errors.New("token has no expiration") } return time.Unix(c.ExpiresAt.Unix(), 0), nil } func (j *JWT) DecodeToken(token string) (payload map[string]interface{}, err error) { t, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) { return j.signingKey, nil }) if err != nil { return nil, err } c, ok := t.Claims.(*claims) if !ok { return nil, errors.New("error decoding token") } expiresAt := time.Unix(c.ExpiresAt.Unix(), 0) et := time.Now().Compare(expiresAt) if et != -1 { return nil, errors.New("token has expired") } err = json.Unmarshal(c.J, &payload) if err != nil { return nil, err } return payload, nil } func (j *JWT) HasExpired(token string) (bool, error) { _, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) { return j.signingKey, nil }) if err != nil { if errors.Is(err, jwt.ErrTokenExpired) { return true, nil } return true, err } return false, nil } func mapClaims(data map[string]interface{}, expiresAt time.Time) (jwt.Claims, error) { j, err := json.Marshal(data) if err != nil { return claims{}, err } r := claims{ j, jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresAt), }, } return r, nil }