mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-04-18 19:56:08 +00:00
Compare commits
5 Commits
c7bb6d61af
...
ff1c63bc7f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff1c63bc7f | ||
|
|
245fa4de78 | ||
|
|
36c2004bf6 | ||
|
|
b60e546ecd | ||
|
|
15a3753622 |
@@ -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())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user