Compare commits

...

5 Commits

Author SHA1 Message Date
Stavros
ff1c63bc7f tests: fix get cookie domain tests 2026-03-29 20:41:44 +03:00
Stavros
245fa4de78 chore: cancel lockdown in testing 2026-03-29 20:38:03 +03:00
Stavros
36c2004bf6 chore: more review comments 2026-03-29 20:20:53 +03:00
Stavros
b60e546ecd chore: review comments 2026-03-29 19:38:59 +03:00
Stavros
15a3753622 test: add proxy controller tests 2026-03-29 19:18:53 +03:00
8 changed files with 444 additions and 367 deletions

View File

@@ -125,7 +125,7 @@ func TestContextController(t *testing.T) {
router.ServeHTTP(recorder, request) router.ServeHTTP(recorder, request)
assert.Equal(t, recorder.Result().StatusCode, http.StatusOK) assert.Equal(t, http.StatusOK, recorder.Code)
assert.Equal(t, test.expected, recorder.Body.String()) assert.Equal(t, test.expected, recorder.Body.String())
}) })
} }

View File

@@ -4,20 +4,24 @@ import (
"encoding/json" "encoding/json"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os" "path"
"strings" "strings"
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/go-querystring/query"
"github.com/steveiliop56/tinyauth/internal/bootstrap" "github.com/steveiliop56/tinyauth/internal/bootstrap"
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/controller" "github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/repository" "github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service" "github.com/steveiliop56/tinyauth/internal/service"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestOIDCController(t *testing.T) { func TestOIDCController(t *testing.T) {
tempDir := t.TempDir()
oidcServiceCfg := service.OIDCServiceConfig{ oidcServiceCfg := service.OIDCServiceConfig{
Clients: map[string]config.OIDCClientConfig{ Clients: map[string]config.OIDCClientConfig{
"test": { "test": {
@@ -27,8 +31,8 @@ func TestOIDCController(t *testing.T) {
Name: "Test Client", Name: "Test Client",
}, },
}, },
PrivateKeyPath: "/tmp/tinyauth_testing_key.pem", PrivateKeyPath: path.Join(tempDir, "key.pem"),
PublicKeyPath: "/tmp/tinyauth_testing_key.pub", PublicKeyPath: path.Join(tempDir, "key.pub"),
Issuer: "https://tinyauth.example.com", Issuer: "https://tinyauth.example.com",
SessionExpiry: 500, SessionExpiry: 500,
} }
@@ -170,11 +174,11 @@ func TestOIDCController(t *testing.T) {
Code: "", Code: "",
RedirectURI: "https://test.example.com/callback", RedirectURI: "https://test.example.com/callback",
} }
reqBodyBytes, err := json.Marshal(reqBody) reqBodyEncoded, err := query.Values(reqBody)
assert.NoError(t, err) assert.NoError(t, err)
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(string(reqBodyBytes))) req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
router.ServeHTTP(recorder, req) router.ServeHTTP(recorder, req)
var res map[string]any var res map[string]any
@@ -193,11 +197,11 @@ func TestOIDCController(t *testing.T) {
Code: "some-code", Code: "some-code",
RedirectURI: "https://test.example.com/callback", RedirectURI: "https://test.example.com/callback",
} }
reqBodyBytes, err := json.Marshal(reqBody) reqBodyEncoded, err := query.Values(reqBody)
assert.NoError(t, err) assert.NoError(t, err)
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(string(reqBodyBytes))) req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth("some-client-id", "some-client-secret") req.SetBasicAuth("some-client-id", "some-client-secret")
router.ServeHTTP(recorder, req) router.ServeHTTP(recorder, req)
@@ -231,11 +235,11 @@ func TestOIDCController(t *testing.T) {
Code: "some-code", Code: "some-code",
RedirectURI: "https://test.example.com/callback", RedirectURI: "https://test.example.com/callback",
} }
reqBodyBytes, err := json.Marshal(reqBody) reqBodyEncoded, err := query.Values(reqBody)
assert.NoError(t, err) assert.NoError(t, err)
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(string(reqBodyBytes))) req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
router.ServeHTTP(recorder, req) router.ServeHTTP(recorder, req)
authHeader := recorder.Header().Get("www-authenticate") authHeader := recorder.Header().Get("www-authenticate")
@@ -270,11 +274,11 @@ func TestOIDCController(t *testing.T) {
Code: code, Code: code,
RedirectURI: "https://test.example.com/callback", RedirectURI: "https://test.example.com/callback",
} }
reqBodyBytes, err := json.Marshal(reqBody) reqBodyEncoded, err := query.Values(reqBody)
assert.NoError(t, err) assert.NoError(t, err)
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(string(reqBodyBytes))) req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth("some-client-id", "some-client-secret") req.SetBasicAuth("some-client-id", "some-client-secret")
router.ServeHTTP(recorder, req) router.ServeHTTP(recorder, req)
@@ -307,11 +311,11 @@ func TestOIDCController(t *testing.T) {
ClientID: "some-client-id", ClientID: "some-client-id",
ClientSecret: "some-client-secret", ClientSecret: "some-client-secret",
} }
reqBodyBytes, err := json.Marshal(reqBody) reqBodyEncoded, err := query.Values(reqBody)
assert.NoError(t, err) assert.NoError(t, err)
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(string(reqBodyBytes))) req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
router.ServeHTTP(recorder, req) router.ServeHTTP(recorder, req)
assert.NotEmpty(t, recorder.Header().Get("cache-control")) assert.NotEmpty(t, recorder.Header().Get("cache-control"))
@@ -329,7 +333,7 @@ func TestOIDCController(t *testing.T) {
}, },
}, },
{ {
description: "Ensure token endpoint deletes code afer use", description: "Ensure token endpoint deletes code after use",
middlewares: []gin.HandlerFunc{ middlewares: []gin.HandlerFunc{
simpleCtx, simpleCtx,
}, },
@@ -356,19 +360,19 @@ func TestOIDCController(t *testing.T) {
Code: code, Code: code,
RedirectURI: "https://test.example.com/callback", RedirectURI: "https://test.example.com/callback",
} }
reqBodyBytes, err := json.Marshal(reqBody) reqBodyEncoded, err := query.Values(reqBody)
assert.NoError(t, err) assert.NoError(t, err)
req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(string(reqBodyBytes))) req := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth("some-client-id", "some-client-secret") req.SetBasicAuth("some-client-id", "some-client-secret")
router.ServeHTTP(recorder, req) router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code) assert.Equal(t, 200, recorder.Code)
// Try to use the same code again // Try to use the same code again
secondReq := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(string(reqBodyBytes))) secondReq := httptest.NewRequest("POST", "/api/oidc/token", strings.NewReader(reqBodyEncoded.Encode()))
secondReq.Header.Set("Content-Type", "application/json") secondReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
secondReq.SetBasicAuth("some-client-id", "some-client-secret") secondReq.SetBasicAuth("some-client-id", "some-client-secret")
secondRecorder := httptest.NewRecorder() secondRecorder := httptest.NewRecorder()
router.ServeHTTP(secondRecorder, secondReq) router.ServeHTTP(secondRecorder, secondReq)
@@ -431,13 +435,13 @@ func TestOIDCController(t *testing.T) {
app := bootstrap.NewBootstrapApp(config.Config{}) app := bootstrap.NewBootstrapApp(config.Config{})
db, err := app.SetupDatabase("/tmp/tinyauth_test.db") db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
assert.NoError(t, err) require.NoError(t, err)
queries := repository.New(db) queries := repository.New(db)
oidcService := service.NewOIDCService(oidcServiceCfg, queries) oidcService := service.NewOIDCService(oidcServiceCfg, queries)
err = oidcService.Init() err = oidcService.Init()
assert.NoError(t, err) require.NoError(t, err)
for _, test := range tests { for _, test := range tests {
t.Run(test.description, func(t *testing.T) { t.Run(test.description, func(t *testing.T) {
@@ -459,15 +463,8 @@ func TestOIDCController(t *testing.T) {
}) })
} }
err = db.Close() t.Cleanup(func() {
assert.NoError(t, err) err = db.Close()
require.NoError(t, err)
err = os.Remove("/tmp/tinyauth_test.db") })
assert.NoError(t, err)
err = os.Remove(oidcServiceCfg.PrivateKeyPath)
assert.NoError(t, err)
err = os.Remove(oidcServiceCfg.PublicKeyPath)
assert.NoError(t, err)
} }

View File

@@ -1,302 +1,373 @@
package controller_test package controller_test
import ( import (
"net/http"
"net/http/httptest" "net/http/httptest"
"path"
"testing" "testing"
"github.com/gin-gonic/gin"
"github.com/steveiliop56/tinyauth/internal/bootstrap" "github.com/steveiliop56/tinyauth/internal/bootstrap"
"github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/config"
"github.com/steveiliop56/tinyauth/internal/controller" "github.com/steveiliop56/tinyauth/internal/controller"
"github.com/steveiliop56/tinyauth/internal/repository" "github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service" "github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/gin-gonic/gin" "github.com/stretchr/testify/assert"
"gotest.tools/v3/assert" "github.com/stretchr/testify/require"
) )
var loggedInCtx = config.UserContext{ func TestProxyController(t *testing.T) {
Username: "test", tempDir := t.TempDir()
Name: "Test",
Email: "test@example.com",
IsLoggedIn: true,
Provider: "local",
}
func setupProxyController(t *testing.T, middlewares []gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) { authServiceCfg := service.AuthServiceConfig{
// Setup
gin.SetMode(gin.TestMode)
router := gin.Default()
if len(middlewares) > 0 {
for _, m := range middlewares {
router.Use(m)
}
}
group := router.Group("/api")
recorder := httptest.NewRecorder()
// Mock app
app := bootstrap.NewBootstrapApp(config.Config{})
// Database
db, err := app.SetupDatabase(":memory:")
assert.NilError(t, err)
// Queries
queries := repository.New(db)
// Docker
dockerService := service.NewDockerService()
assert.NilError(t, dockerService.Init())
// Access controls
accessControlsService := service.NewAccessControlsService(dockerService, map[string]config.App{
"whoami": {
Path: config.AppPath{
Allow: "/allow",
},
},
})
assert.NilError(t, accessControlsService.Init())
// Auth service
authService := service.NewAuthService(service.AuthServiceConfig{
Users: []config.User{ Users: []config.User{
{ {
Username: "testuser", Username: "testuser",
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa",
}, },
{ {
Username: "totpuser", Username: "totpuser",
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa",
TotpSecret: "foo", TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
}, },
}, },
OauthWhitelist: []string{}, SessionExpiry: 10, // 10 seconds, useful for testing
SessionExpiry: 3600, CookieDomain: "example.com",
SessionMaxLifetime: 0, LoginTimeout: 10, // 10 seconds, useful for testing
SecureCookie: false, LoginMaxRetries: 3,
CookieDomain: "localhost", SessionCookieName: "tinyauth-session",
LoginTimeout: 300, }
LoginMaxRetries: 3,
SessionCookieName: "tinyauth-session",
}, dockerService, nil, queries, &service.OAuthBrokerService{})
// Controller controllerCfg := controller.ProxyControllerConfig{
ctrl := controller.NewProxyController(controller.ProxyControllerConfig{ AppURL: "https://tinyauth.example.com",
AppURL: "http://tinyauth.example.com", }
}, group, accessControlsService, authService)
ctrl.SetupRoutes()
return router, recorder acls := map[string]config.App{
} "app_path_allow": {
Config: config.AppConfig{
// TODO: Needs tests for context middleware Domain: "path-allow.example.com",
},
func TestProxyHandler(t *testing.T) { Path: config.AppPath{
// Test logged out user traefik/caddy (forward_auth) Allow: "/allowed",
router, recorder := setupProxyController(t, nil) },
},
req, err := http.NewRequest("GET", "/api/auth/traefik", nil) "app_user_allow": {
assert.NilError(t, err) Config: config.AppConfig{
Domain: "user-allow.example.com",
req.Header.Set("x-forwarded-host", "whoami.example.com") },
req.Header.Set("x-forwarded-proto", "http") Users: config.AppUsers{
req.Header.Set("x-forwarded-uri", "/") Allow: "testuser",
},
router.ServeHTTP(recorder, req) },
assert.Equal(t, recorder.Code, http.StatusUnauthorized) "ip_bypass": {
Config: config.AppConfig{
// Test logged out user nginx (auth_request) Domain: "ip-bypass.example.com",
router, recorder = setupProxyController(t, nil) },
IP: config.AppIP{
req, err = http.NewRequest("GET", "/api/auth/nginx", nil) Bypass: []string{"10.10.10.10"},
assert.NilError(t, err) },
},
req.Header.Set("x-original-url", "http://whoami.example.com/") }
router.ServeHTTP(recorder, req) const browserUserAgent = `
assert.Equal(t, recorder.Code, http.StatusUnauthorized) 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`
// Test logged out user envoy (ext_authz) simpleCtx := func(c *gin.Context) {
router, recorder = setupProxyController(t, nil) c.Set("context", &config.UserContext{
Username: "testuser",
req, err = http.NewRequest("GET", "/api/auth/envoy?path=/", nil) Name: "Testuser",
assert.NilError(t, err) Email: "testuser@example.com",
IsLoggedIn: true,
req.Host = "whoami.example.com" Provider: "local",
req.Header.Set("x-forwarded-proto", "http") })
c.Next()
router.ServeHTTP(recorder, req) }
assert.Equal(t, recorder.Code, http.StatusUnauthorized)
simpleCtxTotp := func(c *gin.Context) {
// Test logged in user traefik/caddy (forward_auth) c.Set("context", &config.UserContext{
router, recorder = setupProxyController(t, []gin.HandlerFunc{ Username: "totpuser",
func(c *gin.Context) { Name: "Totpuser",
c.Set("context", &loggedInCtx) Email: "totpuser@example.com",
c.Next() IsLoggedIn: true,
}, Provider: "local",
}) TotpEnabled: true,
})
req, err = http.NewRequest("GET", "/api/auth/traefik", nil) c.Next()
assert.NilError(t, err) }
req.Header.Set("x-forwarded-host", "whoami.example.com") type testCase struct {
req.Header.Set("x-forwarded-proto", "http") description string
req.Header.Set("x-forwarded-uri", "/") middlewares []gin.HandlerFunc
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
router.ServeHTTP(recorder, req) }
assert.Equal(t, recorder.Code, http.StatusOK)
tests := []testCase{
// Test logged in user nginx (auth_request) {
router, recorder = setupProxyController(t, []gin.HandlerFunc{ description: "Default forward auth should be detected and used",
func(c *gin.Context) { middlewares: []gin.HandlerFunc{},
c.Set("context", &loggedInCtx) run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
c.Next() 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, err = http.NewRequest("GET", "/api/auth/nginx", nil) req.Header.Set("user-agent", browserUserAgent)
assert.NilError(t, err) router.ServeHTTP(recorder, req)
req.Header.Set("x-original-url", "http://whoami.example.com/") assert.Equal(t, 307, recorder.Code)
location := recorder.Header().Get("Location")
router.ServeHTTP(recorder, req) assert.Contains(t, location, "https://tinyauth.example.com/login?redirect_uri=")
assert.Equal(t, recorder.Code, http.StatusOK) assert.Contains(t, location, "https%3A%2F%2Ftest.example.com%2F")
},
// Test logged in user envoy (ext_authz) },
router, recorder = setupProxyController(t, []gin.HandlerFunc{ {
func(c *gin.Context) { description: "Auth request (nginx) should be detected and used",
c.Set("context", &loggedInCtx) middlewares: []gin.HandlerFunc{},
c.Next() 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)
req, err = http.NewRequest("GET", "/api/auth/envoy?path=/", nil) assert.Equal(t, 401, recorder.Code)
assert.NilError(t, err) },
},
req.Host = "whoami.example.com" {
req.Header.Set("x-forwarded-proto", "http") description: "Ext authz (envoy) should be detected and used",
middlewares: []gin.HandlerFunc{},
router.ServeHTTP(recorder, req) run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
assert.Equal(t, recorder.Code, http.StatusOK) req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil) // test a different method for envoy
req.Host = "test.example.com"
// Test ACL allow caddy/traefik (forward_auth) req.Header.Set("x-forwarded-proto", "https")
router, recorder = setupProxyController(t, nil) router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
req, err = http.NewRequest("GET", "/api/auth/traefik", nil) },
assert.NilError(t, err) },
{
req.Header.Set("x-forwarded-host", "whoami.example.com") description: "Ensure forward auth fallback for nginx",
req.Header.Set("x-forwarded-proto", "http") middlewares: []gin.HandlerFunc{},
req.Header.Set("x-forwarded-uri", "/allow") run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
router.ServeHTTP(recorder, req) req.Header.Set("x-forwarded-host", "test.example.com")
assert.Equal(t, recorder.Code, http.StatusOK) req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/")
// Test ACL allow nginx router.ServeHTTP(recorder, req)
router, recorder = setupProxyController(t, nil) assert.Equal(t, 401, recorder.Code)
},
req, err = http.NewRequest("GET", "/api/auth/nginx", nil) },
assert.NilError(t, err) {
description: "Ensure forward auth fallback for envoy",
req.Header.Set("x-original-url", "http://whoami.example.com/allow") middlewares: []gin.HandlerFunc{},
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
router.ServeHTTP(recorder, req) req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
assert.Equal(t, recorder.Code, http.StatusOK) req.Header.Set("x-forwarded-host", "test.example.com")
req.Header.Set("x-forwarded-proto", "https")
// Test ACL allow envoy req.Header.Set("x-forwarded-uri", "/hello")
router, recorder = setupProxyController(t, nil) router.ServeHTTP(recorder, req)
assert.Equal(t, 401, recorder.Code)
req, err = http.NewRequest("GET", "/api/auth/envoy?path=/allow", nil) },
assert.NilError(t, err) },
{
req.Host = "whoami.example.com" description: "Ensure normal authentication flow for forward auth",
req.Header.Set("x-forwarded-proto", "http") middlewares: []gin.HandlerFunc{
simpleCtx,
router.ServeHTTP(recorder, req) },
assert.Equal(t, recorder.Code, http.StatusOK) run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
// Test traefik/caddy (forward_auth) without required headers req.Header.Set("x-forwarded-host", "test.example.com")
router, recorder = setupProxyController(t, nil) req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/")
req, err = http.NewRequest("GET", "/api/auth/traefik", nil) router.ServeHTTP(recorder, req)
assert.NilError(t, err)
assert.Equal(t, 200, recorder.Code)
router.ServeHTTP(recorder, req) assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
assert.Equal(t, recorder.Code, http.StatusBadRequest) assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
// Test nginx (forward_auth) without required headers },
router, recorder = setupProxyController(t, nil) },
{
req, err = http.NewRequest("GET", "/api/auth/nginx", nil) description: "Ensure normal authentication flow for nginx auth request",
assert.NilError(t, err) middlewares: []gin.HandlerFunc{
simpleCtx,
router.ServeHTTP(recorder, req) },
assert.Equal(t, recorder.Code, http.StatusBadRequest) run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
// Test envoy (forward_auth) without required headers req.Header.Set("x-original-url", "https://test.example.com/")
router, recorder = setupProxyController(t, nil) router.ServeHTTP(recorder, req)
req, err = http.NewRequest("GET", "/api/auth/envoy", nil) assert.Equal(t, 200, recorder.Code)
assert.NilError(t, err) assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
router.ServeHTTP(recorder, req) assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
assert.Equal(t, recorder.Code, http.StatusBadRequest) },
},
// Test nginx (auth_request) with forward_auth fallback with ACLs {
router, recorder = setupProxyController(t, nil) description: "Ensure normal authentication flow for envoy ext authz",
middlewares: []gin.HandlerFunc{
req, err = http.NewRequest("GET", "/api/auth/nginx", nil) simpleCtx,
assert.NilError(t, err) },
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req.Header.Set("x-forwarded-host", "whoami.example.com") req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/hello", nil)
req.Header.Set("x-forwarded-proto", "http") req.Host = "test.example.com"
req.Header.Set("x-forwarded-uri", "/allow") req.Header.Set("x-forwarded-proto", "https")
router.ServeHTTP(recorder, req)
router.ServeHTTP(recorder, req)
assert.Equal(t, recorder.Code, http.StatusOK) assert.Equal(t, 200, recorder.Code)
assert.Equal(t, "testuser", recorder.Header().Get("remote-user"))
// Test envoy (ext_authz) with forward_auth fallback with ACLs assert.Equal(t, "Testuser", recorder.Header().Get("remote-name"))
router, recorder = setupProxyController(t, nil) assert.Equal(t, "testuser@example.com", recorder.Header().Get("remote-email"))
},
req, err = http.NewRequest("GET", "/api/auth/envoy", nil) },
assert.NilError(t, err) {
description: "Ensure path allow ACL works on forward auth",
req.Header.Set("x-forwarded-host", "whoami.example.com") middlewares: []gin.HandlerFunc{},
req.Header.Set("x-forwarded-proto", "http") run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req.Header.Set("x-forwarded-uri", "/allow") req := httptest.NewRequest("GET", "/api/auth/traefik", nil)
req.Header.Set("x-forwarded-host", "path-allow.example.com")
router.ServeHTTP(recorder, req) req.Header.Set("x-forwarded-proto", "https")
assert.Equal(t, recorder.Code, http.StatusOK) req.Header.Set("x-forwarded-uri", "/allowed")
router.ServeHTTP(recorder, req)
// Test envoy (ext_authz) with empty path assert.Equal(t, 200, recorder.Code)
router, recorder = setupProxyController(t, nil) },
},
req, err = http.NewRequest("GET", "/api/auth/envoy", nil) {
assert.NilError(t, err) description: "Ensure path allow ACL works on nginx auth request",
middlewares: []gin.HandlerFunc{},
req.Host = "whoami.example.com" run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req.Header.Set("x-forwarded-proto", "http") req := httptest.NewRequest("GET", "/api/auth/nginx", nil)
req.Header.Set("x-original-url", "https://path-allow.example.com/allowed")
router.ServeHTTP(recorder, req) router.ServeHTTP(recorder, req)
assert.Equal(t, recorder.Code, http.StatusUnauthorized) assert.Equal(t, 200, recorder.Code)
},
// Ensure forward_auth fallback works with path (should ignore) },
router, recorder = setupProxyController(t, nil) {
description: "Ensure path allow ACL works on envoy ext authz",
req, err = http.NewRequest("GET", "/api/auth/traefik?path=/allow", nil) middlewares: []gin.HandlerFunc{},
assert.NilError(t, err) run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
req := httptest.NewRequest("HEAD", "/api/auth/envoy?path=/allowed", nil)
req.Header.Set("x-forwarded-proto", "http") req.Host = "path-allow.example.com"
req.Header.Set("x-forwarded-host", "whoami.example.com") req.Header.Set("x-forwarded-proto", "https")
req.Header.Set("x-forwarded-uri", "/allow") router.ServeHTTP(recorder, req)
assert.Equal(t, 200, recorder.Code)
router.ServeHTTP(recorder, req) },
assert.Equal(t, recorder.Code, http.StatusOK) },
{
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)
})
} }

View File

@@ -3,19 +3,26 @@ package controller_test
import ( import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"path"
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/steveiliop56/tinyauth/internal/controller" "github.com/steveiliop56/tinyauth/internal/controller"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestResourcesController(t *testing.T) { func TestResourcesController(t *testing.T) {
tempDir := t.TempDir()
resourcesControllerCfg := controller.ResourcesControllerConfig{ resourcesControllerCfg := controller.ResourcesControllerConfig{
Path: "/tmp/testfiles", Path: path.Join(tempDir, "resources"),
Enabled: true, Enabled: true,
} }
err := os.Mkdir(resourcesControllerCfg.Path, 0777)
require.NoError(t, err)
type testCase struct { type testCase struct {
description string description string
run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) run func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder)
@@ -52,16 +59,13 @@ func TestResourcesController(t *testing.T) {
}, },
} }
err := os.MkdirAll(resourcesControllerCfg.Path, 0777)
assert.NoError(t, err)
testFilePath := resourcesControllerCfg.Path + "/testfile.txt" testFilePath := resourcesControllerCfg.Path + "/testfile.txt"
err = os.WriteFile(testFilePath, []byte("This is a test file."), 0777) err = os.WriteFile(testFilePath, []byte("This is a test file."), 0777)
assert.NoError(t, err) require.NoError(t, err)
testFilePathParent := resourcesControllerCfg.Path + "/../somefile.txt" testFilePathParent := tempDir + "/somefile.txt"
err = os.WriteFile(testFilePathParent, []byte("This file should not be accessible."), 0777) err = os.WriteFile(testFilePathParent, []byte("This file should not be accessible."), 0777)
assert.NoError(t, err) require.NoError(t, err)
for _, test := range tests { for _, test := range tests {
t.Run(test.description, func(t *testing.T) { t.Run(test.description, func(t *testing.T) {
@@ -76,13 +80,4 @@ func TestResourcesController(t *testing.T) {
test.run(t, router, recorder) test.run(t, router, recorder)
}) })
} }
err = os.Remove(testFilePath)
assert.NoError(t, err)
err = os.Remove(testFilePathParent)
assert.NoError(t, err)
err = os.Remove(resourcesControllerCfg.Path)
assert.NoError(t, err)
} }

View File

@@ -3,7 +3,7 @@ package controller_test
import ( import (
"encoding/json" "encoding/json"
"net/http/httptest" "net/http/httptest"
"os" "path"
"slices" "slices"
"strings" "strings"
"testing" "testing"
@@ -18,9 +18,12 @@ import (
"github.com/steveiliop56/tinyauth/internal/service" "github.com/steveiliop56/tinyauth/internal/service"
"github.com/steveiliop56/tinyauth/internal/utils/tlog" "github.com/steveiliop56/tinyauth/internal/utils/tlog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestUserController(t *testing.T) { func TestUserController(t *testing.T) {
tempDir := t.TempDir()
authServiceCfg := service.AuthServiceConfig{ authServiceCfg := service.AuthServiceConfig{
Users: []config.User{ Users: []config.User{
{ {
@@ -74,7 +77,7 @@ func TestUserController(t *testing.T) {
assert.Equal(t, "tinyauth-session", cookie.Name) assert.Equal(t, "tinyauth-session", cookie.Name)
assert.True(t, cookie.HttpOnly) assert.True(t, cookie.HttpOnly)
assert.Equal(t, "example.com", cookie.Domain) assert.Equal(t, "example.com", cookie.Domain)
assert.Equal(t, cookie.MaxAge, 10) assert.Equal(t, 10, cookie.MaxAge)
}, },
}, },
{ {
@@ -163,7 +166,7 @@ func TestUserController(t *testing.T) {
assert.Equal(t, "tinyauth-session", cookie.Name) assert.Equal(t, "tinyauth-session", cookie.Name)
assert.True(t, cookie.HttpOnly) assert.True(t, cookie.HttpOnly)
assert.Equal(t, "example.com", cookie.Domain) assert.Equal(t, "example.com", cookie.Domain)
assert.Equal(t, cookie.MaxAge, 3600) // 1 hour, default for totp pending sessions assert.Equal(t, 3600, cookie.MaxAge) // 1 hour, default for totp pending sessions
}, },
}, },
{ {
@@ -233,7 +236,7 @@ func TestUserController(t *testing.T) {
assert.Equal(t, "tinyauth-session", totpCookie.Name) assert.Equal(t, "tinyauth-session", totpCookie.Name)
assert.True(t, totpCookie.HttpOnly) assert.True(t, totpCookie.HttpOnly)
assert.Equal(t, "example.com", totpCookie.Domain) assert.Equal(t, "example.com", totpCookie.Domain)
assert.Equal(t, totpCookie.MaxAge, 10) // should use the regular session expiry time assert.Equal(t, 10, totpCookie.MaxAge) // should use the regular session expiry time
}, },
}, },
{ {
@@ -277,26 +280,26 @@ func TestUserController(t *testing.T) {
app := bootstrap.NewBootstrapApp(config.Config{}) app := bootstrap.NewBootstrapApp(config.Config{})
db, err := app.SetupDatabase("/tmp/tinyauth_test.db") db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
assert.NoError(t, err) require.NoError(t, err)
queries := repository.New(db) queries := repository.New(db)
docker := service.NewDockerService() docker := service.NewDockerService()
err = docker.Init() err = docker.Init()
assert.NoError(t, err) require.NoError(t, err)
ldap := service.NewLdapService(service.LdapServiceConfig{}) ldap := service.NewLdapService(service.LdapServiceConfig{})
err = ldap.Init() err = ldap.Init()
assert.NoError(t, err) require.NoError(t, err)
broker := service.NewOAuthBrokerService(oauthBrokerCfgs) broker := service.NewOAuthBrokerService(oauthBrokerCfgs)
err = broker.Init() err = broker.Init()
assert.NoError(t, err) require.NoError(t, err)
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker) authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
err = authService.Init() err = authService.Init()
assert.NoError(t, err) require.NoError(t, err)
beforeEach := func() { beforeEach := func() {
// Clear failed login attempts before each test // Clear failed login attempts before each test
@@ -346,9 +349,8 @@ func TestUserController(t *testing.T) {
}) })
} }
err = db.Close() t.Cleanup(func() {
assert.NoError(t, err) err = db.Close()
require.NoError(t, err)
err = os.Remove("/tmp/tinyauth_test.db") })
assert.NoError(t, err)
} }

View File

@@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http/httptest" "net/http/httptest"
"os" "path"
"testing" "testing"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -14,9 +14,12 @@ import (
"github.com/steveiliop56/tinyauth/internal/repository" "github.com/steveiliop56/tinyauth/internal/repository"
"github.com/steveiliop56/tinyauth/internal/service" "github.com/steveiliop56/tinyauth/internal/service"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestWellKnownController(t *testing.T) { func TestWellKnownController(t *testing.T) {
tempDir := t.TempDir()
oidcServiceCfg := service.OIDCServiceConfig{ oidcServiceCfg := service.OIDCServiceConfig{
Clients: map[string]config.OIDCClientConfig{ Clients: map[string]config.OIDCClientConfig{
"test": { "test": {
@@ -26,8 +29,8 @@ func TestWellKnownController(t *testing.T) {
Name: "Test Client", Name: "Test Client",
}, },
}, },
PrivateKeyPath: "/tmp/tinyauth_testing_key.pem", PrivateKeyPath: path.Join(tempDir, "key.pem"),
PublicKeyPath: "/tmp/tinyauth_testing_key.pub", PublicKeyPath: path.Join(tempDir, "key.pub"),
Issuer: "https://tinyauth.example.com", Issuer: "https://tinyauth.example.com",
SessionExpiry: 500, SessionExpiry: 500,
} }
@@ -66,7 +69,7 @@ func TestWellKnownController(t *testing.T) {
ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc", ServiceDocumentation: "https://tinyauth.app/docs/guides/oidc",
} }
assert.Equal(t, res, expected) assert.Equal(t, expected, res)
}, },
}, },
{ {
@@ -96,14 +99,14 @@ func TestWellKnownController(t *testing.T) {
app := bootstrap.NewBootstrapApp(config.Config{}) app := bootstrap.NewBootstrapApp(config.Config{})
db, err := app.SetupDatabase("/tmp/tinyauth_test.db") db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth.db"))
assert.NoError(t, err) require.NoError(t, err)
queries := repository.New(db) queries := repository.New(db)
oidcService := service.NewOIDCService(oidcServiceCfg, queries) oidcService := service.NewOIDCService(oidcServiceCfg, queries)
err = oidcService.Init() err = oidcService.Init()
assert.NoError(t, err) require.NoError(t, err)
for _, test := range tests { for _, test := range tests {
t.Run(test.description, func(t *testing.T) { t.Run(test.description, func(t *testing.T) {
@@ -119,9 +122,8 @@ func TestWellKnownController(t *testing.T) {
}) })
} }
err = db.Close() t.Cleanup(func() {
assert.NoError(t, err) err = db.Close()
require.NoError(t, err)
err = os.Remove("/tmp/tinyauth_test.db") })
assert.NoError(t, err)
} }

View File

@@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
@@ -78,6 +79,8 @@ type AuthService struct {
queries *repository.Queries queries *repository.Queries
oauthBroker *OAuthBrokerService oauthBroker *OAuthBrokerService
lockdown *Lockdown lockdown *Lockdown
lockdownCtx context.Context
lockdownCancelFunc context.CancelFunc
} }
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService { func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService {
@@ -770,6 +773,11 @@ func (auth *AuthService) ensureOAuthSessionLimit() {
} }
func (auth *AuthService) lockdownMode() { func (auth *AuthService) lockdownMode() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
auth.lockdownCtx = ctx
auth.lockdownCancelFunc = cancel
auth.loginMutex.Lock() auth.loginMutex.Lock()
tlog.App.Warn().Msg("Multiple login attempts detected, possibly DDOS attack. Activating temporary lockdown.") tlog.App.Warn().Msg("Multiple login attempts detected, possibly DDOS attack. Activating temporary lockdown.")
@@ -788,7 +796,12 @@ func (auth *AuthService) lockdownMode() {
auth.loginMutex.Unlock() auth.loginMutex.Unlock()
<-timer.C select {
case <-timer.C:
// Timer expired, end lockdown
case <-ctx.Done():
// Context cancelled, end lockdown
}
auth.loginMutex.Lock() auth.loginMutex.Lock()
@@ -801,5 +814,8 @@ func (auth *AuthService) lockdownMode() {
func (auth *AuthService) ClearRateLimitsTestingOnly() { func (auth *AuthService) ClearRateLimitsTestingOnly() {
auth.loginMutex.Lock() auth.loginMutex.Lock()
auth.loginAttempts = make(map[string]*LoginAttempt) auth.loginAttempts = make(map[string]*LoginAttempt)
auth.loginMutex.Unlock() if auth.lockdown != nil {
auth.lockdownCancelFunc()
}
auth.loginMutex.Unlock()
} }

View File

@@ -25,12 +25,6 @@ func TestGetRootDomain(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result)
// Domain with no subdomain
domain = "http://tinyauth.app"
expected = "tinyauth.app"
_, err = utils.GetCookieDomain(domain)
assert.Error(t, err, "invalid app url, must be at least second level domain")
// Invalid domain (only TLD) // Invalid domain (only TLD)
domain = "com" domain = "com"
_, err = utils.GetCookieDomain(domain) _, err = utils.GetCookieDomain(domain)