Skip to content

Commit 66a443b

Browse files
authored
v2: validator for golang-jwt/jwt (#91)
1 parent 665e7da commit 66a443b

File tree

5 files changed

+335
-0
lines changed

5 files changed

+335
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/auth0/go-jwt-middleware
33
go 1.14
44

55
require (
6+
github.com/golang-jwt/jwt v3.2.1+incompatible
67
github.com/google/go-cmp v0.5.5
78
github.com/stretchr/testify v1.7.0 // indirect
89
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
22
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
4+
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
35
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
46
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
57
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

validate/jwt-go/examples/main.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
10+
jwtmiddleware "github.com/auth0/go-jwt-middleware"
11+
jwtgo "github.com/auth0/go-jwt-middleware/validate/jwt-go"
12+
"github.com/golang-jwt/jwt"
13+
)
14+
15+
// CustomClaimsExample contains custom data we want from the token.
16+
type CustomClaimsExample struct {
17+
Username string `json:"username"`
18+
ShouldReject bool `json:"shouldReject,omitempty"`
19+
jwt.StandardClaims
20+
}
21+
22+
// Validate does nothing for this example
23+
func (c *CustomClaimsExample) Validate(ctx context.Context) error {
24+
if c.ShouldReject {
25+
return errors.New("should reject was set to true")
26+
}
27+
return nil
28+
}
29+
30+
var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31+
claims := r.Context().Value(jwtmiddleware.ContextKey{})
32+
j, err := json.MarshalIndent(claims, "", "\t")
33+
if err != nil {
34+
w.WriteHeader(http.StatusInternalServerError)
35+
fmt.Println(err)
36+
}
37+
38+
fmt.Fprintf(w, "This is an authenticated request\n")
39+
fmt.Fprintf(w, "Claim content: %s\n", string(j))
40+
})
41+
42+
func main() {
43+
keyFunc := func(t *jwt.Token) (interface{}, error) {
44+
// our token must be signed using this data
45+
return []byte("secret"), nil
46+
}
47+
/*expectedClaims := func() jwt.Expected {
48+
// By setting up expected claims we are saying a token must
49+
// have the data we specify.
50+
return jwt.Expected{
51+
Issuer: "josev2-example",
52+
Time: time.Now(),
53+
}
54+
}*/
55+
customClaims := func() jwtgo.CustomClaims {
56+
// we want this struct to be filled in with our custom claims
57+
// from the token
58+
return &CustomClaimsExample{}
59+
}
60+
61+
// setup the jwt-go validator
62+
validator, err := jwtgo.New(
63+
keyFunc,
64+
"HS256",
65+
//jwtgo.WithExpectedClaims(expectedClaims),
66+
jwtgo.WithCustomClaims(customClaims),
67+
//jwtgo.WithAllowedClockSkew(30*time.Second),
68+
)
69+
70+
if err != nil {
71+
// we'll panic in order to fail fast
72+
panic(err)
73+
}
74+
75+
// setup the middleware
76+
m := jwtmiddleware.New(validator.ValidateToken)
77+
78+
http.ListenAndServe("0.0.0.0:3000", m.CheckJWT(handler))
79+
// try it out with eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqd3Rnby1leGFtcGxlIiwic3ViIjoiMTIzNDU2Nzg5MCIsImlhdCI6MTUxNjIzOTAyMiwidXNlcm5hbWUiOiJ1c2VyMTIzIn0.ha_JgA29vSAb3HboPRXEi9Dm5zy7ARzd4P8AFoYP9t0
80+
// which is signed with 'secret' and has the data:
81+
// {
82+
// "iss": "jwtgo-example",
83+
// "sub": "1234567890",
84+
// "iat": 1516239022,
85+
// "username": "user123"
86+
// }
87+
88+
// you can also try out the custom validation with eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqd3Rnby1leGFtcGxlIiwic3ViIjoiMTIzNDU2Nzg5MCIsImlhdCI6MTUxNjIzOTAyMiwidXNlcm5hbWUiOiJ1c2VyMTIzIiwic2hvdWxkUmVqZWN0Ijp0cnVlfQ.awZ0DFpJ-hH5xn-q-sZHJWj7oTAOkPULwgFO4O6D67o
89+
// which is signed with 'secret' and has the data:
90+
// {
91+
// "iss": "jwtgo-example",
92+
// "sub": "1234567890",
93+
// "iat": 1516239022,
94+
// "username": "user123",
95+
// "shouldReject": true
96+
// }
97+
}

validate/jwt-go/jwtgo.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package jwtgo
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/golang-jwt/jwt"
9+
)
10+
11+
// CustomClaims defines any custom data / claims wanted. The validator will
12+
// call the Validate function which is where custom validation logic can be
13+
// defined.
14+
type CustomClaims interface {
15+
jwt.Claims
16+
Validate(context.Context) error
17+
}
18+
19+
// Option is how options for the validator are setup.
20+
type Option func(*validator)
21+
22+
// WithCustomClaims sets up a function that returns the object CustomClaims are
23+
// unmarshalled into and the object which Validate is called on for custom
24+
// validation. If this option is not used the validator will do nothing for
25+
// custom claims.
26+
func WithCustomClaims(f func() CustomClaims) Option {
27+
return func(v *validator) {
28+
v.customClaims = f
29+
}
30+
}
31+
32+
// New sets up a new Validator. With the required keyFunc and
33+
// signatureAlgorithm as well as options.
34+
func New(keyFunc jwt.Keyfunc,
35+
signatureAlgorithm string,
36+
opts ...Option) (*validator, error) {
37+
38+
if keyFunc == nil {
39+
return nil, errors.New("keyFunc is required but was nil")
40+
}
41+
42+
v := &validator{
43+
keyFunc: keyFunc,
44+
signatureAlgorithm: signatureAlgorithm,
45+
customClaims: nil,
46+
}
47+
48+
for _, opt := range opts {
49+
opt(v)
50+
}
51+
52+
return v, nil
53+
}
54+
55+
type validator struct {
56+
// required options
57+
58+
keyFunc func(*jwt.Token) (interface{}, error)
59+
signatureAlgorithm string
60+
61+
// optional options
62+
customClaims func() CustomClaims
63+
}
64+
65+
// ValidateToken validates the passed in JWT using the jwt-go package.
66+
func (v *validator) ValidateToken(ctx context.Context, token string) (interface{}, error) {
67+
var claims jwt.Claims
68+
69+
if v.customClaims != nil {
70+
claims = v.customClaims()
71+
} else {
72+
claims = &jwt.StandardClaims{}
73+
}
74+
75+
p := new(jwt.Parser)
76+
77+
if v.signatureAlgorithm != "" {
78+
p.ValidMethods = []string{v.signatureAlgorithm}
79+
}
80+
81+
_, err := p.ParseWithClaims(token, claims, v.keyFunc)
82+
if err != nil {
83+
return nil, fmt.Errorf("could not parse the token: %w", err)
84+
}
85+
86+
if customClaims, ok := claims.(CustomClaims); ok {
87+
if err = customClaims.Validate(ctx); err != nil {
88+
return nil, fmt.Errorf("custom claims not validated: %w", err)
89+
}
90+
}
91+
92+
return claims, nil
93+
}

validate/jwt-go/jwtgo_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package jwtgo
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"github.com/golang-jwt/jwt"
9+
"github.com/google/go-cmp/cmp"
10+
)
11+
12+
type testingCustomClaims struct {
13+
Foo string `json:"foo"`
14+
ReturnError error
15+
jwt.StandardClaims
16+
}
17+
18+
func (tcc *testingCustomClaims) Validate(ctx context.Context) error {
19+
return tcc.ReturnError
20+
}
21+
22+
func equalErrors(actual error, expected string) bool {
23+
if actual == nil {
24+
return expected == ""
25+
}
26+
return actual.Error() == expected
27+
}
28+
29+
func Test_Validate(t *testing.T) {
30+
testCases := []struct {
31+
name string
32+
signatureAlgorithm string
33+
token string
34+
keyFuncReturnError error
35+
customClaims CustomClaims
36+
expectedError string
37+
expectedContext jwt.Claims
38+
}{
39+
{
40+
name: "happy path",
41+
token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I`,
42+
expectedContext: &jwt.StandardClaims{Subject: "1234567890"},
43+
},
44+
{
45+
// we want to test that when it expects RSA but we send
46+
// HMAC encrypted with the server public key it will
47+
// error
48+
name: "errors on wrong algorithm",
49+
signatureAlgorithm: "PS256",
50+
token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`,
51+
expectedError: "could not parse the token: signing method HS256 is invalid",
52+
},
53+
{
54+
name: "errors on wrong token format errors",
55+
expectedError: "could not parse the token: token contains an invalid number of segments",
56+
},
57+
{
58+
name: "errors when the key func errors",
59+
token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`,
60+
keyFuncReturnError: errors.New("key func error message"),
61+
expectedError: "could not parse the token: key func error message",
62+
},
63+
{
64+
name: "errors when signature is invalid",
65+
token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.hDyICUnkCrwFJnkJHRSkwMZNSYZ9LI6z2EFJdtwFurA`,
66+
expectedError: "could not parse the token: signature is invalid",
67+
},
68+
{
69+
name: "errors when custom claims errors",
70+
token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZm9vIjoiYmFyIiwiaWF0IjoxNTE2MjM5MDIyfQ.DFTWyYib4-xFdMaEZFAYx5AKMPNS7Hhl4kcyjQVinYc`,
71+
customClaims: &testingCustomClaims{ReturnError: errors.New("custom claims error message")},
72+
expectedError: "custom claims not validated: custom claims error message",
73+
},
74+
}
75+
76+
for _, testCase := range testCases {
77+
t.Run(testCase.name, func(t *testing.T) {
78+
var customClaimsFunc func() CustomClaims = nil
79+
if testCase.customClaims != nil {
80+
customClaimsFunc = func() CustomClaims { return testCase.customClaims }
81+
}
82+
83+
v, _ := New(func(token *jwt.Token) (interface{}, error) {
84+
return []byte("secret"), testCase.keyFuncReturnError
85+
},
86+
testCase.signatureAlgorithm,
87+
WithCustomClaims(customClaimsFunc),
88+
)
89+
actualContext, err := v.ValidateToken(context.Background(), testCase.token)
90+
if !equalErrors(err, testCase.expectedError) {
91+
t.Fatalf("wanted err:\n%s\ngot:\n%+v\n", testCase.expectedError, err)
92+
}
93+
94+
if (testCase.expectedContext == nil && actualContext != nil) || (testCase.expectedContext != nil && actualContext == nil) {
95+
t.Fatalf("wanted user context:\n%+v\ngot:\n%+v\n", testCase.expectedContext, actualContext)
96+
} else if testCase.expectedContext != nil {
97+
if diff := cmp.Diff(testCase.expectedContext, actualContext.(jwt.Claims)); diff != "" {
98+
t.Errorf("user context mismatch (-want +got):\n%s", diff)
99+
}
100+
101+
}
102+
})
103+
}
104+
}
105+
106+
func Test_New(t *testing.T) {
107+
t.Run("happy path", func(t *testing.T) {
108+
keyFunc := func(t *jwt.Token) (interface{}, error) { return nil, nil }
109+
customClaims := func() CustomClaims { return nil }
110+
111+
v, err := New(keyFunc, "HS256", WithCustomClaims(customClaims))
112+
113+
if !equalErrors(err, "") {
114+
t.Fatalf("wanted err:\n%s\ngot:\n%+v\n", "", err)
115+
}
116+
117+
if v.keyFunc == nil {
118+
t.Log("keyFunc was nil when it should not have been")
119+
t.Fail()
120+
}
121+
122+
if v.signatureAlgorithm != "HS256" {
123+
t.Logf("signatureAlgorithm was %q when it should have been %q", v.signatureAlgorithm, "HS256")
124+
t.Fail()
125+
}
126+
127+
if v.customClaims == nil {
128+
t.Log("customClaims was nil when it should not have been")
129+
t.Fail()
130+
}
131+
})
132+
133+
t.Run("error on no keyFunc", func(t *testing.T) {
134+
_, err := New(nil, "HS256")
135+
136+
expectedErr := "keyFunc is required but was nil"
137+
if !equalErrors(err, expectedErr) {
138+
t.Fatalf("wanted err:\n%s\ngot:\n%+v\n", expectedErr, err)
139+
}
140+
})
141+
142+
}

0 commit comments

Comments
 (0)