mirror of
https://github.com/labstack/echo.git
synced 2025-05-31 23:19:42 +02:00
Bring over changes from master (latest commit f36d5662fbb1850f03c9ac78f02a699a492ecc2d)
This commit is contained in:
parent
74b8c4368c
commit
5cb7cefcc5
47
.github/workflows/checks.yml
vendored
Normal file
47
.github/workflows/checks.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
name: Run checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
# run static analysis only with the latest Go version
|
||||
LATEST_GO_VERSION: 1.19
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.LATEST_GO_VERSION }}
|
||||
check-latest: true
|
||||
|
||||
- name: Run golint
|
||||
run: |
|
||||
go install golang.org/x/lint/golint@latest
|
||||
golint -set_exit_status ./...
|
||||
|
||||
- name: Run staticcheck
|
||||
run: |
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
staticcheck ./...
|
||||
|
||||
- name: Run govulncheck
|
||||
run: |
|
||||
go version
|
||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
govulncheck ./...
|
||||
|
47
.github/workflows/echo.yml
vendored
47
.github/workflows/echo.yml
vendored
@ -4,23 +4,18 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '_fixture/**'
|
||||
- '.github/**'
|
||||
- 'codecov.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '**.go'
|
||||
- 'go.*'
|
||||
- '_fixture/**'
|
||||
- '.github/**'
|
||||
- 'codecov.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
# run coverage and benchmarks only with the latest Go version
|
||||
LATEST_GO_VERSION: 1.19
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
@ -30,14 +25,12 @@ jobs:
|
||||
# Echo tests with last four major releases (unless there are pressing vulnerabilities)
|
||||
# As we depend on `golang.org/x/` libraries which only support last 2 Go releases we could have situations when
|
||||
# we derive from last four major releases promise.
|
||||
go: [1.17, 1.18, 1.19]
|
||||
go: [1.18, 1.19]
|
||||
name: ${{ matrix.os }} @ Go ${{ matrix.go }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v3
|
||||
@ -47,31 +40,17 @@ jobs:
|
||||
- name: Run Tests
|
||||
run: go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
|
||||
|
||||
- name: Install dependencies for checks
|
||||
run: |
|
||||
go install golang.org/x/lint/golint@latest
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
|
||||
- name: Run golint
|
||||
run: golint -set_exit_status ./...
|
||||
|
||||
- name: Run staticcheck
|
||||
run: staticcheck ./...
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: success() && matrix.go == 1.19 && matrix.os == 'ubuntu-latest'
|
||||
if: success() && matrix.go == env.LATEST_GO_VERSION && matrix.os == 'ubuntu-latest'
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token:
|
||||
fail_ci_if_error: false
|
||||
|
||||
benchmark:
|
||||
needs: test
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go: [1.19]
|
||||
name: Benchmark comparison ${{ matrix.os }} @ Go ${{ matrix.go }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: Benchmark comparison
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code (Previous)
|
||||
uses: actions/checkout@v3
|
||||
@ -87,7 +66,7 @@ jobs:
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
go-version: ${{ env.LATEST_GO_VERSION }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: go install golang.org/x/perf/cmd/benchstat@latest
|
||||
|
33
echo.go
33
echo.go
@ -55,8 +55,9 @@ import (
|
||||
|
||||
// Echo is the top-level framework instance.
|
||||
//
|
||||
// Note: replacing/nilling public fields is not coroutine/thread-safe and can cause data-races/panics. This is very likely
|
||||
// to happen when you access Echo instances through Context.Echo() method.
|
||||
// Goroutine safety: Do not mutate Echo instance fields after server has started. Accessing these
|
||||
// fields from handlers/middlewares and changing field values at the same time leads to data-races.
|
||||
// Same rule applies to adding new routes after server has been started - Adding a route is not Goroutine safe action.
|
||||
type Echo struct {
|
||||
// premiddleware are middlewares that are run for every request before routing is done
|
||||
premiddleware []MiddlewareFunc
|
||||
@ -90,6 +91,10 @@ type Echo struct {
|
||||
// prefix for directory path. This is necessary as `//go:embed assets/images` embeds files with paths
|
||||
// including `assets/images` as their prefix.
|
||||
Filesystem fs.FS
|
||||
|
||||
// OnAddRoute is called when Echo adds new route to specific host router. Handler is called for every router
|
||||
// and before route is added to the host router.
|
||||
OnAddRoute func(host string, route Routable) error
|
||||
}
|
||||
|
||||
// JSONSerializer is the interface that encodes and decodes JSON to and from interfaces.
|
||||
@ -278,14 +283,22 @@ func (e *Echo) Router() Router {
|
||||
return e.router
|
||||
}
|
||||
|
||||
// Routers returns the map of host => router.
|
||||
// Routers returns the new map of host => router.
|
||||
func (e *Echo) Routers() map[string]Router {
|
||||
return e.routers
|
||||
result := make(map[string]Router)
|
||||
for host, r := range e.routers {
|
||||
result[host] = r
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RouterFor returns Router for given host.
|
||||
func (e *Echo) RouterFor(host string) Router {
|
||||
return e.routers[host]
|
||||
// RouterFor returns Router for given host. When host is left empty the default router is returned.
|
||||
func (e *Echo) RouterFor(host string) (Router, bool) {
|
||||
if host == "" {
|
||||
return e.router, true
|
||||
}
|
||||
router, ok := e.routers[host]
|
||||
return router, ok
|
||||
}
|
||||
|
||||
// ResetRouterCreator resets callback for creating new router instances.
|
||||
@ -549,6 +562,12 @@ func (e *Echo) AddRoute(route Routable) (RouteInfo, error) {
|
||||
}
|
||||
|
||||
func (e *Echo) add(host string, route Routable) (RouteInfo, error) {
|
||||
if e.OnAddRoute != nil {
|
||||
if err := e.OnAddRoute(host, route); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
router := e.findRouter(host)
|
||||
ri, err := router.Add(route)
|
||||
if err != nil {
|
||||
|
144
echo_test.go
144
echo_test.go
@ -1013,6 +1013,150 @@ func TestEchoMethodNotAllowed(t *testing.T) {
|
||||
assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow))
|
||||
}
|
||||
|
||||
func TestEcho_OnAddRoute(t *testing.T) {
|
||||
type rr struct {
|
||||
host string
|
||||
path string
|
||||
}
|
||||
exampleRoute := Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/files/:id",
|
||||
Handler: notFoundHandler,
|
||||
Middlewares: nil,
|
||||
Name: "x",
|
||||
}
|
||||
|
||||
var testCases = []struct {
|
||||
name string
|
||||
whenHost string
|
||||
whenRoute Routable
|
||||
whenError error
|
||||
expectLen int
|
||||
expectAdded []rr
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "ok, for default host",
|
||||
whenHost: "",
|
||||
whenRoute: exampleRoute,
|
||||
whenError: nil,
|
||||
expectAdded: []rr{
|
||||
{host: "", path: "/static"},
|
||||
{host: "", path: "/api/files/:id"},
|
||||
},
|
||||
expectError: "",
|
||||
expectLen: 2,
|
||||
},
|
||||
{
|
||||
name: "ok, for specific host",
|
||||
whenHost: "test.com",
|
||||
whenRoute: exampleRoute,
|
||||
whenError: nil,
|
||||
expectAdded: []rr{
|
||||
{host: "", path: "/static"},
|
||||
{host: "test.com", path: "/api/files/:id"},
|
||||
},
|
||||
expectError: "",
|
||||
expectLen: 1,
|
||||
},
|
||||
{
|
||||
name: "nok, error is returned",
|
||||
whenHost: "test.com",
|
||||
whenRoute: exampleRoute,
|
||||
whenError: errors.New("nope"),
|
||||
expectAdded: []rr{
|
||||
{host: "", path: "/static"},
|
||||
},
|
||||
expectError: "nope",
|
||||
expectLen: 0,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
e := New()
|
||||
|
||||
added := make([]rr, 0)
|
||||
cnt := 0
|
||||
e.OnAddRoute = func(host string, route Routable) error {
|
||||
if cnt > 0 && tc.whenError != nil { // we want to GET /static to succeed for nok tests
|
||||
return tc.whenError
|
||||
}
|
||||
cnt++
|
||||
added = append(added, rr{
|
||||
host: host,
|
||||
path: route.ToRoute().Path,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
e.GET("/static", notFoundHandler)
|
||||
|
||||
var err error
|
||||
if tc.whenHost != "" {
|
||||
_, err = e.Host(tc.whenHost).AddRoute(tc.whenRoute)
|
||||
} else {
|
||||
_, err = e.AddRoute(tc.whenRoute)
|
||||
}
|
||||
|
||||
if tc.expectError != "" {
|
||||
assert.EqualError(t, err, tc.expectError)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
r, _ := e.RouterFor(tc.whenHost)
|
||||
assert.Len(t, r.Routes(), tc.expectLen)
|
||||
assert.Equal(t, tc.expectAdded, added)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcho_RouterFor(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
whenHost string
|
||||
expectLen int
|
||||
expectOk bool
|
||||
}{
|
||||
{
|
||||
name: "ok, default host",
|
||||
whenHost: "",
|
||||
expectLen: 2,
|
||||
expectOk: true,
|
||||
},
|
||||
{
|
||||
name: "ok, specific host with routes",
|
||||
whenHost: "test.com",
|
||||
expectLen: 1,
|
||||
expectOk: true,
|
||||
},
|
||||
{
|
||||
name: "ok, non-existent host",
|
||||
whenHost: "oups.com",
|
||||
expectLen: 0,
|
||||
expectOk: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := New()
|
||||
|
||||
e.GET("/1", notFoundHandler)
|
||||
e.GET("/2", notFoundHandler)
|
||||
e.Host("test.com").GET("/3", notFoundHandler)
|
||||
|
||||
r, ok := e.RouterFor(tc.whenHost)
|
||||
assert.Equal(t, tc.expectOk, ok)
|
||||
if tc.expectLen > 0 {
|
||||
assert.Len(t, r.Routes(), tc.expectLen)
|
||||
} else {
|
||||
assert.Nil(t, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEchoContext(t *testing.T) {
|
||||
e := New()
|
||||
c := e.AcquireContext()
|
||||
|
7
go.mod
7
go.mod
@ -1,12 +1,11 @@
|
||||
module github.com/labstack/echo/v5
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/valyala/fasttemplate v1.2.2
|
||||
golang.org/x/net v0.2.0
|
||||
golang.org/x/net v0.4.0
|
||||
golang.org/x/time v0.3.0
|
||||
)
|
||||
|
||||
@ -14,6 +13,6 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
35
go.sum
35
go.sum
@ -1,8 +1,6 @@
|
||||
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/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -16,37 +14,12 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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=
|
||||
|
@ -1,193 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/labstack/echo/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// 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.
|
||||
SuccessHandler JWTSuccessHandler
|
||||
|
||||
// ErrorHandler defines a function which is executed when all lookups have been done and none of them passed Validator
|
||||
// function. ErrorHandler is executed with last missing (ErrExtractionValueMissing) or an invalid key.
|
||||
// It may be used to define a custom JWT error.
|
||||
//
|
||||
// Note: when error handler swallows the error (returns nil) middleware continues handler chain execution towards handler.
|
||||
// This is useful in cases when portion of your site/api is publicly accessible and has extra features for authorized users
|
||||
// In that case you can use ErrorHandler to set default public JWT token value to request and continue with handler chain.
|
||||
ErrorHandler 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
|
||||
|
||||
// Context key to store user information from the token into context.
|
||||
// Optional. Default value "user".
|
||||
ContextKey string
|
||||
|
||||
// TokenLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used
|
||||
// to extract token from the request.
|
||||
// Optional. Default value "header:Authorization".
|
||||
// Possible values:
|
||||
// - "header:<name>" or "header:<name>:<cut-prefix>"
|
||||
// `<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header
|
||||
// value has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we
|
||||
// want to cut is `<auth-scheme> ` note the space at the end.
|
||||
// In case of JWT tokens `Authorization: Bearer <token>` prefix we cut is `Bearer `.
|
||||
// If prefix is left empty the whole value is returned.
|
||||
// - "query:<name>"
|
||||
// - "param:<name>"
|
||||
// - "cookie:<name>"
|
||||
// - "form:<name>"
|
||||
// Multiple sources example:
|
||||
// - "header:Authorization:Bearer ,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
|
||||
|
||||
// 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(c echo.Context, auth string, source ExtractorSource) (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(c echo.Context, err error) error
|
||||
|
||||
const (
|
||||
// AlgorithmHS256 is token signing algorithm
|
||||
AlgorithmHS256 = "HS256"
|
||||
)
|
||||
|
||||
// ErrJWTMissing denotes an error raised when JWT token value could not be extracted from request
|
||||
var ErrJWTMissing = echo.NewHTTPError(http.StatusUnauthorized, "missing or malformed jwt")
|
||||
|
||||
// ErrJWTInvalid denotes an error raised when JWT token value is invalid or expired
|
||||
var ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired jwt")
|
||||
|
||||
// DefaultJWTConfig is the default JWT auth middleware config.
|
||||
var DefaultJWTConfig = JWTConfig{
|
||||
Skipper: DefaultSkipper,
|
||||
ContextKey: "user",
|
||||
TokenLookup: "header:" + echo.HeaderAuthorization + ":Bearer ",
|
||||
}
|
||||
|
||||
// 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
|
||||
func JWT(parseTokenFunc func(c echo.Context, auth string, source ExtractorSource) (interface{}, error)) echo.MiddlewareFunc {
|
||||
c := DefaultJWTConfig
|
||||
c.ParseTokenFunc = parseTokenFunc
|
||||
return JWTWithConfig(c)
|
||||
}
|
||||
|
||||
// JWTWithConfig returns a JSON Web Token (JWT) auth middleware or panics if configuration is invalid.
|
||||
//
|
||||
// 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
|
||||
func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
|
||||
return toMiddlewareOrPanic(config)
|
||||
}
|
||||
|
||||
// ToMiddleware converts JWTConfig to middleware or returns an error for invalid configuration
|
||||
func (config JWTConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultJWTConfig.Skipper
|
||||
}
|
||||
if config.ParseTokenFunc == nil {
|
||||
return nil, errors.New("echo jwt middleware requires parse token function")
|
||||
}
|
||||
if config.ContextKey == "" {
|
||||
config.ContextKey = DefaultJWTConfig.ContextKey
|
||||
}
|
||||
if config.TokenLookup == "" && len(config.TokenLookupFuncs) == 0 {
|
||||
config.TokenLookup = DefaultJWTConfig.TokenLookup
|
||||
}
|
||||
extractors, err := createExtractors(config.TokenLookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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, source, extrErr := extractor(c)
|
||||
if extrErr != nil {
|
||||
lastExtractorErr = extrErr
|
||||
continue
|
||||
}
|
||||
for _, auth := range auths {
|
||||
token, err := config.ParseTokenFunc(c, auth, source)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// prioritize token errors over extracting errors
|
||||
err := lastTokenErr
|
||||
if err == nil {
|
||||
err = lastExtractorErr
|
||||
}
|
||||
if config.ErrorHandler != nil {
|
||||
tmpErr := config.ErrorHandler(c, err)
|
||||
if config.ContinueOnIgnoredError && tmpErr == nil {
|
||||
return next(c)
|
||||
}
|
||||
return tmpErr
|
||||
}
|
||||
if lastTokenErr == nil {
|
||||
return ErrJWTMissing.WithInternal(err)
|
||||
}
|
||||
return ErrJWTInvalid.WithInternal(err)
|
||||
}
|
||||
}, nil
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
package middleware_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/labstack/echo/v5/middleware"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
// CreateJWTGoParseTokenFunc creates JWTGo implementation for ParseTokenFunc
|
||||
//
|
||||
// signingKey is signing key to validate token.
|
||||
// This is one of the options to provide a token validation key.
|
||||
// The order of precedence is a user-defined SigningKeys and SigningKey.
|
||||
// Required if signingKeys is not provided.
|
||||
//
|
||||
// signingKeys is Map of signing keys to validate token with kid field usage.
|
||||
// This is one of the options to provide a token validation key.
|
||||
// The order of precedence is a user-defined SigningKeys and SigningKey.
|
||||
// Required if signingKey is not provided
|
||||
func CreateJWTGoParseTokenFunc(signingKey interface{}, signingKeys map[string]interface{}) func(c echo.Context, auth string, source middleware.ExtractorSource) (interface{}, error) {
|
||||
// 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.
|
||||
keyFunc := func(t *jwt.Token) (interface{}, error) {
|
||||
if t.Method.Alg() != middleware.AlgorithmHS256 {
|
||||
return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"])
|
||||
}
|
||||
if len(signingKeys) == 0 {
|
||||
return signingKey, nil
|
||||
}
|
||||
|
||||
if kid, ok := t.Header["kid"].(string); ok {
|
||||
if key, ok := signingKeys[kid]; ok {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected jwt key id=%v", t.Header["kid"])
|
||||
}
|
||||
|
||||
return func(c echo.Context, auth string, source middleware.ExtractorSource) (interface{}, error) {
|
||||
token, err := jwt.ParseWithClaims(auth, jwt.MapClaims{}, keyFunc) // you could add your default claims here
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleJWTConfig_withJWTGoAsTokenParser() {
|
||||
mw := middleware.JWTWithConfig(middleware.JWTConfig{
|
||||
ParseTokenFunc: CreateJWTGoParseTokenFunc([]byte("secret"), nil),
|
||||
})
|
||||
|
||||
e := echo.New()
|
||||
e.Use(mw)
|
||||
|
||||
e.GET("/", func(c echo.Context) error {
|
||||
user := c.Get("user").(*jwt.Token)
|
||||
return c.JSON(http.StatusTeapot, user.Claims)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set(echo.HeaderAuthorization, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
|
||||
res := httptest.NewRecorder()
|
||||
e.ServeHTTP(res, req)
|
||||
|
||||
fmt.Printf("status: %v, body: %v", res.Code, res.Body.String())
|
||||
// Output: status: 418, body: {"admin":true,"name":"John Doe","sub":"1234567890"}
|
||||
}
|
@ -1,666 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func createTestParseTokenFuncForJWTGo(signingMethod string, signingKey interface{}) func(c echo.Context, auth string, source ExtractorSource) (interface{}, error) {
|
||||
// This is minimal implementation for github.com/golang-jwt/jwt as JWT parser library. good enough to get old tests running
|
||||
keyFunc := func(t *jwt.Token) (interface{}, error) {
|
||||
if t.Method.Alg() != signingMethod {
|
||||
return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"])
|
||||
}
|
||||
return signingKey, nil
|
||||
}
|
||||
|
||||
return func(c echo.Context, auth string, source ExtractorSource) (interface{}, error) {
|
||||
token, err := jwt.ParseWithClaims(auth, jwt.MapClaims{}, keyFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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.RegisteredClaims
|
||||
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(createTestParseTokenFuncForJWTGo(AlgorithmHS256, []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 TestJWT_combinations(t *testing.T) {
|
||||
e := echo.New()
|
||||
handler := func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, "test")
|
||||
}
|
||||
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
|
||||
validKey := []byte("secret")
|
||||
invalidKey := []byte("invalid-key")
|
||||
validAuth := "Bearer " + token
|
||||
|
||||
var testCases = []struct {
|
||||
name string
|
||||
config JWTConfig
|
||||
reqURL string // "/" if empty
|
||||
hdrAuth string
|
||||
hdrCookie string // test.Request doesn't provide SetCookie(); use name=val
|
||||
formValues map[string]string
|
||||
expectPanic bool
|
||||
expectToMiddlewareError string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "No signing key provided",
|
||||
expectToMiddlewareError: "echo jwt middleware requires parse token function",
|
||||
},
|
||||
{
|
||||
name: "invalid TokenLookup",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo("RS256", validKey),
|
||||
TokenLookup: "q",
|
||||
},
|
||||
expectToMiddlewareError: "extractor source for lookup could not be split into needed parts: q",
|
||||
},
|
||||
{
|
||||
name: "Unexpected signing method",
|
||||
hdrAuth: validAuth,
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo("RS256", validKey),
|
||||
},
|
||||
expectError: "code=401, message=invalid or expired jwt, internal=unexpected jwt signing method=HS256",
|
||||
},
|
||||
{
|
||||
name: "Invalid key",
|
||||
hdrAuth: validAuth,
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, invalidKey),
|
||||
},
|
||||
expectError: "code=401, message=invalid or expired jwt, internal=signature is invalid",
|
||||
},
|
||||
{
|
||||
name: "Valid JWT",
|
||||
hdrAuth: validAuth,
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid JWT with custom AuthScheme",
|
||||
hdrAuth: "Token" + " " + token,
|
||||
config: JWTConfig{
|
||||
TokenLookup: "header:" + echo.HeaderAuthorization + ":Token ",
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid JWT with custom claims",
|
||||
hdrAuth: validAuth,
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, []byte("secret")),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid Authorization header",
|
||||
hdrAuth: "invalid-auth",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
},
|
||||
expectError: "code=401, message=missing or malformed jwt, internal=invalid value in request header",
|
||||
},
|
||||
{
|
||||
name: "Empty header auth field",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
},
|
||||
expectError: "code=401, message=missing or malformed jwt, internal=invalid value in request header",
|
||||
},
|
||||
{
|
||||
name: "Valid query method",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "query:jwt",
|
||||
},
|
||||
reqURL: "/?a=b&jwt=" + token,
|
||||
},
|
||||
{
|
||||
name: "Invalid query param name",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "query:jwt",
|
||||
},
|
||||
reqURL: "/?a=b&jwtxyz=" + token,
|
||||
expectError: "code=401, message=missing or malformed jwt, internal=missing value in the query string",
|
||||
},
|
||||
{
|
||||
name: "Invalid query param value",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "query:jwt",
|
||||
},
|
||||
reqURL: "/?a=b&jwt=invalid-token",
|
||||
expectError: "code=401, message=invalid or expired jwt, internal=token contains an invalid number of segments",
|
||||
},
|
||||
{
|
||||
name: "Empty query",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "query:jwt",
|
||||
},
|
||||
reqURL: "/?a=b",
|
||||
expectError: "code=401, message=missing or malformed jwt, internal=missing value in the query string",
|
||||
},
|
||||
{
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "param:jwt",
|
||||
},
|
||||
reqURL: "/" + token,
|
||||
name: "Valid param method",
|
||||
},
|
||||
{
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "cookie:jwt",
|
||||
},
|
||||
hdrCookie: "jwt=" + token,
|
||||
name: "Valid cookie method",
|
||||
},
|
||||
{
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "query:jwt,cookie:jwt",
|
||||
},
|
||||
hdrCookie: "jwt=" + token,
|
||||
name: "Multiple jwt lookuop",
|
||||
},
|
||||
{
|
||||
name: "Invalid token with cookie method",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "cookie:jwt",
|
||||
},
|
||||
hdrCookie: "jwt=invalid",
|
||||
expectError: "code=401, message=invalid or expired jwt, internal=token contains an invalid number of segments",
|
||||
},
|
||||
{
|
||||
name: "Empty cookie",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "cookie:jwt",
|
||||
},
|
||||
expectError: "code=401, message=missing or malformed jwt, internal=missing value in cookies",
|
||||
},
|
||||
{
|
||||
name: "Valid form method",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "form:jwt",
|
||||
},
|
||||
formValues: map[string]string{"jwt": token},
|
||||
},
|
||||
{
|
||||
name: "Invalid token with form method",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "form:jwt",
|
||||
},
|
||||
formValues: map[string]string{"jwt": "invalid"},
|
||||
expectError: "code=401, message=invalid or expired jwt, internal=token contains an invalid number of segments",
|
||||
},
|
||||
{
|
||||
name: "Empty form field",
|
||||
config: JWTConfig{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, validKey),
|
||||
TokenLookup: "form:jwt",
|
||||
},
|
||||
expectError: "code=401, message=missing or malformed jwt, internal=missing value in the form",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
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 {
|
||||
cc := c.(echo.ServableContext)
|
||||
cc.SetPathParams(echo.PathParams{
|
||||
{Name: "jwt", Value: token},
|
||||
})
|
||||
}
|
||||
|
||||
mw, err := tc.config.ToMiddleware()
|
||||
if tc.expectToMiddlewareError != "" {
|
||||
assert.EqualError(t, err, tc.expectToMiddlewareError)
|
||||
return
|
||||
}
|
||||
|
||||
hErr := mw(handler)(c)
|
||||
if tc.expectError != "" {
|
||||
assert.EqualError(t, hErr, tc.expectError)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, hErr)
|
||||
|
||||
user := c.Get("user").(*jwt.Token)
|
||||
switch claims := user.Claims.(type) {
|
||||
case jwt.MapClaims:
|
||||
assert.Equal(t, claims["name"], "John Doe")
|
||||
case *jwtCustomClaims:
|
||||
assert.Equal(t, claims.Name, "John Doe")
|
||||
assert.Equal(t, claims.Admin, true)
|
||||
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
|
||||
},
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, []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
|
||||
},
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, []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.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{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, []byte("secret")),
|
||||
ErrorHandler: func(c echo.Context, err error) 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{
|
||||
ErrorHandler: func(c echo.Context, err error) error {
|
||||
return echo.NewHTTPError(http.StatusTeapot, "ErrorHandler: "+err.Error())
|
||||
},
|
||||
},
|
||||
expectErr: "{\"message\":\"ErrorHandler: 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(c echo.Context, auth string, source ExtractorSource) (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, "Bearer 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")
|
||||
|
||||
var fromSource ExtractorSource
|
||||
config := JWTConfig{
|
||||
ParseTokenFunc: func(c echo.Context, auth string, source ExtractorSource) (interface{}, error) {
|
||||
fromSource = source
|
||||
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, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ")
|
||||
res := httptest.NewRecorder()
|
||||
e.ServeHTTP(res, req)
|
||||
|
||||
assert.Equal(t, fromSource, ExtractorSourceHeader)
|
||||
assert.Equal(t, http.StatusTeapot, res.Code)
|
||||
}
|
||||
|
||||
func TestMustJWTWithConfig_SuccessHandler(t *testing.T) {
|
||||
e := echo.New()
|
||||
|
||||
e.GET("/", func(c echo.Context) error {
|
||||
success := c.Get("success").(string)
|
||||
user := c.Get("user").(string)
|
||||
return c.String(http.StatusTeapot, fmt.Sprintf("%v:%v", success, user))
|
||||
})
|
||||
|
||||
mw, err := JWTConfig{
|
||||
ParseTokenFunc: func(c echo.Context, auth string, source ExtractorSource) (interface{}, error) {
|
||||
return auth, nil
|
||||
},
|
||||
SuccessHandler: func(c echo.Context) {
|
||||
c.Set("success", "yes")
|
||||
},
|
||||
}.ToMiddleware()
|
||||
assert.NoError(t, err)
|
||||
e.Use(mw)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Add(echo.HeaderAuthorization, "Bearer valid_token_base64")
|
||||
res := httptest.NewRecorder()
|
||||
e.ServeHTTP(res, req)
|
||||
|
||||
assert.Equal(t, "yes:valid_token_base64", res.Body.String())
|
||||
assert.Equal(t, http.StatusTeapot, res.Code)
|
||||
}
|
||||
|
||||
func TestJWTWithConfig_ContinueOnIgnoredError(t *testing.T) {
|
||||
var testCases = []struct {
|
||||
name string
|
||||
givenContinueOnIgnoredError bool
|
||||
givenErrorHandler JWTErrorHandlerWithContext
|
||||
givenTokenLookup string
|
||||
whenAuthHeaders []string
|
||||
whenCookies []string
|
||||
whenParseReturn string
|
||||
whenParseError error
|
||||
expectHandlerCalled bool
|
||||
expect string
|
||||
expectCode int
|
||||
}{
|
||||
{
|
||||
name: "ok, with valid JWT from auth header",
|
||||
givenContinueOnIgnoredError: true,
|
||||
givenErrorHandler: func(c echo.Context, err error) error {
|
||||
return nil
|
||||
},
|
||||
whenAuthHeaders: []string{"Bearer valid_token_base64"},
|
||||
whenParseReturn: "valid_token",
|
||||
expectCode: http.StatusTeapot,
|
||||
expect: "valid_token",
|
||||
},
|
||||
{
|
||||
name: "ok, missing header, callNext and set public_token from error handler",
|
||||
givenContinueOnIgnoredError: true,
|
||||
givenErrorHandler: func(c echo.Context, err error) error {
|
||||
if errors.Is(err, &ValueExtractorError{}) {
|
||||
panic("must get ErrJWTMissing")
|
||||
}
|
||||
c.Set("user", "public_token")
|
||||
return nil
|
||||
},
|
||||
whenAuthHeaders: []string{}, // no JWT header
|
||||
expectCode: http.StatusTeapot,
|
||||
expect: "public_token",
|
||||
},
|
||||
{
|
||||
name: "ok, invalid token, callNext and set public_token from error handler",
|
||||
givenContinueOnIgnoredError: true,
|
||||
givenErrorHandler: func(c echo.Context, err error) error {
|
||||
// this is probably not realistic usecase. on parse error you probably want to return error
|
||||
if err.Error() != "parser_error" {
|
||||
panic("must get parser_error")
|
||||
}
|
||||
c.Set("user", "public_token")
|
||||
return nil
|
||||
},
|
||||
whenAuthHeaders: []string{"Bearer invalid_header"},
|
||||
whenParseError: errors.New("parser_error"),
|
||||
expectCode: http.StatusTeapot,
|
||||
expect: "public_token",
|
||||
},
|
||||
{
|
||||
name: "nok, invalid token, return error from error handler",
|
||||
givenContinueOnIgnoredError: true,
|
||||
givenErrorHandler: func(c echo.Context, err error) error {
|
||||
if err.Error() != "parser_error" {
|
||||
panic("must get parser_error")
|
||||
}
|
||||
return err
|
||||
},
|
||||
whenAuthHeaders: []string{"Bearer invalid_header"},
|
||||
whenParseError: errors.New("parser_error"),
|
||||
expectCode: http.StatusInternalServerError,
|
||||
expect: "{\"message\":\"Internal Server Error\"}\n",
|
||||
},
|
||||
{
|
||||
name: "nok, ContinueOnIgnoredError but return error from error handler",
|
||||
givenContinueOnIgnoredError: true,
|
||||
givenErrorHandler: func(c echo.Context, err error) error {
|
||||
return echo.ErrUnauthorized.WithInternal(err)
|
||||
},
|
||||
whenAuthHeaders: []string{}, // no JWT header
|
||||
expectCode: http.StatusUnauthorized,
|
||||
expect: "{\"message\":\"Unauthorized\"}\n",
|
||||
},
|
||||
{
|
||||
name: "nok, ContinueOnIgnoredError=false",
|
||||
givenContinueOnIgnoredError: false,
|
||||
givenErrorHandler: func(c echo.Context, err error) error {
|
||||
return echo.ErrUnauthorized.WithInternal(err)
|
||||
},
|
||||
whenAuthHeaders: []string{}, // no JWT header
|
||||
expectCode: http.StatusUnauthorized,
|
||||
expect: "{\"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 {
|
||||
token := c.Get("user").(string)
|
||||
return c.String(http.StatusTeapot, token)
|
||||
})
|
||||
|
||||
mw, err := JWTConfig{
|
||||
ContinueOnIgnoredError: tc.givenContinueOnIgnoredError,
|
||||
TokenLookup: tc.givenTokenLookup,
|
||||
ParseTokenFunc: func(c echo.Context, auth string, source ExtractorSource) (interface{}, error) {
|
||||
return tc.whenParseReturn, tc.whenParseError
|
||||
},
|
||||
ErrorHandler: tc.givenErrorHandler,
|
||||
}.ToMiddleware()
|
||||
assert.NoError(t, err)
|
||||
e.Use(mw)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
for _, a := range tc.whenAuthHeaders {
|
||||
req.Header.Add(echo.HeaderAuthorization, a)
|
||||
}
|
||||
res := httptest.NewRecorder()
|
||||
e.ServeHTTP(res, req)
|
||||
|
||||
assert.Equal(t, tc.expect, res.Body.String())
|
||||
assert.Equal(t, tc.expectCode, 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{
|
||||
ParseTokenFunc: createTestParseTokenFuncForJWTGo(AlgorithmHS256, []byte("secret")),
|
||||
TokenLookupFuncs: []ValuesExtractor{
|
||||
func(c echo.Context) ([]string, ExtractorSource, error) {
|
||||
return []string{c.Request().Header.Get("X-API-Key")}, ExtractorSourceCustom, nil
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
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())
|
||||
}
|
@ -166,15 +166,16 @@ type Visitor struct {
|
||||
|
||||
/*
|
||||
NewRateLimiterMemoryStore returns an instance of RateLimiterMemoryStore with
|
||||
the provided rate (as req/s). The provided rate less than 1 will be treated as zero.
|
||||
the provided rate (as req/s).
|
||||
for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
|
||||
|
||||
Burst and ExpiresIn will be set to default values.
|
||||
|
||||
Note that if the provided rate is a float number and Burst is zero, Burst will be treated as the rounded down value of the rate.
|
||||
|
||||
Example (with 20 requests/sec):
|
||||
|
||||
limiterStore := middleware.NewRateLimiterMemoryStore(20)
|
||||
|
||||
*/
|
||||
func NewRateLimiterMemoryStore(rate rate.Limit) (store *RateLimiterMemoryStore) {
|
||||
return NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{
|
||||
@ -184,7 +185,7 @@ func NewRateLimiterMemoryStore(rate rate.Limit) (store *RateLimiterMemoryStore)
|
||||
|
||||
/*
|
||||
NewRateLimiterMemoryStoreWithConfig returns an instance of RateLimiterMemoryStore
|
||||
with the provided configuration. Rate must be provided. Burst will be set to the value of
|
||||
with the provided configuration. Rate must be provided. Burst will be set to the rounded down value of
|
||||
the configured rate if not provided or set to 0.
|
||||
|
||||
The build-in memory store is usually capable for modest loads. For higher loads other
|
||||
@ -221,7 +222,7 @@ func NewRateLimiterMemoryStoreWithConfig(config RateLimiterMemoryStoreConfig) (s
|
||||
// RateLimiterMemoryStoreConfig represents configuration for RateLimiterMemoryStore
|
||||
type RateLimiterMemoryStoreConfig struct {
|
||||
Rate rate.Limit // Rate of requests allowed to pass as req/s. For more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
|
||||
Burst int // Burst additionally allows a number of requests to pass when rate limit is reached
|
||||
Burst int // Burst is maximum number of requests to pass at the same moment. It additionally allows a number of requests to pass when rate limit is reached.
|
||||
ExpiresIn time.Duration // ExpiresIn is the duration after that a rate limiter is cleaned up
|
||||
}
|
||||
|
||||
|
@ -64,11 +64,13 @@ type Routable interface {
|
||||
// This method is meant to be used by Router after it parses url for path parameters, to store information about
|
||||
// route just added.
|
||||
ToRouteInfo(params []string) RouteInfo
|
||||
|
||||
// ToRoute converts Routable to Route which Router uses to register the method handler for path.
|
||||
//
|
||||
// This method is meant to be used by Router to get fields (including handler and middleware functions) needed to
|
||||
// add Route to Router.
|
||||
ToRoute() Route
|
||||
|
||||
// ForGroup recreates routable with added group prefix and group middlewares it is grouped to.
|
||||
//
|
||||
// Is necessary for Echo.Group to be able to add/register Routable with Router and having group prefix and group
|
||||
|
Loading…
x
Reference in New Issue
Block a user