Repository: go-chi/jwtauth Branch: master Commit: 21df58695c5d Files: 10 Total size: 33.0 KB Directory structure: gitextract_pgl6yup7/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── _example/ │ └── main.go ├── go.mod ├── go.sum ├── jwtauth.go └── jwtauth_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ on: push: branches: '**' paths-ignore: - 'docs/**' pull_request: branches: '**' paths-ignore: - 'docs/**' name: Test jobs: test: env: GOPATH: ${{ github.workspace }} defaults: run: working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} strategy: matrix: go-version: [1.22.x, 1.23.x, 1.24.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v3 with: path: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} - name: Test run: | go get -d -t ./... go test -v ./... ================================================ FILE: .gitignore ================================================ vendor/ Gopkg.lock .idea/ ================================================ FILE: LICENSE ================================================ Copyright (c) 2015-Present https://github.com/go-chi authors MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ SHELL = bash -o pipefail TEST_FLAGS ?= -v -race all: @echo "make " @echo "" @echo "commands:" @echo "" @echo " + Development:" @echo " - build" @echo " - test" @echo " - todo" @echo " - clean" @echo "" @echo "" ## ## Development ## build: go build ./... clean: go clean -cache -testcache test: test-clean GOGC=off go test $(TEST_FLAGS) -run=$(TEST) ./... test-clean: GOGC=off go clean -testcache bench: @go test -timeout=25m -bench=. todo: @git grep TODO -- './*' ':!./vendor/' ':!./Makefile' || : ================================================ FILE: README.md ================================================ # jwtauth - JWT authentication middleware for HTTP services [![GoDoc Widget]][godoc] The `jwtauth` http middleware package provides a simple way to verify a JWT token from a http request and send the result down the request context (`context.Context`). Please note, `jwtauth` works with any Go http router, but resides under the go-chi group for maintenance and organization - its only 3rd party dependency is the underlying jwt library "github.com/lestrrat-go/jwx". In a complete JWT-authentication flow, you'll first capture the token from a http request, decode it, verify it and then validate that its correctly signed and hasn't expired - the `jwtauth.Verifier` middleware handler takes care of all of that. The `jwtauth.Verifier` will set the context values on keys `jwtauth.TokenCtxKey` and `jwtauth.ErrorCtxKey`. Next, it's up to an authentication handler to respond or continue processing after the `jwtauth.Verifier`. The `jwtauth.Authenticator` middleware responds with a 401 Unauthorized plain-text payload for all unverified tokens and passes the good ones through. You can also copy the Authenticator and customize it to handle invalid tokens to better fit your flow (ie. with a JSON error response body). By default, the `Verifier` will search for a JWT token in a http request, in the order: 1. 'Authorization: BEARER T' request header 2. 'jwt' Cookie value The first JWT string that is found as an authorization header or cookie header is then decoded by the `lestrrat-go/jwx` library and a jwt.Token object is set on the request context. In the case of a signature decoding error the Verifier will also set the error on the request context. The Verifier always calls the next http handler in sequence, which can either be the generic `jwtauth.Authenticator` middleware or your own custom handler which checks the request context jwt token and error to prepare a custom http response. Note: jwtauth supports custom verification sequences for finding a token from a request by using the `Verify` middleware instantiator directly. The default `Verifier` is instantiated by calling `Verify(ja, TokenFromHeader, TokenFromCookie)`. # Usage See the full [example](https://github.com/go-chi/jwtauth/blob/master/_example/main.go). ```go package main import ( "fmt" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/jwtauth/v5" ) var tokenAuth *jwtauth.JWTAuth func init() { tokenAuth = jwtauth.New("HS256", []byte("secret"), nil) // For debugging/example purposes, we generate and print // a sample jwt token with claims `user_id:123` here: _, tokenString, _ := tokenAuth.Encode(map[string]interface{}{"user_id": 123}) fmt.Printf("DEBUG: a sample jwt is %s\n\n", tokenString) } func main() { addr := ":3333" fmt.Printf("Starting server on %v\n", addr) http.ListenAndServe(addr, router()) } func router() http.Handler { r := chi.NewRouter() // Protected routes r.Group(func(r chi.Router) { // Seek, verify and validate JWT tokens r.Use(jwtauth.Verifier(tokenAuth)) // Handle valid / invalid tokens. In this example, we use // 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(tokenAuth)) r.Get("/admin", func(w http.ResponseWriter, r *http.Request) { _, claims, _ := jwtauth.FromContext(r.Context()) w.Write([]byte(fmt.Sprintf("protected area. hi %v", claims["user_id"]))) }) }) // Public routes r.Group(func(r chi.Router) { r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("welcome anonymous")) }) }) return r } ``` # Util See https://github.com/goware/jwtutil for utility to help you generate JWT tokens. `go install github.com/goware/jwtutil` Usage: `jwtutil -secret=secret -encode -claims='{"user_id":111}'` Output: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMTF9._cLJn0xFS0Mdr_4L_8XF8-8tv7bHyOQJXyWaNsSqlEs` # LICENSE [MIT](/LICENSE) [godoc]: https://pkg.go.dev/github.com/go-chi/jwtauth/v5 [godoc widget]: https://godoc.org/github.com/go-chi/jwtauth?status.svg ================================================ FILE: _example/main.go ================================================ // // jwtauth example // // Sample output: // // [peter@pak ~]$ curl -v http://localhost:3333/ // * Trying ::1... // * Connected to localhost (::1) port 3333 (#0) // > GET / HTTP/1.1 // > Host: localhost:3333 // > User-Agent: curl/7.49.1 // > Accept: */* // > // < HTTP/1.1 200 OK // < Date: Tue, 13 Sep 2016 15:53:17 GMT // < Content-Length: 17 // < Content-Type: text/plain; charset=utf-8 // < // * Connection #0 to host localhost left intact // welcome anonymous% // // // [peter@pak ~]$ curl -v http://localhost:3333/admin // * Trying ::1... // * Connected to localhost (::1) port 3333 (#0) // > GET /admin HTTP/1.1 // > Host: localhost:3333 // > User-Agent: curl/7.49.1 // > Accept: */* // > // < HTTP/1.1 401 Unauthorized // < Content-Type: text/plain; charset=utf-8 // < X-Content-Type-Options: nosniff // < Date: Tue, 13 Sep 2016 15:53:19 GMT // < Content-Length: 13 // < // Unauthorized // * Connection #0 to host localhost left intact // // // [peter@pak ~]$ curl -H"Authorization: BEARER eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjN9.PZLMJBT9OIVG2qgp9hQr685oVYFgRgWpcSPmNcw6y7M" -v http://localhost:3333/admin // * Trying ::1... // * Connected to localhost (::1) port 3333 (#0) // > GET /admin HTTP/1.1 // > Host: localhost:3333 // > User-Agent: curl/7.49.1 // > Accept: */* // > Authorization: BEARER eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjN9.PZLMJBT9OIVG2qgp9hQr685oVYFgRgWpcSPmNcw6y7M // > // < HTTP/1.1 200 OK // < Date: Tue, 13 Sep 2016 15:54:26 GMT // < Content-Length: 22 // < Content-Type: text/plain; charset=utf-8 // < // * Connection #0 to host localhost left intact // protected area. hi 123% // package main import ( "fmt" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/jwtauth/v5" "github.com/lestrrat-go/jwx/v3/jwt" ) var tokenAuth *jwtauth.JWTAuth func init() { 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: _, tokenString, _ := tokenAuth.Encode(map[string]interface{}{"user_id": 123}) fmt.Printf("DEBUG: a sample jwt is %s\n\n", tokenString) } func main() { addr := ":3333" fmt.Printf("Starting server on %v\n", addr) http.ListenAndServe(addr, router()) } func router() http.Handler { r := chi.NewRouter() // Protected routes r.Group(func(r chi.Router) { // Seek, verify and validate JWT tokens r.Use(jwtauth.Verifier(tokenAuth)) // Handle valid / invalid tokens. In this example, we use // 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(tokenAuth)) r.Get("/admin", func(w http.ResponseWriter, r *http.Request) { _, claims, _ := jwtauth.FromContext(r.Context()) w.Write([]byte(fmt.Sprintf("protected area. hi %v", claims["user_id"]))) }) }) // Public routes r.Group(func(r chi.Router) { r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("welcome anonymous")) }) }) return r } ================================================ FILE: go.mod ================================================ module github.com/go-chi/jwtauth/v5 go 1.23.0 toolchain go1.24.2 require ( github.com/go-chi/chi/v5 v5.2.1 github.com/lestrrat-go/jwx/v3 v3.0.2 ) require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/lestrrat-go/blackmagic v1.0.3 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/segmentio/asm v1.2.0 // indirect golang.org/x/crypto v0.38.0 // indirect golang.org/x/sys v0.33.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs= github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= 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/v3 v3.0.0-beta2 h1:SDxjGoH7qj0nBXVrcrxX8eD94wEnjR+EEuqqmeqQYlY= github.com/lestrrat-go/httprc/v3 v3.0.0-beta2/go.mod h1:Nwo81sMxE0DcvTB+rJyynNhv/DUu2yZErV7sscw9pHE= github.com/lestrrat-go/jwx/v3 v3.0.2 h1:N+XLjTJEzDZRP3S0SezclXFAfopwL+o5vaL+qg6rX1I= github.com/lestrrat-go/jwx/v3 v3.0.2/go.mod h1:qO9w1qkQH77a0r9OXNM33YQPnV/evetKYRg58h1rBNE= 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= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: jwtauth.go ================================================ package jwtauth import ( "context" "errors" "net/http" "strings" "time" "github.com/lestrrat-go/jwx/v3/jwa" "github.com/lestrrat-go/jwx/v3/jwt" "github.com/lestrrat-go/jwx/v3/transform" ) type JWTAuth struct { 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 ( TokenCtxKey = &contextKey{"Token"} ErrorCtxKey = &contextKey{"Error"} ) var ( ErrUnauthorized = errors.New("token is unauthorized") ErrExpired = errors.New("token is expired") ErrNBFInvalid = errors.New("token nbf validation failed") ErrIATInvalid = errors.New("token iat validation failed") ErrNoTokenFound = errors.New("no token found") ErrAlgoInvalid = errors.New("algorithm mismatch") ) func New(alg string, signKey interface{}, verifyKey interface{}, validateOptions ...jwt.ValidateOption) *JWTAuth { sigAlg, _ := jwa.LookupSignatureAlgorithm(alg) ja := &JWTAuth{ alg: sigAlg, signKey: signKey, verifyKey: verifyKey, validateOptions: validateOptions, } if ja.verifyKey != nil { ja.verifier = jwt.WithKey(ja.alg, ja.verifyKey) } else { ja.verifier = jwt.WithKey(ja.alg, ja.signKey) } return ja } // Verifier http middleware handler will verify a JWT string from a http request. // // Verifier will search for a JWT token in a http request, in the order: // 1. 'Authorization: BEARER T' request header // 2. Cookie 'jwt' value // // The first JWT string that is found as a query parameter, authorization header // or cookie header is then decoded by the `jwt-go` library and a *jwt.Token // object is set on the request context. In the case of a signature decoding error // the Verifier will also set the error on the request context. // // The Verifier always calls the next http handler in sequence, which can either // be the generic `jwtauth.Authenticator` middleware or your own custom handler // which checks the request context jwt token and error to prepare a custom // http response. func Verifier(ja *JWTAuth) func(http.Handler) http.Handler { return Verify(ja, TokenFromHeader, TokenFromCookie) } func Verify(ja *JWTAuth, findTokenFns ...func(r *http.Request) string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { hfn := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() token, err := VerifyRequest(ja, r, findTokenFns...) ctx = NewContext(ctx, token, err) next.ServeHTTP(w, r.WithContext(ctx)) } return http.HandlerFunc(hfn) } } func VerifyRequest(ja *JWTAuth, r *http.Request, findTokenFns ...func(r *http.Request) string) (jwt.Token, error) { var tokenString string // Extract token string from the request by calling token find functions in // the order they where provided. Further extraction stops if a function // returns a non-empty string. for _, fn := range findTokenFns { tokenString = fn(r) if tokenString != "" { break } } if tokenString == "" { return nil, ErrNoTokenFound } return VerifyToken(ja, tokenString) } func VerifyToken(ja *JWTAuth, tokenString string) (jwt.Token, error) { // Decode & verify the token token, err := ja.Decode(tokenString) if err != nil { return token, ErrorReason(err) } if token == nil { return nil, ErrUnauthorized } if err := jwt.Validate(token, ja.validateOptions...); err != nil { return token, ErrorReason(err) } // Valid! return token, nil } func (ja *JWTAuth) Encode(claims map[string]interface{}) (t jwt.Token, tokenString string, err error) { t = jwt.New() for k, v := range claims { if err := t.Set(k, v); err != nil { return nil, "", err } } payload, err := ja.sign(t) if err != nil { return nil, "", err } tokenString = string(payload) return } func (ja *JWTAuth) Decode(tokenString string) (jwt.Token, error) { return ja.parse([]byte(tokenString)) } func (ja *JWTAuth) ValidateOptions() []jwt.ValidateOption { return ja.validateOptions } func (ja *JWTAuth) sign(token jwt.Token) ([]byte, error) { return jwt.Sign(token, jwt.WithKey(ja.alg, ja.signKey)) } func (ja *JWTAuth) parse(payload []byte) (jwt.Token, error) { // we disable validation here because we use jwt.Validate to validate tokens return jwt.Parse(payload, ja.verifier, jwt.WithValidate(false)) } // ErrorReason will normalize the error message from the underlining // jwt library func ErrorReason(err error) error { switch { case errors.Is(err, jwt.TokenExpiredError()), err == ErrExpired: return ErrExpired case errors.Is(err, jwt.InvalidIssuedAtError()), err == ErrIATInvalid: return ErrIATInvalid case errors.Is(err, jwt.TokenNotYetValidError()), err == ErrNBFInvalid: return ErrNBFInvalid default: return ErrUnauthorized } } // Authenticator is a default authentication middleware to enforce access from the // 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(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 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 { ctx = context.WithValue(ctx, TokenCtxKey, t) ctx = context.WithValue(ctx, ErrorCtxKey, err) return ctx } func FromContext(ctx context.Context) (jwt.Token, map[string]interface{}, error) { token, _ := ctx.Value(TokenCtxKey).(jwt.Token) var err error claims := map[string]interface{}{} if token != nil { if err = transform.AsMap(token, claims); err != nil { return token, nil, err } } err, _ = ctx.Value(ErrorCtxKey).(error) return token, claims, err } // UnixTime returns the given time in UTC milliseconds func UnixTime(tm time.Time) int64 { return tm.UTC().Unix() } // EpochNow is a helper function that returns the NumericDate time value used by the spec func EpochNow() int64 { return time.Now().UTC().Unix() } // ExpireIn is a helper function to return calculated time in the future for "exp" claim func ExpireIn(tm time.Duration) int64 { return EpochNow() + int64(tm.Seconds()) } // Set issued at ("iat") to specified time in the claims func SetIssuedAt(claims map[string]interface{}, tm time.Time) { claims["iat"] = tm.UTC().Unix() } // Set issued at ("iat") to present time in the claims func SetIssuedNow(claims map[string]interface{}) { claims["iat"] = EpochNow() } // Set expiry ("exp") in the claims func SetExpiry(claims map[string]interface{}, tm time.Time) { claims["exp"] = tm.UTC().Unix() } // Set expiry ("exp") in the claims to some duration from the present time func SetExpiryIn(claims map[string]interface{}, tm time.Duration) { claims["exp"] = ExpireIn(tm) } // TokenFromCookie tries to retrieve the token string from a cookie named // "jwt". func TokenFromCookie(r *http.Request) string { cookie, err := r.Cookie("jwt") if err != nil { return "" } return cookie.Value } // TokenFromHeader tries to retrieve the token string from the // "Authorization" request header: "Authorization: BEARER T". func TokenFromHeader(r *http.Request) string { // Get token from authorization header. bearer := r.Header.Get("Authorization") if len(bearer) > 7 && strings.ToUpper(bearer[0:7]) == "BEARER " { return bearer[7:] } return "" } // TokenFromQuery tries to retrieve the token string from the "jwt" URI // query parameter. // // To use it, build our own middleware handler, such as: // // func Verifier(ja *JWTAuth) func(http.Handler) http.Handler { // return func(next http.Handler) http.Handler { // return Verify(ja, TokenFromQuery, TokenFromHeader, TokenFromCookie)(next) // } // } func TokenFromQuery(r *http.Request) string { // Get token from query param named "jwt". return r.URL.Query().Get("jwt") } // contextKey is a value for use with context.WithValue. It's used as // a pointer so it fits in an interface{} without allocation. This technique // for defining context keys was copied from Go 1.7's new use of context in net/http. type contextKey struct { name string } func (k *contextKey) String() string { return "jwtauth context value " + k.name } ================================================ FILE: jwtauth_test.go ================================================ package jwtauth_test import ( "crypto/x509" "encoding/pem" "fmt" "io" "log" "net/http" "net/http/httptest" "reflect" "testing" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/jwtauth/v5" "github.com/lestrrat-go/jwx/v3/jwa" "github.com/lestrrat-go/jwx/v3/jwt" "github.com/lestrrat-go/jwx/v3/transform" ) var ( TokenAuthHS256 *jwtauth.JWTAuth TokenSecret = []byte("secretpass") TokenAuthRS256 *jwtauth.JWTAuth PrivateKeyRS256String = `-----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDsH6T+WrdRLKHEhbhbnItRo7X5tj0xssOSCJUiZbCHr52XftIr hBD6HxbGaKUEzuaCDYGEcQZZRJ1KHfYmJtXPCz4Zp3qlhjNugvTaZoFtQ8RqiWVY cHqCY6cmI+3cq2mVrd7MstpXKhC8dZ2MZnzx/zqaeiV21SiwxHed8LmWmQIDAQAB AoGAff9I0L1hkrxJOg/M133KTe8Y3L4lG07z0wonYmp274CDjGKNDdF0KbPLOGaA n/czw3Qnh5+0LpBRikpAng0dC06z0YnyzrkoPPawC4s2zJeY3NnajK9IfRAAVlby cIJVmEL/xF3FFHhCfrJNWd+zthcHxCATJOBpH2pwhb4WLfECQQD/geZ/B6p8WlGb amHFhBd/hQN6cq63RGujf3ecz5H+h4RqFyycaVr3t8QZBBd3O3jRB9FCcan2IxRa UoYNGNB9AkEA7JQtfmb0p8cTHiDyV6qb8aNJFWipwQVVMmpaXvfC6Aue5uJiyHnx iScLsj1ozewCgTvzL7MAsfj0k6qX3c01TQJBAPL2JCdhM8XB4N4Hf+dhHzMcWd1j Fi6hOjWjrSsI2owNc2Wqmbo2GNF8BlW/ZUz02YLzixJCoVqzqtPkqyHjGcUCQQDk msrbOeFvvo5arrt+uv21oXMdnOVr/xs0fFCXNBLC53fE4z1RO4SKY5CJy41abpR9 DNERZodlcovjpRTa31CBAkEAw8geqJ1+cfEDZYfJxJigFSwbwoLw6BH+GD4KAEdX G1u9SGGYP19eC2mpQei4V5MqAYEbb82bqcebhwg8kAReNQ== -----END RSA PRIVATE KEY----- ` PublicKeyRS256String = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDsH6T+WrdRLKHEhbhbnItRo7X5 tj0xssOSCJUiZbCHr52XftIrhBD6HxbGaKUEzuaCDYGEcQZZRJ1KHfYmJtXPCz4Z p3qlhjNugvTaZoFtQ8RqiWVYcHqCY6cmI+3cq2mVrd7MstpXKhC8dZ2MZnzx/zqa eiV21SiwxHed8LmWmQIDAQAB -----END PUBLIC KEY----- ` ) func init() { TokenAuthHS256 = jwtauth.New(jwa.HS256().String(), TokenSecret, nil, jwt.WithAcceptableSkew(30*time.Second)) } // // Tests // func TestSimple(t *testing.T) { r := chi.NewRouter() r.Use( jwtauth.Verifier(TokenAuthHS256), jwtauth.Authenticator(TokenAuthHS256), ) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("welcome")) }) ts := httptest.NewServer(r) defer ts.Close() tt := []struct { Name string Authorization string Status int Resp string }{ {Name: "empty token", Authorization: "", Status: 401, Resp: "no token found\n"}, {Name: "wrong token", Authorization: "Bearer asdf", Status: 401, Resp: "token is unauthorized\n"}, {Name: "wrong secret", Authorization: "Bearer " + newJwtToken([]byte("wrong")), Status: 401, Resp: "token is unauthorized\n"}, {Name: "wrong secret/alg", Authorization: "Bearer " + newJwt512Token([]byte("wrong")), Status: 401, Resp: "token is unauthorized\n"}, {Name: "wrong alg", Authorization: "Bearer " + newJwt512Token(TokenSecret, map[string]interface{}{}), Status: 401, Resp: "token is unauthorized\n"}, {Name: "expired within skew", Authorization: "Bearer " + newJwtToken(TokenSecret, map[string]interface{}{"exp": time.Now().Unix() - 29}), Status: 200, Resp: "welcome"}, {Name: "expired outside skew", Authorization: "Bearer " + newJwtToken(TokenSecret, map[string]interface{}{"exp": time.Now().Unix() - 31}), Status: 401, Resp: "token is expired\n"}, {Name: "valid token", Authorization: "Bearer " + newJwtToken(TokenSecret), Status: 200, Resp: "welcome"}, {Name: "valid Bearer", Authorization: "Bearer " + newJwtToken(TokenSecret, map[string]interface{}{"service": "test"}), Status: 200, Resp: "welcome"}, {Name: "valid BEARER", Authorization: "BEARER " + newJwtToken(TokenSecret), Status: 200, Resp: "welcome"}, {Name: "valid bearer", Authorization: "bearer " + newJwtToken(TokenSecret), Status: 200, Resp: "welcome"}, {Name: "valid claim", Authorization: "Bearer " + newJwtToken(TokenSecret, map[string]interface{}{"service": "test"}), Status: 200, Resp: "welcome"}, {Name: "invalid bearer_", Authorization: "BEARER_" + newJwtToken(TokenSecret), Status: 401, Resp: "no token found\n"}, {Name: "invalid bearerx", Authorization: "BEARERx" + newJwtToken(TokenSecret), Status: 401, Resp: "no token found\n"}, } for _, tc := range tt { h := http.Header{} if tc.Authorization != "" { h.Set("Authorization", tc.Authorization) } status, resp := testRequest(t, ts, "GET", "/", h, nil) if status != tc.Status || resp != tc.Resp { t.Errorf("test '%s' failed: expected Status: %d %q, got %d %q", tc.Name, tc.Status, tc.Resp, status, resp) } } } func TestSimpleRSA(t *testing.T) { privateKeyBlock, _ := pem.Decode([]byte(PrivateKeyRS256String)) privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes) if err != nil { t.Fatal(err.Error()) } publicKeyBlock, _ := pem.Decode([]byte(PublicKeyRS256String)) publicKey, err := x509.ParsePKIXPublicKey(publicKeyBlock.Bytes) if err != nil { t.Fatal(err.Error()) } TokenAuthRS256 = jwtauth.New(jwa.RS256().String(), privateKey, publicKey) claims := map[string]interface{}{ "key": "val", "key2": "val2", "key3": "val3", } _, tokenString, err := TokenAuthRS256.Encode(claims) if err != nil { t.Fatalf("Failed to encode claims %s\n", err.Error()) } token, err := TokenAuthRS256.Decode(tokenString) if err != nil { t.Fatalf("Failed to decode token string %s\n", err.Error()) } tokenClaims := map[string]interface{}{} if err := transform.AsMap(token, tokenClaims); err != nil { t.Fatalf("Failed to get claims %s\n", err.Error()) } if !reflect.DeepEqual(claims, tokenClaims) { t.Fatalf("The decoded claims don't match the original ones\n") } } func TestSimpleRSAVerifyOnly(t *testing.T) { tokenString := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWwiLCJrZXkyIjoidmFsMiIsImtleTMiOiJ2YWwzIn0.IK0G0Qi_c6N6uHRokHMSHQEeYxoi_T73A4RdEzJIfnbs5kA5hF0UhApSWUMZfsTYFNC2buYvWqbyj2kDdXcStpqTUPENGTKvJi66puwhN16BqEOS-jb7kVyf3vWif7XabY0_5S8H_aeqazaj4FemHvWnywJznuMWJRXWw83edpA" claims := map[string]interface{}{ "key": "val", "key2": "val2", "key3": "val3", } publicKeyBlock, _ := pem.Decode([]byte(PublicKeyRS256String)) publicKey, err := x509.ParsePKIXPublicKey(publicKeyBlock.Bytes) if err != nil { t.Fatal(err.Error()) } TokenAuthRS256 = jwtauth.New(jwa.RS256().String(), nil, publicKey) _, _, err = TokenAuthRS256.Encode(claims) if err == nil { t.Fatalf("Expecting error when encoding claims without signing key") } token, err := TokenAuthRS256.Decode(tokenString) if err != nil { t.Fatalf("Failed to decode token string %s\n", err.Error()) } tokenClaims := map[string]interface{}{} if err := transform.AsMap(token, tokenClaims); err != nil { t.Fatalf("Failed to get claims %s\n", err.Error()) } if !reflect.DeepEqual(claims, tokenClaims) { t.Fatalf("The decoded claims don't match the original ones\n") } } func TestMore(t *testing.T) { r := chi.NewRouter() // Protected routes r.Group(func(r chi.Router) { r.Use(jwtauth.Verifier(TokenAuthHS256)) authenticator := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token, _, err := jwtauth.FromContext(r.Context()) if err != nil { http.Error(w, jwtauth.ErrorReason(err).Error(), http.StatusUnauthorized) return } if err := jwt.Validate(token); err != nil { http.Error(w, jwtauth.ErrorReason(err).Error(), http.StatusUnauthorized) return } // Token is authenticated, pass it through next.ServeHTTP(w, r) }) } r.Use(authenticator) r.Get("/admin", func(w http.ResponseWriter, r *http.Request) { _, claims, err := jwtauth.FromContext(r.Context()) if err != nil { w.Write([]byte(fmt.Sprintf("error! %v", err))) return } w.Write([]byte(fmt.Sprintf("protected, user:%v", claims["user_id"]))) }) }) // Public routes r.Group(func(r chi.Router) { r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("welcome")) }) }) ts := httptest.NewServer(r) defer ts.Close() // sending unauthorized requests if status, resp := testRequest(t, ts, "GET", "/admin", nil, nil); status != 401 || resp != "token is unauthorized\n" { t.Fatal(resp) } h := http.Header{} h.Set("Authorization", "BEARER "+newJwtToken([]byte("wrong"), map[string]interface{}{})) if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 401 || resp != "token is unauthorized\n" { t.Fatal(resp) } h.Set("Authorization", "BEARER asdf") if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 401 || resp != "token is unauthorized\n" { t.Fatal(resp) } // wrong token secret and wrong alg h.Set("Authorization", "BEARER "+newJwt512Token([]byte("wrong"), map[string]interface{}{})) if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 401 || resp != "token is unauthorized\n" { t.Fatal(resp) } // correct token secret but wrong alg h.Set("Authorization", "BEARER "+newJwt512Token(TokenSecret, map[string]interface{}{})) if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 401 || resp != "token is unauthorized\n" { t.Fatal(resp) } h = newAuthHeader(map[string]interface{}{"exp": jwtauth.EpochNow() - 1000}) if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 401 || resp != "token is expired\n" { t.Fatal(resp) } // sending authorized requests if status, resp := testRequest(t, ts, "GET", "/", nil, nil); status != 200 || resp != "welcome" { t.Fatal(resp) } h = newAuthHeader((map[string]interface{}{"user_id": 31337, "exp": jwtauth.ExpireIn(5 * time.Minute)})) if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 200 || resp != "protected, user:31337" { t.Fatal(resp) } } func TestEncodeClaims(t *testing.T) { claims := map[string]interface{}{ "key1": "val1", "key2": 2, "key3": time.Now(), "key4": []string{"1", "2"}, } claims[jwt.JwtIDKey] = 1 if _, _, err := TokenAuthHS256.Encode(claims); err == nil { t.Fatal("encoding invalid claims succeeded") } claims[jwt.JwtIDKey] = "123" if _, _, err := TokenAuthHS256.Encode(claims); err != nil { t.Fatalf("unexpected error encoding valid claims: %v", err) } } // // Test helper functions // func testRequest(t *testing.T, ts *httptest.Server, method, path string, header http.Header, body io.Reader) (int, string) { req, err := http.NewRequest(method, ts.URL+path, body) if err != nil { t.Fatal(err) return 0, "" } for k, v := range header { req.Header.Set(k, v[0]) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) return 0, "" } respBody, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) return 0, "" } defer resp.Body.Close() return resp.StatusCode, string(respBody) } func newJwtToken(secret []byte, claims ...map[string]interface{}) string { token := jwt.New() if len(claims) > 0 { for k, v := range claims[0] { token.Set(k, v) } } tokenPayload, err := jwt.Sign(token, jwt.WithKey(jwa.HS256(), secret)) if err != nil { log.Fatal(err) } return string(tokenPayload) } func newJwt512Token(secret []byte, claims ...map[string]interface{}) string { // use-case: when token is signed with a different alg than expected token := jwt.New() if len(claims) > 0 { for k, v := range claims[0] { token.Set(k, v) } } tokenPayload, err := jwt.Sign(token, jwt.WithKey(jwa.HS512(), secret)) if err != nil { log.Fatal(err) } return string(tokenPayload) } func newAuthHeader(claims ...map[string]interface{}) http.Header { h := http.Header{} h.Set("Authorization", "BEARER "+newJwtToken(TokenSecret, claims...)) return h }