Skip to content
This repository has been archived by the owner on May 21, 2022. It is now read-only.

Allow int types in MapClaims validations #162

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

leetal
Copy link

@leetal leetal commented Aug 22, 2016

This will allow the MapClaims validators to be used with Unix timestamps generated like time.Now().Unix(). Prior to this PR, only json.Number and float64 types were allowed.
This will allow the validation to pass when supplying int values as well.

The following types were added as allowed:

  • int (architecture dependent)
  • int64 (non-architecture dependent)

Signed-off-by: Alexander Widerberg <alexander.widerberg@cybercom.com>
@dgrijalva
Copy link
Owner

How will a MapClaims object be created that has int values? The reason we look at json.Number and float64 is that those are the two possible types returned from the json parser.

@leetal
Copy link
Author

leetal commented Aug 23, 2016

Ok, an example :)

We have an API that was constructed with jwt-go 2.7.0. To not break any APIs, we simply made the following to be able to utilize the validators in v3.0.0 (map_claims.go) in a smart manner:

example.go

// .... package + imports skipped
type ExtJWT struct {
    // ...not complete claims set, there are more claims in real struct...
    ExpiresAt time.Time
    Extra     map[string]interface{}
}
func (e *ExtJWT) ToTest() jwt.MapClaims {
    test := make(map[string]interface{}, len(e.Extra))
    for k, v := range e.Extra {
        test[k] = v
    }

    test["exp"] = e.ExpiresAt.Unix()              // <-- This will not work, wrong type
    //test["exp"] = float64(e.ExpiresAt.Unix())   // <-- This will work, correct type

    return test
}

example_test.go

// .... package + imports skipped
func TestToTest(t *testing.T) {
    // This should be OK. It is.... Sort of...
    err := (&ExtJWT{ExpiresAt: time.Now().Add(time.Hour)}).
        ToTest().Valid()
    fmt.Println(err)
    if err != nil {
        t.Errorf("expected nil")
    }
    // This should fail! But it does not! Err == nil!
    err = (&ExtJWT{ExpiresAt: time.Now().Add(-2 * time.Hour)}).
        ToTest().Valid()
    if err == nil {
        t.Errorf("expected not nil")
    }
}

Since we have to do this to not break any internal (or external) structures, we will have the ability to test our implementations as well with wrong types.. Of course, after jwt.New -> jwt.Parse() the types will be correct, but for certain internal tests BEFORE we actually create a token (and regular testing), the validators will fail, since the values have not been marshalled yet. I know, this is a super rare condition and probably "the wrong way" of doing this, but we like being more safe than sorry ;)

@johnlockwood-wf
Copy link
Contributor

In the spirit of being safe, would you add some unit tests to this?

@johnlockwood-wf
Copy link
Contributor

It would help me understand your example test if there was a before and after of what the change unbreaks. What went from red to green?

@dgrijalva
Copy link
Owner

Sorry for the delay. I've been on vacation.

@johnlockwood-wf I don't understand your question about the example test.

@johnlockwood-wf
Copy link
Contributor

@dgrijalva In the example @leetal posted, I want to understand what part is broken before this code change and thus what is fixed after. Maybe if he shows the output of go test before and after.

@dgrijalva
Copy link
Owner

I'm sort of on the fence about it. There's no real harm in adding extra tests, however, I expect I'll get support tickets from people who are confused by this. The number type thing is already a source of confusion for users of this library.

@johnlockwood-wf
Copy link
Contributor

I do think this change would add confusion.

@dgrijalva
Copy link
Owner

Oh. The issue is, the MapClaims types expects numbers to be either float64 or json.Number. If you make a token by hand (vs parsing one), you can put int numbers in there and it will not validate correctly.

@johnlockwood-wf
Copy link
Contributor

I don't understand the need to have the number anything but float64 or json.Number.

@johnlockwood-wf
Copy link
Contributor

Adding support of some arbitrary other number types to this lib doesn't make sense.

@leetal
Copy link
Author

leetal commented Aug 30, 2016

Actually @johnlockwood-wf. It would make sense if you used the MapClaims type internally BEFORE actually sending it to the marshaller. Since by using the MapClaims reduces the need for some internal struct i many cases. An example is OAuth2/OpenID Connect implementations where you have to do lots of work on the claims BEFORE actually creating the token. Thus, this change would benefit such cases. And in reality, a type assertion should not confuse a GO developer that much. This is such a mere change that wouldn't break a thing. It would benefit cases where you actually have to work with the claims internally (thus using timestamps as Unix-time int64´s for example). That's where i'm standing. But i think that if @dgrijalva thinks this will confuse people more, we'll just have to cast all of our internal timestamps to float64. It just feels more ugly to do that, than add an extra type assertion in jwt-go.

@johnlockwood-wf
Copy link
Contributor

@leetal I have been looking at the Valid method as if it is exclusively used for validating what has been unmarshalled from JSON. But, you're using it to validate a MapClaims before it has been marshalled.

@leetal
Copy link
Author

leetal commented Sep 1, 2016

@johnlockwood-wf Yes, i must use it prior to marshalling the token since that will solve lots of issues later on, especially due to the nature of OAuth2/OpenID Connect. Actually there is no need to do extra cpu-cycles just to marshal when i actually can validate the claims prior to marshalling them into the token. Perhaps i'm misusing the library, but i thought it was far more giving (in my case at least) to validate the claims prior to marshal.

@kekoav
Copy link

kekoav commented Dec 21, 2016

My opinion on this as a user of the library is that types and their associated functions should work the same no matter how they were created. It can be very disconcerting to manually create a MapClaims object, for a variety of purposes, only to realize that it actually only fully works if it was created by parsing a JWT.

This is a byproduct of using a map[string]interface{} as a data structure. If the structure can store anything-- it does allow storing interface{} after all-- it is pretty harsh to say to users, "Actually, I didn't mean it could be anything... I really mean it depends on what the types the Json parser generates...". 🤕 As a new user to the library, I can empathize with the intent of this PR, as I have seen the same in my experience with claims in general. Users are given a very long rope with which to hang themselves.

Barring a redesign of MapClaims, I think it would be wise to embrace the dynamic nature of the type and put up guard rails for users. Certainly testability should not be disregarded as that will only make things better for users in the long run.

A data structure should be designed to maintain it's integrity, so that all preconditions for any functions operating on this data are valid. Basically, users should not be able to create a MapClaims object that the associated functions fail to work on. At a minimum, placing some panics and explaining what went wrong would be helpful to guide users to using the library correctly.

@ghost
Copy link

ghost commented Jul 21, 2017

For me, storing an int in the Claims is being retrieved as a float64

tokenString, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"id": 5}).SignedString(JWTSecret)

token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
	return JWTSecret, nil
})

_, ok := token.Claims.(jwt.MapClaims)["id"].(int)
if !ok {
	fmt.Println("not an int")
}

_, ok = token.Claims.(jwt.MapClaims)["id"].(float64)
if ok {
	fmt.Println("is a float64")
}

Is that solved with this PR? Or is there a way within the master branch to store and retrieve ints in the JWT without type conversion?

@dgrijalva
Copy link
Owner

@everdev this is a side effect of how the standard library JSON parser works. You can use the JSONNumber flag to use that library's number type, which is more flexible. Alternatively, you can define a struct type for your claims and follow that path.

@mitar
Copy link

mitar commented Oct 19, 2020

I would also love if this could get merged. For all the reasons mentioned above. But even more. In OIDC setting, now we have to make claims be float to pass validation done by this library, but then if we use same claims to make a JSON, sometimes those floats can be encoded in their exponential form. Which does not work well with some consumers which assume those numbers will always be represented as integers. See here for similar issue. Yes, exponential form is valid JSON number but it is still better to not do it if it is not necessary. So that outputs are more compatible.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants