fix(api): tighten public path auth bypass matching
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c6034031a3
commit
da9bb918f7
4 changed files with 84 additions and 2 deletions
|
|
@ -148,7 +148,7 @@ func authentikMiddleware(cfg AuthentikConfig) gin.HandlerFunc {
|
|||
// Skip public paths.
|
||||
path := c.Request.URL.Path
|
||||
for p := range public {
|
||||
if strings.HasPrefix(path, p) {
|
||||
if isPublicPath(path, p) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,27 @@ func TestHealthBypassesAuthentik_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPublicPaths_Good_SimilarPrefixDoesNotBypassAuth(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
cfg := api.AuthentikConfig{
|
||||
TrustedProxy: true,
|
||||
PublicPaths: []string{"/public"},
|
||||
}
|
||||
e, _ := api.New(api.WithAuthentik(cfg))
|
||||
e.Register(&publicPrefixGroup{})
|
||||
|
||||
h := e.Handler()
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/publicity/secure", nil)
|
||||
req.Header.Set("X-authentik-username", "alice")
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 for /publicity/secure with auth header, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUser_Good_NilContext(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
|
@ -458,3 +479,15 @@ func (g *groupRequireGroup) RegisterRoutes(rg *gin.RouterGroup) {
|
|||
c.JSON(200, api.OK("admin panel"))
|
||||
})
|
||||
}
|
||||
|
||||
// publicPrefixGroup provides a route that should still be processed by auth
|
||||
// middleware even though its path shares a prefix with a public path.
|
||||
type publicPrefixGroup struct{}
|
||||
|
||||
func (g *publicPrefixGroup) Name() string { return "public-prefix" }
|
||||
func (g *publicPrefixGroup) BasePath() string { return "/publicity" }
|
||||
func (g *publicPrefixGroup) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
rg.GET("/secure", api.RequireAuth(), func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, api.OK("protected"))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func bearerAuthMiddleware(token string, skip []string) gin.HandlerFunc {
|
|||
return func(c *gin.Context) {
|
||||
// Check whether the request path should bypass authentication.
|
||||
for _, path := range skip {
|
||||
if strings.HasPrefix(c.Request.URL.Path, path) {
|
||||
if isPublicPath(c.Request.URL.Path, path) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
|
@ -64,6 +64,30 @@ func bearerAuthMiddleware(token string, skip []string) gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// isPublicPath reports whether requestPath should bypass auth for publicPath.
|
||||
// It matches the exact path and any nested subpath, but not sibling prefixes
|
||||
// such as /swaggerx when the public path is /swagger.
|
||||
func isPublicPath(requestPath, publicPath string) bool {
|
||||
if publicPath == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
normalized := strings.TrimRight(publicPath, "/")
|
||||
if normalized == "" {
|
||||
normalized = "/"
|
||||
}
|
||||
|
||||
if requestPath == normalized {
|
||||
return true
|
||||
}
|
||||
|
||||
if normalized == "/" {
|
||||
return true
|
||||
}
|
||||
|
||||
return strings.HasPrefix(requestPath, normalized+"/")
|
||||
}
|
||||
|
||||
// requestIDMiddleware ensures every response carries an X-Request-ID header.
|
||||
// If the client sends one, it is preserved; otherwise a random 16-byte hex
|
||||
// string is generated. The ID is also stored in the Gin context as "request_id".
|
||||
|
|
|
|||
|
|
@ -27,6 +27,16 @@ func (m *mwTestGroup) RegisterRoutes(rg *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
type swaggerLikeGroup struct{}
|
||||
|
||||
func (g *swaggerLikeGroup) Name() string { return "swagger-like" }
|
||||
func (g *swaggerLikeGroup) BasePath() string { return "/swaggerx" }
|
||||
func (g *swaggerLikeGroup) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
rg.GET("/secret", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, api.OK("classified"))
|
||||
})
|
||||
}
|
||||
|
||||
type requestIDTestGroup struct {
|
||||
gotID *string
|
||||
}
|
||||
|
|
@ -151,6 +161,21 @@ func TestBearerAuth_Good_HealthBypassesAuth(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBearerAuth_Bad_SimilarPrefixDoesNotBypassAuth(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
e, _ := api.New(api.WithBearerAuth("s3cret"))
|
||||
e.Register(&swaggerLikeGroup{})
|
||||
|
||||
h := e.Handler()
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/swaggerx/secret", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401 for /swaggerx/secret, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Request ID ──────────────────────────────────────────────────────────
|
||||
|
||||
func TestRequestID_Good_GeneratedWhenMissing(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue