package security import ( "errors" "time" "github.com/golang-jwt/jwt/v5" ) const TokenAudience = "KRA" var ( ErrEmptySigningKey = errors.New("empty JWT signing key") ErrInvalidClaims = errors.New("invalid token claims") ErrTokenExpired = errors.New("token expired") ErrTokenMalformed = errors.New("token malformed") ErrTokenSignatureInvalid = errors.New("token signature invalid") ErrTokenNotValidYet = errors.New("token not valid yet") ErrTokenInvalid = errors.New("token invalid") ) type Claims struct { UUID string ID uint Username string NickName string AuthorityID uint `json:"AuthorityId"` BufferTime int64 UserType string MustChangePwd bool `json:"mustChangePwd"` PasswordVersion int64 `json:"passwordVersion,omitempty"` jwt.RegisteredClaims } func Generate(secret, issuer string, expires, buffer time.Duration, userID, authorityID uint, uuid, username, nickname string, mustChange bool, passwordVersion int64) (string, *Claims, error) { if err := validateSigningOptions(secret, expires, buffer); err != nil { return "", nil, err } now := time.Now() claims := &Claims{UUID: uuid, ID: userID, Username: username, NickName: nickname, AuthorityID: authorityID, BufferTime: int64(buffer / time.Second), UserType: "admin", MustChangePwd: mustChange, PasswordVersion: passwordVersion, RegisteredClaims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{TokenAudience}, Issuer: issuer, IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now.Add(-time.Second)), ExpiresAt: jwt.NewNumericDate(now.Add(expires))}} token, err := Sign(secret, claims) return token, claims, err } func Sign(secret string, claims *Claims) (string, error) { if secret == "" { return "", ErrEmptySigningKey } if claims == nil { return "", ErrInvalidClaims } return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret)) } func Parse(tokenString, secret string) (*Claims, error) { return parse(tokenString, secret, "") } // ParseWithIssuer validates a KRA admin token and its configured issuer. func ParseWithIssuer(tokenString, secret, issuer string) (*Claims, error) { return parse(tokenString, secret, issuer) } func parse(tokenString, secret, issuer string) (*Claims, error) { if secret == "" { return nil, ErrEmptySigningKey } options := []jwt.ParserOption{ jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), jwt.WithAudience(TokenAudience), jwt.WithExpirationRequired(), jwt.WithNotBeforeRequired(), jwt.WithLeeway(time.Second), } if issuer != "" { options = append(options, jwt.WithIssuer(issuer)) } token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) { if token.Method != jwt.SigningMethodHS256 { return nil, errors.New("unexpected signing method") } return []byte(secret), nil }, options...) if err != nil { switch { case errors.Is(err, jwt.ErrTokenExpired): return nil, ErrTokenExpired case errors.Is(err, jwt.ErrTokenMalformed): return nil, ErrTokenMalformed case errors.Is(err, jwt.ErrTokenSignatureInvalid): return nil, ErrTokenSignatureInvalid case errors.Is(err, jwt.ErrTokenNotValidYet): return nil, ErrTokenNotValidYet default: return nil, ErrTokenInvalid } } if !token.Valid { return nil, ErrTokenInvalid } claims, ok := token.Claims.(*Claims) if !ok { return nil, ErrInvalidClaims } return claims, nil } func validateSigningOptions(secret string, expires, buffer time.Duration) error { if secret == "" { return ErrEmptySigningKey } if expires <= 0 { return errors.New("JWT expiration must be positive") } if buffer < 0 { return errors.New("JWT buffer must not be negative") } return nil }