Incorrect documentation / breaking change lead to failed build
truescotian opened this issue ยท 26 comments
Description
Provide a clear and concise description of the issue, including what you expected to happen.
It seems there is incorrect documentation for Auth0 to address the most recent commit 1c6db3c. When providing options to github.com/auth0/go-jwt-middleware
, specifically ValidationKeyGetter
, we've always used github.com/dgrijalva/jwt-go
*jwt.Token
, which is currently invalid and throwns the shown error:
cannot use func literal (type func(*"github.com/dgrijalva/jwt-go".Token) (interface {}, error)) as type "github.com/form3tech-oss/jwt-go".Keyfunc in field value
I thought that I could just look at the documentation again to build auth (https://auth0.com/docs/quickstart/backend/golang/01-authorization) for an updated solution, but it is still using the old github.com/dgrijalva/jwt-go
package.
I'm not sure why this was deployed to master branch without updating documentation on Auth0's website. If I'm incorrect on this and missing some information, my apologies. Please let me know.
Reproduction
Detail the steps taken to reproduce this error, what was expected, and whether this issue can be reproduced consistently or if it is intermittent.
I built my package and it failed. I expected it not to fail. This is a consistent issue. If you would like more information please let me know, otherwise I have followed the Auth0 docs and have not updated my package in over a year.
Environment
Please provide the following:
- Version of this library used: Master branch
- Other relevant versions (language, server software, OS, browser): Golang, Ubuntu
- Other modules/plugins/libraries that might be involved:
github.com/auth0/go-jwt-middleware
github.com/dgrijalva/jwt-go
https://github.com/urfave/negroni
Hey @truescotian, my apologies for this. If you look at #69 you can see what lead to this breaking change. The decision was made to have this breaking change in order to fix a security vulnerability. The documentation issue is an oversight on our part and I'll make sure that gets fixed up.
A contributing factor to this issue is that up until this point this package has not been versioned.
Actually, it looks like the documentation issue has already been fixed and is awaiting merge: kylekampy/docs@9cdddf6
Hi @grounded042, thanks for the quick response! Glad to see that the docs have been updated. However, I just wanted you to be aware of two potential issues:
form3tech-oss/jwt-go#7
form3tech-oss/jwt-go#5
I seem to be running into similar issues. I hope I go in to enough detail for you:
-
My JWT's
aud
is[http://xxxxx.xxx.xxxxx.com https://xxxxx-xxxx.auth0.com/xxx]
-
To confirm, I print this out:
fmt.Println(token.Claims.(jwt.MapClaims)["aud"])
. Output is the same as the previous lineaud
-
I kept getting invalid audience using this
aud
. Not sure why at this point. -
After looking into the
form3tech-oss/jwt-go
package, specificallyfunc(m MapClaims) VerifyAudience(cmp string, req bool) bool {
, I noticedaud, ok := m["aud"].([]string)
,ok
was always false. -
So I thought there might be some type issue, such as the one in form3tech-oss/jwt-go#7. To check this out, before
VerifyAudience(AUDIENCE_HERE, false)
I just set theaud
totest
withtoken.Claims.(jwt.MapClaims)["aud"] = "test"
, and then calledcheckAud := token.Claims.(jwt.MapClaims).VerifyAudience("test", false)
. This worked successfully. Super weird. Thought it might be because originally I was using a[]string
, and this one uses astring
, which shouldn't even happen since RFC states both should be supported. So I tried to then set the token'saud
to a []string, and verify that audience, which also worked. -
So I thought it was a good time to check what my original
aud
type is according toform3tech-oss/jwt-go
within theVerifyAudience
method. Here's what I used:
aud, ok := m["aud"].([]string)
fmt.Println(fmt.Sprintf("%T", m["aud"]))
Output: []interface {}
So my aud
is not being asserted to []string it seems.
At this point, I'm not really sure where things went wrong when parsing jwt.MapClaims
have you run into this issue yet? It seems at least some must be.
In the code snippet
aud, ok := m["aud"].([]string)
fmt.Println(fmt.Sprintf("%T", m["aud"]))
it makes sense with what you are seeing. You would need to do
aud, ok := m["aud"].([]string)
fmt.Println(fmt.Sprintf("%T", aud))
to have it show up as []string
.
EDIT: actually, this is a little strange based on: https://play.golang.org/p/botHkNRY-PG
@truescotian Thinking about this some more I think this has to do with how JSON is parsed into the map claims. I don't have time to dig deeper right now, but that's where I would start looking.
Same here; to make things work again, I had to explicitly convert the []interface{}
to []string
by adding the following segment to the ValidationKeyGetter
code:
jwtmiddleware.New(jwtmiddleware.Options{
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return token, errors.New("invalid claims type")
}
if audienceList, ok := claims["aud"].([]interface{}); ok{
auds := make([]string, len(audienceList))
for _, aud := range(audienceList) {
audStr, ok := aud.(string)
if !ok {
return token, errors.New("invalid audience type")
}
auds = append(auds, audStr)
}
claims["aud"] = auds
}
// Verify 'aud' claim
checkAud := claims.VerifyAudience(auth0Aud, false)
if !checkAud {
return token, errors.New("invalid audience")
}
// ... etc
Not a big problem perse, but sadly it makes the function way more verbose.
Is there a shorter way to cast []interface{}
to []string
? I understand this is not as easy as you'd expect by doing some searching on this.
Is there a shorter way to cast
[]interface{}
to[]string
? I understand this is not as easy as you'd expect by doing some searching on this.
Unfortunately not: https://stackoverflow.com/questions/44027826/convert-interface-to-string-in-golang
Regarding form3tech-oss/jwt-go
jwt.Parse looks like it was planned to be phased out or kept for backwards compatibility.
By default jwt.Parse calls jwt.ParseWithClaims and passes in MapClaims{} which is of type map[string]interface{}
Take a look at: https://github.com/form3tech-oss/jwt-go/blob/master/MIGRATION_GUIDE.md
But you can instead use ParseWithClaims with custom or StandardClaims and have direct access to claims.Audience (type []string) much nicer and supported by VerifyAudience!
Also, by implementing Custom Claims you could kill 3 issues at once
##58
##53
const issuedAtLeewaySecs = 10
// MyClaims custom claim type to provide leeway support.
type MyClaims struct {
*jwt.StandardClaims
}
// Valid validates custom claim and also standard claim
func (c *MyClaims) Valid() error {
fmt.Println("Custom Claim validation...")
c.StandardClaims.IssuedAt -= issuedAtLeewaySecs
err := c.StandardClaims.Valid()
c.StandardClaims.IssuedAt += issuedAtLeewaySecs
return err
}
// Now parse the token with Custom Claim
parsedToken, err := jwt.ParseWithClaims(token, &MyClaims{}, m.Options.ValidationKeyGetter)
go-jwt-middleware/jwtmiddleware.go
Line 204 in 1c6db3c
Here's my full solution hopefully it helps someone.
main.go:
import "github.com/form3tech-oss/jwt-go/"
...
// V4 of jwt-go should include leeway support, until then we need to customize the claims to provide a small window of time for differences in clocks. Error: "Token used before issued"
// Custom Claims also fixes the claim["aud"] issue where you get back multiple values of type []interface{}
const leeway = 10
// MyClaims custom claim type to provide leeway support.
type MyClaims struct {
*jwt.StandardClaims
}
// Valid validates custom claim and also standard claim
func (c *MyClaims) Valid() error {
c.StandardClaims.IssuedAt -= leeway // subtract our leeway value
err := c.StandardClaims.Valid() // Check Standard claims validation
c.StandardClaims.IssuedAt += leeway // reset to original
return err
}
func main() {
jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
claims, ok := token.Claims.(*MyClaims) // Custom-Claims allows leeway support
// claims, ok := token.Claims.(*jwt.StandardClaims) // Using StandardClaims also works
if !ok {
return token, errors.New("invalid claims type")
}
// verify 'audience' claim
auth0Aud := "https://api.youraudience.com"
checkAud := claims.VerifyAudience(auth0Aud, false)
if !checkAud {
return token, errors.New("invalid audience")
}
// Verify 'issuer' claim
iss := "https://wia.us.auth0.com/"
checkIss := claims.VerifyIssuer(iss, false)
if !checkIss {
return token, errors.New("invalid issuer")
}
cert, err := getPemCert(token)
if err != nil {
panic(err.Error())
}
result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
return result, nil
},
SigningMethod: jwt.SigningMethodRS256,
Debug: true,
Claims: &MyClaims{}, // Pass in Custom Claims
})
.....
Next, need to modify go-jwt-middleware so vendor it
$ go mod vendor
First add an option for claims
vendor/github.com/auth0/go-jwt-middleware/jwtmiddleware.go:
type Options struct {
....
// Optional: (custom Claims)
// if not set we'll use StandardClaims
// Default: nil,
Claims jwt.Claims // Add Custom claims support
}
Also need to modify the New() method to set default claims
func New(options ...Options) *JWTMiddleware {
...
// use StandardClaims if custom Claims option not set
if opts.Claims == nil {
opts.Claims = &jwt.StandardClaims{}
}
...
}
Finally lets use ParseWithClaims() instead of Parse() and pass in our Claims Option
func (m *JWTMiddleware) CheckJWT(w http.ResponseWriter, r *http.Request) error {
...
// Now parse the token
// Migrate to ParseWithClaims instead of old Parse method
// ParseWithClaims allows us to use custom Claims, or StandardClaims
parsedToken, err := jwt.ParseWithClaims(token, m.Options.Claims, m.Options.ValidationKeyGetter)
...
This worked for me, I'd love to get some feedback on it but it allows leeway support, custom claims support, and solves the issue with []interface{} audiences. All of this could be better solved on the go-jwt side but seems to be a lack of maintenance
Hey @aaronprice00, sorry for the late reply here. Great work on putting all of that together. Looking through this it looks like a great candidate for v2
. In v2
we are working on separating the logic of JWT validation. Right now the middleware provides some validation, but most people end up adding their own setup. In v2
validation should be more easily handled by a token validator which can be switched out for different implementations.
The work you've here is a major change to the contract people rely on for this package and if we were to include it we would also look at releasing a new major version of the package. In light of that I'd like to look at moving this work to v2
. I also think that because we're looking at switching away from the jwt-go
package in v2
we might solve some of these problems off the bat.
@aaronprice00 Thanks for your solution. Unfortunately I get the following error:
interface conversion: jwt.Claims is *main.MyClaims, not jwt.MapClaims
go 1.16
@urbantrout can you show use the code for the error you are hitting?
I think problem solved. I tried those versions of modules
github.com/auth0/go-jwt-middleware v1.0.0
github.com/form3tech-oss/jwt-go v3.2.3+incompatible
And with them was no need to convert interface
into string
.
I was using
github.com/auth0/go-jwt-middleware v1.0.0
github.com/form3tech-oss/jwt-go v3.2.2+incompatible
Upgrading to
github.com/auth0/go-jwt-middleware v1.0.0
github.com/form3tech-oss/jwt-go v3.2.3+incompatible
did not solve the problem.
I think it has something to do with this line of code:
claims, ok := token.Claims.(jwt.MapClaims)
which according to #72 (comment) I have to change to
claims, ok := token.Claims.(*MyClaims) // Custom-Claims allows leeway support
I'm experiencing the same issue and have a work-around for it. I've implemented a custom json parser for my CustomClaims
which supports both string
& []string
. However, in my opinion, the underlying jwt.StandardClaims should implement aud
as type []string
instead of string
.
type CustomClaims struct {
jwt.StandardClaims
Permissions []string `json:"permissions"`
Audience []string `json:"aud,omitempty"`
}
func (t *CustomClaims) UnmarshalJSON(data []byte) error {
type Alias CustomClaims
aux := &struct {
Audience interface{} `json:"aud"`
*Alias
}{
Alias: (*Alias)(t),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
aud, ok := aux.Audience.(string)
if ok {
t.Audience = []string{aud}
return nil
}
audList, ok := aux.Audience.([]interface{})
if !ok {
errors.Errorf("failed to unmarshal audience of type: %T, into CustomClaims", aux.Audience)
return nil
}
t.Audience = make([]string, len(audList))
for i, v := range audList {
aud, ok = v.(string)
if !ok {
return errors.Errorf("expected audience of type string in list, found type: %T", v)
}
t.Audience[i] = aud
}
return nil
}
I'm using the following packages:
github.com/auth0/go-jwt-middleware v0.0.0-20201030150249-d783b5c46b39
github.com/dgrijalva/jwt-go v3.2.0+incompatible
@hspens
try to use those versions
github.com/auth0/go-jwt-middleware v1.0.0
github.com/form3tech-oss/jwt-go v3.2.3+incompatible
We just released the v2.0.0-beta ๐ฅณ !
You can start testing it by running go get github.com/auth0/go-jwt-middleware/v2@v2.0.0-beta
.
In case of issues fetching the v2 you might want to try go clean --modcache
first before doing go get
.
I'm closing this issue as now this is part of v2, but feel free to reopen if needed.
go get github.com/auth0/go-jwt-middleware/v2@v2.0.0-beta
getting error
go: github.com/auth0/go-jwt-middleware/v2/v2@v2.0.0-beta: reading github.com/auth0/go-jwt-middleware/v2/v2/go.mod at revision v2/v2.0.0-beta: unknown revision v2/v2.0.0-beta
Hey @ashish-scalent ! It seems you're trying to do /v2/v2 twice in your go get:) It should be just go get github.com/auth0/go-jwt-middleware/v2@v2.0.0-beta
.
previously we are fetching tokens from requests after jwt middle handler validate the token set in the req context
but now CheckJwt middleware set that as
// No err means we have a valid token, so set
// it into the context and continue onto next.
r = r.Clone(context.WithValue(r.Context(), ContextKey{}, validToken))
so in this case if we retrieve the ctxToken from req context something like
ctxToken := r.Context().Value(jwtmiddleware.ContextKey{})
so token is interface having type string actually so when I tried to retrieve (*jwt.Token) to map with claims it will cause an nil pointer error
token := ctxToken.(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)
is anyone have any idea about this?
Hey @ashish-scalent , you need to do something like this as described in the README.md:
claims := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)
as now the value will be of type validator.ValidatedClaims
.
r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)
PANIC: interface conversion: interface {} is string, not *validator.ValidatedClaims
getting panic in that way I forgot to mention that previously
@ashish-scalent Could you please open a separate issue describing in full detail the issue following the template we provide please?:) It would be great to provide a reproducible of your setup code.
@ashish-scalent Could you please open a separate issue describing in full detail the issue following the template we provide please?:) It would be great to provide a reproducible of your setup code.
Hey Sorry my bad you were right I actually forgot to pass *validator.ValidateToken
function in middleware
no issue from my side, thanks for the instant support
@ashish-scalent Happy to help!:) Glad you fixed it!
Hi everyone, we also encountered this problem, I'm putting our solution here in case it will useful for someone.
This solution doesn't require using the auth0 v2 sdk which was in beta until recently (if you are implementing from scratch I suggest using it), and does not require vendoring the jwt package to your repository and modify it, and does not require github.com/form3tech-oss/jwt-go which is no longer maintained.
The problem as we understand it relies in golang-jwt v3.X which only have a StandardClaims
struct that allows a single Audience, so when unmarshaling a jwt with multiple audience, it fails.
type StandardClaims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
However, this was solved in golang-jwt v4, which features a RegisteredClaims
that allows multiple audience (ClaimStrings
is []string
)
type RegisteredClaims struct {
Issuer string `json:"iss,omitempty"`
Subject string `json:"sub,omitempty"`
Audience ClaimStrings `json:"aud,omitempty"`
ExpiresAt *NumericDate `json:"exp,omitempty"`
NotBefore *NumericDate `json:"nbf,omitempty"`
IssuedAt *NumericDate `json:"iat,omitempty"`
ID string `json:"jti,omitempty"`
}
So we decided to import both versions (v3.2.1 and v4) of golang-jwt (unfortunately using only v4 breaks the currently required jwt-go version).
Then, we used the v4 version for the CustomClaim
struct, and the old version for the rest of the validation procedures as stated in the docs
import (
"github.com/golang-jwt/jwt"
jwt_modern_claims "github.com/golang-jwt/jwt/v4"
)
type CustomClaims struct {
Scope string `json:"scope"`
jwt_modern_claims.RegisteredClaims
}
...
After that, everything worked as expected, no unmarshaling errors and we were able to implement functions such as
func (c CustomClaims) verifyAudience() bool {
for _, aud := range c.Audience {
if aud == c.expectedAudience {
return true
}
}
return false
}