Replies: 1 comment 10 replies
-
I am not really sure if it is a good idea to allow route level middlewares communicate up in middleware chain. I think this makes following application "flow" more complex. In large applications which is divided into packages per business domain, you have routes declared all over the application. It gets really messi to understand where some of the middlewares are "disabled"/"modified". Using skippers and other callbacks at the place where "global" middleware is added at least forces all this logic to be concentrated at one place - near where the middleware is created/added. To be able to switch authentication of for some routes - this requirement is usually done with Line 18 in ce0b12a Instead of adding data to Route you can achieve same with route level middlewares that extract that from context added by global middlewares. This of course only works when "global" middleware does not depend on route middleware as global middleware is run before route level middleware. NB: settings roles/privileges in JWT is not a good idea. This is naive example of combining skipper with JWT middleware with addition of route level privilege checks. package main
import (
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"log/slog"
"net/http"
"slices"
)
/*
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwOTkyMTU1NzgsImlhdCI6MTczOTIxNTU3OCwicHJpdmlsZWdlcyI6WyJhZGRfdXNlciIsInZpZXdfdXNlciJdLCJzdWIiOiJ1c2VyMTIzIn0.FIkm-BSYSUtDk9yDCAe8jkFjuZUNbCTaNq-HMIhK5zg
// note: holding privileges/roles in JWT is not a good idea.
{
"exp": 2099215578,
"iat": 1739215578,
"privileges": [
"add_user",
"view_user"
],
"sub": "user123"
}
*/
const jwtTokenKey = "jwt_token"
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(echojwt.WithConfig(echojwt.Config{
Skipper: func(c echo.Context) bool {
// routes `/login` and `/public` are excluded from JWT checks
return slices.Contains([]string{"/", "/login"}, c.Path())
},
ContextKey: jwtTokenKey,
SigningKey: []byte("secret"),
//ContinueOnIgnoredError: true, // <-- see this field usage
}))
e.GET("/", handler)
e.GET("/login", handler)
e.GET("/users", handler, PrivilegeCheck("view_user", "root"))
e.GET("/users/:id", handler, PrivilegeCheck("view_user"))
if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("failed to start server")
}
}
func handler(c echo.Context) error {
return c.String(http.StatusOK, fmt.Sprintf("Hello from '%s'", c.Path()))
}
func PrivilegeCheck(privileges ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token, ok := c.Get(jwtTokenKey).(*jwt.Token)
if !ok {
return echo.ErrUnauthorized // status: 401
}
// next line potentially panics but is good enough for example
currentPrivileges := token.Claims.(jwt.MapClaims)["privileges"].([]interface{})
// given privileges act like `ANY OF GIVEN` will give access to route
for _, privilege := range privileges {
for _, currentPrivilege := range currentPrivileges {
if privilege == currentPrivilege {
return next(c)
}
}
}
return echo.ErrUnauthorized // status: 401
}
}
} Test this out: Request guarded route without privilege curl -v http://localhost:8080/users/1 output x@x:~/code$ curl -v http://localhost:8080/users/1
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET /users/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Content-Type: application/json
< Date: Mon, 10 Feb 2025 19:59:25 GMT
< Content-Length: 39
<
{"message":"missing or malformed jwt"} Request guarded route curl -v -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwOTkyMTU1NzgsImlhdCI6MTczOTIxNTU3OCwicHJpdmlsZWdlcyI6WyJhZGRfdXNlciIsInZpZXdfdXNlciJdLCJzdWIiOiJ1c2VyMTIzIn0.FIkm-BSYSUtDk9yDCAe8jkFjuZUNbCTaNq-HMIhK5zg" http://localhost:8080/users/1 output x@x:~/code$ curl -v -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwOTkyMTU1NzgsImlhdCI6MTczOTIxNTU3OCwicHJpdmlsZWdlcyI6WyJhZGRfdXNlciIsInZpZXdfdXNlciJdLCJzdWIiOiJ1c2VyMTIzIn0.FIkm-BSYSUtDk9yDCAe8jkFjuZUNbCTaNq-HMIhK5zg" http://localhost:8080/users/1
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET /users/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.5.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwOTkyMTU1NzgsImlhdCI6MTczOTIxNTU3OCwicHJpdmlsZWdlcyI6WyJhZGRfdXNlciIsInZpZXdfdXNlciJdLCJzdWIiOiJ1c2VyMTIzIn0.FIkm-BSYSUtDk9yDCAe8jkFjuZUNbCTaNq-HMIhK5zg
>
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
< Date: Mon, 10 Feb 2025 20:00:12 GMT
< Content-Length: 23
<
* Connection #0 to host localhost left intact
Hello from '/users/:id' |
Beta Was this translation helpful? Give feedback.
-
Hello! I would like the ability to pass static data via context to my middleware chain. Data I can set at registration.
Why would I want this?
I consider authentication a fairly important part of any web app that requires it and I typically want everyone to be authenticated all the time! However there are some endpoints I might not want that, like for example registration, login, static data, or for some reasons links I want to be shareable.
I consider it much safer to have an authentication middleware to protect my entire application and opt out the few ones I don't want very explicitly than having to remember to protect every endpoint every time I make one. Or make sure it is a group that is protected. If I could pass data via context trough all the middlewares I could extract this information in the authentication middleware., and bypass it if needed.
This could also be used for other uses like disabling timeouts, or bytesize limits on endpoints that are known to do slow work, like uploading files, or returning them. It could give every middleware layer more information about specific endpoints you want more or less logging at.
I think this would be a great enhancement.
Now, how to implement it?
It could be implemented where the last parameter of route registrations is an array of options where one option could be a "Use" middleware, and another be "Context" option to embed data in the context ahead of every middleware.
Or the parameters could be expanded with a new map second to last, which get its values put on the context.
Both of these could be done on the existing route registration functions or a new one like
GETWithContext
or something like that.I favor the first proposal with the options pattern as it is the cleanest in my opinion and it also gives the option of seamless expansion in the future without breaking changes.
What do you all think?
Beta Was this translation helpful? Give feedback.
All reactions