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!
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)
}
}