mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-04-21 04:58:15 +00:00
Compare commits
1 Commits
v5.0.3
..
b217e29fd3
| Author | SHA1 | Date | |
|---|---|---|---|
| b217e29fd3 |
@@ -1,5 +1,6 @@
|
|||||||
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:
|
||||||
@@ -8,6 +9,7 @@ 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
|
||||||
@@ -15,6 +17,7 @@ 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
|
||||||
@@ -27,6 +30,7 @@ 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,5 +1,6 @@
|
|||||||
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:
|
||||||
@@ -8,6 +9,7 @@ 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
|
||||||
@@ -15,7 +17,8 @@ services:
|
|||||||
traefik.http.routers.whoami.middlewares: tinyauth
|
traefik.http.routers.whoami.middlewares: tinyauth
|
||||||
|
|
||||||
tinyauth:
|
tinyauth:
|
||||||
image: ghcr.io/steveiliop56/tinyauth:v5
|
container_name: tinyauth
|
||||||
|
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
|
||||||
|
|||||||
@@ -58,8 +58,8 @@
|
|||||||
"invalidInput": "Saisie non valide",
|
"invalidInput": "Saisie non valide",
|
||||||
"domainWarningTitle": "Domaine invalide",
|
"domainWarningTitle": "Domaine invalide",
|
||||||
"domainWarningSubtitle": "Cette instance est configurée pour être accédée depuis <code>{{appUrl}}</code>, mais <code>{{currentUrl}}</code> est utilisé. Si vous continuez, vous pourriez rencontrer des problèmes d'authentification.",
|
"domainWarningSubtitle": "Cette instance est configurée pour être accédée depuis <code>{{appUrl}}</code>, mais <code>{{currentUrl}}</code> est utilisé. Si vous continuez, vous pourriez rencontrer des problèmes d'authentification.",
|
||||||
"domainWarningCurrent": "Actuellement :",
|
"domainWarningCurrent": "Current:",
|
||||||
"domainWarningExpected": "Attendu :",
|
"domainWarningExpected": "Expected:",
|
||||||
"ignoreTitle": "Ignorer",
|
"ignoreTitle": "Ignorer",
|
||||||
"goToCorrectDomainTitle": "Aller au bon domaine",
|
"goToCorrectDomainTitle": "Aller au bon domaine",
|
||||||
"authorizeTitle": "Autoriser",
|
"authorizeTitle": "Autoriser",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ require (
|
|||||||
github.com/weppos/publicsuffix-go v0.50.3
|
github.com/weppos/publicsuffix-go v0.50.3
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.48.0
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546
|
||||||
golang.org/x/oauth2 v0.36.0
|
golang.org/x/oauth2 v0.35.0
|
||||||
gotest.tools/v3 v3.5.2
|
gotest.tools/v3 v3.5.2
|
||||||
modernc.org/sqlite v1.46.1
|
modernc.org/sqlite v1.46.1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -322,8 +322,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
|||||||
@@ -115,11 +115,6 @@ 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)
|
||||||
@@ -270,7 +265,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), client.ClientID)
|
entry, err := controller.oidc.GetCodeEntry(c, controller.oidc.Hash(req.Code))
|
||||||
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")
|
||||||
@@ -286,13 +281,6 @@ 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,21 +90,9 @@ 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, ok := controller.requireHeader(c, "x-forwarded-uri")
|
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
||||||
|
proto := c.Request.Header.Get("X-Forwarded-Proto")
|
||||||
if !ok {
|
host := c.Request.Header.Get("X-Forwarded-Host")
|
||||||
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)
|
||||||
@@ -185,6 +173,11 @@ 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)
|
||||||
|
|
||||||
@@ -332,16 +325,3 @@ 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,11 +59,6 @@ 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,
|
||||||
@@ -84,11 +79,9 @@ 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, _ := setupProxyController(t, nil)
|
router, recorder, authService := 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)
|
||||||
@@ -143,14 +136,26 @@ 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{
|
||||||
@@ -169,15 +174,38 @@ func TestProxyHandler(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
req = httptest.NewRequest("GET", "/api/auth/traefik", nil)
|
||||||
req.Header.Set("X-Forwarded-Proto", "https")
|
req.Header.Set("Cookie", cookie)
|
||||||
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,17 +182,13 @@ 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, clientId string) (repository.OidcCode, error) {
|
func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash 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,10 +374,6 @@ func (service *OIDCService) GetCodeEntry(c *gin.Context, codeHash string, client
|
|||||||
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