package controller_test import ( "net/http/httptest" "path" "testing" "github.com/gin-gonic/gin" "github.com/steveiliop56/tinyauth/internal/bootstrap" "github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/controller" "github.com/steveiliop56/tinyauth/internal/repository" "github.com/steveiliop56/tinyauth/internal/service" "github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestProxyController(t *testing.T) { tempDir := t.TempDir() authServiceCfg := service.AuthServiceConfig{ Users: []config.User{ { Username: "testuser", Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", }, { Username: "totpuser", Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", }, }, SessionExpiry: 10, // 10 seconds, useful for testing CookieDomain: "example.com", LoginTimeout: 10, // 10 seconds, useful for testing LoginMaxRetries: 3, SessionCookieName: "tinyauth-session", } controllerCfg := controller.ProxyControllerConfig{ AppURL: "https://tinyauth.example.com", } acls := map[string]config.App{ "app_path_allow": { Config: config.AppConfig{ Domain: "path-allow.example.com", }, Path: config.AppPath{ Allow: "/allowed", }, }, "app_user_allow": { Config: config.AppConfig{ Domain: "user-allow.example.com", }, Users: config.AppUsers{ Allow: "testuser", }, }, "ip_bypass": { Config: config.AppConfig{ Domain: "ip-bypass.example.com", }, IP: config.AppIP{ Bypass: []string{"10.10.10.10"}, }, }, } const browserUserAgent = ` Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36` simpleCtx := func(c *gin.Context) { c.Set("context", &config.UserContext{ Username: "testuser", Name: "Testuser", Email: "testuser@example.com", IsLoggedIn: true, Provider: "local", }) c.Next() } simpleCtxTotp := func(c *gin.Context) { c.Set("context", &config.UserContext{ Username: "totpuser", Name: "Totpuser", Email: "totpuser@example.com", IsLoggedIn: true, Provider: "local", TotpEnabled: true, }) c.Next() } type testCase struct { description string middlewares []gin.HandlerFunc run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) } tests := []testCase{ { description: "Default forward auth should be detected and used", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/traefik", nil) req.Header.Set("x-forwarded-host", "test.example.com") req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-uri", "/") req.Header.Set("user-agent", browserUserAgent) router.ServeHTTP(recorder, req) assert.Equal(t, 307, recorder.Code) location := recorder.Header().Get("Location") assert.Contains(t, location, "https://tinyauth.example.com/login?redirect_uri=") assert.Contains(t, location, "https%3A%2F%2Ftest.example.com%2F") }, }, { description: "Auth request (nginx) should be detected and used", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("x-original-url", "https://test.example.com/") router.ServeHTTP(recorder, req) assert.Equal(t, 401, recorder.Code) }, }, { description: "Ext authz (envoy) should be detected and used", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) // test a different method for envoy req.Host = "test.example.com" req.Header.Set("x-forwarded-proto", "https") router.ServeHTTP(recorder, req) assert.Equal(t, 401, recorder.Code) }, }, { description: "Ensure forward auth fallback for nginx", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("x-forwarded-host", "test.example.com") req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-uri", "/") router.ServeHTTP(recorder, req) assert.Equal(t, 401, recorder.Code) }, }, { description: "Ensure forward auth fallback for envoy", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) req.Header.Set("x-forwarded-host", "test.example.com") req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-uri", "/hello") router.ServeHTTP(recorder, req) assert.Equal(t, 401, recorder.Code) }, }, { description: "Ensure normal authentication flow for forward auth", middlewares: []gin.HandlerFunc{ simpleCtx, }, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/traefik", nil) req.Header.Set("x-forwarded-host", "test.example.com") req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-uri", "/") router.ServeHTTP(recorder, req) assert.Equal(t, 200, recorder.Code) assert.Equal(t, "testuser", recorder.Header().Get("remote-user")) assert.Equal(t, "Testuser", recorder.Header().Get("remote-name")) assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email")) }, }, { description: "Ensure normal authentication flow for nginx auth request", middlewares: []gin.HandlerFunc{ simpleCtx, }, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("x-original-url", "https://test.example.com/") router.ServeHTTP(recorder, req) assert.Equal(t, 200, recorder.Code) assert.Equal(t, "testuser", recorder.Header().Get("remote-user")) assert.Equal(t, "Testuser", recorder.Header().Get("remote-name")) assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email")) }, }, { description: "Ensure normal authentication flow for envoy ext authz", middlewares: []gin.HandlerFunc{ simpleCtx, }, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) req.Host = "test.example.com" req.Header.Set("x-forwarded-proto", "https") router.ServeHTTP(recorder, req) assert.Equal(t, 200, recorder.Code) assert.Equal(t, "testuser", recorder.Header().Get("remote-user")) assert.Equal(t, "Testuser", recorder.Header().Get("remote-name")) assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email")) }, }, { description: "Ensure path allow ACL works on forward auth", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/traefik", nil) req.Header.Set("x-forwarded-host", "path-allow.example.com") req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-uri", "/allowed") router.ServeHTTP(recorder, req) assert.Equal(t, 200, recorder.Code) }, }, { description: "Ensure path allow ACL works on nginx auth request", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("x-original-url", "https://path-allow.example.com/allowed") router.ServeHTTP(recorder, req) assert.Equal(t, 200, recorder.Code) }, }, { description: "Ensure path allow ACL works on envoy ext authz", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/allowed", nil) req.Host = "path-allow.example.com" req.Header.Set("x-forwarded-proto", "https") router.ServeHTTP(recorder, req) assert.Equal(t, 200, recorder.Code) }, }, { description: "Ensure ip bypass ACL works on forward auth", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/traefik", nil) req.Header.Set("x-forwarded-host", "ip-bypass.example.com") req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-uri", "/") req.Header.Set("x-forwarded-for", "10.10.10.10") router.ServeHTTP(recorder, req) assert.Equal(t, 200, recorder.Code) }, }, { description: "Ensure ip bypass ACL works on nginx auth request", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("x-original-url", "https://ip-bypass.example.com/") req.Header.Set("x-forwarded-for", "10.10.10.10") router.ServeHTTP(recorder, req) assert.Equal(t, 200, recorder.Code) }, }, { description: "Ensure ip bypass ACL works on envoy ext authz", middlewares: []gin.HandlerFunc{}, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) req.Host = "ip-bypass.example.com" req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-for", "10.10.10.10") router.ServeHTTP(recorder, req) assert.Equal(t, 200, recorder.Code) }, }, { description: "Ensure user allow ACL allows correct user (should allow testuser)", middlewares: []gin.HandlerFunc{ simpleCtx, }, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/traefik", nil) req.Header.Set("x-forwarded-host", "user-allow.example.com") req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-uri", "/") router.ServeHTTP(recorder, req) assert.Equal(t, 200, recorder.Code) }, }, { description: "Ensure user allow ACL blocks incorrect user (should block totpuser)", middlewares: []gin.HandlerFunc{ simpleCtxTotp, }, run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { req := httptest.NewRequest("GET", "/api/auth/traefik", nil) req.Header.Set("x-forwarded-host", "user-allow.example.com") req.Header.Set("x-forwarded-proto", "https") req.Header.Set("x-forwarded-uri", "/") router.ServeHTTP(recorder, req) assert.Equal(t, 403, recorder.Code) assert.Equal(t, "", recorder.Header().Get("remote-user")) assert.Equal(t, "", recorder.Header().Get("remote-name")) assert.Equal(t, "", recorder.Header().Get("remote-email")) }, }, } tlog.NewSimpleLogger().Init() oauthBrokerCfgs := make(map[string]config.OAuthServiceConfig) app := bootstrap.NewBootstrapApp(config.Config{}) db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db")) require.NoError(t, err) queries := repository.New(db) docker := service.NewDockerService() err = docker.Init() require.NoError(t, err) ldap := service.NewLdapService(service.LdapServiceConfig{}) err = ldap.Init() require.NoError(t, err) broker := service.NewOAuthBrokerService(oauthBrokerCfgs) err = broker.Init() require.NoError(t, err) authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker) err = authService.Init() require.NoError(t, err) aclsService := service.NewAccessControlsService(docker, acls) for _, test := range tests { t.Run(test.description, func(t *testing.T) { router := gin.Default() for _, m := range test.middlewares { router.Use(m) } group := router.Group("/api") gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() proxyController := controller.NewProxyController(controllerCfg, group, aclsService, authService) proxyController.SetupRoutes() test.run(t, router, recorder) }) } t.Cleanup(func() { err = db.Close() require.NoError(t, err) }) }