diff --git a/go.mod b/go.mod index 980f822b3..b05271ff7 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/labstack/echo/v4 go 1.18 require ( - github.com/golang-jwt/jwt v3.2.2+incompatible github.com/labstack/gommon v0.4.2 github.com/stretchr/testify v1.8.4 github.com/valyala/fasttemplate v1.2.2 diff --git a/go.sum b/go.sum index 89a316cb7..0a839d807 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ 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/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= diff --git a/middleware/jwt.go b/middleware/jwt.go deleted file mode 100644 index a6bf16f95..000000000 --- a/middleware/jwt.go +++ /dev/null @@ -1,303 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors - -//go:build go1.15 -// +build go1.15 - -package middleware - -import ( - "errors" - "fmt" - "github.com/golang-jwt/jwt" - "github.com/labstack/echo/v4" - "net/http" - "reflect" -) - -// JWTConfig defines the config for JWT middleware. -type JWTConfig struct { - // Skipper defines a function to skip middleware. - Skipper Skipper - - // BeforeFunc defines a function which is executed just before the middleware. - BeforeFunc BeforeFunc - - // SuccessHandler defines a function which is executed for a valid token before middleware chain continues with next - // middleware or handler. - SuccessHandler JWTSuccessHandler - - // ErrorHandler defines a function which is executed for an invalid token. - // It may be used to define a custom JWT error. - ErrorHandler JWTErrorHandler - - // ErrorHandlerWithContext is almost identical to ErrorHandler, but it's passed the current context. - ErrorHandlerWithContext JWTErrorHandlerWithContext - - // ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandlerWithContext decides to - // ignore the error (by returning `nil`). - // This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality. - // In that case you can use ErrorHandlerWithContext to set a default public JWT token value in the request context - // and continue. Some logic down the remaining execution chain needs to check that (public) token value then. - ContinueOnIgnoredError bool - - // Signing key to validate token. - // This is one of the three options to provide a token validation key. - // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. - // Required if neither user-defined KeyFunc nor SigningKeys is provided. - SigningKey interface{} - - // Map of signing keys to validate token with kid field usage. - // This is one of the three options to provide a token validation key. - // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. - // Required if neither user-defined KeyFunc nor SigningKey is provided. - SigningKeys map[string]interface{} - - // Signing method used to check the token's signing algorithm. - // Optional. Default value HS256. - SigningMethod string - - // Context key to store user information from the token into context. - // Optional. Default value "user". - ContextKey string - - // Claims are extendable claims data defining token content. Used by default ParseTokenFunc implementation. - // Not used if custom ParseTokenFunc is set. - // Optional. Default value jwt.MapClaims - Claims jwt.Claims - - // TokenLookup is a string in the form of ":" or ":,:" that is used - // to extract token from the request. - // Optional. Default value "header:Authorization". - // Possible values: - // - "header:" or "header::" - // `` is argument value to cut/trim prefix of the extracted value. This is useful if header - // value has static prefix like `Authorization: ` where part that we - // want to cut is ` ` note the space at the end. - // In case of JWT tokens `Authorization: Bearer ` prefix we cut is `Bearer `. - // If prefix is left empty the whole value is returned. - // - "query:" - // - "param:" - // - "cookie:" - // - "form:" - // Multiple sources example: - // - "header:Authorization,cookie:myowncookie" - TokenLookup string - - // TokenLookupFuncs defines a list of user-defined functions that extract JWT token from the given context. - // This is one of the two options to provide a token extractor. - // The order of precedence is user-defined TokenLookupFuncs, and TokenLookup. - // You can also provide both if you want. - TokenLookupFuncs []ValuesExtractor - - // AuthScheme to be used in the Authorization header. - // Optional. Default value "Bearer". - AuthScheme string - - // KeyFunc defines a user-defined function that supplies the public key for a token validation. - // The function shall take care of verifying the signing algorithm and selecting the proper key. - // A user-defined KeyFunc can be useful if tokens are issued by an external party. - // Used by default ParseTokenFunc implementation. - // - // When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored. - // This is one of the three options to provide a token validation key. - // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. - // Required if neither SigningKeys nor SigningKey is provided. - // Not used if custom ParseTokenFunc is set. - // Default to an internal implementation verifying the signing algorithm and selecting the proper key. - KeyFunc jwt.Keyfunc - - // ParseTokenFunc defines a user-defined function that parses token from given auth. Returns an error when token - // parsing fails or parsed token is invalid. - // Defaults to implementation using `github.com/golang-jwt/jwt` as JWT implementation library - ParseTokenFunc func(auth string, c echo.Context) (interface{}, error) -} - -// JWTSuccessHandler defines a function which is executed for a valid token. -type JWTSuccessHandler func(c echo.Context) - -// JWTErrorHandler defines a function which is executed for an invalid token. -type JWTErrorHandler func(err error) error - -// JWTErrorHandlerWithContext is almost identical to JWTErrorHandler, but it's passed the current context. -type JWTErrorHandlerWithContext func(err error, c echo.Context) error - -// Algorithms -const ( - AlgorithmHS256 = "HS256" -) - -// ErrJWTMissing is error that is returned when no JWToken was extracted from the request. -var ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "missing or malformed jwt") - -// ErrJWTInvalid is error that is returned when middleware could not parse JWT correctly. -var ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired jwt") - -// DefaultJWTConfig is the default JWT auth middleware config. -var DefaultJWTConfig = JWTConfig{ - Skipper: DefaultSkipper, - SigningMethod: AlgorithmHS256, - ContextKey: "user", - TokenLookup: "header:" + echo.HeaderAuthorization, - TokenLookupFuncs: nil, - AuthScheme: "Bearer", - Claims: jwt.MapClaims{}, - KeyFunc: nil, -} - -// JWT returns a JSON Web Token (JWT) auth middleware. -// -// For valid token, it sets the user in context and calls next handler. -// For invalid token, it returns "401 - Unauthorized" error. -// For missing token, it returns "400 - Bad Request" error. -// -// See: https://jwt.io/introduction -// See `JWTConfig.TokenLookup` -// -// Deprecated: Please use https://github.com/labstack/echo-jwt instead -func JWT(key interface{}) echo.MiddlewareFunc { - c := DefaultJWTConfig - c.SigningKey = key - return JWTWithConfig(c) -} - -// JWTWithConfig returns a JWT auth middleware with config. -// See: `JWT()`. -// -// Deprecated: Please use https://github.com/labstack/echo-jwt instead -func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc { - // Defaults - if config.Skipper == nil { - config.Skipper = DefaultJWTConfig.Skipper - } - if config.SigningKey == nil && len(config.SigningKeys) == 0 && config.KeyFunc == nil && config.ParseTokenFunc == nil { - panic("echo: jwt middleware requires signing key") - } - if config.SigningMethod == "" { - config.SigningMethod = DefaultJWTConfig.SigningMethod - } - if config.ContextKey == "" { - config.ContextKey = DefaultJWTConfig.ContextKey - } - if config.Claims == nil { - config.Claims = DefaultJWTConfig.Claims - } - if config.TokenLookup == "" && len(config.TokenLookupFuncs) == 0 { - config.TokenLookup = DefaultJWTConfig.TokenLookup - } - if config.AuthScheme == "" { - config.AuthScheme = DefaultJWTConfig.AuthScheme - } - if config.KeyFunc == nil { - config.KeyFunc = config.defaultKeyFunc - } - if config.ParseTokenFunc == nil { - config.ParseTokenFunc = config.defaultParseToken - } - - extractors, cErr := createExtractors(config.TokenLookup, config.AuthScheme) - if cErr != nil { - panic(cErr) - } - if len(config.TokenLookupFuncs) > 0 { - extractors = append(config.TokenLookupFuncs, extractors...) - } - - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - if config.Skipper(c) { - return next(c) - } - - if config.BeforeFunc != nil { - config.BeforeFunc(c) - } - - var lastExtractorErr error - var lastTokenErr error - for _, extractor := range extractors { - auths, err := extractor(c) - if err != nil { - lastExtractorErr = ErrJWTMissing // backwards compatibility: all extraction errors are same (unlike KeyAuth) - continue - } - for _, auth := range auths { - token, err := config.ParseTokenFunc(auth, c) - if err != nil { - lastTokenErr = err - continue - } - // Store user information from token into context. - c.Set(config.ContextKey, token) - if config.SuccessHandler != nil { - config.SuccessHandler(c) - } - return next(c) - } - } - // we are here only when we did not successfully extract or parse any of the tokens - err := lastTokenErr - if err == nil { // prioritize token errors over extracting errors - err = lastExtractorErr - } - if config.ErrorHandler != nil { - return config.ErrorHandler(err) - } - if config.ErrorHandlerWithContext != nil { - tmpErr := config.ErrorHandlerWithContext(err, c) - if config.ContinueOnIgnoredError && tmpErr == nil { - return next(c) - } - return tmpErr - } - - // backwards compatible errors codes - if lastTokenErr != nil { - return &echo.HTTPError{ - Code: ErrJWTInvalid.Code, - Message: ErrJWTInvalid.Message, - Internal: err, - } - } - return err // this is lastExtractorErr value - } - } -} - -func (config *JWTConfig) defaultParseToken(auth string, c echo.Context) (interface{}, error) { - var token *jwt.Token - var err error - // Issue #647, #656 - if _, ok := config.Claims.(jwt.MapClaims); ok { - token, err = jwt.Parse(auth, config.KeyFunc) - } else { - t := reflect.ValueOf(config.Claims).Type().Elem() - claims := reflect.New(t).Interface().(jwt.Claims) - token, err = jwt.ParseWithClaims(auth, claims, config.KeyFunc) - } - if err != nil { - return nil, err - } - if !token.Valid { - return nil, errors.New("invalid token") - } - return token, nil -} - -// defaultKeyFunc returns a signing key of the given token. -func (config *JWTConfig) defaultKeyFunc(t *jwt.Token) (interface{}, error) { - // Check the signing method - if t.Method.Alg() != config.SigningMethod { - return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"]) - } - if len(config.SigningKeys) > 0 { - if kid, ok := t.Header["kid"].(string); ok { - if key, ok := config.SigningKeys[kid]; ok { - return key, nil - } - } - return nil, fmt.Errorf("unexpected jwt key id=%v", t.Header["kid"]) - } - - return config.SigningKey, nil -} diff --git a/middleware/jwt_test.go b/middleware/jwt_test.go deleted file mode 100644 index bbe4b8808..000000000 --- a/middleware/jwt_test.go +++ /dev/null @@ -1,780 +0,0 @@ -// SPDX-License-Identifier: MIT -// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors - -//go:build go1.15 -// +build go1.15 - -package middleware - -import ( - "errors" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - - "github.com/golang-jwt/jwt" - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" -) - -// jwtCustomInfo defines some custom types we're going to use within our tokens. -type jwtCustomInfo struct { - Name string `json:"name"` - Admin bool `json:"admin"` -} - -// jwtCustomClaims are custom claims expanding default ones. -type jwtCustomClaims struct { - *jwt.StandardClaims - jwtCustomInfo -} - -func TestJWT(t *testing.T) { - e := echo.New() - - e.GET("/", func(c echo.Context) error { - token := c.Get("user").(*jwt.Token) - return c.JSON(http.StatusOK, token.Claims) - }) - - e.Use(JWT([]byte("secret"))) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set(echo.HeaderAuthorization, "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") - res := httptest.NewRecorder() - - e.ServeHTTP(res, req) - - assert.Equal(t, http.StatusOK, res.Code) - assert.Equal(t, `{"admin":true,"name":"John Doe","sub":"1234567890"}`+"\n", res.Body.String()) -} - -func TestJWTRace(t *testing.T) { - e := echo.New() - handler := func(c echo.Context) error { - return c.String(http.StatusOK, "test") - } - initialToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" - raceToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlJhY2UgQ29uZGl0aW9uIiwiYWRtaW4iOmZhbHNlfQ.Xzkx9mcgGqYMTkuxSCbJ67lsDyk5J2aB7hu65cEE-Ss" - validKey := []byte("secret") - - h := JWTWithConfig(JWTConfig{ - Claims: &jwtCustomClaims{}, - SigningKey: validKey, - })(handler) - - makeReq := func(token string) echo.Context { - req := httptest.NewRequest(http.MethodGet, "/", nil) - res := httptest.NewRecorder() - req.Header.Set(echo.HeaderAuthorization, DefaultJWTConfig.AuthScheme+" "+token) - c := e.NewContext(req, res) - assert.NoError(t, h(c)) - return c - } - - c := makeReq(initialToken) - user := c.Get("user").(*jwt.Token) - claims := user.Claims.(*jwtCustomClaims) - assert.Equal(t, claims.Name, "John Doe") - - makeReq(raceToken) - user = c.Get("user").(*jwt.Token) - claims = user.Claims.(*jwtCustomClaims) - // Initial context should still be "John Doe", not "Race Condition" - assert.Equal(t, claims.Name, "John Doe") - assert.Equal(t, claims.Admin, true) -} - -func TestJWTConfig(t *testing.T) { - handler := func(c echo.Context) error { - return c.String(http.StatusOK, "test") - } - token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" - validKey := []byte("secret") - invalidKey := []byte("invalid-key") - validAuth := DefaultJWTConfig.AuthScheme + " " + token - - testCases := []struct { - name string - expPanic bool - expErrCode int // 0 for Success - config JWTConfig - reqURL string // "/" if empty - hdrAuth string - hdrCookie string // test.Request doesn't provide SetCookie(); use name=val - formValues map[string]string - }{ - { - name: "No signing key provided", - expPanic: true, - }, - { - name: "Unexpected signing method", - expErrCode: http.StatusBadRequest, - config: JWTConfig{ - SigningKey: validKey, - SigningMethod: "RS256", - }, - }, - { - name: "Invalid key", - expErrCode: http.StatusUnauthorized, - hdrAuth: validAuth, - config: JWTConfig{SigningKey: invalidKey}, - }, - { - name: "Valid JWT", - hdrAuth: validAuth, - config: JWTConfig{SigningKey: validKey}, - }, - { - name: "Valid JWT with custom AuthScheme", - hdrAuth: "Token" + " " + token, - config: JWTConfig{AuthScheme: "Token", SigningKey: validKey}, - }, - { - name: "Valid JWT with custom claims", - hdrAuth: validAuth, - config: JWTConfig{ - Claims: &jwtCustomClaims{}, - SigningKey: []byte("secret"), - }, - }, - { - name: "Invalid Authorization header", - hdrAuth: "invalid-auth", - expErrCode: http.StatusBadRequest, - config: JWTConfig{SigningKey: validKey}, - }, - { - name: "Empty header auth field", - config: JWTConfig{SigningKey: validKey}, - expErrCode: http.StatusBadRequest, - }, - { - name: "Valid query method", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "query:jwt", - }, - reqURL: "/?a=b&jwt=" + token, - }, - { - name: "Invalid query param name", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "query:jwt", - }, - reqURL: "/?a=b&jwtxyz=" + token, - expErrCode: http.StatusBadRequest, - }, - { - name: "Invalid query param value", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "query:jwt", - }, - reqURL: "/?a=b&jwt=invalid-token", - expErrCode: http.StatusUnauthorized, - }, - { - name: "Empty query", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "query:jwt", - }, - reqURL: "/?a=b", - expErrCode: http.StatusBadRequest, - }, - { - name: "Valid param method", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "param:jwt", - }, - reqURL: "/" + token, - }, - { - name: "Valid cookie method", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "cookie:jwt", - }, - hdrCookie: "jwt=" + token, - }, - { - name: "Multiple jwt lookuop", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "query:jwt,cookie:jwt", - }, - hdrCookie: "jwt=" + token, - }, - { - name: "Invalid token with cookie method", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "cookie:jwt", - }, - expErrCode: http.StatusUnauthorized, - hdrCookie: "jwt=invalid", - }, - { - name: "Empty cookie", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "cookie:jwt", - }, - expErrCode: http.StatusBadRequest, - }, - { - name: "Valid form method", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "form:jwt", - }, - formValues: map[string]string{"jwt": token}, - }, - { - name: "Invalid token with form method", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "form:jwt", - }, - expErrCode: http.StatusUnauthorized, - formValues: map[string]string{"jwt": "invalid"}, - }, - { - name: "Empty form field", - config: JWTConfig{ - SigningKey: validKey, - TokenLookup: "form:jwt", - }, - expErrCode: http.StatusBadRequest, - }, - { - name: "Valid JWT with a valid key using a user-defined KeyFunc", - hdrAuth: validAuth, - config: JWTConfig{ - KeyFunc: func(*jwt.Token) (interface{}, error) { - return validKey, nil - }, - }, - }, - { - name: "Valid JWT with an invalid key using a user-defined KeyFunc", - hdrAuth: validAuth, - config: JWTConfig{ - KeyFunc: func(*jwt.Token) (interface{}, error) { - return invalidKey, nil - }, - }, - expErrCode: http.StatusUnauthorized, - }, - { - name: "Token verification does not pass using a user-defined KeyFunc", - hdrAuth: validAuth, - config: JWTConfig{ - KeyFunc: func(*jwt.Token) (interface{}, error) { - return nil, errors.New("faulty KeyFunc") - }, - }, - expErrCode: http.StatusUnauthorized, - }, - { - name: "Valid JWT with lower case AuthScheme", - hdrAuth: strings.ToLower(DefaultJWTConfig.AuthScheme) + " " + token, - config: JWTConfig{SigningKey: validKey}, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - e := echo.New() - if tc.reqURL == "" { - tc.reqURL = "/" - } - - var req *http.Request - if len(tc.formValues) > 0 { - form := url.Values{} - for k, v := range tc.formValues { - form.Set(k, v) - } - req = httptest.NewRequest(http.MethodPost, tc.reqURL, strings.NewReader(form.Encode())) - req.Header.Set(echo.HeaderContentType, "application/x-www-form-urlencoded") - req.ParseForm() - } else { - req = httptest.NewRequest(http.MethodGet, tc.reqURL, nil) - } - res := httptest.NewRecorder() - req.Header.Set(echo.HeaderAuthorization, tc.hdrAuth) - req.Header.Set(echo.HeaderCookie, tc.hdrCookie) - c := e.NewContext(req, res) - - if tc.reqURL == "/"+token { - c.SetParamNames("jwt") - c.SetParamValues(token) - } - - if tc.expPanic { - assert.Panics(t, func() { - JWTWithConfig(tc.config) - }, tc.name) - return - } - - if tc.expErrCode != 0 { - h := JWTWithConfig(tc.config)(handler) - he := h(c).(*echo.HTTPError) - assert.Equal(t, tc.expErrCode, he.Code, tc.name) - return - } - - h := JWTWithConfig(tc.config)(handler) - if assert.NoError(t, h(c), tc.name) { - user := c.Get("user").(*jwt.Token) - switch claims := user.Claims.(type) { - case jwt.MapClaims: - assert.Equal(t, claims["name"], "John Doe", tc.name) - case *jwtCustomClaims: - assert.Equal(t, claims.Name, "John Doe", tc.name) - assert.Equal(t, claims.Admin, true, tc.name) - default: - panic("unexpected type of claims") - } - } - }) - } -} - -func TestJWTwithKID(t *testing.T) { - e := echo.New() - handler := func(c echo.Context) error { - return c.String(http.StatusOK, "test") - } - firstToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6ImZpcnN0T25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.w5VGpHOe0jlNgf7jMVLHzIYH_XULmpUlreJnilwSkWk" - secondToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6InNlY29uZE9uZSJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.sdghDYQ85jdh0hgQ6bKbMguLI_NSPYWjkhVJkee-yZM" - wrongToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6InNlY29uZE9uZSJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.RyhLybtVLpoewF6nz9YN79oXo32kAtgUxp8FNwTkb90" - staticToken := "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.1_-XFYUPpJfgsaGwYhgZEt7hfySMg-a3GN-nfZmbW7o" - validKeys := map[string]interface{}{"firstOne": []byte("first_secret"), "secondOne": []byte("second_secret")} - invalidKeys := map[string]interface{}{"thirdOne": []byte("third_secret")} - staticSecret := []byte("static_secret") - invalidStaticSecret := []byte("invalid_secret") - - for _, tc := range []struct { - expErrCode int // 0 for Success - config JWTConfig - hdrAuth string - info string - }{ - { - hdrAuth: DefaultJWTConfig.AuthScheme + " " + firstToken, - config: JWTConfig{SigningKeys: validKeys}, - info: "First token valid", - }, - { - hdrAuth: DefaultJWTConfig.AuthScheme + " " + secondToken, - config: JWTConfig{SigningKeys: validKeys}, - info: "Second token valid", - }, - { - expErrCode: http.StatusUnauthorized, - hdrAuth: DefaultJWTConfig.AuthScheme + " " + wrongToken, - config: JWTConfig{SigningKeys: validKeys}, - info: "Wrong key id token", - }, - { - hdrAuth: DefaultJWTConfig.AuthScheme + " " + staticToken, - config: JWTConfig{SigningKey: staticSecret}, - info: "Valid static secret token", - }, - { - expErrCode: http.StatusUnauthorized, - hdrAuth: DefaultJWTConfig.AuthScheme + " " + staticToken, - config: JWTConfig{SigningKey: invalidStaticSecret}, - info: "Invalid static secret", - }, - { - expErrCode: http.StatusUnauthorized, - hdrAuth: DefaultJWTConfig.AuthScheme + " " + firstToken, - config: JWTConfig{SigningKeys: invalidKeys}, - info: "Invalid keys first token", - }, - { - expErrCode: http.StatusUnauthorized, - hdrAuth: DefaultJWTConfig.AuthScheme + " " + secondToken, - config: JWTConfig{SigningKeys: invalidKeys}, - info: "Invalid keys second token", - }, - } { - req := httptest.NewRequest(http.MethodGet, "/", nil) - res := httptest.NewRecorder() - req.Header.Set(echo.HeaderAuthorization, tc.hdrAuth) - c := e.NewContext(req, res) - - if tc.expErrCode != 0 { - h := JWTWithConfig(tc.config)(handler) - he := h(c).(*echo.HTTPError) - assert.Equal(t, tc.expErrCode, he.Code, tc.info) - continue - } - - h := JWTWithConfig(tc.config)(handler) - if assert.NoError(t, h(c), tc.info) { - user := c.Get("user").(*jwt.Token) - switch claims := user.Claims.(type) { - case jwt.MapClaims: - assert.Equal(t, claims["name"], "John Doe", tc.info) - case *jwtCustomClaims: - assert.Equal(t, claims.Name, "John Doe", tc.info) - assert.Equal(t, claims.Admin, true, tc.info) - default: - panic("unexpected type of claims") - } - } - } -} - -func TestJWTConfig_skipper(t *testing.T) { - e := echo.New() - - e.Use(JWTWithConfig(JWTConfig{ - Skipper: func(context echo.Context) bool { - return true // skip everything - }, - SigningKey: []byte("secret"), - })) - - isCalled := false - e.GET("/", func(c echo.Context) error { - isCalled = true - return c.String(http.StatusTeapot, "test") - }) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - res := httptest.NewRecorder() - e.ServeHTTP(res, req) - - assert.Equal(t, http.StatusTeapot, res.Code) - assert.True(t, isCalled) -} - -func TestJWTConfig_BeforeFunc(t *testing.T) { - e := echo.New() - e.GET("/", func(c echo.Context) error { - return c.String(http.StatusTeapot, "test") - }) - - isCalled := false - e.Use(JWTWithConfig(JWTConfig{ - BeforeFunc: func(context echo.Context) { - isCalled = true - }, - SigningKey: []byte("secret"), - })) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set(echo.HeaderAuthorization, DefaultJWTConfig.AuthScheme+" eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") - res := httptest.NewRecorder() - e.ServeHTTP(res, req) - - assert.Equal(t, http.StatusTeapot, res.Code) - assert.True(t, isCalled) -} - -func TestJWTConfig_extractorErrorHandling(t *testing.T) { - var testCases = []struct { - name string - given JWTConfig - expectStatusCode int - }{ - { - name: "ok, ErrorHandler is executed", - given: JWTConfig{ - SigningKey: []byte("secret"), - ErrorHandler: func(err error) error { - return echo.NewHTTPError(http.StatusTeapot, "custom_error") - }, - }, - expectStatusCode: http.StatusTeapot, - }, - { - name: "ok, ErrorHandlerWithContext is executed", - given: JWTConfig{ - SigningKey: []byte("secret"), - ErrorHandlerWithContext: func(err error, context echo.Context) error { - return echo.NewHTTPError(http.StatusTeapot, "custom_error") - }, - }, - expectStatusCode: http.StatusTeapot, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - e := echo.New() - e.GET("/", func(c echo.Context) error { - return c.String(http.StatusNotImplemented, "should not end up here") - }) - - e.Use(JWTWithConfig(tc.given)) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - res := httptest.NewRecorder() - e.ServeHTTP(res, req) - - assert.Equal(t, tc.expectStatusCode, res.Code) - }) - } -} - -func TestJWTConfig_parseTokenErrorHandling(t *testing.T) { - var testCases = []struct { - name string - given JWTConfig - expectErr string - }{ - { - name: "ok, ErrorHandler is executed", - given: JWTConfig{ - SigningKey: []byte("secret"), - ErrorHandler: func(err error) error { - return echo.NewHTTPError(http.StatusTeapot, "ErrorHandler: "+err.Error()) - }, - }, - expectErr: "{\"message\":\"ErrorHandler: parsing failed\"}\n", - }, - { - name: "ok, ErrorHandlerWithContext is executed", - given: JWTConfig{ - SigningKey: []byte("secret"), - ErrorHandlerWithContext: func(err error, context echo.Context) error { - return echo.NewHTTPError(http.StatusTeapot, "ErrorHandlerWithContext: "+err.Error()) - }, - }, - expectErr: "{\"message\":\"ErrorHandlerWithContext: parsing failed\"}\n", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - e := echo.New() - //e.Debug = true - e.GET("/", func(c echo.Context) error { - return c.String(http.StatusNotImplemented, "should not end up here") - }) - - config := tc.given - parseTokenCalled := false - config.ParseTokenFunc = func(auth string, c echo.Context) (interface{}, error) { - parseTokenCalled = true - return nil, errors.New("parsing failed") - } - e.Use(JWTWithConfig(config)) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set(echo.HeaderAuthorization, DefaultJWTConfig.AuthScheme+" eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") - res := httptest.NewRecorder() - - e.ServeHTTP(res, req) - - assert.Equal(t, http.StatusTeapot, res.Code) - assert.Equal(t, tc.expectErr, res.Body.String()) - assert.True(t, parseTokenCalled) - }) - } -} - -func TestJWTConfig_custom_ParseTokenFunc_Keyfunc(t *testing.T) { - e := echo.New() - e.GET("/", func(c echo.Context) error { - return c.String(http.StatusTeapot, "test") - }) - - // example of minimal custom ParseTokenFunc implementation. Allows you to use different versions of `github.com/golang-jwt/jwt` - // with current JWT middleware - signingKey := []byte("secret") - - config := JWTConfig{ - ParseTokenFunc: func(auth string, c echo.Context) (interface{}, error) { - keyFunc := func(t *jwt.Token) (interface{}, error) { - if t.Method.Alg() != "HS256" { - return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"]) - } - return signingKey, nil - } - - // claims are of type `jwt.MapClaims` when token is created with `jwt.Parse` - token, err := jwt.Parse(auth, keyFunc) - if err != nil { - return nil, err - } - if !token.Valid { - return nil, errors.New("invalid token") - } - return token, nil - }, - } - - e.Use(JWTWithConfig(config)) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set(echo.HeaderAuthorization, DefaultJWTConfig.AuthScheme+" eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") - res := httptest.NewRecorder() - e.ServeHTTP(res, req) - - assert.Equal(t, http.StatusTeapot, res.Code) -} - -func TestJWTConfig_TokenLookupFuncs(t *testing.T) { - e := echo.New() - - e.GET("/", func(c echo.Context) error { - token := c.Get("user").(*jwt.Token) - return c.JSON(http.StatusOK, token.Claims) - }) - - e.Use(JWTWithConfig(JWTConfig{ - TokenLookupFuncs: []ValuesExtractor{ - func(c echo.Context) ([]string, error) { - return []string{c.Request().Header.Get("X-API-Key")}, nil - }, - }, - SigningKey: []byte("secret"), - })) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("X-API-Key", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ") - res := httptest.NewRecorder() - e.ServeHTTP(res, req) - - assert.Equal(t, http.StatusOK, res.Code) - assert.Equal(t, `{"admin":true,"name":"John Doe","sub":"1234567890"}`+"\n", res.Body.String()) -} - -func TestJWTConfig_SuccessHandler(t *testing.T) { - var testCases = []struct { - name string - givenToken string - expectCalled bool - expectStatus int - }{ - { - name: "ok, success handler is called", - givenToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ", - expectCalled: true, - expectStatus: http.StatusOK, - }, - { - name: "nok, success handler is not called", - givenToken: "x.x.x", - expectCalled: false, - expectStatus: http.StatusUnauthorized, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - e := echo.New() - - e.GET("/", func(c echo.Context) error { - token := c.Get("user").(*jwt.Token) - return c.JSON(http.StatusOK, token.Claims) - }) - - wasCalled := false - e.Use(JWTWithConfig(JWTConfig{ - SuccessHandler: func(c echo.Context) { - wasCalled = true - }, - SigningKey: []byte("secret"), - })) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set(echo.HeaderAuthorization, "bearer "+tc.givenToken) - res := httptest.NewRecorder() - - e.ServeHTTP(res, req) - - assert.Equal(t, tc.expectCalled, wasCalled) - assert.Equal(t, tc.expectStatus, res.Code) - }) - } -} - -func TestJWTConfig_ContinueOnIgnoredError(t *testing.T) { - var testCases = []struct { - name string - whenContinueOnIgnoredError bool - givenToken string - expectStatus int - expectBody string - }{ - { - name: "no error handler is called", - whenContinueOnIgnoredError: true, - givenToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ", - expectStatus: http.StatusTeapot, - expectBody: "", - }, - { - name: "ContinueOnIgnoredError is false and error handler is called for missing token", - whenContinueOnIgnoredError: false, - givenToken: "", - // empty response with 200. This emulates previous behaviour when error handler swallowed the error - expectStatus: http.StatusOK, - expectBody: "", - }, - { - name: "error handler is called for missing token", - whenContinueOnIgnoredError: true, - givenToken: "", - expectStatus: http.StatusTeapot, - expectBody: "public-token", - }, - { - name: "error handler is called for invalid token", - whenContinueOnIgnoredError: true, - givenToken: "x.x.x", - expectStatus: http.StatusUnauthorized, - expectBody: "{\"message\":\"Unauthorized\"}\n", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - e := echo.New() - - e.GET("/", func(c echo.Context) error { - testValue, _ := c.Get("test").(string) - return c.String(http.StatusTeapot, testValue) - }) - - e.Use(JWTWithConfig(JWTConfig{ - ContinueOnIgnoredError: tc.whenContinueOnIgnoredError, - SigningKey: []byte("secret"), - ErrorHandlerWithContext: func(err error, c echo.Context) error { - if err == ErrJWTMissing { - c.Set("test", "public-token") - return nil - } - return echo.ErrUnauthorized - }, - })) - - req := httptest.NewRequest(http.MethodGet, "/", nil) - if tc.givenToken != "" { - req.Header.Set(echo.HeaderAuthorization, "bearer "+tc.givenToken) - } - res := httptest.NewRecorder() - - e.ServeHTTP(res, req) - - assert.Equal(t, tc.expectStatus, res.Code) - assert.Equal(t, tc.expectBody, res.Body.String()) - }) - } -}