diff --git a/examples/echo-example/go.mod b/examples/echo-example/go.mod index ddfabcc..88b19f6 100644 --- a/examples/echo-example/go.mod +++ b/examples/echo-example/go.mod @@ -5,11 +5,11 @@ go 1.24.0 toolchain go1.24.8 require ( - github.com/auth0/go-jwt-middleware/v2 v2.3.0 + github.com/auth0/go-jwt-middleware/v3 v3.0.0 github.com/labstack/echo/v4 v4.13.4 ) -replace github.com/auth0/go-jwt-middleware/v2 => ./../../ +replace github.com/auth0/go-jwt-middleware/v3 => ./../../ require ( github.com/labstack/gommon v0.4.2 // indirect diff --git a/examples/echo-example/middleware.go b/examples/echo-example/middleware.go index eebd01e..5da2209 100644 --- a/examples/echo-example/middleware.go +++ b/examples/echo-example/middleware.go @@ -26,11 +26,6 @@ var ( return signingKey, nil } - // We want this struct to be filled in with - // our custom claims from the token. - customClaims = func() validator.CustomClaims { - return &CustomClaimsExample{} - } ) // checkJWT is an echo.HandlerFunc middleware @@ -38,11 +33,14 @@ var ( func checkJWT(next echo.HandlerFunc) echo.HandlerFunc { // Set up the validator. jwtValidator, err := validator.New( - keyFunc, - validator.HS256, - issuer, - audience, - validator.WithCustomClaims(customClaims), + validator.WithKeyFunc(keyFunc), + validator.WithAlgorithm(validator.HS256), + validator.WithIssuer(issuer), + validator.WithAudiences(audience), + // WithCustomClaims now uses generics - no need to return interface type + validator.WithCustomClaims(func() *CustomClaimsExample { + return &CustomClaimsExample{} + }), validator.WithAllowedClockSkew(30*time.Second), ) if err != nil { diff --git a/examples/gin-example/go.mod b/examples/gin-example/go.mod index 6f5f490..881a272 100644 --- a/examples/gin-example/go.mod +++ b/examples/gin-example/go.mod @@ -5,11 +5,11 @@ go 1.24.0 toolchain go1.24.8 require ( - github.com/auth0/go-jwt-middleware/v2 v2.3.0 + github.com/auth0/go-jwt-middleware/v3 v3.0.0 github.com/gin-gonic/gin v1.10.1 ) -replace github.com/auth0/go-jwt-middleware/v2 => ./../../ +replace github.com/auth0/go-jwt-middleware/v3 => ./../../ require ( github.com/bytedance/gopkg v0.1.3 // indirect diff --git a/examples/gin-example/middleware.go b/examples/gin-example/middleware.go index 90ca761..a02758c 100644 --- a/examples/gin-example/middleware.go +++ b/examples/gin-example/middleware.go @@ -26,11 +26,6 @@ var ( return signingKey, nil } - // We want this struct to be filled in with - // our custom claims from the token. - customClaims = func() validator.CustomClaims { - return &CustomClaimsExample{} - } ) // checkJWT is a gin.HandlerFunc middleware @@ -38,11 +33,14 @@ var ( func checkJWT() gin.HandlerFunc { // Set up the validator. jwtValidator, err := validator.New( - keyFunc, - validator.HS256, - issuer, - audience, - validator.WithCustomClaims(customClaims), + validator.WithKeyFunc(keyFunc), + validator.WithAlgorithm(validator.HS256), + validator.WithIssuer(issuer), + validator.WithAudiences(audience), + // WithCustomClaims now uses generics - no need to return interface type + validator.WithCustomClaims(func() *CustomClaimsExample { + return &CustomClaimsExample{} + }), validator.WithAllowedClockSkew(30*time.Second), ) if err != nil { diff --git a/examples/http-example/go.mod b/examples/http-example/go.mod index 4300950..a603c8b 100644 --- a/examples/http-example/go.mod +++ b/examples/http-example/go.mod @@ -5,10 +5,10 @@ go 1.24.0 toolchain go1.24.8 require ( - github.com/auth0/go-jwt-middleware/v2 v2.3.0 + github.com/auth0/go-jwt-middleware/v3 v3.0.0 gopkg.in/go-jose/go-jose.v2 v2.6.3 ) -replace github.com/auth0/go-jwt-middleware/v2 => ./../../ +replace github.com/auth0/go-jwt-middleware/v3 => ./../../ require golang.org/x/crypto v0.45.0 // indirect diff --git a/examples/http-example/main.go b/examples/http-example/main.go index 4fd70a8..caa866a 100644 --- a/examples/http-example/main.go +++ b/examples/http-example/main.go @@ -65,19 +65,16 @@ func setupHandler() http.Handler { return signingKey, nil } - // We want this struct to be filled in with - // our custom claims from the token. - customClaims := func() validator.CustomClaims { - return &CustomClaimsExample{} - } - // Set up the validator. jwtValidator, err := validator.New( - keyFunc, - validator.HS256, - issuer, - audience, - validator.WithCustomClaims(customClaims), + validator.WithKeyFunc(keyFunc), + validator.WithAlgorithm(validator.HS256), + validator.WithIssuer(issuer), + validator.WithAudiences(audience), + // WithCustomClaims now uses generics - no need to return interface type + validator.WithCustomClaims(func() *CustomClaimsExample { + return &CustomClaimsExample{} + }), validator.WithAllowedClockSkew(30*time.Second), ) if err != nil { diff --git a/examples/http-jwks-example/go.mod b/examples/http-jwks-example/go.mod index bb1c6a9..a228aee 100644 --- a/examples/http-jwks-example/go.mod +++ b/examples/http-jwks-example/go.mod @@ -5,11 +5,11 @@ go 1.24.0 toolchain go1.24.8 require ( - github.com/auth0/go-jwt-middleware/v2 v2.3.0 + github.com/auth0/go-jwt-middleware/v3 v3.0.0 gopkg.in/go-jose/go-jose.v2 v2.6.3 ) -replace github.com/auth0/go-jwt-middleware/v2 => ./../../ +replace github.com/auth0/go-jwt-middleware/v3 => ./../../ require ( golang.org/x/crypto v0.45.0 // indirect diff --git a/examples/http-jwks-example/main.go b/examples/http-jwks-example/main.go index a8a4384..a7fc55f 100644 --- a/examples/http-jwks-example/main.go +++ b/examples/http-jwks-example/main.go @@ -43,10 +43,10 @@ func setupHandler(issuer string, audience []string) http.Handler { // Set up the validator. jwtValidator, err := validator.New( - provider.KeyFunc, - validator.RS256, - issuerURL.String(), - audience, + validator.WithKeyFunc(provider.KeyFunc), + validator.WithAlgorithm(validator.RS256), + validator.WithIssuer(issuerURL.String()), + validator.WithAudiences(audience), ) if err != nil { log.Fatalf("failed to set up the validator: %v", err) diff --git a/examples/iris-example/go.mod b/examples/iris-example/go.mod index 9a2f487..04251bc 100644 --- a/examples/iris-example/go.mod +++ b/examples/iris-example/go.mod @@ -5,11 +5,11 @@ go 1.24.0 toolchain go1.24.8 require ( - github.com/auth0/go-jwt-middleware/v2 v2.2.2 + github.com/auth0/go-jwt-middleware/v3 v3.0.0 github.com/kataras/iris/v12 v12.2.11 ) -replace github.com/auth0/go-jwt-middleware/v2 => ./../../ +replace github.com/auth0/go-jwt-middleware/v3 => ./../../ require ( github.com/BurntSushi/toml v1.3.2 // indirect diff --git a/examples/iris-example/middleware.go b/examples/iris-example/middleware.go index 70fa4ab..67fc295 100644 --- a/examples/iris-example/middleware.go +++ b/examples/iris-example/middleware.go @@ -25,12 +25,6 @@ var ( keyFunc = func(ctx context.Context) (interface{}, error) { return signingKey, nil } - - // We want this struct to be filled in with - // our custom claims from the token. - customClaims = func() validator.CustomClaims { - return &CustomClaims{} - } ) // checkJWT is an iris.Handler middleware @@ -38,11 +32,14 @@ var ( func checkJWT() iris.Handler { // Set up the validator. jwtValidator, err := validator.New( - keyFunc, - validator.HS256, - issuer, - audience, - validator.WithCustomClaims(customClaims), + validator.WithKeyFunc(keyFunc), + validator.WithAlgorithm(validator.HS256), + validator.WithIssuer(issuer), + validator.WithAudiences(audience), + // WithCustomClaims now uses generics - no need to return interface type + validator.WithCustomClaims(func() *CustomClaims { + return &CustomClaims{} + }), validator.WithAllowedClockSkew(30*time.Second), ) if err != nil { diff --git a/middleware_test.go b/middleware_test.go index 224a98c..a05b604 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -34,7 +34,12 @@ func Test_CheckJWT(t *testing.T) { return []byte("secret"), nil } - jwtValidator, err := validator.New(keyFunc, validator.HS256, issuer, []string{audience}) + jwtValidator, err := validator.New( + validator.WithKeyFunc(keyFunc), + validator.WithAlgorithm(validator.HS256), + validator.WithIssuer(issuer), + validator.WithAudience(audience), + ) require.NoError(t, err) testCases := []struct { diff --git a/validator/option.go b/validator/option.go index 12c1cc6..57aaedf 100644 --- a/validator/option.go +++ b/validator/option.go @@ -1,28 +1,152 @@ package validator import ( + "context" + "errors" + "fmt" + "net/url" "time" ) // Option is how options for the Validator are set up. -type Option func(*Validator) +// Options return errors to enable validation during construction. +type Option func(*Validator) error -// WithAllowedClockSkew is an option which sets up the allowed -// clock skew for the token. Note that in order to use this -// the expected claims Time field MUST not be time.IsZero(). -// If this option is not used clock skew is not allowed. +// WithKeyFunc sets the function that provides the key for token verification. +// This is a required option. +// +// The keyFunc is called during token validation to retrieve the key(s) used +// to verify the token signature. For JWKS-based validation, use jwks.Provider.KeyFunc. +func WithKeyFunc(keyFunc func(context.Context) (any, error)) Option { + return func(v *Validator) error { + if keyFunc == nil { + return errors.New("keyFunc cannot be nil") + } + v.keyFunc = keyFunc + return nil + } +} + +// WithAlgorithm sets the signature algorithm that tokens must use. +// This is a required option. +// +// Supported algorithms: RS256, RS384, RS512, ES256, ES384, ES512, +// PS256, PS384, PS512, HS256, HS384, HS512, EdDSA. +func WithAlgorithm(algorithm SignatureAlgorithm) Option { + return func(v *Validator) error { + if _, ok := allowedSigningAlgorithms[algorithm]; !ok { + return fmt.Errorf("unsupported signature algorithm: %s", algorithm) + } + v.signatureAlgorithm = algorithm + return nil + } +} + +// WithIssuer sets the expected issuer claim (iss) for token validation. +// This is a required option. +// +// The issuer URL should match the iss claim in the JWT. Tokens with a +// different issuer will be rejected. +func WithIssuer(issuerURL string) Option { + return func(v *Validator) error { + if issuerURL == "" { + return errors.New("issuer cannot be empty") + } + // Optional: Validate URL format + if _, err := url.Parse(issuerURL); err != nil { + return fmt.Errorf("invalid issuer URL: %w", err) + } + v.expectedClaims.Issuer = issuerURL + return nil + } +} + +// WithAudience sets a single expected audience claim (aud) for token validation. +// This is a required option (use either WithAudience or WithAudiences, not both). +// +// The audience should match one of the aud claims in the JWT. Tokens without +// a matching audience will be rejected. +func WithAudience(audience string) Option { + return func(v *Validator) error { + if audience == "" { + return errors.New("audience cannot be empty") + } + v.expectedClaims.Audience = []string{audience} + return nil + } +} + +// WithAudiences sets multiple expected audience claims (aud) for token validation. +// This is a required option (use either WithAudience or WithAudiences, not both). +// +// The token must contain at least one of the specified audiences. Tokens without +// any matching audience will be rejected. +func WithAudiences(audiences []string) Option { + return func(v *Validator) error { + if len(audiences) == 0 { + return errors.New("audiences cannot be empty") + } + for i, aud := range audiences { + if aud == "" { + return fmt.Errorf("audience at index %d cannot be empty", i) + } + } + v.expectedClaims.Audience = audiences + return nil + } +} + +// WithAllowedClockSkew sets the allowed clock skew for time-based claims. +// +// This allows for some tolerance when validating exp, nbf, and iat claims +// to account for clock differences between systems. If not set, the default +// is 0 (no clock skew allowed). func WithAllowedClockSkew(skew time.Duration) Option { - return func(v *Validator) { + return func(v *Validator) error { + if skew < 0 { + return errors.New("clock skew cannot be negative") + } v.allowedClockSkew = skew + return nil } } -// WithCustomClaims sets up a function that returns the object -// CustomClaims that will be unmarshalled into and on which -// Validate is called on for custom validation. If this option -// is not used the Validator will do nothing for custom claims. -func WithCustomClaims(f func() CustomClaims) Option { - return func(v *Validator) { - v.customClaims = f +// WithCustomClaims sets a function that returns a CustomClaims object +// for unmarshalling and validation. +// +// The function is called during construction to validate it returns a non-nil +// value, and then called for each token validation to create a new instance. +// +// Using generics allows you to return your concrete claims type directly +// without needing to explicitly cast to the CustomClaims interface. +// +// IMPORTANT: The function must be: +// - Thread-safe (called concurrently by multiple requests) +// - Idempotent (returns a new instance each time, no shared state) +// - Fast (called on every token validation) +// - Panic-free (panics will crash the request handler) +// +// Example: +// +// validator.New( +// // ... other options +// validator.WithCustomClaims(func() *MyClaims { +// return &MyClaims{} // No interface cast needed +// }), +// ) +func WithCustomClaims[T CustomClaims](f func() T) Option { + return func(v *Validator) error { + if f == nil { + return errors.New("custom claims function cannot be nil") + } + + // Wrap to return interface type for internal storage + // Note: The function can return nil at runtime for conditional custom claims, + // which is handled by customClaimsExist() during validation + v.customClaims = func() CustomClaims { + return f() + } + + return nil } } diff --git a/validator/security_test.go b/validator/security_test.go index 482c1a9..fafa29b 100644 --- a/validator/security_test.go +++ b/validator/security_test.go @@ -82,12 +82,12 @@ func TestValidateTokenFormat(t *testing.T) { func TestValidateToken_CVE_2025_27144_Protection(t *testing.T) { // This test ensures the CVE-2025-27144 mitigation is in place v, err := New( - func(_ context.Context) (interface{}, error) { + WithKeyFunc(func(_ context.Context) (interface{}, error) { return []byte("secret"), nil - }, - HS256, - "https://issuer.example.com/", - []string{"audience"}, + }), + WithAlgorithm(HS256), + WithIssuer("https://issuer.example.com/"), + WithAudience("audience"), ) if err != nil { t.Fatalf("failed to create validator: %v", err) diff --git a/validator/validator.go b/validator/validator.go index 8fa7119..c4f7570 100644 --- a/validator/validator.go +++ b/validator/validator.go @@ -54,44 +54,73 @@ var allowedSigningAlgorithms = map[SignatureAlgorithm]bool{ PS512: true, } -// New sets up a new Validator with the required keyFunc -// and signatureAlgorithm as well as custom options. -func New( - keyFunc func(context.Context) (interface{}, error), - signatureAlgorithm SignatureAlgorithm, - issuerURL string, - audience []string, - opts ...Option, -) (*Validator, error) { - if keyFunc == nil { - return nil, errors.New("keyFunc is required but was nil") - } - if issuerURL == "" { - return nil, errors.New("issuer url is required but was empty") - } - if len(audience) == 0 { - return nil, errors.New("audience is required but was empty") - } - if _, ok := allowedSigningAlgorithms[signatureAlgorithm]; !ok { - return nil, errors.New("unsupported signature algorithm") - } - +// New creates a new Validator with the provided options. +// +// Required options: +// - WithKeyFunc: Function to provide verification key(s) +// - WithAlgorithm: Signature algorithm to validate +// - WithIssuer: Expected issuer claim (iss) +// - WithAudience or WithAudiences: Expected audience claim(s) (aud) +// +// Optional options: +// - WithCustomClaims: Custom claims validation +// - WithAllowedClockSkew: Clock skew tolerance for time-based claims +// +// Example: +// +// validator, err := validator.New( +// validator.WithKeyFunc(keyFunc), +// validator.WithAlgorithm(validator.RS256), +// validator.WithIssuer("https://issuer.example.com/"), +// validator.WithAudience("my-api"), +// validator.WithAllowedClockSkew(30*time.Second), +// ) +// if err != nil { +// log.Fatal(err) +// } +func New(opts ...Option) (*Validator, error) { v := &Validator{ - keyFunc: keyFunc, - signatureAlgorithm: signatureAlgorithm, - expectedClaims: jwt.Expected{ - Issuer: issuerURL, - Audience: audience, - }, + allowedClockSkew: 0, // Secure default: no clock skew } + // Apply all options for _, opt := range opts { - opt(v) + if err := opt(v); err != nil { + return nil, fmt.Errorf("invalid option: %w", err) + } + } + + // Validate required configuration + if err := v.validate(); err != nil { + return nil, fmt.Errorf("invalid validator configuration: %w", err) } return v, nil } +// validate ensures all required fields are set. +func (v *Validator) validate() error { + var errs []error + + if v.keyFunc == nil { + errs = append(errs, errors.New("keyFunc is required (use WithKeyFunc)")) + } + if v.signatureAlgorithm == "" { + errs = append(errs, errors.New("signature algorithm is required (use WithAlgorithm)")) + } + if v.expectedClaims.Issuer == "" { + errs = append(errs, errors.New("issuer is required (use WithIssuer)")) + } + if len(v.expectedClaims.Audience) == 0 { + errs = append(errs, errors.New("audience is required (use WithAudience or WithAudiences)")) + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} + // ValidateToken validates the passed in JWT using the jose v2 package. func (v *Validator) ValidateToken(ctx context.Context, tokenString string) (interface{}, error) { // CVE-2025-27144 mitigation: Validate token format before parsing diff --git a/validator/validator_test.go b/validator/validator_test.go index c97d6f0..90e2fa5 100644 --- a/validator/validator_test.go +++ b/validator/validator_test.go @@ -212,14 +212,18 @@ func TestValidator_ValidateToken(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { t.Parallel() - validator, err := New( - testCase.keyFunc, - testCase.algorithm, - issuer, - []string{audience, "another-audience"}, - WithCustomClaims(testCase.customClaims), + opts := []Option{ + WithKeyFunc(testCase.keyFunc), + WithAlgorithm(testCase.algorithm), + WithIssuer(issuer), + WithAudiences([]string{audience, "another-audience"}), WithAllowedClockSkew(time.Second), - ) + } + if testCase.customClaims != nil { + opts = append(opts, WithCustomClaims(testCase.customClaims)) + } + + validator, err := New(opts...) require.NoError(t, err) tokenClaims, err := validator.ValidateToken(context.Background(), testCase.token) @@ -245,33 +249,190 @@ func TestNewValidator(t *testing.T) { return []byte("secret"), nil } + t.Run("successful creation with all required options", func(t *testing.T) { + v, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer(issuer), + WithAudience(audience), + ) + assert.NoError(t, err) + assert.NotNil(t, v) + }) + + t.Run("successful creation with WithAudiences", func(t *testing.T) { + v, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer(issuer), + WithAudiences([]string{audience, "another-audience"}), + ) + assert.NoError(t, err) + assert.NotNil(t, v) + }) + + t.Run("successful creation with optional parameters", func(t *testing.T) { + v, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer(issuer), + WithAudience(audience), + WithAllowedClockSkew(30*time.Second), + ) + assert.NoError(t, err) + assert.NotNil(t, v) + assert.Equal(t, 30*time.Second, v.allowedClockSkew) + }) + t.Run("it throws an error when the keyFunc is nil", func(t *testing.T) { - _, err := New(nil, algorithm, issuer, []string{audience}) - assert.EqualError(t, err, "keyFunc is required but was nil") + _, err := New( + WithKeyFunc(nil), + WithAlgorithm(algorithm), + WithIssuer(issuer), + WithAudience(audience), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "keyFunc cannot be nil") + }) + + t.Run("it throws an error when keyFunc is missing", func(t *testing.T) { + _, err := New( + WithAlgorithm(algorithm), + WithIssuer(issuer), + WithAudience(audience), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "keyFunc is required") }) t.Run("it throws an error when the signature algorithm is empty", func(t *testing.T) { - _, err := New(keyFunc, "", issuer, []string{audience}) - assert.EqualError(t, err, "unsupported signature algorithm") + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(""), + WithIssuer(issuer), + WithAudience(audience), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported signature algorithm") }) t.Run("it throws an error when the signature algorithm is unsupported", func(t *testing.T) { - _, err := New(keyFunc, "none", issuer, []string{audience}) - assert.EqualError(t, err, "unsupported signature algorithm") + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm("none"), + WithIssuer(issuer), + WithAudience(audience), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported signature algorithm") + }) + + t.Run("it throws an error when algorithm is missing", func(t *testing.T) { + _, err := New( + WithKeyFunc(keyFunc), + WithIssuer(issuer), + WithAudience(audience), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "signature algorithm is required") }) t.Run("it throws an error when the issuerURL is empty", func(t *testing.T) { - _, err := New(keyFunc, algorithm, "", []string{audience}) - assert.EqualError(t, err, "issuer url is required but was empty") + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer(""), + WithAudience(audience), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "issuer cannot be empty") }) - t.Run("it throws an error when the audience is nil", func(t *testing.T) { - _, err := New(keyFunc, algorithm, issuer, nil) - assert.EqualError(t, err, "audience is required but was empty") + t.Run("it throws an error when the issuerURL is invalid", func(t *testing.T) { + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer("ht!tp://invalid url with spaces"), + WithAudience(audience), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid issuer URL") + }) + + t.Run("it throws an error when issuer is missing", func(t *testing.T) { + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithAudience(audience), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "issuer is required") }) t.Run("it throws an error when the audience is empty", func(t *testing.T) { - _, err := New(keyFunc, algorithm, issuer, []string{}) - assert.EqualError(t, err, "audience is required but was empty") + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer(issuer), + WithAudience(""), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "audience cannot be empty") + }) + + t.Run("it throws an error when audiences list is empty", func(t *testing.T) { + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer(issuer), + WithAudiences([]string{}), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "audiences cannot be empty") + }) + + t.Run("it throws an error when audience is missing", func(t *testing.T) { + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer(issuer), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "audience is required") + }) + + t.Run("it throws an error when audiences contains empty string", func(t *testing.T) { + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer(issuer), + WithAudiences([]string{"valid-aud", ""}), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "audience at index 1 cannot be empty") + }) + + t.Run("it throws an error when clock skew is negative", func(t *testing.T) { + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer(issuer), + WithAudience(audience), + WithAllowedClockSkew(-1*time.Second), + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "clock skew cannot be negative") + }) + + t.Run("it throws an error when custom claims function is nil", func(t *testing.T) { + _, err := New( + WithKeyFunc(keyFunc), + WithAlgorithm(algorithm), + WithIssuer(issuer), + WithAudience(audience), + WithCustomClaims[*testClaims](nil), // Need to specify type for nil + ) + assert.Error(t, err) + assert.Contains(t, err.Error(), "custom claims function cannot be nil") }) }