package system import ( "errors" "kra/internal/biz/system" "time" jwt "github.com/golang-jwt/jwt/v5" ) // tokenAudience is the KRA administrator audience claim. Claims, audience, // issuer and password-version fields are this application's security // protocol, so they stay beside the only issuer that signs them. const tokenAudience = "KRA" var ( errEmptySigningKey = errors.New("empty JWT signing key") errInvalidClaims = errors.New("invalid token claims") ) type tokenClaims 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 generateToken(secret, issuer string, expires, buffer time.Duration, userID, authorityID uint, uuid, username, nickname string, mustChange bool, passwordVersion int64) (string, *tokenClaims, error) { if err := validateSigningOptions(secret, expires, buffer); err != nil { return "", nil, err } now := time.Now() claims := &tokenClaims{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 := signTokenUnchecked(secret, claims) return token, claims, err } func signToken(secret string, claims *tokenClaims) (string, error) { if secret == "" { return "", errEmptySigningKey } if claims == nil { return "", errInvalidClaims } return signTokenUnchecked(secret, claims) } func signTokenUnchecked(secret string, claims *tokenClaims) (string, error) { return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret)) } // parseTokenWithIssuer validates a KRA admin token and maps driver errors to // the biz token errors callers above already branch on. func parseTokenWithIssuer(tokenString, secret, issuer string) (*tokenClaims, 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, &tokenClaims{}, 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, system.ErrTokenExpired case errors.Is(err, jwt.ErrTokenMalformed): return nil, system.ErrTokenMalformed case errors.Is(err, jwt.ErrTokenSignatureInvalid): return nil, system.ErrTokenSignatureInvalid case errors.Is(err, jwt.ErrTokenNotValidYet): return nil, system.ErrTokenNotValidYet default: return nil, system.ErrTokenInvalid } } if !token.Valid { return nil, system.ErrTokenInvalid } claims, ok := token.Claims.(*tokenClaims) if !ok { return nil, system.ErrTokenInvalid } 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 }