Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/echo-example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 8 additions & 10 deletions examples/echo-example/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,21 @@ 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
// that will check the validity of our JWT.
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 {
Expand Down
4 changes: 2 additions & 2 deletions examples/gin-example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 8 additions & 10 deletions examples/gin-example/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,21 @@ 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
// that will check the validity of our JWT.
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 {
Expand Down
4 changes: 2 additions & 2 deletions examples/http-example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 8 additions & 11 deletions examples/http-example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions examples/http-jwks-example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions examples/http-jwks-example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions examples/iris-example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 8 additions & 11 deletions examples/iris-example/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,21 @@ 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
// that will check the validity of our JWT.
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 {
Expand Down
7 changes: 6 additions & 1 deletion middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
150 changes: 137 additions & 13 deletions validator/option.go
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Loading