mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-03-13 10:12:06 +00:00
Compare commits
7 Commits
v5.0.2-bet
...
refactor/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c8a9e93ff | ||
|
|
b2a1bfb1f5 | ||
|
|
f1e869a920 | ||
|
|
cc5a6d73cf | ||
|
|
b2e3a85f42 | ||
|
|
2e03eb9612 | ||
|
|
55c33f7a8e |
12
README.md
12
README.md
@@ -1,7 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png">
|
<img alt="Tinyauth" title="Tinyauth" width="96" src="assets/logo-rounded.png">
|
||||||
<h1>Tinyauth</h1>
|
<h1>Tinyauth</h1>
|
||||||
<p>The simplest way to protect your apps with a login screen.</p>
|
<p>The tiniest authentication and authorization server you have ever seen.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github or any other provider to all of your apps. It supports all the popular proxies like Traefik, Nginx and Caddy.
|
Tinyauth is the simplest and tiniest authentication and authorization server you have ever seen. It is designed to both work as an authentication middleware for your apps, offering support for OAuth, LDAP and access-controls, and as a standalone authentication server. It supports all the popular proxies like Traefik, Nginx and Caddy.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ Tinyauth is a simple authentication middleware that adds a simple login screen o
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
You can easily get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities.
|
You can get started with Tinyauth by following the guide in the [documentation](https://tinyauth.app/docs/getting-started). There is also an available [docker-compose](./docker-compose.example.yml) file that has Traefik, Whoami and Tinyauth to demonstrate its capabilities (keep in mind that this file lives in the development branch so it may have updates that are not yet released).
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
@@ -40,15 +40,15 @@ If you wish to contribute to the documentation head over to the [repository](htt
|
|||||||
|
|
||||||
## Discord
|
## Discord
|
||||||
|
|
||||||
Tinyauth has a [discord](https://discord.gg/eHzVaCzRRd) server. Feel free to hop in to chat about self-hosting, homelabs and of course Tinyauth. See you there!
|
Tinyauth has a [Discord](https://discord.gg/eHzVaCzRRd) server. Feel free to hop in to chat about self-hosting, homelabs and of course Tinyauth. See you there!
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
All contributions to the codebase are welcome! If you have any free time feel free to pick up an [issue](https://github.com/steveiliop56/tinyauth/issues) or add your own missing features. Make sure to check out the [contributing guide](./CONTRIBUTING.md) for instructions on how to get the development server up and running.
|
All contributions to the codebase are welcome! If you have any free time, feel free to pick up an [issue](https://github.com/steveiliop56/tinyauth/issues) or add your own missing features. Make sure to check out the [contributing guide](./CONTRIBUTING.md) for instructions on how to get the development server up and running.
|
||||||
|
|
||||||
## Localization
|
## Localization
|
||||||
|
|
||||||
If you would like to help translate Tinyauth into more languages, visit the [Crowdin](https://crowdin.com/project/tinyauth) page.
|
If you like, you can help translate Tinyauth into more languages by visiting the [Crowdin](https://crowdin.com/project/tinyauth) page.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
container_name: traefik
|
|
||||||
image: traefik:v3.6
|
image: traefik:v3.6
|
||||||
command: --api.insecure=true --providers.docker
|
command: --api.insecure=true --providers.docker
|
||||||
ports:
|
ports:
|
||||||
@@ -9,7 +8,6 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
whoami:
|
whoami:
|
||||||
container_name: whoami
|
|
||||||
image: traefik/whoami:latest
|
image: traefik/whoami:latest
|
||||||
labels:
|
labels:
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
@@ -17,7 +15,6 @@ services:
|
|||||||
traefik.http.routers.whoami.middlewares: tinyauth
|
traefik.http.routers.whoami.middlewares: tinyauth
|
||||||
|
|
||||||
tinyauth-frontend:
|
tinyauth-frontend:
|
||||||
container_name: tinyauth-frontend
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: frontend/Dockerfile.dev
|
dockerfile: frontend/Dockerfile.dev
|
||||||
@@ -30,7 +27,6 @@ services:
|
|||||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.127.0.0.1.sslip.io`)
|
traefik.http.routers.tinyauth.rule: Host(`tinyauth.127.0.0.1.sslip.io`)
|
||||||
|
|
||||||
tinyauth-backend:
|
tinyauth-backend:
|
||||||
container_name: tinyauth-backend
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.dev
|
dockerfile: Dockerfile.dev
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
container_name: traefik
|
|
||||||
image: traefik:v3.6
|
image: traefik:v3.6
|
||||||
command: --api.insecure=true --providers.docker
|
command: --api.insecure=true --providers.docker
|
||||||
ports:
|
ports:
|
||||||
@@ -9,7 +8,6 @@ services:
|
|||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
whoami:
|
whoami:
|
||||||
container_name: whoami
|
|
||||||
image: traefik/whoami:latest
|
image: traefik/whoami:latest
|
||||||
labels:
|
labels:
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
@@ -17,8 +15,7 @@ services:
|
|||||||
traefik.http.routers.whoami.middlewares: tinyauth
|
traefik.http.routers.whoami.middlewares: tinyauth
|
||||||
|
|
||||||
tinyauth:
|
tinyauth:
|
||||||
container_name: tinyauth
|
image: ghcr.io/steveiliop56/tinyauth:v5
|
||||||
image: ghcr.io/steveiliop56/tinyauth:v3
|
|
||||||
environment:
|
environment:
|
||||||
- TINYAUTH_APPURL=https://tinyauth.example.com
|
- TINYAUTH_APPURL=https://tinyauth.example.com
|
||||||
- TINYAUTH_AUTH_USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
- TINYAUTH_AUTH_USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||||
|
|||||||
@@ -2,77 +2,77 @@ package controller_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http/httptest"
|
"io"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/controller"
|
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var contextControllerCfg = controller.ContextControllerConfig{
|
func TestUserContextController(t *testing.T) {
|
||||||
Providers: []controller.Provider{
|
// Controller setup
|
||||||
{
|
suite := NewControllerTest(func(router *gin.RouterGroup) *controller.ContextController {
|
||||||
Name: "Local",
|
ctrl := controller.NewContextController(contextControllerCfg, router)
|
||||||
ID: "local",
|
|
||||||
OAuth: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Google",
|
|
||||||
ID: "google",
|
|
||||||
OAuth: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Title: "Test App",
|
|
||||||
AppURL: "http://localhost:8080",
|
|
||||||
CookieDomain: "localhost",
|
|
||||||
ForgotPasswordMessage: "Contact admin to reset your password.",
|
|
||||||
BackgroundImage: "/assets/bg.jpg",
|
|
||||||
OAuthAutoRedirect: "google",
|
|
||||||
WarningsEnabled: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
var contextCtrlTestContext = config.UserContext{
|
|
||||||
Username: "testuser",
|
|
||||||
Name: "testuser",
|
|
||||||
Email: "test@example.com",
|
|
||||||
IsLoggedIn: true,
|
|
||||||
IsBasicAuth: false,
|
|
||||||
OAuth: false,
|
|
||||||
Provider: "local",
|
|
||||||
TotpPending: false,
|
|
||||||
OAuthGroups: "",
|
|
||||||
TotpEnabled: false,
|
|
||||||
OAuthSub: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupContextController(middlewares *[]gin.HandlerFunc) (*gin.Engine, *httptest.ResponseRecorder) {
|
|
||||||
tlog.NewSimpleLogger().Init()
|
|
||||||
|
|
||||||
// Setup
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
router := gin.Default()
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
if middlewares != nil {
|
|
||||||
for _, m := range *middlewares {
|
|
||||||
router.Use(m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group := router.Group("/api")
|
|
||||||
|
|
||||||
ctrl := controller.NewContextController(contextControllerCfg, group)
|
|
||||||
ctrl.SetupRoutes()
|
ctrl.SetupRoutes()
|
||||||
|
return ctrl
|
||||||
|
})
|
||||||
|
|
||||||
return router, recorder
|
// Test user context
|
||||||
}
|
req, err := http.NewRequest("GET", "/api/context/user", nil)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
func TestAppContextHandler(t *testing.T) {
|
ctx := testContext
|
||||||
expectedRes := controller.AppContextResponse{
|
ctx.IsLoggedIn = true
|
||||||
|
ctx.Provider = "local"
|
||||||
|
|
||||||
|
expected, err := json.Marshal(controller.UserContextResponse{
|
||||||
|
Status: 200,
|
||||||
|
Message: "Success",
|
||||||
|
IsLoggedIn: ctx.IsLoggedIn,
|
||||||
|
Username: ctx.Username,
|
||||||
|
Name: ctx.Name,
|
||||||
|
Email: ctx.Email,
|
||||||
|
Provider: ctx.Provider,
|
||||||
|
OAuth: ctx.OAuth,
|
||||||
|
TotpPending: ctx.TotpPending,
|
||||||
|
OAuthName: ctx.OAuthName,
|
||||||
|
})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
resp := suite.RequestWithMiddleware(req, []gin.HandlerFunc{
|
||||||
|
func(c *gin.Context) {
|
||||||
|
c.Set("context", &ctx)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
bytes, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.DeepEqual(t, expected, bytes)
|
||||||
|
|
||||||
|
// Ensure user context is not available when not logged in
|
||||||
|
req, err = http.NewRequest("GET", "/api/context/user", nil)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
expected, err = json.Marshal(controller.UserContextResponse{
|
||||||
|
Status: http.StatusUnauthorized,
|
||||||
|
Message: "Unauthorized",
|
||||||
|
})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
resp = suite.RequestWithMiddleware(req, nil)
|
||||||
|
assert.Equal(t, 200, resp.Code)
|
||||||
|
bytes, err = io.ReadAll(resp.Body)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.DeepEqual(t, expected, bytes)
|
||||||
|
|
||||||
|
// Test app context
|
||||||
|
req, err = http.NewRequest("GET", "/api/context/app", nil)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
expected, err = json.Marshal(controller.AppContextResponse{
|
||||||
Status: 200,
|
Status: 200,
|
||||||
Message: "Success",
|
Message: "Success",
|
||||||
Providers: contextControllerCfg.Providers,
|
Providers: contextControllerCfg.Providers,
|
||||||
@@ -83,71 +83,13 @@ func TestAppContextHandler(t *testing.T) {
|
|||||||
BackgroundImage: contextControllerCfg.BackgroundImage,
|
BackgroundImage: contextControllerCfg.BackgroundImage,
|
||||||
OAuthAutoRedirect: contextControllerCfg.OAuthAutoRedirect,
|
OAuthAutoRedirect: contextControllerCfg.OAuthAutoRedirect,
|
||||||
WarningsEnabled: contextControllerCfg.WarningsEnabled,
|
WarningsEnabled: contextControllerCfg.WarningsEnabled,
|
||||||
}
|
|
||||||
|
|
||||||
router, recorder := setupContextController(nil)
|
|
||||||
req := httptest.NewRequest("GET", "/api/context/app", nil)
|
|
||||||
router.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
assert.Equal(t, 200, recorder.Code)
|
|
||||||
|
|
||||||
var ctrlRes controller.AppContextResponse
|
|
||||||
|
|
||||||
err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
|
|
||||||
|
|
||||||
assert.NilError(t, err)
|
|
||||||
assert.DeepEqual(t, expectedRes, ctrlRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserContextHandler(t *testing.T) {
|
|
||||||
expectedRes := controller.UserContextResponse{
|
|
||||||
Status: 200,
|
|
||||||
Message: "Success",
|
|
||||||
IsLoggedIn: contextCtrlTestContext.IsLoggedIn,
|
|
||||||
Username: contextCtrlTestContext.Username,
|
|
||||||
Name: contextCtrlTestContext.Name,
|
|
||||||
Email: contextCtrlTestContext.Email,
|
|
||||||
Provider: contextCtrlTestContext.Provider,
|
|
||||||
OAuth: contextCtrlTestContext.OAuth,
|
|
||||||
TotpPending: contextCtrlTestContext.TotpPending,
|
|
||||||
OAuthName: contextCtrlTestContext.OAuthName,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with context
|
|
||||||
router, recorder := setupContextController(&[]gin.HandlerFunc{
|
|
||||||
func(c *gin.Context) {
|
|
||||||
c.Set("context", &contextCtrlTestContext)
|
|
||||||
c.Next()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/api/context/user", nil)
|
|
||||||
router.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
assert.Equal(t, 200, recorder.Code)
|
|
||||||
|
|
||||||
var ctrlRes controller.UserContextResponse
|
|
||||||
|
|
||||||
err := json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
|
|
||||||
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, expectedRes, ctrlRes)
|
|
||||||
|
|
||||||
// Test no context
|
resp = suite.RequestWithMiddleware(req, nil)
|
||||||
expectedRes = controller.UserContextResponse{
|
|
||||||
Status: 401,
|
|
||||||
Message: "Unauthorized",
|
|
||||||
IsLoggedIn: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
router, recorder = setupContextController(nil)
|
|
||||||
req = httptest.NewRequest("GET", "/api/context/user", nil)
|
|
||||||
router.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
assert.Equal(t, 200, recorder.Code)
|
|
||||||
|
|
||||||
err = json.Unmarshal(recorder.Body.Bytes(), &ctrlRes)
|
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
bytes, err = io.ReadAll(resp.Body)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, expectedRes, ctrlRes)
|
assert.DeepEqual(t, expected, bytes)
|
||||||
}
|
}
|
||||||
|
|||||||
89
internal/controller/controller_test.go
Normal file
89
internal/controller/controller_test.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package controller_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Testing suite
|
||||||
|
|
||||||
|
type ControllerTest[T any] struct {
|
||||||
|
ctrlSetup func(router *gin.RouterGroup) T
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewControllerTest[T any](setup func(router *gin.RouterGroup) T) *ControllerTest[T] {
|
||||||
|
return &ControllerTest[T]{ctrlSetup: setup}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrlt *ControllerTest[T]) newEngine(middlewares []gin.HandlerFunc) *gin.Engine {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
engine := gin.New()
|
||||||
|
|
||||||
|
for _, mw := range middlewares {
|
||||||
|
engine.Use(mw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrlrt *ControllerTest[T]) newControllerInstance(engine *gin.Engine) T {
|
||||||
|
ctrl := ctrlrt.ctrlSetup(engine.Group("/api"))
|
||||||
|
return ctrl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrlt *ControllerTest[T]) RequestWithMiddleware(http *http.Request, middlewares []gin.HandlerFunc) *httptest.ResponseRecorder {
|
||||||
|
engine := ctrlt.newEngine(middlewares)
|
||||||
|
ctrlt.newControllerInstance(engine)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
engine.ServeHTTP(recorder, http)
|
||||||
|
return recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrlt *ControllerTest[T]) Request(http *http.Request) *httptest.ResponseRecorder {
|
||||||
|
return ctrlt.RequestWithMiddleware(http, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controller configs
|
||||||
|
|
||||||
|
var contextControllerCfg = controller.ContextControllerConfig{
|
||||||
|
Providers: []controller.Provider{
|
||||||
|
{
|
||||||
|
Name: "Local",
|
||||||
|
ID: "local",
|
||||||
|
OAuth: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Google",
|
||||||
|
ID: "google",
|
||||||
|
OAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Title: "Tinyauth Testing",
|
||||||
|
AppURL: "http://tinyauth.example.com:3000",
|
||||||
|
CookieDomain: "example.com",
|
||||||
|
ForgotPasswordMessage: "Foo bar",
|
||||||
|
BackgroundImage: "/background.jpg",
|
||||||
|
OAuthAutoRedirect: "google",
|
||||||
|
WarningsEnabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var testContext = config.UserContext{
|
||||||
|
Username: "user",
|
||||||
|
Name: "User",
|
||||||
|
Email: "user@example.com",
|
||||||
|
IsLoggedIn: false,
|
||||||
|
IsBasicAuth: false,
|
||||||
|
OAuth: false,
|
||||||
|
Provider: "",
|
||||||
|
TotpPending: false,
|
||||||
|
OAuthGroups: "group1,group2",
|
||||||
|
TotpEnabled: false,
|
||||||
|
OAuthName: "test",
|
||||||
|
OAuthSub: "test",
|
||||||
|
LdapGroups: "group1,group2",
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ func (controller *HealthController) SetupRoutes() {
|
|||||||
|
|
||||||
func (controller *HealthController) healthHandler(c *gin.Context) {
|
func (controller *HealthController) healthHandler(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": "ok",
|
"status": 200,
|
||||||
"message": "Healthy",
|
"message": "Healthy",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
49
internal/controller/health_controller_test.go
Normal file
49
internal/controller/health_controller_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package controller_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/steveiliop56/tinyauth/internal/controller"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHealthController(t *testing.T) {
|
||||||
|
// Controller setup
|
||||||
|
suite := NewControllerTest(func(router *gin.RouterGroup) *controller.HealthController {
|
||||||
|
ctrl := controller.NewHealthController(router)
|
||||||
|
ctrl.SetupRoutes()
|
||||||
|
return ctrl
|
||||||
|
})
|
||||||
|
|
||||||
|
expected, err := json.Marshal(map[string]any{
|
||||||
|
"status": 200,
|
||||||
|
"message": "Healthy",
|
||||||
|
})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
// Test we are healthy with GET
|
||||||
|
req, err := http.NewRequest("GET", "/api/healthz", nil)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
resp := suite.Request(req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
bytes, err := io.ReadAll(resp.Body)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.DeepEqual(t, bytes, expected)
|
||||||
|
|
||||||
|
// Test we are healthy with HEAD
|
||||||
|
req, err = http.NewRequest("HEAD", "/api/healthz", nil)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
resp = suite.Request(req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.Code)
|
||||||
|
bytes, err = io.ReadAll(resp.Body)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.DeepEqual(t, expected, bytes)
|
||||||
|
}
|
||||||
@@ -115,6 +115,11 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !userContext.IsLoggedIn {
|
||||||
|
controller.authorizeError(c, errors.New("err user not logged in"), "User not logged in", "The user is not logged in", "", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var req service.AuthorizeRequest
|
var req service.AuthorizeRequest
|
||||||
|
|
||||||
err = c.BindJSON(&req)
|
err = c.BindJSON(&req)
|
||||||
@@ -265,7 +270,7 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
|
|
||||||
switch req.GrantType {
|
switch req.GrantType {
|
||||||
case "authorization_code":
|
case "authorization_code":
|
||||||
entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code))
|
entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code), client.ClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, service.ErrCodeNotFound) {
|
if errors.Is(err, service.ErrCodeNotFound) {
|
||||||
tlog.App.Warn().Msg("Code not found")
|
tlog.App.Warn().Msg("Code not found")
|
||||||
@@ -281,6 +286,13 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, service.ErrInvalidClient) {
|
||||||
|
tlog.App.Warn().Msg("Invalid client ID")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"error": "invalid_client",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
tlog.App.Warn().Err(err).Msg("Failed to get OIDC code entry")
|
tlog.App.Warn().Err(err).Msg("Failed to get OIDC code entry")
|
||||||
c.JSON(400, gin.H{
|
c.JSON(400, gin.H{
|
||||||
"error": "server_error",
|
"error": "server_error",
|
||||||
|
|||||||
@@ -90,9 +90,21 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
tlog.App.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
|
tlog.App.Debug().Msg("Request identified as (most likely) coming from a non-browser client")
|
||||||
}
|
}
|
||||||
|
|
||||||
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
uri, ok := controller.requireHeader(c, "x-forwarded-uri")
|
||||||
proto := c.Request.Header.Get("X-Forwarded-Proto")
|
|
||||||
host := c.Request.Header.Get("X-Forwarded-Host")
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
host, ok := controller.requireHeader(c, "x-forwarded-host")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proto, ok := controller.requireHeader(c, "x-forwarded-proto")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get acls
|
// Get acls
|
||||||
acls, err := controller.acls.GetAccessControls(host)
|
acls, err := controller.acls.GetAccessControls(host)
|
||||||
@@ -173,11 +185,6 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) {
|
|||||||
|
|
||||||
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
tlog.App.Trace().Interface("context", userContext).Msg("User context from request")
|
||||||
|
|
||||||
if userContext.IsBasicAuth && userContext.TotpEnabled {
|
|
||||||
tlog.App.Debug().Msg("User has TOTP enabled, denying basic auth access")
|
|
||||||
userContext.IsLoggedIn = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if userContext.IsLoggedIn {
|
if userContext.IsLoggedIn {
|
||||||
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
|
userAllowed := controller.auth.IsUserAllowed(c, userContext, acls)
|
||||||
|
|
||||||
@@ -325,3 +332,16 @@ func (controller *ProxyController) handleError(c *gin.Context, req Proxy, isBrow
|
|||||||
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (controller *ProxyController) requireHeader(c *gin.Context, header string) (string, bool) {
|
||||||
|
val := c.Request.Header.Get(header)
|
||||||
|
if strings.TrimSpace(val) == "" {
|
||||||
|
tlog.App.Error().Str("header", header).Msg("Header not found")
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": 400,
|
||||||
|
"message": "Bad Request",
|
||||||
|
})
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return val, true
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
|
|||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
|
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Username: "totpuser",
|
||||||
|
Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.",
|
||||||
|
TotpSecret: "foo",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
OauthWhitelist: []string{},
|
OauthWhitelist: []string{},
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 3600,
|
||||||
@@ -79,9 +84,11 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En
|
|||||||
return router, recorder, authService
|
return router, recorder, authService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Needs tests for context middleware
|
||||||
|
|
||||||
func TestProxyHandler(t *testing.T) {
|
func TestProxyHandler(t *testing.T) {
|
||||||
// Setup
|
// Setup
|
||||||
router, recorder, authService := setupProxyController(t, nil)
|
router, recorder, _ := setupProxyController(t, nil)
|
||||||
|
|
||||||
// Test invalid proxy
|
// Test invalid proxy
|
||||||
req := httptest.NewRequest("GET", "/api/auth/invalidproxy", nil)
|
req := httptest.NewRequest("GET", "/api/auth/invalidproxy", nil)
|
||||||
@@ -136,26 +143,14 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
// Test logged out user (nginx)
|
// Test logged out user (nginx)
|
||||||
recorder = httptest.NewRecorder()
|
recorder = httptest.NewRecorder()
|
||||||
req = httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
req = httptest.NewRequest("GET", "/api/auth/nginx", nil)
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
|
req.Header.Set("X-Forwarded-Host", "example.com")
|
||||||
|
req.Header.Set("X-Forwarded-Uri", "/somepath")
|
||||||
router.ServeHTTP(recorder, req)
|
router.ServeHTTP(recorder, req)
|
||||||
|
|
||||||
assert.Equal(t, 401, recorder.Code)
|
assert.Equal(t, 401, recorder.Code)
|
||||||
|
|
||||||
// Test logged in user
|
// Test logged in user
|
||||||
c := gin.CreateTestContextOnly(recorder, router)
|
|
||||||
|
|
||||||
err := authService.CreateSessionCookie(c, &repository.Session{
|
|
||||||
Username: "testuser",
|
|
||||||
Name: "testuser",
|
|
||||||
Email: "testuser@example.com",
|
|
||||||
Provider: "local",
|
|
||||||
TotpPending: false,
|
|
||||||
OAuthGroups: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.NilError(t, err)
|
|
||||||
|
|
||||||
cookie := c.Writer.Header().Get("Set-Cookie")
|
|
||||||
|
|
||||||
router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{
|
router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{
|
||||||
func(c *gin.Context) {
|
func(c *gin.Context) {
|
||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
@@ -174,38 +169,15 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
req.Header.Set("Cookie", cookie)
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
|
req.Header.Set("X-Forwarded-Host", "example.com")
|
||||||
|
req.Header.Set("X-Forwarded-Uri", "/somepath")
|
||||||
req.Header.Set("Accept", "text/html")
|
req.Header.Set("Accept", "text/html")
|
||||||
router.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
assert.Equal(t, 200, recorder.Code)
|
assert.Equal(t, 200, recorder.Code)
|
||||||
|
|
||||||
assert.Equal(t, "testuser", recorder.Header().Get("Remote-User"))
|
assert.Equal(t, "testuser", recorder.Header().Get("Remote-User"))
|
||||||
assert.Equal(t, "testuser", recorder.Header().Get("Remote-Name"))
|
assert.Equal(t, "testuser", recorder.Header().Get("Remote-Name"))
|
||||||
assert.Equal(t, "testuser@example.com", recorder.Header().Get("Remote-Email"))
|
assert.Equal(t, "testuser@example.com", recorder.Header().Get("Remote-Email"))
|
||||||
|
|
||||||
// Ensure basic auth is disabled for TOTP enabled users
|
|
||||||
router, recorder, _ = setupProxyController(t, &[]gin.HandlerFunc{
|
|
||||||
func(c *gin.Context) {
|
|
||||||
c.Set("context", &config.UserContext{
|
|
||||||
Username: "testuser",
|
|
||||||
Name: "testuser",
|
|
||||||
Email: "testuser@example.com",
|
|
||||||
IsLoggedIn: true,
|
|
||||||
IsBasicAuth: true,
|
|
||||||
OAuth: false,
|
|
||||||
Provider: "local",
|
|
||||||
TotpPending: false,
|
|
||||||
OAuthGroups: "",
|
|
||||||
TotpEnabled: true,
|
|
||||||
})
|
|
||||||
c.Next()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
|
||||||
req.SetBasicAuth("testuser", "test")
|
|
||||||
router.ServeHTTP(recorder, req)
|
|
||||||
|
|
||||||
assert.Equal(t, 401, recorder.Code)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,13 +182,17 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
user := m.auth.GetLocalUser(basic.Username)
|
user := m.auth.GetLocalUser(basic.Username)
|
||||||
|
|
||||||
|
if user.TotpSecret != "" {
|
||||||
|
tlog.App.Debug().Msg("User with TOTP not allowed to login via basic auth")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Set("context", &config.UserContext{
|
c.Set("context", &config.UserContext{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Name: utils.Capitalize(user.Username),
|
Name: utils.Capitalize(user.Username),
|
||||||
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain),
|
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain),
|
||||||
Provider: "local",
|
Provider: "local",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: true,
|
||||||
TotpEnabled: user.TotpSecret != "",
|
|
||||||
IsBasicAuth: true,
|
IsBasicAuth: true,
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ func (service *OIDCService) ValidateGrantType(grantType string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string) (repository.OidcCode, error) {
|
func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string, clientId string) (repository.OidcCode, error) {
|
||||||
oidcCode, err := service.queries.GetOidcCode(c, codeHash)
|
oidcCode, err := service.queries.GetOidcCode(c, codeHash)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -374,6 +374,10 @@ func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string) (repos
|
|||||||
return repository.OidcCode{}, ErrCodeExpired
|
return repository.OidcCode{}, ErrCodeExpired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if oidcCode.ClientID != clientId {
|
||||||
|
return repository.OidcCode{}, ErrInvalidClient
|
||||||
|
}
|
||||||
|
|
||||||
return oidcCode, nil
|
return oidcCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user