How to correctly use the validator with custom claims in the github.com/auth0/go-jwt-middleware module?

Hi there!

I have been considering the go-jwt-middleware module for possible adaptation into our stack at work and have because of that been playing around with it for a while. We have the need to support some custom claims in our JWTs and I see that that is in principle supported by the module.

In the examples and documentation provided it seems like one is supposed to create a single instance of the validator and use that for validating tokens. Is this correct? Or should I be creating a new validator each time I need to validate a token?

The reason I am questioning how this works is because the validator (or really it’s jose.v2 dependency) unmarshalls the token into the same instance of the “CustomClaims” interface (you have to pass a pointer to a structure when creating the validator) every time. My question then is what happens if you first validate a token that contains custom claims and then a token that does not have custom claims?

I created a quick POC (code attached) where I create two tokens where the only difference is that one has custom claims and one does not. I then ran two tests. In the first test I validated both tokens using the same validator instance and checked for custom claims. In the second test I create separate validators (one for each token) and do the same checks.

In the first case both validated tokens have the same custom claims - even if the second token does not contain custom claims? For convenience, here is the output from the attached code sample:

2021/12/22 14:52:46 Validating tokens with the same validator instance:
2021/12/22 14:52:46 Validation of token with custom claim succeeded: &{CustomClaims:0xc000012da0 RegisteredClaims:{Issuer:some issuer Subject:some subject Audience:[some audience] Expiry:0 NotBefore:1609459200 IssuedAt:0 ID:}} Custom claims: &{Custom:some custom value}
2021/12/22 14:52:46 Validation of token without custom claim succeeded: &{CustomClaims:0xc000012da0 RegisteredClaims:{Issuer:some issuer Subject:some subject Audience:[some audience] Expiry:0 NotBefore:1609459200 IssuedAt:0 ID:}} Custom claims: &{Custom:some custom value}
2021/12/22 14:52:46 Validating tokens with the different validator instances:
2021/12/22 14:52:46 Validation of token with custom claim succeeded: &{CustomClaims:0xc000013090 RegisteredClaims:{Issuer:some issuer Subject:some subject Audience:[some audience] Expiry:0 NotBefore:1609459200 IssuedAt:0 ID:}} Custom claims: &{Custom:some custom value}
2021/12/22 14:52:46 Validation of token without custom claim failed: custom claims not validated: missing custom claim

I assume this is because one validator instance is always unmarshalling into the same custom claims structure and if a value is missing it is not cleared. Since the same backing memory is used on each validation it also sounds to me like the validator is unsafe for concurrent use - at least in our use case?

It’s not been that long since this functionality was changed. In this pull request the functional option WithCustomClaims was changed from accepting a function that produces a custom claims structure on demand to accepting just one custom claims structure. In the earlier version I don’t think I would had this problem.

Is this a bug? Is the documentation/examples incorrect (e.g I’m supposed to make a new validator each time I want to verify a token)? Or am I maybe missing something important?

Versions details:
go version: 1.17
go-jwt-middlware version: v2.0.0-beta
go-jose version: v2.6.0

Thank you so much for any information anyone can provide! :slight_smile:

package main

import (
	"context"
	"log"
	"time"

	"github.com/auth0/go-jwt-middleware/v2/validator"
	"github.com/pkg/errors"
	jose "gopkg.in/square/go-jose.v2"
	jwt "gopkg.in/square/go-jose.v2/jwt"
)

type CustomClaims struct {
	Custom string `json:"custom"`
}

func (c *CustomClaims) Validate(context.Context) error {
	if c.Custom == "" {
		return errors.Errorf("missing custom claim")
	}
	return nil
}

func main() {
	var (
		secret             = []byte("secret")
		issuer             = "some issuer"
		audience           = []string{"some audience"}
		subject            = "some subject"
		signatureAlgorithm = validator.HS256
	)

	// Make two tokens. One with CustomClaims and one without
	sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: secret}, (&jose.SignerOptions{}).WithType("JWT"))
	fatal(err)

	cl := jwt.Claims{
		Subject:   subject,
		Issuer:    issuer,
		NotBefore: jwt.NewNumericDate(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
		Audience:  audience,
	}

	tokenWithCustomClaim, err := jwt.Signed(sig).Claims(cl).Claims(CustomClaims{Custom: "some custom value"}).CompactSerialize()
	fatal(err)

	tokenWithoutCustomClaim, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
	fatal(err)

	keyFunc := func(ctx context.Context) (interface{}, error) { return secret, nil }

	log.Printf("Validating tokens with the same validator instance:\n")
	{
		customClaims := &CustomClaims{}
		jwtValidator, err := validator.New(keyFunc, signatureAlgorithm, issuer, audience, validator.WithCustomClaims(customClaims))
		fatal(err)

		validatedTokenWithCustomClaim, err := jwtValidator.ValidateToken(context.Background(), tokenWithCustomClaim)
		if err != nil {
			log.Printf("Validation of token with custom claim failed: %s\n", err)
		} else {
			log.Printf("Validation of token with custom claim succeeded: %+v Custom claims: %+v\n", validatedTokenWithCustomClaim, validatedTokenWithCustomClaim.(*validator.ValidatedClaims).CustomClaims)
		}

		validatedTokenWithoutCustomClaim, err := jwtValidator.ValidateToken(context.Background(), tokenWithoutCustomClaim)
		if err != nil {
			log.Printf("Validation of token without custom claim failed: %s\n", err)
		} else {
			log.Printf("Validation of token without custom claim succeeded: %+v Custom claims: %+v\n", validatedTokenWithoutCustomClaim, validatedTokenWithoutCustomClaim.(*validator.ValidatedClaims).CustomClaims)
		}
	}

	log.Printf("Validating tokens with the different validator instances:\n")
	{
		customClaims := &CustomClaims{}
		jwtValidator, err := validator.New(keyFunc, signatureAlgorithm, issuer, audience, validator.WithCustomClaims(customClaims))
		fatal(err)

		validatedTokenWithCustomClaim, err := jwtValidator.ValidateToken(context.Background(), tokenWithCustomClaim)
		if err != nil {
			log.Printf("Validation of token with custom claim failed: %s\n", err)
		} else {
			log.Printf("Validation of token with custom claim succeeded: %+v Custom claims: %+v\n", validatedTokenWithCustomClaim, validatedTokenWithCustomClaim.(*validator.ValidatedClaims).CustomClaims)
		}

		// Make new instances of CustomClaims and validator
		customClaims = &CustomClaims{}
		jwtValidator, err = validator.New(keyFunc, signatureAlgorithm, issuer, audience, validator.WithCustomClaims(customClaims))
		fatal(err)

		validatedTokenWithoutCustomClaim, err := jwtValidator.ValidateToken(context.Background(), tokenWithoutCustomClaim)
		if err != nil {
			log.Printf("Validation of token without custom claim failed: %s\n", err)
		} else {
			log.Printf("Validation of token without custom claim succeeded: %+v Custom claims: %+v\n", validatedTokenWithoutCustomClaim, validatedTokenWithoutCustomClaim.(*validator.ValidatedClaims).CustomClaims)
		}
	}
}

func fatal(err error) {
	if err != nil {
		log.Fatal(err)
	}
}