diff --git a/README.md b/README.md index 6ece66e..3dadd01 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ func router() http.Handler { // the provided authenticator middleware, but you can write your // own very easily, look at the Authenticator method in jwtauth.go // and tweak it, its not scary. - r.Use(jwtauth.Authenticator) + r.Use(jwtauth.Authenticator(tokenAuth)) r.Get("/admin", func(w http.ResponseWriter, r *http.Request) { _, claims, _ := jwtauth.FromContext(r.Context()) diff --git a/_example/main.go b/_example/main.go index 116dee7..8f07eb5 100644 --- a/_example/main.go +++ b/_example/main.go @@ -61,15 +61,17 @@ package main import ( "fmt" "net/http" + "time" "github.com/go-chi/chi/v5" "github.com/go-chi/jwtauth/v5" + "github.com/lestrrat-go/jwx/jwt" ) var tokenAuth *jwtauth.JWTAuth func init() { - tokenAuth = jwtauth.New("HS256", []byte("secret"), nil) + tokenAuth = jwtauth.New("HS256", []byte("secret"), nil, jwt.WithAcceptableSkew(30*time.Second)) // For debugging/example purposes, we generate and print // a sample jwt token with claims `user_id:123` here: @@ -95,7 +97,7 @@ func router() http.Handler { // the provided authenticator middleware, but you can write your // own very easily, look at the Authenticator method in jwtauth.go // and tweak it, its not scary. - r.Use(jwtauth.Authenticator) + r.Use(jwtauth.Authenticator(tokenAuth)) r.Get("/admin", func(w http.ResponseWriter, r *http.Request) { _, claims, _ := jwtauth.FromContext(r.Context()) diff --git a/go.mod b/go.mod index 987d81f..3d9c599 100644 --- a/go.mod +++ b/go.mod @@ -4,18 +4,18 @@ go 1.18 require ( github.com/go-chi/chi/v5 v5.0.7 - github.com/lestrrat-go/jwx/v2 v2.0.11 + github.com/lestrrat-go/jwx/v2 v2.0.17 ) require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/lestrrat-go/blackmagic v1.0.1 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/segmentio/asm v1.2.0 // indirect - golang.org/x/crypto v0.10.0 // indirect - golang.org/x/sys v0.9.0 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/sys v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index ec7a58f..471c745 100644 --- a/go.sum +++ b/go.sum @@ -8,16 +8,16 @@ github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= -github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.11 h1:ViHMnaMeaO0qV16RZWBHM7GTrAnX2aFLVKofc7FuKLQ= -github.com/lestrrat-go/jwx/v2 v2.0.11/go.mod h1:ZtPtMFlrfDrH2Y0iwfa3dRFn8VzwBrB+cyrm3IBWdDg= +github.com/lestrrat-go/jwx/v2 v2.0.17 h1:+WavkdKVWO90ECnIzUetOnjY+kcqqw4WXEUmil7sMCE= +github.com/lestrrat-go/jwx/v2 v2.0.17/go.mod h1:G8randPHLGAqhcNCqtt6/V/7E6fvJRl3Sf9z777eTQ0= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= @@ -36,9 +36,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -57,17 +56,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/jwtauth.go b/jwtauth.go index bc42ae3..105677b 100644 --- a/jwtauth.go +++ b/jwtauth.go @@ -12,10 +12,11 @@ import ( ) type JWTAuth struct { - alg jwa.SignatureAlgorithm - signKey interface{} // private-key - verifyKey interface{} // public-key, only used by RSA and ECDSA algorithms - verifier jwt.ParseOption + alg jwa.SignatureAlgorithm + signKey interface{} // private-key + verifyKey interface{} // public-key, only used by RSA and ECDSA algorithms + verifier jwt.ParseOption + validateOptions []jwt.ValidateOption } var ( @@ -32,8 +33,13 @@ var ( ErrAlgoInvalid = errors.New("algorithm mismatch") ) -func New(alg string, signKey interface{}, verifyKey interface{}) *JWTAuth { - ja := &JWTAuth{alg: jwa.SignatureAlgorithm(alg), signKey: signKey, verifyKey: verifyKey} +func New(alg string, signKey interface{}, verifyKey interface{}, validateOptions ...jwt.ValidateOption) *JWTAuth { + ja := &JWTAuth{ + alg: jwa.SignatureAlgorithm(alg), + signKey: signKey, + verifyKey: verifyKey, + validateOptions: validateOptions, + } if ja.verifyKey != nil { ja.verifier = jwt.WithKey(ja.alg, ja.verifyKey) @@ -105,7 +111,7 @@ func VerifyToken(ja *JWTAuth, tokenString string) (jwt.Token, error) { return nil, ErrUnauthorized } - if err := jwt.Validate(token); err != nil { + if err := jwt.Validate(token, ja.validateOptions...); err != nil { return token, ErrorReason(err) } @@ -158,23 +164,26 @@ func ErrorReason(err error) error { // Verifier middleware request context values. The Authenticator sends a 401 Unauthorized // response for any unverified tokens and passes the good ones through. It's just fine // until you decide to write something similar and customize your client response. -func Authenticator(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token, _, err := FromContext(r.Context()) +func Authenticator(ja *JWTAuth) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + hfn := func(w http.ResponseWriter, r *http.Request) { + token, _, err := FromContext(r.Context()) - if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + if token == nil || jwt.Validate(token, ja.validateOptions...) != nil { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + // Token is authenticated, pass it through + next.ServeHTTP(w, r) } - - if token == nil || jwt.Validate(token) != nil { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - - // Token is authenticated, pass it through - next.ServeHTTP(w, r) - }) + return http.HandlerFunc(hfn) + } } func NewContext(ctx context.Context, t jwt.Token, err error) context.Context { diff --git a/jwtauth_test.go b/jwtauth_test.go index ea0b0d4..1d5a2f0 100644 --- a/jwtauth_test.go +++ b/jwtauth_test.go @@ -44,7 +44,7 @@ DLxxa5/7QyH6y77nCRQyJ3x3UwF9rUD0RCsp4sNdX5kOQ9PUyHyOtCUCAwEAAQ== ) func init() { - TokenAuthHS256 = jwtauth.New(jwa.HS256.String(), TokenSecret, nil) + TokenAuthHS256 = jwtauth.New(jwa.HS256.String(), TokenSecret, nil, jwt.WithAcceptableSkew(30*time.Second)) } // @@ -101,7 +101,10 @@ func TestSimpleRSA(t *testing.T) { func TestSimple(t *testing.T) { r := chi.NewRouter() - r.Use(jwtauth.Verifier(TokenAuthHS256), jwtauth.Authenticator) + r.Use( + jwtauth.Verifier(TokenAuthHS256), + jwtauth.Authenticator(TokenAuthHS256), + ) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("welcome")) @@ -135,6 +138,19 @@ func TestSimple(t *testing.T) { t.Fatalf(resp) } + // correct token, but has expired within the skew time + h.Set("Authorization", "BEARER "+newJwtToken(TokenSecret, map[string]interface{}{"exp": time.Now().Unix() - 29})) + if status, resp := testRequest(t, ts, "GET", "/", h, nil); status != 200 || resp != "welcome" { + fmt.Println("status", status, "resp", resp) + t.Fatalf(resp) + } + + // correct token, but has expired outside of the skew time + h.Set("Authorization", "BEARER "+newJwtToken(TokenSecret, map[string]interface{}{"exp": time.Now().Unix() - 31})) + if status, resp := testRequest(t, ts, "GET", "/", h, nil); status != 401 || resp != "token is expired\n" { + t.Fatalf(resp) + } + // sending authorized requests if status, resp := testRequest(t, ts, "GET", "/", newAuthHeader(), nil); status != 200 || resp != "welcome" { t.Fatalf(resp) @@ -269,6 +285,7 @@ func newJwtToken(secret []byte, claims ...map[string]interface{}) string { token.Set(k, v) } } + tokenPayload, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, secret)) if err != nil { log.Fatal(err)